Compare commits

..
Author SHA1 Message Date
Cline Evaluation bf1f665de8 Refactor reasoning effort option and checkpoint handling
• Replace "o3MiniReasoningEffort" with "reasoningEffort" in API providers
• Remove deprecated configuration properties from package.json
• Guard checkpoint tracker initialization and saving using the enableCheckpoints flag
2025-05-13 20:53:41 +04:00
pashpashpash 95cc15a142 Releasing memory after every diff edit - greyscreen fix? (#3459) 2025-05-12 11:17:13 -07:00
Sarah Fortune f7d464a51d Add request param to accountLoginClicked. (#3471)
All the handlers need to have the same signature f(controller, request),
otherwise the typechecker will be unhappy when setting up the gRPC server.
2025-05-12 18:52:58 +01:00
Hiroki Nakashima a6c4c0c0ea feat: Add detailed configuration options for LiteLLM provider (#2056)
* add configuration to litellm

* update defualt model name

* fix typo

* add changeset

* update default model

* remove model cost setting

* add temperature setting

* remove redandant comment

* use const

* handle model change

* fix unsaved bug
2025-05-12 23:10:44 +05:30
AraandCline Evaluation 976a8fa85e Migrate Browsertools settings to the webview from Vscode settings (#3444)
* Removing redundant settings

* Add chromeExecutablePath to BrowserSettings and UpdateBrowserSettingsRequest

- Introduced optional chromeExecutablePath field in BrowserSettings and UpdateBrowserSettingsRequest.
- Updated updateBrowserSettings function to merge new settings with existing ones, preserving previous values.
- Enhanced BrowserSession to check for the chromeExecutablePath in global state.
- Modified BrowserSettingsSection to include a UI input for specifying the Chrome executable path.

* Removing browser stuff

* Removing browser stuff

* Removing browser stuff

* Removing browser stuff

* Add cute animation

* Add cute animation

* Add cute animation

* Add cute animation

* Add cute animation

* Add cute animation

* Add cute animation

* adding stuff

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-12 23:06:37 +05:30
canvrno 33413e91c6 refreshRequestyModels protobus migration (#3422) 2025-05-11 22:42:14 -07:00
Sarah Fortune 801c59e75e Use correct type for return value of accountLoginClicked. (#3447)
Return type should be cline.String not String from JS global namespace.
Use await when calling async function vscode.env.openExternal.
2025-05-11 22:39:36 -07:00
Trevor Hudson df9c8e2e80 Trevhud/vite auto (#3448)
* move enable all

* add tooltip

* changeset

* fix spacing and move notifications to other section
2025-05-11 16:47:59 -07:00
canvrno 4d480ea3fe Add telemetry enable/disable controls by category to TelemetryService (#3450) 2025-05-11 14:53:33 -07:00
7e26d1117a Changeset version bump (#3440)
* changeset version bump

* Updating CHANGELOG.md format

* package lock

* changelog

* brackets

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-10 19:39:04 -05:00
b04810c480 increased error timeout from 500 -> 5000 for windows users (#3439)
* increased error timeout from 500 -> 5000

* conditionally setting to 5000 if windows

* Create rude-bats-brush.md

---------

Co-authored-by: Cline Evaluation <cline@example.com>
Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-05-10 17:23:14 -07:00
github-actions[bot] 5255da936f v3.15.0 Release Notes
v3.15.0 Release Notes
2025-05-09 17:18:17 -07:00
EvanandElephant Lumps 248871d770 Simple home header (#3424)
* simplified home header

* changeset

* add variable color logo for different themes

* random slash

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-09 17:26:07 -05:00
nutstore-devandweiwenhan c634bf6368 fix: make sure clineIgnoreController initialized before task start (#3410)
Co-authored-by: weiwenhan <weiwenhan@cn.nutstore.net>
2025-05-09 13:21:02 -07:00
Ara f5dbfaf234 fix: Restore native copy functionality in chat input text area (#3416)
* fix: Restore native copy functionality in chat input text area

* fix: Restore native copy functionality in chat input text area
2025-05-09 10:32:06 -07:00
Trevor Hudsonandellipsis-dev[bot] aa4d97f05d Trevhud/auto approve menu (#3405)
* improved auto-approve

* roll back chevron

* changeset

* add pills

* back to checkboxes

* turn on parent when subAction is turned on

* use vscode colors

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

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

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

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

* improve responsiveness

* remove opacity animation

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-09 22:47:14 +05:30
Toshii c20a513b70 prompt wording (#3409)
* base

* changeset
2025-05-09 01:19:02 -07:00
738c03ff3e slash command report bug (#3387)
* slash command report bug

* nits

* nits

* sigh, portible way to open urls with proper escaping because vs code api is broken

* only asking for non-algorithmically derived info

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

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

* gather user system info

* Revert "gather user system info"

This reverts commit fb16c72224.

---------

Co-authored-by: Cline Evaluation <cline@example.com>
Co-authored-by: pashpashpash <nik@cline.bot>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-09 01:10:44 -07:00
canvrno e8a68c49ce [PROTOBUS] Move refreshOpenAiModels to protobus (#3403)
* refreshOpenAiModels protobus migration

* changeset

* Debounce OpenAi model list refresh when users are typing

* debounce cleanup
2025-05-08 23:33:12 -07:00
pashpashpashandCline Evaluation 961400fdca Fixing task lockout after shell integration stream bug leading to terminal hang (#3404)
* shell timeout bug throw error

* changeset

* explanation in comments

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-08 17:23:00 -10:00
canvrno 8827b167ca [PROTOBUS] Move refreshOpenRouterModels to protobus (#3401)
* refreshOpenRouterModels protobus migration

* changeset

* cleanup

* Ellipsis inspired changes

* one small change
2025-05-08 20:14:49 -07:00
Andrei EternalandAndrei Edell e1389a62c7 run prettier correctly on generated protos (#3399)
Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-05-08 13:21:54 -10:00
canvrno 7b416ccc70 Feat: Task Favorites ️ (#3392)
* Task Favorites

* Task management docs
2025-05-08 15:49:05 -07:00
Alex 29f3cfa894 Update index.css (#3367) 2025-05-09 03:18:53 +05:30
Araandellipsis-dev[bot] 978f34e30b Supporting implicit Caching in Gemini (#3394)
* Refactor GeminiHandler to remove caching logic and update pricing structure

* Removed the enhanced caching system and related logic from GeminiHandler.
* Updated the pricing structure for cache reads in both geminiModels and vertexModels.
* Simplified the message creation process by eliminating unnecessary cache checks and operations.

* Fixing Gemini and vertex cache pricing

* Fixing Gemini and vertex cache pricing

* Update src/api/providers/gemini.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-05-09 03:17:41 +05:30
canvrno 445e25221a [PROTOBUS] Move requestVsCodeLmModels to protobus (#3344)
* Task Favorites

* getOllamaModels protobus migration

* VsCodeLmModels protobus migration

* cleanup
2025-05-08 10:50:23 -10:00
Araandellipsis-dev[bot] 489a05117c Increasing file sizes for files that can be read by cline (#3396)
* Increasing file sizes for files that can be read by cline

* Update src/integrations/misc/extract-text.ts

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

* Increasing file sizes for files that can be read by cline

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-08 12:55:40 -07:00
WinterYukky e572ee44f9 fix(bedrock): application inference profile is not work (#3388)
* fix(bedrock): application inference profile is not work

* chore: add change set

* chore: change the encoding condition to whether it contains a slash
2025-05-09 00:26:05 +05:30
pashpashpashandCline Evaluation f4e14bfe3b removing sparkle from command name (#3395)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-09 00:16:40 +05:30
watany bddc1b5e96 fix(bedrock); update bedrock api (#3157)
* fix nova

* haiku

* changeset

* changeset

* clean up duplicate changeset

* commented caching write
2025-05-08 10:52:08 -07:00
pashpashpashandCline Evaluation cb0de8f17e tracking models in diff edit failures (#3297)
* tracking models in diff edit failures

* prettier

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-08 12:33:29 -05:00
EvanandElephant Lumps e1a0b244de Conditionally initialize posthog webview (#3381)
* conditionally initialize posthog client webview

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-08 12:08:40 -05:00
Dennis Bartlett a9d5411bf0 Revert "Update deployer team name (#3377)" 2025-05-08 06:57:18 -05:00
Dennis Bartlett 16af9125ec Update variable name (#3384) 2025-05-08 06:51:55 -05:00
Ara 2792e7698f Raise Errors when users try to upload images larger than 7500x7500 pixels (#3336)
* Adding iamge dimension check

* Adding iamge dimension check

* Adding iamge dimension check

* Adding iamge dimension check

* Adding iamge dimension check

* Adding iamge dimension check
2025-05-07 23:48:04 -07:00
d02e5a89e5 fix excessive markdown format character escaping (#3355)
* fix excessive markdown format character escaping

* add changeset

* made it a little more robust

---------

Co-authored-by: Wesley Smith <wes@neofactory.ai>
Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-07 22:31:31 -07:00
Dennis Bartlett 20f19917d3 Add org to team affiliation check (#3380) 2025-05-08 00:01:42 -05:00
EvanandElephant Lumps 7e5cd52864 Always allow textarea typing (#3356)
* enable text area while cline is doing stuff

* changeset

* add sendingDisabled to dependency array

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-07 22:01:07 -07:00
pashpashpashandCline Evaluation 4622ad767b Copy buttons (#3373)
* copy button in task header

* changeset

* added copy buttons to assistant messages that show up on hover

* added aria

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-07 21:59:42 -07:00
Dennis Bartlett 96048d5ac5 Update deployer team name (#3377)
* Update deployer team name

* Create clever-balloons-wave.md
2025-05-07 23:37:33 -05:00
Ara facec93082 Adding Mistral 3 medium model (#3366)
* Fixing Gemini and vertex cache pricing

* Fixing Gemini and vertex cache pricing
2025-05-08 05:46:09 +05:30
Saoud Rizwan c040be9eb1 Disables autocaptures when initializing feature flags 2025-05-07 15:55:33 -05:00
EvanandElephant Lumps 8d3cf53289 Docs: image links (#3350)
* add cdn image links

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-07 13:41:42 -05:00
Trevor Hudson 83c4a82e6d Diable auto track (#3364)
* disable autocatpure

* changeset
2025-05-07 11:32:33 -07:00
Toshii 6ad11badf3 Systematic selection of gemini models w/ caching (#3343)
* no more updating gemini models

* changeset
2025-05-07 10:07:45 -07:00
Tomás Barreiro 39c7da301c fix path tests on windows (#3276) 2025-05-07 22:22:58 +05:30
DrobConsulting 5275f2eabc Updated OpenAiHandler to support Azure GCC region (#3235)
- Added a check for azureApiVersion to determine if the endpoint is an Azure endpoint.
    - Included conditions to check for 'azure.com' and 'azure.us' in the openAiBaseUrl.
    - Ensured that the openAiModelId does not include 'deepseek' when determining the Azure endpoint.
2025-05-07 01:06:12 -07:00
Caleb EomandCline Evaluation 7cf68ff279 Improve time display and filter out resume_task in Task Timeline (#3333)
* Improve time display and filter out resume_task in Task Timeline

* changeset

* polishing it up a little

* a little bigger

* more tooltips + task header

* further refinement

* spacing

* moving delete button up one row conditionally

* removed log

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-07 02:02:41 -05:00
EvanandElephant Lumps b8af02ebaa Stop doomscrolling (#3354)
* disable auto scroll on user scroll up

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-07 01:29:57 -05:00
147 changed files with 5980 additions and 2163 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add the o4-mini model in the isOminiModel
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Allow option to collect events to send them in a bundle to avoid sending too many events
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
add open ai cache to ui
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Moved rule file conversions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
suggested model exists again
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
getRelativePaths protobus migration
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
getOllamaModels protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
refreshRequestyModels protobus migration
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add FeatureFlagProvider service for the Node.js extension side
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add confirmation dialog to Delete All History button
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
breakpoint just in system prompt for gemini for OR and cline provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Remove vite files and make enable all the first item
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
fetch cache details from generation endpoint
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Bump ollama from 0.5.13 to 0.5.15
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add npm script for issue creation
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Migrate more info section to new docs
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Extend ReasoningEffort to non-o3-mini reasoning models for all providers
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Re-enable tests in workflow
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Adds switch to enabler/disable telemtry categories
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Finishing the migration of Vscode Advanced settings to Settings Webview
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add detailed configuration options for LiteLLM provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Allow the user to form their next message while Cline is taking action
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
updated gemini caching for OR and cline provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
searchFiles protobus migration
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add ability to generate commit message via cline
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
getLmStudioModels protobus migration
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
adding quote reply support
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add enterprise section to new docs
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Make Previous Updates in the Announcement a dropdown
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Migrate prompting section to new docs
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Releasing memory after every diff edit to help fix grey screen webview crashes
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Migrate running models locally section to new docs
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Migrate MCP section to new docs
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Migrate custom model config section to new docs
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
add ui for windsurf and cursor rules
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Batch selection and deletion of tasks in history
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Migrate getting-started section to new docs
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Task Timeline
@@ -33,6 +33,7 @@ jobs:
uses: morfien101/actions-authorized-user@4a3cfbf0bcb3cafe4a71710a278920c5d94bb38b
with:
username: ${{ github.actor }}
org: ${{ github.repository_owner }}
team: "deployer"
github_token: ${{ secrets.GITHUB_TOKEN }}
+44
View File
@@ -1,5 +1,49 @@
# Changelog
## [3.15.1]
- Fix bug where PowerShell commands weren't given enough time before giving up and showing an error
## [3.15.0]
- Add Task Timeline visualization to tasks (Thanks eomcaleb!)
- Add cache to ui for OpenAi provider
- Add FeatureFlagProvider service for the Node.js extension side
- Add copy buttons to task header and assistant messages
- Add a more simplified home header was added
- Add ability to favorite a task, allowing it to be kept when clearing all tasks
- Add npm script for issue creation (Thanks DaveFres!)
- Add confirmation dialog to Delete All History button
- Add ability to allow the user to type their next message into the chat while Cline is taking action
- Add ability to generate commit message via cline (Thanks zapp88!)
- Add improvements to caching for gemini models on OpenRouter and Cline providers
- Add improvements to allow scrolling the file being edited.
- Add ui for windsurf and cursor rules
- Add mistral medium-3 model
- Add option to collect events to send them in a bundle to avoid sending too many events
- Add support to quote a previous message in chat
- Add support for Gemini Implicit Caching
- Add support for batch selection and deletion of tasks in history (Thanks danix800!)
- Update change suggested models
- Update fetch cache details from generation endpoint
- Update converted docs to Mintlify
- Update the isOminiModel to include o4-mini model (Thanks PeterDaveHello!)
- Update file size that can be read by Cline, allowing larger files
- Update defaults for bedrock API models (Thanks Watany!)
- Update to extend ReasoningEffort to non-o3-mini reasoning models for all providers (Thanks PeterDaveHello!)
- Update to give error when a user tries to upload an image larger than 7500x7500 pixels
- Update announcement so that previous updates are in a dropdown
- Update UI for auto approve with favorited settings
- Fix bug where certain terminal commands would lock you out of a task
- Fix only initialize posthog in the webview if the user has opted into telemetry
- Fix bug where autocapture was on for front-end telemetry
- Fix for markdown copy excessively escaping characters (Thanks weshoke!)
- Fix an issue where loading never finished when using an application inference profile for the model ID (Thanks WinterYukky!)
## [3.14.1]
- Disables autocaptures when initializing feature flags
## [3.14.0]
- Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile (Thanks @clicube!)
@@ -36,7 +36,7 @@ Cline offers the option of utilizing AWS credentials or AWS profiles to access A
<Frame>
<img
src="/assets/robot_panel_dark.png"
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-aws-setup-markup%20(1).png"
alt="AWS Bedrock configuration in Cline settings showing profile authentication setup"
/>
</Frame>
+4 -1
View File
@@ -76,7 +76,10 @@ Vertex AI supports eight regions. Select a region that meets your latency, compl
- Search for **Cline** and install the extension
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Cline extension in VS Code" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-extension-arrow.png"
alt="Cline extension in VS Code"
/>
</Frame>
#### 3.2 Configure Cline Settings
+1
View File
@@ -62,6 +62,7 @@
"getting-started/installing-dev-essentials",
"getting-started/model-selection-guide",
"getting-started/our-favorite-tech-stack",
"getting-started/task-management",
"getting-started/understanding-context-management",
"getting-started/what-is-cline"
]
@@ -6,7 +6,7 @@ Cline supports major cloud providers like AWS Bedrock and Google's Cloud Vertex;
For the purpose of this document, we assume your organization will use cloud-based frontier models. Cloud inference providers offer cutting-edge capabilities and the flexibility to select models which best suit your needs.
Certain scenarios may warrant using local models, including handling highly sensitive data, applications requiring consistent low-latency responses, or compliance with strict data sovereignty requirements. If your team needs to utilize local models, see [Running Local Models ](/running-local-models/read-me-first.mdx)with Cline.
Certain scenarios may warrant using local models, including handling highly sensitive data, applications requiring consistent low-latency responses, or compliance with strict data sovereignty requirements. If your team needs to utilize local models, see [Running Local Models ](/running-models-locally/read-me-first.mdx)with Cline.
---
@@ -14,9 +14,9 @@ Certain scenarios may warrant using local models, including handling highly sens
#### [IAM Security Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) (For administrators)
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/custom-model-configs/aws-bedrock.mdx)
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/custom-model-configs/aws-bedrock-with-credentials-authentication.mdx)
#### [AWS Bedrock setup for SSO token (AWS Profile)](/custom-model-configs/aws-bedrock-w-profile-authentication.mdx)
#### [AWS Bedrock setup for SSO token (AWS Profile)](/custom-model-configs/aws-bedrock-with-profile-authentication.mdx)
#### VPC Endpoint Setup
@@ -28,12 +28,12 @@ To protect your team's data, Cline supports VPC (Virtual Private Cloud) endpoint
2. Follow the directions for [creating a VPC endpoint](https://docs.aws.amazon.com/vpc/latest/privatelink/create-interface-endpoint.html#create-interface-endpoint-aws) in the AWS console. The image below pertains to steps 4 and 5 of the AWS guide linked above.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="VPC Console" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/vpc-console.png" alt="VPC Console" />
</Frame>
3. Note the IP address of your VPC endpoint, open Cline's settings menu, and select `AWS Bedrock`from the API Provider dropdown.
4. Click the `Use Custom VPC endpoint`checkbox and enter the IP address of your VPC endpoint
<Frame>
<img src="/assets/robot_panel_dark.png" alt="VPC Settings Menu" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/vpc-settings-menu.png" alt="VPC Settings Menu" />
</Frame>
@@ -13,7 +13,10 @@ title: "Security Concerns"
Cline operates exclusively as a client-side VSCode extension with zero server-side components. This fundamental design choice ensures that your code and data remain within your secure environment at all times. Unlike traditional AI assistants that send data to external servers for processing, Cline connects directly to your chosen cloud provider's AI endpoints, keeping all sensitive information within your infrastructure boundaries.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Cline's relationship to local and remote assets" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-arch.png"
alt="Cline's relationship to local and remote assets"
/>
</Frame>
### Data Privacy Commitment
+9 -3
View File
@@ -28,7 +28,10 @@ After each tool use, you can:
2. Click the "Restore" button to open restore options
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Checkpoint comparison and restore options" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(13).png"
alt="Checkpoint comparison and restore options"
/>
</Frame>
#### Rolling Back
@@ -60,7 +63,7 @@ Checkpoints let you be more experimental with Cline. While human coding is often
- Ideal for exploring different design patterns or architectural approaches
<Frame caption="In this case, I didn't like the changes Cline made to my robot dog-walking website (still working on the robots) and I wanted to revert both the codebase and the task to before any changes were made so I could start fresh.">
<img src="/assets/robot_panel_dark.png" alt="Checkpoint restore demo" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/checkpointsDemo.gif" alt="Checkpoint restore demo" />
</Frame>
### ✨ Best Practices
@@ -104,5 +107,8 @@ Perhaps you didn't get the results you wanted, thought of a better way to phrase
- Shift + Enter: Insert new line / line break
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Message editing interface" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/message-editing.png"
alt="Message editing interface"
/>
</Frame>
@@ -7,7 +7,10 @@ title: "Plan & Act Modes: A Guide to Effective AI Development"
Plan & Act modes represent Cline's approach to structured AI development, emphasizing thoughtful planning before implementation. This dual-mode system helps developers create more maintainable, accurate code while reducing iteration time.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Use Plan to gather context before using Act to implement the plan" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/planningThenActing%20(1).gif"
alt="Use Plan to gather context before using Act to implement the plan"
/>
</Frame>
### Understanding the Modes
@@ -27,7 +30,7 @@ Plan & Act modes represent Cline's approach to structured AI development, emphas
- Can execute changes to your codebase
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Act mode capabilities" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(5).png" alt="Act mode capabilities" />
</Frame>
### Workflow Guide
@@ -39,7 +42,7 @@ Begin every significant development task in Plan mode:
In this mode:
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Plan mode workflow" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(5)%20(1).png" alt="Plan mode workflow" />
</Frame>
- Share your requirements
@@ -48,7 +51,10 @@ In this mode:
- Develop implementation strategy
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Planning phase" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(2)%20(1)%20(1)%20(1).png"
alt="Planning phase"
/>
</Frame>
#### 2. Switch to Act Mode
@@ -56,7 +62,7 @@ In this mode:
Once you have a clear plan, switch to Act mode:
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Switching to Act mode" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/switching-to-act.gif" alt="Switching to Act mode" />
</Frame>
Act mode allows Cline to:
@@ -90,7 +96,10 @@ Complex projects often require multiple plan-act cycles:
4. Document significant decisions
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Implementation best practices" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(3)%20(1).png"
alt="Implementation best practices"
/>
</Frame>
### Power User Tips
@@ -119,7 +128,7 @@ Complex projects often require multiple plan-act cycles:
- Executing test cases
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Mode usage patterns" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(6).png" alt="Mode usage patterns" />
</Frame>
### Contributing
+4 -1
View File
@@ -22,7 +22,10 @@ Follow these steps to get Cline up and running:
4. **Search for 'Cline':** In the Extensions search bar, type `Cline`.
<Frame caption="VS Code marketplace with Cline extension ready to install">
<img src="/assets/robot_panel_dark.png" alt="VS Code marketplace showing Cline extension" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(20).png"
alt="VS Code marketplace showing Cline extension"
/>
</Frame>
1. **Install the Extension:** Click the "Install" button next to the Cline extension.
@@ -28,7 +28,10 @@ Cline helps you manage this limitation with its Context Window Progress Bar, whi
- The total capacity for your chosen model
<Frame caption="Visual representation of the context window usage in Cline">
<img src="/assets/robot_panel_light.png" alt="Context window progress bar example" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(11).png"
alt="Context window progress bar example"
/>
</Frame>
This visibility helps you work more effectively with Cline by letting you know when you might need to start fresh or break tasks into smaller chunks.
+67
View File
@@ -0,0 +1,67 @@
---
title: "Task Management in Cline"
description: "Learn how to effectively manage your task history, use favorites, and organize your work in Cline."
---
# Task Management
As you use Cline, you'll accumulate many tasks over time. The task management system helps you organize, filter, search, and clean up your task history to keep your workspace efficient.
## Accessing Task History
You can access your task history by:
1. Clicking on the "History" button in the Cline sidebar
2. Using the command palette to search for "Cline: Show Task History"
## Task History Features
The task history view provides several powerful features:
### Searching and Filtering
- **Search Bar**: Use the fuzzy search at the top to quickly find tasks by content
- **Sort Options**: Sort tasks by:
- Newest (default)
- Oldest
- Most Expensive (highest API cost)
- Most Tokens (highest token usage)
- Most Relevant (when searching)
- **Favorites Filter**: Toggle to show only favorited tasks
### Task Actions
Each task in the history view has several actions available:
- **Open**: Click on a task to reopen it in the Cline chat
- **Favorite**: Click the star icon to mark a task as a favorite
- **Delete**: Remove individual tasks (favorites are protected from deletion)
- **Export**: Export a task's conversation to markdown
## ⭐ Task Favorites
The favorites feature allows you to mark important tasks that you want to preserve and find quickly.
### How Favorites Work
- **Marking Favorites**: Click the star icon next to any task to toggle its favorite status
- **Protection**: Favorited tasks are protected from individual and bulk deletion operations (can be overridden)
- **Filtering**: Use the favorites filter to quickly access your important tasks
## Batch Operations
The task history view supports several batch operations:
- **Select Multiple**: Use the checkboxes to select multiple tasks
- **Select All/None**: Quickly select or deselect all tasks
- **Delete Selected**: Remove all selected tasks
- **Delete All**: Remove all tasks from history (favorites are preserved unless you choose to include them)
## Best Practices
1. **Favorite Important Tasks**: Mark reference tasks or frequently accessed conversations as favorites
2. **Regular Cleanup**: Periodically remove old or unused tasks to improve performance
3. **Use Search**: Leverage the fuzzy search to quickly find specific conversations
4. **Export Valuable Tasks**: Export important tasks to markdown for external reference
Task management helps you maintain an organized workflow when using Cline, allowing you to quickly find past conversations, preserve important work, and keep your history clean and efficient.
@@ -14,7 +14,7 @@ description: "Context is key to getting the most out of Cline"
<Frame caption="In a world of infinite context, the context window is what Cline currently has available">
<img
src="/assets/robot_panel_dark.png"
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(2).png"
alt="In a world of infinite context, the context window is what Cline currently has available"
/>
</Frame>
@@ -65,7 +65,10 @@ Think of context like a whiteboard you and Cline share:
Cline provides a visual way to monitor your context window usage through a progress bar:
<Frame caption="Visual representation of the context window usage">
<img src="/assets/robot_panel_light.png" alt="Context window progress bar" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1)%20(1).png"
alt="Context window progress bar"
/>
</Frame>
### Reading the Bar
+8 -6
View File
@@ -11,7 +11,10 @@ Utilizing MCP servers will increase your token usage. Cline offers the ability t
3. Cline will open a new settings window. find `Cline>Mcp:Mode` and make your selection from the dropdown menu.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="MCP settings edit" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/MCP-settings-edit%20(1).png"
alt="MCP settings edit"
/>
</Frame>
## Managing Individual MCP Servers
@@ -22,7 +25,10 @@ Each MCP server has its own configuration panel where you can modify settings, m
2. Locate the MCP server you want to manage in the list, and open it by clicking on its name.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="MCP settings individual" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/MCP-settings-individual.png"
alt="MCP settings individual"
/>
</Frame>
### Deleting a Server
@@ -45,10 +51,6 @@ To set the maximum time to wait for a response after a tool call to the MCP serv
1. Click the `Network Timeout` dropdown at the bottom of the individual MCP server's config box and change the time. Default is 1 minute but it can be set between 30 seconds and 1 hour.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Network Timeout pulldown" />
</Frame>
## Editing MCP Settings Files
Settings for all installed MCP servers are located in the `cline_mcp_settings.json` file:
+1 -1
View File
@@ -14,7 +14,7 @@ Model Context Protocol is an open protocol that standardizes how applications pr
<Frame>
<img
src="/assets/robot_panel_dark.png"
src="https://storage.googleapis.com/cline_public_images/docs/assets/mcp-diagram.png"
alt="MCP diagram showing how MCP servers connect LLMs to external tools and data sources"
/>
</Frame>
+8 -2
View File
@@ -242,7 +242,10 @@ Let's walk through the development process of our AlphaAdvantage MCP server, whi
### Planning Phase
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Planning phase demonstration" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/planning-phase.gif"
alt="Planning phase demonstration"
/>
</Frame>
During the planning phase, we:
@@ -267,7 +270,10 @@ During the planning phase, we:
### Implementation
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Building MCP plugin demonstration" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/building-mcp-plugin.gif"
alt="Building MCP plugin demonstration"
/>
</Frame>
We began by bootstrapping the project:
+5 -5
View File
@@ -13,7 +13,7 @@ To get started with Cline Memory Bank:
3. **Paste into Cline** - Add as custom instructions or in a .clinerules file
4. **Initialize** - Ask Cline to "initialize memory bank"
[See detailed setup instructions](cline-memory-bank.md#getting-started-with-memory-bank)
[See detailed setup instructions](#getting-started-with-memory-bank)
### Cline Memory Bank Custom Instructions \[COPY THIS]
@@ -152,7 +152,7 @@ The Memory Bank is a structured documentation system that allows Cline to mainta
The Memory Bank isn't a Cline-specific feature - it's a methodology for managing AI context through structured documentation. When you instruct Cline to "follow custom instructions," it reads the Memory Bank files to rebuild its understanding of your project.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Workflow" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(15).png" alt="Memory Bank Workflow" />
</Frame>
#### Understanding the Files
@@ -162,7 +162,7 @@ Memory Bank files are simply markdown files you create in your project. They're
Files are organized in a hierarchical structure that builds up a complete picture of your project:
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Memory Bank File Structure" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(16).png" alt="Memory Bank File Structure" />
</Frame>
### Memory Bank Files Explained
@@ -223,7 +223,7 @@ Create additional files when needed to organize:
3. Ask Cline to "initialize memory bank"
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Setup" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(17).png" alt="Memory Bank Setup" />
</Frame>
#### Project Brief Tips
@@ -287,7 +287,7 @@ As you work with Cline, your context window will eventually fill up (note the pr
This workflow ensures that important context is preserved in your Memory Bank files before the context window is cleared, allowing you to continue seamlessly in a fresh conversation.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Context Window" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(18).png" alt="Memory Bank Context Window" />
</Frame>
#### How often should I update the memory bank?
+2 -2
View File
@@ -22,7 +22,7 @@ To add custom instructions:
4. Paste your instructions
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Cline Logo" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
</Frame>
Custom instructions are powerful for:
@@ -207,7 +207,7 @@ Located conveniently under the chat input field, this popover allows you to:
This UI significantly simplifies switching contexts and managing different sets of instructions without needing to manually edit files or configurations during a conversation.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Cline Logo" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
</Frame>
## .clineignore File Guide
+17 -5
View File
@@ -20,7 +20,7 @@ Run AI models locally using LM Studio with Cline.
- Download and install for your operating system
<Frame>
<img src="/assets/robot_panel_dark.png" alt="LM Studio download page" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(7).png" alt="LM Studio download page" />
</Frame>
#### 2. Launch LM Studio
@@ -29,7 +29,10 @@ Run AI models locally using LM Studio with Cline.
- You'll see four tabs on the left: **Chat**, **Developer** (where you will start the server), **My Models** (where your downloaded models are stored), **Discover** (add new models)
<Frame>
<img src="/assets/robot_panel_dark.png" alt="LM Studio interface overview" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(10).png"
alt="LM Studio interface overview"
/>
</Frame>
#### 3. Download a Model
@@ -39,7 +42,10 @@ Run AI models locally using LM Studio with Cline.
- Wait for download to complete
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Downloading a model in LM Studio" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/lm-studio-download-model.gif"
alt="Downloading a model in LM Studio"
/>
</Frame>
#### 4. Start the Server
@@ -49,7 +55,10 @@ Run AI models locally using LM Studio with Cline.
- Note: The server will run at `http://localhost:1234`
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Starting the LM Studio server" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/lm-studio-starting-server.gif"
alt="Starting the LM Studio server"
/>
</Frame>
#### 5. Configure Cline
@@ -60,7 +69,10 @@ Run AI models locally using LM Studio with Cline.
4. Select your model from the available options
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Configuring Cline with LM Studio" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/lm-studio-select-model-cline.gif"
alt="Configuring Cline with LM Studio"
/>
</Frame>
### ⚠️ Important Notes
+16 -4
View File
@@ -16,7 +16,10 @@ description: "A quick guide to setting up Ollama for local AI model execution wi
- Download and install for your operating system
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Ollama download page" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(2)%20(1)%20(1).png"
alt="Ollama download page"
/>
</Frame>
#### 2. Choose and Download a Model
@@ -29,7 +32,10 @@ description: "A quick guide to setting up Ollama for local AI model execution wi
```
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Selecting a model in Ollama" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/ollama-model-grab%20(2).gif"
alt="Selecting a model in Ollama"
/>
</Frame>
- Open your Terminal and run the command:
@@ -41,7 +47,10 @@ description: "A quick guide to setting up Ollama for local AI model execution wi
```
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Running Ollama in terminal" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/starting-ollama-terminal%20(2).gif"
alt="Running Ollama in terminal"
/>
</Frame>
**✨ Your model is now ready to use within Cline!**
@@ -56,7 +65,10 @@ description: "A quick guide to setting up Ollama for local AI model execution wi
- Select the model from your available options
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Configuring Cline with Ollama" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/selecting-ollama-model-cline%20(3).gif"
alt="Configuring Cline with Ollama"
/>
</Frame>
### ⚠️ Important Notes
@@ -20,7 +20,10 @@ Local models are created by training a smaller model to imitate a larger one, bu
Think of it like running your development environment on a calculator instead of a computer it might handle basic tasks, but complex operations become unreliable or impossible.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Local model comparison diagram" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(4).png"
alt="Local model comparison diagram"
/>
</Frame>
### What Actually Happens
+20 -35
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.14.0",
"version": "3.15.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.14.0",
"version": "3.15.1",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
@@ -42,11 +42,11 @@
"globby": "^14.0.2",
"iconv-lite": "^0.6.3",
"ignore": "^7.0.3",
"image-size": "^2.0.2",
"isbinaryfile": "^5.0.2",
"jschardet": "^3.1.4",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"node-cache": "^5.1.2",
"ollama": "^0.5.13",
"open-graph-scraper": "^6.9.0",
"openai": "^4.83.0",
@@ -12866,14 +12866,6 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/clone-deep": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
@@ -16579,6 +16571,18 @@
"node": ">= 4"
}
},
"node_modules/image-size": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz",
"integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==",
"license": "MIT",
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=16.x"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@@ -19760,17 +19764,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/node-cache": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
"integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==",
"dependencies": {
"clone": "2.x"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -34886,11 +34879,6 @@
}
}
},
"clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="
},
"clone-deep": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
@@ -37429,6 +37417,11 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz",
"integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="
},
"image-size": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz",
"integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="
},
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@@ -39565,14 +39558,6 @@
"@types/nlcst": "^2.0.0"
}
},
"node-cache": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
"integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==",
"requires": {
"clone": "2.x"
}
},
"node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+8 -76
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.14.0",
"version": "3.15.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -121,7 +121,7 @@
},
{
"command": "cline.generateGitCommitMessage",
"title": "Generate Commit Message with Cline",
"title": "Generate Commit Message with Cline",
"category": "Cline",
"icon": "$(robot)"
}
@@ -234,77 +234,7 @@
},
"configuration": {
"title": "Cline",
"properties": {
"cline.vsCodeLmModelSelector": {
"type": "object",
"properties": {
"vendor": {
"type": "string",
"description": "The vendor of the language model (e.g. copilot)"
},
"family": {
"type": "string",
"description": "The family of the language model (e.g. gpt-4)"
}
},
"description": "Settings for VSCode Language Model API"
},
"cline.enableCheckpoints": {
"type": "boolean",
"default": true,
"description": "Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which may not work well with large workspaces."
},
"cline.disableBrowserTool": {
"type": "boolean",
"default": false,
"description": "Disables extension from spawning browser session."
},
"cline.modelSettings.o3Mini.reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high"
],
"default": "medium",
"description": "Controls the reasoning effort when using an OpenAI reasoning model. Higher values may result in more thorough but slower responses."
},
"cline.chromeExecutablePath": {
"type": "string",
"default": null,
"description": "Path to Chrome executable for browser use functionality. If not set, the extension will attempt to find or download it automatically."
},
"cline.preferredLanguage": {
"type": "string",
"enum": [
"English",
"Arabic - العربية",
"Portuguese - Português (Brasil)",
"Czech - Čeština",
"French - Français",
"German - Deutsch",
"Hindi - हिन्दी",
"Hungarian - Magyar",
"Italian - Italiano",
"Japanese - 日本語",
"Korean - 한국어",
"Polish - Polski",
"Portuguese - Português (Portugal)",
"Russian - Русский",
"Simplified Chinese - 简体中文",
"Spanish - Español",
"Traditional Chinese - 繁體中文",
"Turkish - Türkçe"
],
"default": "English",
"description": "The language that Cline should use for communication."
},
"cline.mcpMarketplace.enabled": {
"type": "boolean",
"default": true,
"description": "Controls whether the MCP Marketplace is enabled."
}
}
"properties": {}
}
},
"scripts": {
@@ -314,7 +244,7 @@
"watch:esbuild": "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",
"protos": "node proto/build-proto.js && prettier src/shared/proto --write && prettier src/core/controller --write",
"protos": "node proto/build-proto.js && prettier src/shared/proto src/core/controller webview-ui/src/services --write",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
@@ -336,7 +266,9 @@
"prepare": "husky",
"changeset": "changeset",
"version-packages": "changeset version",
"docs:preview": "cd docs && mintlify dev",
"docs": "cd docs && mintlify dev",
"docs:check-links": "cd docs && mintlify broken-links",
"docs:rename-file": "cd docs && mintlify rename",
"report-issue": "node scripts/report-issue.js"
},
"devDependencies": {
@@ -409,11 +341,11 @@
"globby": "^14.0.2",
"iconv-lite": "^0.6.3",
"ignore": "^7.0.3",
"image-size": "^2.0.2",
"isbinaryfile": "^5.0.2",
"jschardet": "^3.1.4",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"node-cache": "^5.1.2",
"ollama": "^0.5.13",
"open-graph-scraper": "^6.9.0",
"openai": "^4.83.0",
+4
View File
@@ -40,6 +40,8 @@ message BrowserSettings {
Viewport viewport = 1;
optional string remote_browser_host = 2;
optional bool remote_browser_enabled = 3;
optional string chrome_executable_path = 4;
optional bool disable_tool_use = 5;
}
message UpdateBrowserSettingsRequest {
@@ -47,4 +49,6 @@ message UpdateBrowserSettingsRequest {
Viewport viewport = 2;
optional string remote_browser_host = 3;
optional bool remote_browser_enabled = 4;
optional string chrome_executable_path = 5;
optional bool disable_tool_use = 6;
}
+46 -1
View File
@@ -10,7 +10,52 @@ import "common.proto";
service ModelsService {
// Fetches available models from Ollama
rpc getOllamaModels(StringRequest) returns (StringArray);
// Fetches available models from LM Studio
rpc getLmStudioModels(StringRequest) returns (StringArray);
// Fetches available models from VS Code LM API
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
// Refreshes and returns OpenRouter models
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns OpenAI models
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
// Refreshes and returns Requesty models
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
message VsCodeLmModelsArray {
repeated VsCodeLmModel models = 1;
}
// Structure representing a VS Code LM model
message VsCodeLmModel {
string vendor = 1;
string family = 2;
string version = 3;
string id = 4;
}
// For OpenRouterCompatibleModelInfo structure in OpenRouterModels
message OpenRouterModelInfo {
int32 max_tokens = 1;
int32 context_window = 2;
bool supports_images = 3;
bool supports_prompt_cache = 4;
double input_price = 5;
double output_price = 6;
double cache_writes_price = 7;
double cache_reads_price = 8;
string description = 9;
}
// Shared response message for model information
message OpenRouterCompatibleModelInfo {
map<string, OpenRouterModelInfo> models = 1;
}
// Request for fetching OpenAI models
message OpenAiModelsRequest {
Metadata metadata = 1;
string baseUrl = 2;
string apiKey = 3;
}
+61 -1
View File
@@ -16,9 +16,15 @@ service TaskService {
// Creates a new task with the given text and optional images
rpc newTask(NewTaskRequest) returns (Empty);
// Shows a task with the specified ID
rpc showTaskWithId(StringRequest) returns (Empty);
rpc showTaskWithId(StringRequest) returns (TaskResponse);
// Exports a task with the given ID to markdown
rpc exportTaskWithId(StringRequest) returns (Empty);
// Toggles the favorite status of a task
rpc toggleTaskFavorite(TaskFavoriteRequest) returns (Empty);
// Deletes all non-favorited tasks
rpc deleteNonFavoritedTasks(EmptyRequest) returns (DeleteNonFavoritedTasksResults);
// Gets filtered task history
rpc getTaskHistory(GetTaskHistoryRequest) returns (TaskHistoryArray);
}
// Request message for creating a new task
@@ -28,3 +34,57 @@ message NewTaskRequest {
repeated string images = 3;
}
// Request message for toggling task favorite status
message TaskFavoriteRequest {
Metadata metadata = 1;
string task_id = 2;
bool is_favorited = 3;
}
// Response for task details
message TaskResponse {
string id = 1;
string task = 2;
int64 ts = 3;
bool is_favorited = 4;
int64 size = 5;
double total_cost = 6;
int32 tokens_in = 7;
int32 tokens_out = 8;
int32 cache_writes = 9;
int32 cache_reads = 10;
}
// Results returned when deleting non-favorited tasks
message DeleteNonFavoritedTasksResults {
int32 tasks_preserved = 1;
int32 tasks_deleted = 2;
}
// Request for getting task history with filtering
message GetTaskHistoryRequest {
Metadata metadata = 1;
bool favorites_only = 2;
string search_query = 3;
string sort_by = 4;
}
// Response for task history
message TaskHistoryArray {
repeated TaskItem tasks = 1;
int32 total_count = 2;
}
// Task item details for history list
message TaskItem {
string id = 1;
string task = 2;
int64 ts = 3;
bool is_favorited = 4;
int64 size = 5;
double total_cost = 6;
int32 tokens_in = 7;
int32 tokens_out = 8;
int32 cache_writes = 9;
int32 cache_reads = 10;
}
+6 -2
View File
@@ -272,10 +272,14 @@ export class AwsBedrockHandler implements ApiHandler {
}
/**
* Gets the appropriate model ID, accounting for cross-region inference if enabled
* Gets the appropriate model ID, accounting for cross-region inference if enabled.
* If the model ID is an ARN that contains a slash, you will get the URL encoded ARN.
*/
async getModelId(): Promise<string> {
if (this.options.awsUseCrossRegionInference) {
if (this.options.awsBedrockCustomSelected && this.getModel().id.includes("/")) {
return encodeURIComponent(this.getModel().id)
}
if (!this.options.awsBedrockCustomSelected && this.options.awsUseCrossRegionInference) {
const regionPrefix = this.getRegion().slice(0, 3)
switch (regionPrefix) {
case "us-":
+1 -1
View File
@@ -33,7 +33,7 @@ export class ClineHandler implements ApiHandler {
systemPrompt,
messages,
this.getModel(),
this.options.o3MiniReasoningEffort,
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
)
+6 -308
View File
@@ -1,7 +1,6 @@
import type { Anthropic } from "@anthropic-ai/sdk"
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
import { GoogleGenAI, type Content, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
import NodeCache from "node-cache"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "@shared/api"
@@ -39,12 +38,6 @@ export class GeminiHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: GoogleGenAI
// Enhanced caching system
private contentCaches: NodeCache // Stores cache details (key, count, etc.)
private isCacheBusy = false
private taskCacheNames: Map<string, string> = new Map() // Maps taskId to cache name for stable lookup
private taskCacheTokens: Map<string, number> = new Map() // Maps taskId to total tokens in cache
constructor(options: GeminiHandlerOptions) {
// Store the options
this.options = options
@@ -67,26 +60,13 @@ export class GeminiHandler implements ApiHandler {
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
}
// Initialize cache with TTL and check period
this.contentCaches = new NodeCache({
stdTTL: DEFAULT_CACHE_TTL_SECONDS,
checkperiod: DEFAULT_CACHE_TTL_SECONDS,
})
}
/**
* Creates a message using the Gemini API with optimized caching and split cost accounting.
*
* This method implements a task-based caching strategy:
* 1. Each task gets its own cache, identified by taskId
* 2. On first call for a task, a new cache is created
* 3. On subsequent calls, the existing cache is reused and only new messages are sent
* 4. Cache operations are tracked for accurate cost accounting
* Creates a message using the Gemini API with implicit caching.
*
* Cost accounting:
* - Immediate costs (returned in the usage object): Input tokens, output tokens, cache read costs
* - Ongoing costs (tracked at task level): Cache storage costs for the TTL period
*
* @param systemPrompt The system prompt to use for the message
* @param messages The conversation history to include in the message
@@ -97,54 +77,6 @@ export class GeminiHandler implements ApiHandler {
const { id: model, info } = this.getModel()
const contents = messages.map(convertAnthropicMessageToGemini)
// Ensure we have a stable cache key (taskId)
if (!this.options.taskId) {
console.warn("[GeminiHandler] No taskId provided, caching will be disabled")
}
const taskId = this.options.taskId
// Calculate total content length for cache eligibility check
const contentsLength = systemPrompt.length + this.getMessagesLength(contents)
// Minimum token threshold for caching (approx 4096 tokens)
const CONTEXT_CACHE_TOKEN_MINIMUM = 4096
let uncachedContent: Content[] | undefined = undefined
let cachedContent: string | undefined = undefined
// Check if caching is available and content is large enough to benefit from caching
// We only enable caching for conversations above a certain size to avoid overhead for small requests
const isCacheAvailable = info.supportsPromptCache && contentsLength > 4 * CONTEXT_CACHE_TOKEN_MINIMUM && taskId
// This flag tracks whether this operation involves a cache write/update
// It's used to track task-level ongoing costs, not immediate costs
let cacheWrite = false
if (isCacheAvailable) {
// Check if we already have a cache for this task
const existingCacheName = this.taskCacheNames.get(taskId)
const cacheEntry = existingCacheName ? this.contentCaches.get<{ key: string; count: number }>(taskId) : undefined
if (cacheEntry) {
// Use existing cache
uncachedContent = contents.slice(cacheEntry.count, contents.length)
cachedContent = cacheEntry.key
console.log(
`[GeminiHandler] using existing cache for task ${taskId}: ${cacheEntry.count} cached messages (${cacheEntry.key}) and ${uncachedContent.length} uncached messages`,
)
}
// Create or update cache only if there's new content to add
const shouldUpdateCache = !existingCacheName || (cacheEntry && uncachedContent && uncachedContent.length > 0)
if (shouldUpdateCache) {
// If we should update the cache, then there will be a cache write
cacheWrite = true
}
}
const isCacheUsed = !!cachedContent
// Configure thinking budget if supported
const thinkingBudget = this.options.thinkingBudgetTokens ?? 0
const maxBudget = info.thinkingConfig?.maxBudget ?? 0
@@ -153,10 +85,7 @@ export class GeminiHandler implements ApiHandler {
const requestConfig: GenerateContentConfig = {
// Add base URL if configured
httpOptions: this.options.geminiBaseUrl ? { baseUrl: this.options.geminiBaseUrl } : undefined,
// Only include systemInstruction if NOT using the cache
...(isCacheUsed ? {} : { systemInstruction: systemPrompt }),
...{ systemInstruction: systemPrompt },
// Set temperature (default to 0)
temperature: 0,
}
@@ -171,19 +100,12 @@ export class GeminiHandler implements ApiHandler {
// Generate content using the configured parameters
const result = await this.client.models.generateContentStream({
model,
contents: uncachedContent ?? contents,
contents: contents,
config: {
...requestConfig,
...(isCacheUsed ? { cachedContent } : {}),
},
})
// Update the cache after the LLM request is already sent to avoid blocking
// We only update the cache if we have a taskId and the cache write flag is set
// This is a non-blocking operation and will not affect the response time
if (cacheWrite && taskId) {
this.updateCacheContent(taskId, model, contents, systemPrompt)
}
// Track usage metadata
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
@@ -207,7 +129,7 @@ export class GeminiHandler implements ApiHandler {
const outputTokens = lastUsageMetadata.candidatesTokenCount ?? 0
const cacheReadTokens = lastUsageMetadata.cachedContentTokenCount
// Calculate immediate costs only (excluding cache write/storage costs)
// Calculate immediate costs
const totalCost = this.calculateCost({
info,
inputTokens,
@@ -215,214 +137,17 @@ export class GeminiHandler implements ApiHandler {
cacheReadTokens,
})
// Store the token count for task-level ongoing cost tracking
// This is not included in the immediate costs returned to the user
const cacheWriteTokens = cacheWrite ? inputTokens : undefined
// If this is a cache write operation, update the task's ongoing costs
if (cacheWrite && this.options.taskId && inputTokens > 0) {
// Log the ongoing costs for debugging
const ongoingCosts = this.getTaskOngoingCosts(this.options.taskId)
console.log(
`[GeminiHandler] Task ${this.options.taskId} ongoing costs: $${ongoingCosts?.toFixed(6) ?? "unknown"}`,
)
}
yield {
type: "usage",
inputTokens,
outputTokens,
cacheReadTokens,
cacheWriteTokens,
cacheWriteTokens: 0,
totalCost,
}
}
}
/**
* Lists all caches for the current API key.
*
* According to the Gemini API documentation, you can retrieve metadata for all uploaded caches
* using the caches.list() method. This is useful for monitoring cache usage and cleanup.
*
* @param pageSize Optional number of caches to return per page (default: 10)
* @returns A promise that resolves to an array of cache metadata objects
*/
public async listCaches(pageSize: number = 10): Promise<any[]> {
try {
const caches: any[] = []
const pager = await this.client.caches.list({ config: { pageSize } })
let page = pager.page
while (true) {
for (const cache of page) {
caches.push(cache)
}
if (!pager.hasNextPage()) {
break
}
page = await pager.nextPage()
}
return caches
} catch (error) {
console.error(`[GeminiHandler] Failed to list caches:`, error)
return []
}
}
/**
* Updates the content of a cache for a specific task.
*
* Since the Gemini API doesn't support incremental updates to cache content,
* this method:
* 1. Creates a new cache with the full content (old + new)
* 2. Deletes the old cache if it exists
* 3. Updates our local tracking to point to the new cache
*
* @param taskId The ID of the task whose cache should be updated
* @param model The model to use for the cache
* @param contents The full content to cache (including both old and new messages)
* @param systemInstruction The system instruction to include in the cache
*/
private async updateCacheContent(
taskId: string,
model: string,
contents: Content[],
systemInstruction: string,
): Promise<void> {
if (this.isCacheBusy) {
console.log(`[GeminiHandler] Cache is busy, skipping update for task ${taskId}`)
return
}
this.isCacheBusy = true
const timestamp = Date.now()
const existingCacheName = this.taskCacheNames.get(taskId)
try {
// 1. Create a new cache with the full content
const result = await this.client.caches.create({
model,
config: {
contents,
systemInstruction,
ttl: `${DEFAULT_CACHE_TTL_SECONDS}s`,
httpOptions: { timeout: 120_000 },
},
})
const { name, usageMetadata } = result
if (name) {
// 2. Delete the old cache if it exists (non-blocking)
// We don't await this operation to avoid blocking the main flow if deletion fails
if (existingCacheName) {
// Schedule cache deletion in the background
setTimeout(() => {
this.client.caches
.delete({ name: existingCacheName })
.then(() => {
console.log(`[GeminiHandler] Deleted old cache ${existingCacheName} for task ${taskId}`)
})
.catch((error) => {
console.error(`[GeminiHandler] Failed to delete old cache ${existingCacheName}:`, error)
console.log(`[GeminiHandler] Continuing without deleting old cache. It will expire after TTL.`)
})
}, 1000)
}
// 3. Update our local tracking
this.contentCaches.set<{ key: string; count: number }>(taskId, {
key: name,
count: contents.length,
})
this.taskCacheNames.set(taskId, name)
// Track total tokens in cache for ongoing cost calculation
const totalTokens = usageMetadata?.totalTokenCount ?? 0
this.taskCacheTokens.set(taskId, totalTokens)
const operation = existingCacheName ? "Updated" : "Created new"
console.log(
`[GeminiHandler] ${operation} cache for task ${taskId}: ${contents.length} messages (${totalTokens} tokens) in ${Date.now() - timestamp}ms`,
)
return // Indicate that a cache write occurred
}
return
} catch (error) {
console.error(`[GeminiHandler] Failed to update cache for task ${taskId}:`, error)
return
} finally {
this.isCacheBusy = false
}
}
/**
* Updates the TTL of an existing cache.
*
* According to the Gemini API documentation, you can update the TTL of a cache
* using the caches.update() method. This is useful for extending the lifetime
* of a cache that's still being used.
*
* @param taskId The ID of the task whose cache TTL should be updated
* @param ttlSeconds The new TTL in seconds
* @returns A promise that resolves to the updated cache, or undefined if the update fails
*/
public async updateCacheTTL(taskId: string, ttlSeconds: number = DEFAULT_CACHE_TTL_SECONDS): Promise<any> {
const cacheName = this.taskCacheNames.get(taskId)
if (!cacheName) {
console.warn(`[GeminiHandler] No cache found for task ${taskId}, cannot update TTL`)
return
}
try {
const updatedCache = await this.client.caches.update({
name: cacheName,
config: { ttl: `${ttlSeconds}s` },
})
console.log(`[GeminiHandler] Updated TTL for cache ${cacheName} to ${ttlSeconds}s`)
return updatedCache
} catch (error) {
console.error(`[GeminiHandler] Failed to update TTL for cache ${cacheName}:`, error)
}
}
/**
* Calculate the ongoing costs for a task based on cache storage.
*
* This method calculates the cost of holding tokens in cache for the TTL period.
* These costs are separate from the immediate costs of API calls and should be
* tracked at the task level rather than the message level.
*
* TODO: Surface these ongoing costs to the user in the UI, possibly in:
* - The task header/summary
* - A dedicated "costs" panel or tooltip
* - As part of the total cost calculation for the task
*
* @param taskId The ID of the task to calculate ongoing costs for
* @returns The ongoing cost in dollars, or undefined if no cache exists for the task
*/
public getTaskOngoingCosts(taskId: string): number | undefined {
const tokens = this.taskCacheTokens.get(taskId)
if (!tokens) {
return undefined
}
const { info } = this.getModel()
if (!info.cacheWritesPrice) {
return undefined
}
// Calculate the cost of holding tokens in cache for the TTL period
// (tokens / 1M) * (price per 1M tokens) * (cache TTL in hours)
return info.cacheWritesPrice * (tokens / 1_000_000) * (DEFAULT_CACHE_TTL_SECONDS / 3600)
}
/**
* Calculate the immediate dollar cost of the API call based on token usage and model pricing.
*
@@ -430,21 +155,18 @@ export class GeminiHandler implements ApiHandler {
* - Input token costs (for uncached tokens)
* - Output token costs
* - Cache read costs
* - Gemini implicit caching has no write costs
*
* It does NOT include ongoing costs like cache storage, which are tracked separately
* at the task level through getTaskOngoingCosts().
*/
public calculateCost({
info,
inputTokens,
outputTokens,
cacheWriteTokens = 0,
cacheReadTokens = 0,
}: {
info: ModelInfo
inputTokens: number
outputTokens: number
cacheWriteTokens?: number
cacheReadTokens?: number
}) {
// Exit early if any required pricing information is missing
@@ -454,9 +176,7 @@ export class GeminiHandler implements ApiHandler {
let inputPrice = info.inputPrice
let outputPrice = info.outputPrice
let cacheWritesPrice = info.cacheWritesPrice ?? 0
// Right now, we only show the immediate costs of caching and not the ongoing costs of storing the cache
cacheWritesPrice = 0
let cacheReadsPrice = info.cacheReadsPrice ?? 0
// If there's tiered pricing then adjust prices based on the input tokens used
@@ -465,7 +185,6 @@ export class GeminiHandler implements ApiHandler {
if (tier) {
inputPrice = tier.inputPrice ?? inputPrice
outputPrice = tier.outputPrice ?? outputPrice
cacheWritesPrice = tier.cacheWritesPrice ?? cacheWritesPrice
cacheReadsPrice = tier.cacheReadsPrice ?? cacheReadsPrice
}
}
@@ -502,27 +221,6 @@ export class GeminiHandler implements ApiHandler {
return totalCost
}
/**
* Calculate the total length of all messages for cache eligibility check
*/
private getMessagesLength(contents: Content[]): number {
return contents.reduce((total, content) => {
if (!content.parts) {
return total
}
return (
total +
content.parts.reduce((partTotal, part) => {
if (typeof part.text === "string") {
return partTotal + part.text.length
}
return partTotal
}, 0)
)
}, 0)
}
/**
* Get the model ID and info for the current configuration
*/
+2 -2
View File
@@ -65,7 +65,7 @@ export class LiteLlmHandler implements ApiHandler {
const reasoningOn = budgetTokens !== 0 ? true : false
const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined
let temperature: number | undefined = 0
let temperature: number | undefined = this.options.liteLlmModelInfo?.temperature ?? 0
if (isOminiModel && reasoningOn) {
temperature = undefined // Thinking mode doesn't support temperature
@@ -169,7 +169,7 @@ export class LiteLlmHandler implements ApiHandler {
getModel() {
return {
id: this.options.liteLlmModelId || liteLlmDefaultModelId,
info: liteLlmModelInfoSaneDefaults,
info: this.options.liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
}
}
}
+2 -1
View File
@@ -66,8 +66,9 @@ export class OpenAiNativeHandler implements ApiHandler {
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
reasoning_effort: (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium",
reasoning_effort: (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium",
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
+3 -2
View File
@@ -18,7 +18,8 @@ export class OpenAiHandler implements ApiHandler {
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
if (
this.options.azureApiVersion ||
(this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") &&
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
) {
this.client = new AzureOpenAI({
@@ -64,7 +65,7 @@ export class OpenAiHandler implements ApiHandler {
if (isReasoningModelFamily) {
openAiMessages = [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
temperature = undefined // does not support temperature
reasoningEffort = (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium"
reasoningEffort = (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium"
}
const stream = await this.client.chat.completions.create({
+1 -1
View File
@@ -35,7 +35,7 @@ export class OpenRouterHandler implements ApiHandler {
systemPrompt,
messages,
this.getModel(),
this.options.o3MiniReasoningEffort,
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
)
+1 -1
View File
@@ -32,7 +32,7 @@ export class RequestyHandler implements ApiHandler {
...convertToOpenAiMessages(messages),
]
const reasoningEffort = this.options.o3MiniReasoningEffort || "medium"
const reasoningEffort = this.options.reasoningEffort || "medium"
const reasoning = { reasoning_effort: reasoningEffort }
const reasoningArgs = model.id.startsWith("openai/o") ? reasoning : {}
+71 -72
View File
@@ -9,7 +9,7 @@ export async function createOpenRouterStream(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
model: { id: string; info: ModelInfo },
o3MiniReasoningEffort?: string,
reasoningEffort?: string,
thinkingBudgetTokens?: number,
openRouterProviderSorting?: string,
) {
@@ -21,7 +21,7 @@ export async function createOpenRouterStream(
// prompt caching: https://openrouter.ai/docs/prompt-caching
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
// includes custom support for gemini which does not have iterative caching
// handles direct model.id match logic
switch (model.id) {
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
@@ -71,79 +71,78 @@ export async function createOpenRouterStream(
}
})
break
case "google/gemini-2.5-pro-preview-03-25":
case "google/gemini-2.0-flash-001":
case "google/gemini-flash-1.5":
case "google/gemini-pro-1.5":
// gemini only uses the last breakpoint for caching, so the others will be ignored
openAiMessages[0] = {
role: "system",
content: [
{
type: "text",
text: systemPrompt,
// @ts-ignore-next-line
cache_control: { type: "ephemeral" },
},
],
}
// for safety, but this should always be the case
if (openAiMessages.length >= 2) {
const msg = openAiMessages[1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}
// it doesn't make sense to alter breakpoints at all with the gemini cache implementation at this time
/*const GEMINI_CACHE_USER_MESSAGE_INTERVAL = 4 // add new breakpoint every 4 turns
const userMessages = openAiMessages.filter((msg) => msg.role === "user")
const userMessageCount = userMessages.length
const targetUserMessageNumber =
Math.floor(userMessageCount / GEMINI_CACHE_USER_MESSAGE_INTERVAL) * GEMINI_CACHE_USER_MESSAGE_INTERVAL
if (targetUserMessageNumber > 0) {
// otherwise dont need to add a breakpoint
const msg = userMessages[targetUserMessageNumber - 1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}*/
break
default:
break
}
// handles gemini caching logic
if (model.id.startsWith("google/") && model.info.supportsPromptCache) {
// gemini only uses the last breakpoint for caching, so the others will be ignored
openAiMessages[0] = {
role: "system",
content: [
{
type: "text",
text: systemPrompt,
// @ts-ignore-next-line
cache_control: { type: "ephemeral" },
},
],
}
// for safety, but this should always be the case
if (openAiMessages.length >= 2) {
const msg = openAiMessages[1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}
// it doesn't make sense to alter breakpoints at all with the gemini cache implementation at this time
/*const GEMINI_CACHE_USER_MESSAGE_INTERVAL = 4 // add new breakpoint every 4 turns
const userMessages = openAiMessages.filter((msg) => msg.role === "user")
const userMessageCount = userMessages.length
const targetUserMessageNumber =
Math.floor(userMessageCount / GEMINI_CACHE_USER_MESSAGE_INTERVAL) * GEMINI_CACHE_USER_MESSAGE_INTERVAL
if (targetUserMessageNumber > 0) {
// otherwise dont need to add a breakpoint
const msg = userMessages[targetUserMessageNumber - 1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}*/
}
// Not sure how openrouter defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
// (models usually default to max tokens allowed)
let maxTokens: number | undefined
@@ -213,7 +212,7 @@ export async function createOpenRouterStream(
stream_options: { include_usage: true },
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
include_reasoning: true,
...(model.id.startsWith("openai/o") ? { reasoning_effort: o3MiniReasoningEffort || "medium" } : {}),
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
...(reasoning ? { reasoning } : {}),
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
})
+6
View File
@@ -25,6 +25,7 @@ export const toolUseNames = [
"attempt_completion",
"new_task",
"condense",
"report_bug",
"new_rule",
] as const
@@ -53,6 +54,11 @@ export const toolParamNames = [
"response",
"result",
"context",
"title",
"what_happened",
"steps_to_reproduce",
"api_request_output",
"additional_context",
] as const
export type ToolParamName = (typeof toolParamNames)[number]
@@ -2,6 +2,7 @@ import * as vscode from "vscode"
import crypto from "crypto"
import { Controller } from "../index"
import { storeSecret } from "../../storage/state"
import { EmptyRequest, String } from "../../../shared/proto/common"
/**
* Handles the user clicking the login link in the UI.
@@ -11,7 +12,7 @@ import { storeSecret } from "../../storage/state"
* @param controller The controller instance.
* @returns The login URL as a string.
*/
export async function accountLoginClicked(controller: Controller): Promise<String> {
export async function accountLoginClicked(controller: Controller, unused: EmptyRequest): Promise<String> {
// Generate nonce for state validation
const nonce = crypto.randomBytes(32).toString("hex")
await storeSecret(controller.context, "authNonce", nonce)
@@ -25,6 +26,8 @@ export async function accountLoginClicked(controller: Controller): Promise<Strin
const authUrl = vscode.Uri.parse(
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
)
vscode.env.openExternal(authUrl)
return authUrl.toString()
await vscode.env.openExternal(authUrl)
return {
value: authUrl.toString(),
}
}
@@ -1,8 +1,8 @@
import { UpdateBrowserSettingsRequest } from "../../../shared/proto/browser"
import { Boolean } from "../../../shared/proto/common"
import { Controller } from "../index"
import { updateGlobalState } from "../../storage/state"
import { BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings"
import { updateGlobalState, getGlobalState } from "../../storage/state"
import { BrowserSettings as SharedBrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../../shared/BrowserSettings"
/**
* Update browser settings
@@ -12,23 +12,39 @@ import { BrowserSettings as SharedBrowserSettings } from "../../../shared/Browse
*/
export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise<Boolean> {
try {
// Convert from protobuf format to shared format
const browserSettings: SharedBrowserSettings = {
// Get current browser settings to preserve fields not in the request
const currentSettings = (await getGlobalState(controller.context, "browserSettings")) as SharedBrowserSettings | undefined
const mergedWithDefaults = { ...DEFAULT_BROWSER_SETTINGS, ...currentSettings }
// Convert from protobuf format to shared format, merging with existing settings
const newBrowserSettings: SharedBrowserSettings = {
...mergedWithDefaults, // Start with existing settings (and defaults)
viewport: {
width: request.viewport?.width || 900,
height: request.viewport?.height || 600,
// Apply updates from request
width: request.viewport?.width || mergedWithDefaults.viewport.width,
height: request.viewport?.height || mergedWithDefaults.viewport.height,
},
remoteBrowserEnabled: request.remoteBrowserEnabled || false,
remoteBrowserHost: request.remoteBrowserHost || undefined,
// Explicitly handle optional boolean and string fields from the request
remoteBrowserEnabled:
request.remoteBrowserEnabled === undefined
? mergedWithDefaults.remoteBrowserEnabled
: request.remoteBrowserEnabled,
remoteBrowserHost:
request.remoteBrowserHost === undefined ? mergedWithDefaults.remoteBrowserHost : request.remoteBrowserHost,
chromeExecutablePath:
// If chromeExecutablePath is explicitly in the request (even as ""), use it.
// Otherwise, fall back to mergedWithDefaults.
"chromeExecutablePath" in request ? request.chromeExecutablePath : mergedWithDefaults.chromeExecutablePath,
disableToolUse: request.disableToolUse === undefined ? mergedWithDefaults.disableToolUse : request.disableToolUse,
}
// Update global state with new settings
await updateGlobalState(controller.context, "browserSettings", browserSettings)
await updateGlobalState(controller.context, "browserSettings", newBrowserSettings)
// Update task browser settings if task exists
if (controller.task) {
controller.task.browserSettings = browserSettings
controller.task.browserSession.browserSettings = browserSettings
controller.task.browserSettings = newBrowserSettings
controller.task.browserSession.browserSettings = newBrowserSettings
}
// Post updated state to webview
+83 -251
View File
@@ -1,6 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import type { AxiosRequestConfig } from "axios"
import fs from "fs/promises"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
@@ -8,6 +7,8 @@ import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
import { handleModelsServiceRequest } from "./models"
import { EmptyRequest } from "@shared/proto/common"
import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "@integrations/misc/export-markdown"
@@ -19,7 +20,6 @@ import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { ClineAccountService } from "@services/account/ClineAccountService"
import { BrowserSession } from "@services/browser/BrowserSession"
import { McpHub } from "@services/mcp/McpHub"
import { searchWorkspaceFiles } from "@services/search/file-search"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { ApiProvider, ModelInfo } from "@shared/api"
import { ChatContent } from "@shared/ChatContent"
@@ -28,11 +28,10 @@ import { ExtensionMessage, ExtensionState, Invoke, Platform } from "@shared/Exte
import { HistoryItem } from "@shared/HistoryItem"
import { McpDownloadResponse, McpMarketplaceCatalog, McpServer } from "@shared/mcp"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { ClineCheckpointRestore, WebviewMessage } from "@shared/WebviewMessage"
import { WebviewMessage } from "@shared/WebviewMessage"
import { fileExistsAtPath } from "@utils/fs"
import { searchCommits, getWorkingState } from "@utils/git"
import { getWorkingState } from "@utils/git"
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
import { getWorkspacePath } from "@utils/path"
import { getTotalTasksSize } from "@utils/storage"
import { openMention } from "../mentions"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
@@ -67,7 +66,7 @@ export class Controller {
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
private latestAnnouncementId = "may-02-2025_16:27:00" // update to some unique identifier when we add a new announcement
private latestAnnouncementId = "may-09-2025_17:11:00" // update to some unique identifier when we add a new announcement
constructor(
readonly context: vscode.ExtensionContext,
@@ -146,6 +145,7 @@ export class Controller {
browserSettings,
chatSettings,
shellIntegrationTimeout,
enableCheckpointsSetting,
} = await getAllExtensionState(this.context)
if (autoApprovalSettings) {
@@ -169,6 +169,7 @@ export class Controller {
browserSettings,
chatSettings,
shellIntegrationTimeout,
enableCheckpointsSetting ?? true,
customInstructions,
task,
images,
@@ -232,15 +233,15 @@ export class Controller {
}
})
this.silentlyRefreshMcpMarketplace()
this.refreshOpenRouterModels().then(async (openRouterModels) => {
if (openRouterModels) {
handleModelsServiceRequest(this, "refreshOpenRouterModels", EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
// update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
const { apiConfiguration } = await getAllExtensionState(this.context)
if (apiConfiguration.openRouterModelId) {
if (apiConfiguration.openRouterModelId && response.models[apiConfiguration.openRouterModelId]) {
await updateGlobalState(
this.context,
"openRouterModelInfo",
openRouterModels[apiConfiguration.openRouterModelId],
response.models[apiConfiguration.openRouterModelId],
)
await this.postStateToWebview()
}
@@ -275,6 +276,9 @@ export class Controller {
case "condense":
this.task?.handleWebviewAskResponse("yesButtonClicked")
break
case "reportBug":
this.task?.handleWebviewAskResponse("yesButtonClicked")
break
case "apiConfiguration":
if (message.apiConfiguration) {
await updateApiConfiguration(this.context, message.apiConfiguration)
@@ -332,21 +336,6 @@ export class Controller {
case "resetState":
await this.resetState()
break
case "requestVsCodeLmModels":
const vsCodeLmModels = await this.getVsCodeLmModels()
this.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
break
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)
this.postMessageToWebview({ type: "openAiModels", openAiModels })
break
case "refreshClineRules":
await refreshClineRulesToggles(this.context, cwd)
await refreshExternalRulesToggles(this.context, cwd)
@@ -595,6 +584,22 @@ export class Controller {
// plan act setting
await updateGlobalState(this.context, "planActSeparateModelsSetting", message.planActSeparateModelsSetting)
if (typeof message.enableCheckpointsSetting === "boolean") {
await updateGlobalState(this.context, "enableCheckpointsSetting", message.enableCheckpointsSetting)
}
if (typeof message.mcpMarketplaceEnabled === "boolean") {
await updateGlobalState(this.context, "mcpMarketplaceEnabled", message.mcpMarketplaceEnabled)
}
// chat settings (including preferredLanguage and openAIReasoningEffort)
if (message.chatSettings) {
await updateGlobalState(this.context, "chatSettings", message.chatSettings)
if (this.task) {
this.task.chatSettings = message.chatSettings
}
}
// after settings are updated, post state to webview
await this.postStateToWebview()
@@ -603,11 +608,18 @@ export class Controller {
}
case "clearAllTaskHistory": {
const answer = await vscode.window.showWarningMessage(
"Are you sure you want to delete all history?",
"Delete",
"What would you like to delete?",
{ modal: true },
"Delete All Except Favorites",
"Delete Everything",
"Cancel",
)
if (answer === "Delete") {
if (answer === "Delete All Except Favorites") {
await this.deleteNonFavoriteTaskHistory()
await this.postStateToWebview()
this.refreshTotalTasksSize()
} else if (answer === "Delete Everything") {
await this.deleteAllTaskHistory()
await this.postStateToWebview()
this.refreshTotalTasksSize()
@@ -759,6 +771,7 @@ export class Controller {
break
case "litellm":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
break
case "requesty":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
@@ -812,7 +825,8 @@ export class Controller {
await updateGlobalState(this.context, "lmStudioModelId", newModelId)
break
case "litellm":
await updateGlobalState(this.context, "liteLlmModelId", newModelId)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
break
case "requesty":
await updateGlobalState(this.context, "requestyModelId", newModelId)
@@ -884,18 +898,6 @@ export class Controller {
}
}
// VSCode LM API
private async getVsCodeLmModels() {
try {
const models = await vscode.lm.selectChatModels({})
return models || []
} catch (error) {
console.error("Error fetching VS Code LM models:", error)
return []
}
}
// Account
async fetchUserCreditsData() {
@@ -1129,32 +1131,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
}
}
// OpenAi
async getOpenAiModels(baseUrl?: string, apiKey?: string) {
try {
if (!baseUrl) {
return []
}
if (!URL.canParse(baseUrl)) {
return []
}
const config: AxiosRequestConfig = {}
if (apiKey) {
config["headers"] = { Authorization: `Bearer ${apiKey}` }
}
const response = await axios.get(`${baseUrl}/models`, config)
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
const models = [...new Set<string>(modelsArray)]
return models
} catch (error) {
return []
}
}
// OpenRouter
async handleOpenRouterCallback(code: string) {
@@ -1190,6 +1166,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
return cacheDir
}
// Read OpenRouter models from disk cache
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
@@ -1200,190 +1177,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
return undefined
}
async refreshOpenRouterModels() {
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
let models: Record<string, ModelInfo> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models")
/*
{
"id": "anthropic/claude-3.5-sonnet",
"name": "Anthropic: Claude 3.5 Sonnet",
"created": 1718841600,
"description": "Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: Autonomously writes, edits, and runs code with reasoning and troubleshooting\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal",
"context_length": 200000,
"architecture": {
"modality": "text+image-\u003Etext",
"tokenizer": "Claude",
"instruct_type": null
},
"pricing": {
"prompt": "0.000003",
"completion": "0.000015",
"image": "0.0048",
"request": "0"
},
"top_provider": {
"context_length": 200000,
"max_completion_tokens": 8192,
"is_moderated": true
},
"per_request_limits": null
},
*/
if (response.data?.data) {
const rawModels = response.data.data
const parsePrice = (price: any) => {
if (price) {
return parseFloat(price) * 1_000_000
}
return undefined
}
for (const rawModel of rawModels) {
const modelInfo: ModelInfo = {
maxTokens: rawModel.top_provider?.max_completion_tokens,
contextWindow: rawModel.context_length,
supportsImages: rawModel.architecture?.modality?.includes("image"),
supportsPromptCache: false,
inputPrice: parsePrice(rawModel.pricing?.prompt),
outputPrice: parsePrice(rawModel.pricing?.completion),
description: rawModel.description,
}
switch (rawModel.id) {
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
// NOTE: this needs to be synced with api.ts/openrouter default model info
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
break
case "anthropic/claude-3.5-sonnet-20240620":
case "anthropic/claude-3.5-sonnet-20240620:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
break
case "anthropic/claude-3-5-haiku":
case "anthropic/claude-3-5-haiku:beta":
case "anthropic/claude-3-5-haiku-20241022":
case "anthropic/claude-3-5-haiku-20241022:beta":
case "anthropic/claude-3.5-haiku":
case "anthropic/claude-3.5-haiku:beta":
case "anthropic/claude-3.5-haiku-20241022":
case "anthropic/claude-3.5-haiku-20241022:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 1.25
modelInfo.cacheReadsPrice = 0.1
break
case "anthropic/claude-3-opus":
case "anthropic/claude-3-opus:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 18.75
modelInfo.cacheReadsPrice = 1.5
break
case "anthropic/claude-3-haiku":
case "anthropic/claude-3-haiku:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 0.3
modelInfo.cacheReadsPrice = 0.03
break
case "deepseek/deepseek-chat":
modelInfo.supportsPromptCache = true
// see api.ts/deepSeekModels for more info
modelInfo.inputPrice = 0
modelInfo.cacheWritesPrice = 0.14
modelInfo.cacheReadsPrice = 0.014
break
case "google/gemini-2.5-pro-preview-03-25":
case "google/gemini-2.0-flash-001":
case "google/gemini-flash-1.5":
case "google/gemini-pro-1.5":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
break
default:
if (rawModel.id.startsWith("openai/")) {
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
if (modelInfo.cacheReadsPrice) {
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
// openrouter charges no cache write pricing for openAI models
}
}
break
}
models[rawModel.id] = modelInfo
}
} else {
console.error("Invalid response from OpenRouter API")
}
await fs.writeFile(openRouterModelsFilePath, JSON.stringify(models))
console.log("OpenRouter models fetched and saved", models)
} catch (error) {
console.error("Error fetching OpenRouter models:", error)
}
await this.postMessageToWebview({
type: "openRouterModels",
openRouterModels: models,
})
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,
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) {
@@ -1558,6 +1351,43 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
// await this.postStateToWebview()
}
async deleteNonFavoriteTaskHistory() {
await this.clearTask()
const taskHistory = ((await getGlobalState(this.context, "taskHistory")) as HistoryItem[]) || []
const favoritedTasks = taskHistory.filter((task) => task.isFavorited === true)
// If user has no favorited tasks, show a warning message
if (favoritedTasks.length === 0) {
vscode.window.showWarningMessage("No favorited tasks found. Please favorite tasks before using this option.")
await this.postStateToWebview()
return
}
await updateGlobalState(this.context, "taskHistory", favoritedTasks)
// Delete non-favorited task directories
try {
const preserveTaskIds = favoritedTasks.map((task) => task.id)
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks")
if (await fileExistsAtPath(taskDirPath)) {
const taskDirs = await fs.readdir(taskDirPath)
for (const taskDir of taskDirs) {
if (!preserveTaskIds.includes(taskDir)) {
await fs.rm(path.join(taskDirPath, taskDir), { recursive: true, force: true })
}
}
}
} catch (error) {
vscode.window.showErrorMessage(
`Error deleting task history: ${error instanceof Error ? error.message : String(error)}`,
)
}
await this.postStateToWebview()
}
async refreshTotalTasksSize() {
getTotalTasksSize(this.context.globalStorageUri.fsPath)
.then((newTotalSize) => {
@@ -1646,6 +1476,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
mcpMarketplaceEnabled,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
globalClineRulesToggles,
shellIntegrationTimeout,
} = await getAllExtensionState(this.context)
@@ -1680,6 +1511,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
mcpMarketplaceEnabled,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
vscMachineId: vscode.env.machineId,
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
@@ -0,0 +1,24 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { VsCodeLmModelsArray } from "../../../shared/proto/models"
import * as vscode from "vscode"
import { convertVsCodeNativeModelsToProtoModels } from "../../../shared/proto-conversions/models/vscode-lm-models-conversion"
/**
* Fetches available models from VS Code LM API
* @param controller The controller instance
* @param request Empty request
* @returns Array of VS Code LM models
*/
export async function getVsCodeLmModels(controller: Controller, request: EmptyRequest): Promise<VsCodeLmModelsArray> {
try {
const models = await vscode.lm.selectChatModels({})
const protoModels = convertVsCodeNativeModelsToProtoModels(models || [])
return VsCodeLmModelsArray.create({ models: protoModels })
} catch (error) {
console.error("Error fetching VS Code LM models:", error)
return VsCodeLmModelsArray.create({ models: [] })
}
}
+8
View File
@@ -5,10 +5,18 @@
import { registerMethod } from "./index"
import { getLmStudioModels } from "./getLmStudioModels"
import { getOllamaModels } from "./getOllamaModels"
import { getVsCodeLmModels } from "./getVsCodeLmModels"
import { refreshOpenAiModels } from "./refreshOpenAiModels"
import { refreshOpenRouterModels } from "./refreshOpenRouterModels"
import { refreshRequestyModels } from "./refreshRequestyModels"
// Register all models service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("getLmStudioModels", getLmStudioModels)
registerMethod("getOllamaModels", getOllamaModels)
registerMethod("getVsCodeLmModels", getVsCodeLmModels)
registerMethod("refreshOpenAiModels", refreshOpenAiModels)
registerMethod("refreshOpenRouterModels", refreshOpenRouterModels)
registerMethod("refreshRequestyModels", refreshRequestyModels)
}
@@ -0,0 +1,37 @@
import { Controller } from ".."
import { OpenAiModelsRequest } from "../../../shared/proto/models"
import { StringArray } from "../../../shared/proto/common"
import axios from "axios"
import type { AxiosRequestConfig } from "axios"
/**
* Fetches available models from the OpenAI API
* @param controller The controller instance
* @param request Request containing the base URL and API key
* @returns Array of model names
*/
export async function refreshOpenAiModels(controller: Controller, request: OpenAiModelsRequest): Promise<StringArray> {
try {
if (!request.baseUrl) {
return StringArray.create({ values: [] })
}
if (!URL.canParse(request.baseUrl)) {
return StringArray.create({ values: [] })
}
const config: AxiosRequestConfig = {}
if (request.apiKey) {
config["headers"] = { Authorization: `Bearer ${request.apiKey}` }
}
const response = await axios.get(`${request.baseUrl}/models`, config)
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
const models = [...new Set<string>(modelsArray)]
return StringArray.create({ values: models })
} catch (error) {
console.error("Error fetching OpenAI models:", error)
return StringArray.create({ values: [] })
}
}
@@ -0,0 +1,175 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
import axios from "axios"
import path from "path"
import fs from "fs/promises"
import { fileExistsAtPath } from "@utils/fs"
import { GlobalFileNames } from "@core/storage/disk"
/**
* Refreshes the OpenRouter models and returns the updated model list
* @param controller The controller instance
* @param request Empty request object
* @returns Response containing the OpenRouter models
*/
export async function refreshOpenRouterModels(
controller: Controller,
request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models")
if (response.data?.data) {
const rawModels = response.data.data
const parsePrice = (price: any) => {
if (price) {
return parseFloat(price) * 1_000_000
}
return undefined
}
for (const rawModel of rawModels) {
const modelInfo: Partial<OpenRouterModelInfo> = {
maxTokens: rawModel.top_provider?.max_completion_tokens,
contextWindow: rawModel.context_length,
supportsImages: rawModel.architecture?.modality?.includes("image"),
supportsPromptCache: false,
inputPrice: parsePrice(rawModel.pricing?.prompt),
outputPrice: parsePrice(rawModel.pricing?.completion),
description: rawModel.description,
}
switch (rawModel.id) {
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
// NOTE: this needs to be synced with api.ts/openrouter default model info
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
break
case "anthropic/claude-3.5-sonnet-20240620":
case "anthropic/claude-3.5-sonnet-20240620:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
break
case "anthropic/claude-3-5-haiku":
case "anthropic/claude-3-5-haiku:beta":
case "anthropic/claude-3-5-haiku-20241022":
case "anthropic/claude-3-5-haiku-20241022:beta":
case "anthropic/claude-3.5-haiku":
case "anthropic/claude-3.5-haiku:beta":
case "anthropic/claude-3.5-haiku-20241022":
case "anthropic/claude-3.5-haiku-20241022:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 1.25
modelInfo.cacheReadsPrice = 0.1
break
case "anthropic/claude-3-opus":
case "anthropic/claude-3-opus:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 18.75
modelInfo.cacheReadsPrice = 1.5
break
case "anthropic/claude-3-haiku":
case "anthropic/claude-3-haiku:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 0.3
modelInfo.cacheReadsPrice = 0.03
break
case "deepseek/deepseek-chat":
modelInfo.supportsPromptCache = true
// see api.ts/deepSeekModels for more info
modelInfo.inputPrice = 0
modelInfo.cacheWritesPrice = 0.14
modelInfo.cacheReadsPrice = 0.014
break
default:
if (rawModel.id.startsWith("openai/")) {
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
if (modelInfo.cacheReadsPrice) {
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
// openrouter charges no cache write pricing for openAI models
}
} else if (rawModel.id.startsWith("google/")) {
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
if (modelInfo.cacheReadsPrice) {
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
}
}
break
}
models[rawModel.id] = modelInfo
}
} else {
console.error("Invalid response from OpenRouter API")
}
await fs.writeFile(openRouterModelsFilePath, JSON.stringify(models))
console.log("OpenRouter models fetched and saved", models)
} catch (error) {
console.error("Error fetching OpenRouter models:", error)
// If we failed to fetch models, try to read cached models
const cachedModels = await readOpenRouterModels(controller)
if (cachedModels) {
models = cachedModels
}
}
// Convert the Record<string, Partial<OpenRouterModelInfo>> to Record<string, OpenRouterModelInfo>
// by filling in any missing required fields with defaults
const typedModels: Record<string, OpenRouterModelInfo> = {}
for (const [key, model] of Object.entries(models)) {
typedModels[key] = {
maxTokens: model.maxTokens ?? 0,
contextWindow: model.contextWindow ?? 0,
supportsImages: model.supportsImages ?? false,
supportsPromptCache: model.supportsPromptCache ?? false,
inputPrice: model.inputPrice ?? 0,
outputPrice: model.outputPrice ?? 0,
cacheWritesPrice: model.cacheWritesPrice ?? 0,
cacheReadsPrice: model.cacheReadsPrice ?? 0,
description: model.description ?? "",
}
}
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
}
/**
* Reads cached OpenRouter models from disk
*/
async function readOpenRouterModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
if (fileExists) {
try {
const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8")
return JSON.parse(fileContents)
} catch (error) {
console.error("Error reading cached OpenRouter models:", error)
return undefined
}
}
return undefined
}
/**
* Ensures the cache directory exists and returns its path
*/
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
@@ -0,0 +1,55 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
import axios from "axios"
import { getSecret } from "@core/storage/state"
/**
* Refreshes the Requesty models and returns the updated model list
* @param controller The controller instance
* @param request Empty request object
* @returns Response containing the Requesty models
*/
export async function refreshRequestyModels(
controller: Controller,
request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const parsePrice = (price: any) => {
if (price) {
return parseFloat(price) * 1_000_000
}
return undefined
}
let models: Record<string, OpenRouterModelInfo> = {}
try {
const apiKey = await getSecret(controller.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: OpenRouterModelInfo = {
maxTokens: model.max_output_tokens || undefined,
contextWindow: model.context_window,
supportsImages: model.supports_vision || undefined,
supportsPromptCache: model.supports_caching || undefined,
inputPrice: parsePrice(model.input_price) || 0,
outputPrice: parsePrice(model.output_price) || 0,
cacheWritesPrice: parsePrice(model.caching_price) || 0,
cacheReadsPrice: parsePrice(model.cached_price) || 0,
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)
}
return OpenRouterCompatibleModelInfo.create({ models })
}
@@ -0,0 +1,88 @@
import path from "path"
import fs from "fs/promises"
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { DeleteNonFavoritedTasksResults } from "../../../shared/proto/task"
import { getGlobalState, updateGlobalState } from "../../storage/state"
import { fileExistsAtPath } from "../../../utils/fs"
/**
* Deletes all non-favorited tasks, preserving only favorited ones
* @param controller The controller instance
* @param request Empty request
* @returns DeleteNonFavoritedTasksResults with counts of preserved and deleted tasks
*/
export async function deleteNonFavoritedTasks(
controller: Controller,
_request: EmptyRequest,
): Promise<DeleteNonFavoritedTasksResults> {
try {
// Clear current task first
await controller.clearTask()
// Get existing task history
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
// Filter out non-favorited tasks
const favoritedTasks = taskHistory.filter((task) => task.isFavorited === true)
const deletedCount = taskHistory.length - favoritedTasks.length
console.log(`[deleteNonFavoritedTasks] Found ${favoritedTasks.length} favorited tasks to preserve`)
// Update global state
if (favoritedTasks.length > 0) {
await updateGlobalState(controller.context, "taskHistory", favoritedTasks)
} else {
await updateGlobalState(controller.context, "taskHistory", undefined)
}
// Handle file system cleanup for deleted tasks
const preserveTaskIds = favoritedTasks.map((task) => task.id)
await cleanupTaskFiles(controller, preserveTaskIds)
// Update webview
try {
await controller.postStateToWebview()
} catch (webviewErr) {
console.error("Error posting to webview:", webviewErr)
}
return {
tasksPreserved: favoritedTasks.length,
tasksDeleted: deletedCount,
}
} catch (error) {
console.error("Error in deleteNonFavoritedTasks:", error)
throw error
}
}
/**
* Helper function to cleanup task files while preserving specified tasks
*/
async function cleanupTaskFiles(controller: Controller, preserveTaskIds: string[]) {
const taskDirPath = path.join(controller.context.globalStorageUri.fsPath, "tasks")
try {
if (await fileExistsAtPath(taskDirPath)) {
if (preserveTaskIds.length > 0) {
const taskDirs = await fs.readdir(taskDirPath)
console.debug(`[cleanupTaskFiles] Found ${taskDirs.length} task directories`)
// Delete only non-preserved task directories
for (const dir of taskDirs) {
if (!preserveTaskIds.includes(dir)) {
await fs.rm(path.join(taskDirPath, dir), { recursive: true, force: true })
}
}
} else {
// No tasks to preserve, delete everything
await fs.rm(taskDirPath, { recursive: true, force: true })
}
}
} catch (error) {
console.error("Error cleaning up task files:", error)
}
return true
}
@@ -0,0 +1,89 @@
import { Controller } from ".."
import { GetTaskHistoryRequest, TaskHistoryArray } from "../../../shared/proto/task"
import { getGlobalState } from "../../storage/state"
/**
* Gets filtered task history
* @param controller The controller instance
* @param request Filter parameters for task history
* @returns TaskHistoryArray with filtered task list
*/
export async function getTaskHistory(controller: Controller, request: GetTaskHistoryRequest): Promise<TaskHistoryArray> {
try {
const { favoritesOnly, searchQuery, sortBy } = request
// Get task history from global state
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
// Apply filters
let filteredTasks = taskHistory.filter((item) => {
// Basic filter: must have timestamp and task content
const hasRequiredFields = item.ts && item.task
// Apply favorites filter if requested
if (favoritesOnly && hasRequiredFields) {
return item.isFavorited === true
}
return hasRequiredFields
})
// Apply search if provided
if (searchQuery) {
// Simple search implementation
const query = searchQuery.toLowerCase()
filteredTasks = filteredTasks.filter((item) => item.task.toLowerCase().includes(query))
}
// Calculate total count before sorting
const totalCount = filteredTasks.length
// Apply sorting
if (sortBy) {
filteredTasks.sort((a, b) => {
switch (sortBy) {
case "oldest":
return a.ts - b.ts
case "mostExpensive":
return (b.totalCost || 0) - (a.totalCost || 0)
case "mostTokens":
return (
(b.tokensIn || 0) +
(b.tokensOut || 0) +
(b.cacheWrites || 0) +
(b.cacheReads || 0) -
((a.tokensIn || 0) + (a.tokensOut || 0) + (a.cacheWrites || 0) + (a.cacheReads || 0))
)
case "newest":
default:
return b.ts - a.ts
}
})
} else {
// Default sort by newest
filteredTasks.sort((a, b) => b.ts - a.ts)
}
// Map to response format
const tasks = filteredTasks.map((item) => ({
id: item.id,
task: item.task,
ts: item.ts,
isFavorited: item.isFavorited || false,
size: item.size || 0,
totalCost: item.totalCost || 0,
tokensIn: item.tokensIn || 0,
tokensOut: item.tokensOut || 0,
cacheWrites: item.cacheWrites || 0,
cacheReads: item.cacheReads || 0,
}))
return {
tasks,
totalCount,
}
} catch (error) {
console.error("Error in getTaskHistory:", error)
throw error
}
}
+6
View File
@@ -5,18 +5,24 @@
import { registerMethod } from "./index"
import { cancelTask } from "./cancelTask"
import { clearTask } from "./clearTask"
import { deleteNonFavoritedTasks } from "./deleteNonFavoritedTasks"
import { deleteTasksWithIds } from "./deleteTasksWithIds"
import { exportTaskWithId } from "./exportTaskWithId"
import { getTaskHistory } from "./getTaskHistory"
import { newTask } from "./newTask"
import { showTaskWithId } from "./showTaskWithId"
import { toggleTaskFavorite } from "./toggleTaskFavorite"
// Register all task service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("cancelTask", cancelTask)
registerMethod("clearTask", clearTask)
registerMethod("deleteNonFavoritedTasks", deleteNonFavoritedTasks)
registerMethod("deleteTasksWithIds", deleteTasksWithIds)
registerMethod("exportTaskWithId", exportTaskWithId)
registerMethod("getTaskHistory", getTaskHistory)
registerMethod("newTask", newTask)
registerMethod("showTaskWithId", showTaskWithId)
registerMethod("toggleTaskFavorite", toggleTaskFavorite)
}
+61 -5
View File
@@ -1,17 +1,73 @@
import { Controller } from ".."
import { Empty, StringRequest } from "../../../shared/proto/common"
import { StringRequest } from "../../../shared/proto/common"
import { TaskResponse } from "../../../shared/proto/task"
/**
* Shows a task with the specified ID
* @param controller The controller instance
* @param request The request containing the task ID
* @returns Empty response
* @returns TaskResponse with task details
*/
export async function showTaskWithId(controller: Controller, request: StringRequest): Promise<Empty> {
export async function showTaskWithId(controller: Controller, request: StringRequest): Promise<TaskResponse> {
try {
await controller.showTaskWithId(request.value)
return Empty.create()
const id = request.value
// First check if task exists in global state for faster access
const taskHistory = ((await controller.context.globalState.get("taskHistory")) as any[]) || []
const historyItem = taskHistory.find((item) => item.id === id)
// We need to initialize the task before returning data
if (historyItem) {
// Always initialize the task with the history item
await controller.initTask(undefined, undefined, historyItem)
// Send UI update to show the chat view
await controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
// Return task data for gRPC response
return {
id: historyItem.id,
task: historyItem.task || "",
ts: historyItem.ts || 0,
isFavorited: historyItem.isFavorited || false,
size: historyItem.size || 0,
totalCost: historyItem.totalCost || 0,
tokensIn: historyItem.tokensIn || 0,
tokensOut: historyItem.tokensOut || 0,
cacheWrites: historyItem.cacheWrites || 0,
cacheReads: historyItem.cacheReads || 0,
}
}
// If not in global state, fetch from storage
const { historyItem: fetchedItem } = await controller.getTaskWithId(id)
// Initialize the task with the fetched item
await controller.initTask(undefined, undefined, fetchedItem)
// Send UI update to show the chat view
await controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
return {
id: fetchedItem.id,
task: fetchedItem.task || "",
ts: fetchedItem.ts || 0,
isFavorited: fetchedItem.isFavorited || false,
size: fetchedItem.size || 0,
totalCost: fetchedItem.totalCost || 0,
tokensIn: fetchedItem.tokensIn || 0,
tokensOut: fetchedItem.tokensOut || 0,
cacheWrites: fetchedItem.cacheWrites || 0,
cacheReads: fetchedItem.cacheReads || 0,
}
} catch (error) {
console.error("Error in showTaskWithId:", error)
throw error
}
}
@@ -0,0 +1,51 @@
import { Controller } from "../"
import { Empty } from "../../../shared/proto/common"
import { TaskFavoriteRequest } from "../../../shared/proto/task"
export async function toggleTaskFavorite(controller: Controller, request: TaskFavoriteRequest): Promise<Empty> {
if (!request.taskId || request.isFavorited === undefined) {
const errorMsg = `[toggleTaskFavorite] Invalid request: taskId or isFavorited missing`
console.error(errorMsg)
return {}
}
try {
// Update in-memory state only
try {
const history = ((await controller.context.globalState.get("taskHistory")) as any[]) || []
const taskIndex = history.findIndex((item) => item.id === request.taskId)
if (taskIndex === -1) {
console.log(`[toggleTaskFavorite] Task not found in history array!`)
} else {
// Create a new array instead of modifying in place to ensure state change
const updatedHistory = [...history]
updatedHistory[taskIndex] = {
...updatedHistory[taskIndex],
isFavorited: request.isFavorited,
}
// Update global state and wait for it to complete
try {
await controller.context.globalState.update("taskHistory", updatedHistory)
} catch (stateErr) {
console.error("Error updating global state:", stateErr)
}
}
} catch (historyErr) {
console.error("Error processing task history:", historyErr)
}
// Post to webview
try {
await controller.postStateToWebview()
} catch (webviewErr) {
console.error("Error posting to webview:", webviewErr)
}
} catch (error) {
console.error("Error in toggleTaskFavorite:", error)
}
return {}
}
+31
View File
@@ -145,3 +145,34 @@ Example:
Below is the user's input when they indicated that they wanted to create a new Cline rule file.
</explicit_instructions>\n
`
export const reportBugToolResponse = () =>
`<explicit_instructions type="report_bug">
The user has explicitly asked you to help them submit a bug to the Cline github page (you MUST now help them with this irrespective of what your conversation up to this point in time was). To do so you will use the report_bug tool which is defined below. However, you must first ensure that you have collected all required information to fill in all the parameters for the tool call. If any of the the required information is apparent through your previous conversation with the user, you can suggest how to fill in those entries. However you should NOT assume you know what the issue about unless it's clear.
Otherwise, you should converse with the user until you are able to gather all the required details. When conversing with the user, make sure you ask for/reference all required information/fields. When referencing the required fields, use human friendly versions like "Steps to reproduce" rather than "steps_to_reproduce". Only then should you use the report_bug tool call.
The report_bug tool can be used in either of the PLAN or ACT modes.
The report_bug tool call is defined below:
Description:
Your task is to fill in all of the required fields for a issue/bug report on github. You should attempt to get the user to be as verbose as possible with their description of the bug/issue they encountered. Still, it's okay, when the user is unaware of some of the details, to set those fields as "N/A".
Parameters:
- title: (required) Concise description of the issue.
- what_happened: (required) What happened and also what the user expected to happen instead.
- steps_to_reproduce: (required) What steps are required to reproduce the bug.
- api_request_output: (optional) Relevant API request output.
- additional_context: (optional) Any other context about this bug not already mentioned.
Usage:
<report_bug>
<title>Title of the issue</title>
<what_happened>Description of the issue</what_happened>
<steps_to_reproduce>Steps to reproduce the issue</steps_to_reproduce>
<api_request_output>Output from the LLM API related to the bug</api_request_output>
<additional_context>Other issue details not already covered</additional_context>
</report_bug>
Below is the user's input when they indicated that they wanted to submit a Github issue.
</explicit_instructions>\n
`
+3 -2
View File
@@ -1,17 +1,18 @@
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse } from "../prompts/commands"
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse, reportBugToolResponse } from "../prompts/commands"
/**
* Processes text for slash commands and transforms them with appropriate instructions
* This is called after parseMentions() to process any slash commands in the user's message
*/
export function parseSlashCommands(text: string): { processedText: string; needsClinerulesFileCheck: boolean } {
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule"]
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
const commandReplacements: Record<string, string> = {
newtask: newTaskToolResponse(),
smol: condenseToolResponse(),
compact: condenseToolResponse(),
newrule: newRuleToolResponse(),
reportbug: reportBugToolResponse(),
}
// this currently allows matching prepended whitespace prior to /slash-command
+3
View File
@@ -67,6 +67,7 @@ export type GlobalStateKey =
| "previousModeModelInfo"
| "liteLlmBaseUrl"
| "liteLlmModelId"
| "liteLlmModelInfo"
| "liteLlmUsePromptCache"
| "qwenApiLine"
| "requestyModelId"
@@ -78,6 +79,8 @@ export type GlobalStateKey =
| "thinkingBudgetTokens"
| "reasoningEffort"
| "planActSeparateModelsSetting"
| "enableCheckpointsSetting"
| "mcpMarketplaceEnabled"
| "favoritedModelIds"
| "requestTimeoutMs"
| "shellIntegrationTimeout"
+42 -7
View File
@@ -1,5 +1,5 @@
import * as vscode from "vscode"
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { DEFAULT_CHAT_SETTINGS, OpenAIReasoningEffort } from "@shared/ChatSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { GlobalStateKey, SecretKey } from "./state-keys"
@@ -51,6 +51,29 @@ export async function getWorkspaceState(context: vscode.ExtensionContext, key: s
return await context.workspaceState.get(key)
}
async function migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw: boolean | undefined): Promise<boolean> {
const config = vscode.workspace.getConfiguration("cline")
const mcpMarketplaceEnabled = config.get<boolean>("mcpMarketplace.enabled")
if (mcpMarketplaceEnabled !== undefined) {
// Remove from VSCode configuration
await config.update("mcpMarketplace.enabled", undefined, true)
return !mcpMarketplaceEnabled
}
return mcpMarketplaceEnabledRaw ?? true
}
async function migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw: boolean | undefined): Promise<boolean> {
const config = vscode.workspace.getConfiguration("cline")
const enableCheckpoints = config.get<boolean>("enableCheckpoints")
if (enableCheckpoints !== undefined) {
// Remove from VSCode configuration
await config.update("enableCheckpoints", undefined, true)
return enableCheckpoints
}
return enableCheckpointsSettingRaw ?? true
}
export async function getAllExtensionState(context: vscode.ExtensionContext) {
const [
storedApiProvider,
@@ -107,6 +130,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmUsePromptCache,
userInfo,
previousModeApiProvider,
@@ -131,6 +155,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
globalClineRulesToggles,
requestTimeoutMs,
shellIntegrationTimeout,
enableCheckpointsSettingRaw,
mcpMarketplaceEnabledRaw,
] = await Promise.all([
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
@@ -186,6 +212,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "liteLlmBaseUrl") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "liteLlmUsePromptCache") as Promise<boolean | undefined>,
getGlobalState(context, "userInfo") as Promise<UserInfo | undefined>,
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
@@ -210,6 +237,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
getGlobalState(context, "requestTimeoutMs") as Promise<number | undefined>,
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
fetch,
])
let apiProvider: ApiProvider
@@ -228,9 +258,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
const o3MiniReasoningEffort = vscode.workspace.getConfiguration("cline.modelSettings.o3Mini").get("reasoningEffort", "medium")
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
const mcpMarketplaceEnabled = await migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw)
const enableCheckpointsSetting = await migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw)
// Plan/Act separate models setting is a boolean indicating whether the user wants to use different models for plan and act. Existing users expect this to be enabled, while we want new users to opt in to this being disabled by default.
// On win11 state sometimes initializes as empty string instead of undefined
@@ -299,11 +328,11 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
openRouterModelInfo,
openRouterProviderSorting,
vsCodeLmModelSelector,
o3MiniReasoningEffort,
thinkingBudgetTokens,
reasoningEffort,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmApiKey,
liteLlmUsePromptCache,
asksageApiKey,
@@ -320,7 +349,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled)
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
chatSettings: {
...DEFAULT_CHAT_SETTINGS, // Apply defaults first
...(chatSettings || {}), // Spread fetched chatSettings, which includes preferredLanguage, and openAIReasoningEffort
},
userInfo,
previousModeApiProvider,
previousModeModelId,
@@ -330,9 +362,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
mcpMarketplaceEnabled,
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting,
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
}
}
@@ -386,6 +419,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmApiKey,
liteLlmUsePromptCache,
qwenApiLine,
@@ -444,6 +478,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
await updateGlobalState(context, "vsCodeLmModelSelector", vsCodeLmModelSelector)
await updateGlobalState(context, "liteLlmBaseUrl", liteLlmBaseUrl)
await updateGlobalState(context, "liteLlmModelId", liteLlmModelId)
await updateGlobalState(context, "liteLlmModelInfo", liteLlmModelInfo)
await updateGlobalState(context, "liteLlmUsePromptCache", liteLlmUsePromptCache)
await updateGlobalState(context, "qwenApiLine", qwenApiLine)
await updateGlobalState(context, "requestyModelId", requestyModelId)
+240 -29
View File
@@ -57,6 +57,7 @@ import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@sha
import { ClineAskResponse, ClineCheckpointRestore } from "@shared/WebviewMessage"
import { calculateApiCostAnthropic } from "@utils/cost"
import { fileExistsAtPath } from "@utils/fs"
import { createAndOpenGitHubIssue } from "@utils/github-url-utils"
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "@core/assistant-message"
@@ -118,6 +119,7 @@ export class Task {
private cancelTask: () => Promise<void>
readonly taskId: string
private taskIsFavorited?: boolean
api: ApiHandler
private terminalManager: TerminalManager
private urlContentFetcher: UrlContentFetcher
@@ -165,6 +167,7 @@ export class Task {
private didAlreadyUseTool = false
private didCompleteReadingStream = false
private didAutomaticallyRetryFailedApiRequest = false
private enableCheckpoints: boolean
constructor(
context: vscode.ExtensionContext,
@@ -180,6 +183,7 @@ export class Task {
browserSettings: BrowserSettings,
chatSettings: ChatSettings,
shellIntegrationTimeout: number,
enableCheckpointsSetting: boolean,
customInstructions?: string,
task?: string,
images?: string[],
@@ -194,9 +198,7 @@ export class Task {
this.reinitExistingTaskFromId = reinitExistingTaskFromId
this.cancelTask = cancelTask
this.clineIgnoreController = new ClineIgnoreController(cwd)
this.clineIgnoreController.initialize().catch((error) => {
console.error("Failed to initialize ClineIgnoreController:", error)
})
// Initialization moved to startTask/resumeTaskFromHistory
this.terminalManager = new TerminalManager()
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
this.urlContentFetcher = new UrlContentFetcher(context)
@@ -207,10 +209,12 @@ export class Task {
this.autoApprovalSettings = autoApprovalSettings
this.browserSettings = browserSettings
this.chatSettings = chatSettings
this.enableCheckpoints = enableCheckpointsSetting
// Initialize taskId first
if (historyItem) {
this.taskId = historyItem.id
this.taskIsFavorited = historyItem.isFavorited
this.conversationHistoryDeletedRange = historyItem.conversationHistoryDeletedRange
} else if (task || images) {
this.taskId = Date.now().toString()
@@ -221,11 +225,19 @@ export class Task {
// Initialize file context tracker
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({
// Prepare effective API configuration
let effectiveApiConfiguration: ApiConfiguration = {
...apiConfiguration,
taskId: this.taskId,
})
}
if (apiConfiguration.apiProvider === "openai" || apiConfiguration.apiProvider === "openai-native") {
effectiveApiConfiguration.reasoningEffort = chatSettings.openAIReasoningEffort
}
// Now that taskId is initialized, we can build the API handler
this.api = buildApiHandler(effectiveApiConfiguration)
// Set taskId on browserSession for telemetry tracking
this.browserSession.setTaskId(this.taskId)
@@ -314,6 +326,7 @@ export class Task {
size: taskDirSize,
shadowGitConfigWorkTree: await this.checkpointTracker?.getShadowGitConfigWorkTree(),
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
isFavorited: this.taskIsFavorited,
})
} catch (error) {
console.error("Failed to save cline messages:", error)
@@ -339,9 +352,19 @@ export class Task {
break
case "taskAndWorkspace":
case "workspace":
if (!this.enableCheckpoints) {
vscode.window.showErrorMessage("Checkpoints are disabled in settings.")
didWorkspaceRestoreFail = true
break
}
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.context.globalStorageUri.fsPath,
this.enableCheckpoints,
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
@@ -448,6 +471,11 @@ export class Task {
const relinquishButton = () => {
this.postMessageToWebview({ type: "relinquishControl" })
}
if (!this.enableCheckpoints) {
vscode.window.showInformationMessage("Checkpoints are disabled in settings. Cannot show diff.")
relinquishButton()
return
}
console.log("presentMultifileDiff", messageTs)
const messageIndex = this.clineMessages.findIndex((m) => m.ts === messageTs)
@@ -465,9 +493,13 @@ export class Task {
}
// TODO: handle if this is called from outside original workspace, in which case we need to show user error message we can't show diff outside of workspace?
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
if (!this.checkpointTracker && this.enableCheckpoints && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.context.globalStorageUri.fsPath,
this.enableCheckpoints,
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
@@ -565,6 +597,8 @@ export class Task {
}
async doesLatestTaskCompletionHaveNewChanges() {
if (!this.enableCheckpoints) return false
const messageIndex = findLastIndex(this.clineMessages, (m) => m.say === "completion_result")
const message = this.clineMessages[messageIndex]
if (!message) {
@@ -577,9 +611,13 @@ export class Task {
return false
}
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
if (this.enableCheckpoints && !this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.context.globalStorageUri.fsPath,
this.enableCheckpoints,
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
@@ -853,6 +891,12 @@ export class Task {
// Task lifecycle
private async startTask(task?: string, images?: string[]): Promise<void> {
try {
await this.clineIgnoreController.initialize()
} catch (error) {
console.error("Failed to initialize ClineIgnoreController:", error)
// Optionally, inform the user or handle the error appropriately
}
// conversationHistory (for API) and clineMessages (for webview) need to be in sync
// if the extension process were killed, then on restart the clineMessages might not be empty, so we need to set it to [] when we create a new Cline client (otherwise webview would show stale messages from previous session)
this.clineMessages = []
@@ -875,6 +919,12 @@ export class Task {
}
private async resumeTaskFromHistory() {
try {
await this.clineIgnoreController.initialize()
} catch (error) {
console.error("Failed to initialize ClineIgnoreController:", error)
// Optionally, inform the user or handle the error appropriately
}
// UPDATE: we don't need this anymore since most tasks are now created with checkpoints enabled
// right now we let users init checkpoints for old tasks, assuming they're continuing them from the same workspace (which we never tied to tasks, so no way for us to know if it's opened in the right workspace)
// const doesShadowGitExist = await CheckpointTracker.doesShadowGitExist(this.taskId, this.controllerRef.deref())
@@ -1066,6 +1116,10 @@ export class Task {
// Checkpoints
async saveCheckpoint(isAttemptCompletionMessage: boolean = false) {
if (!this.enableCheckpoints) {
// If checkpoints are disabled, do nothing.
return
}
// Set isCheckpointCheckedOut to false for all checkpoint_created messages
this.clineMessages.forEach((message) => {
if (message.say === "checkpoint_created") {
@@ -1428,13 +1482,38 @@ export class Task {
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
}
/**
* Migrates the disableBrowserTool setting from VSCode configuration to browserSettings
*/
private async migrateDisableBrowserToolSetting(): Promise<void> {
const config = vscode.workspace.getConfiguration("cline")
const disableBrowserTool = config.get<boolean>("disableBrowserTool")
if (disableBrowserTool !== undefined) {
this.browserSettings.disableToolUse = disableBrowserTool
// Remove from VSCode configuration
await config.update("disableBrowserTool", undefined, true)
}
}
private async migratePreferredLanguageToolSetting(): Promise<void> {
const config = vscode.workspace.getConfiguration("cline")
const preferredLanguage = config.get<LanguageDisplay>("preferredLanguage")
if (preferredLanguage !== undefined) {
this.chatSettings.preferredLanguage = preferredLanguage
// Remove from VSCode configuration
await config.update("preferredLanguage", undefined, true)
}
}
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
// Wait for MCP servers to be connected before generating system prompt
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
console.error("MCP servers failed to connect in time")
})
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool") ?? false
await this.migrateDisableBrowserToolSetting()
const disableBrowserTool = this.browserSettings.disableToolUse ?? false
// cline browser tool uses image recognition for navigation (requires model image support).
const modelSupportsBrowserUse = this.api.getModel().info.supportsImages ?? false
@@ -1443,9 +1522,8 @@ export class Task {
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsBrowserUse, this.mcpHub, this.browserSettings)
let settingsCustomInstructions = this.customInstructions?.trim()
const preferredLanguage = getLanguageKey(
vscode.workspace.getConfiguration("cline").get<LanguageDisplay>("preferredLanguage"),
)
await this.migratePreferredLanguageToolSetting()
const preferredLanguage = getLanguageKey(this.chatSettings.preferredLanguage as LanguageDisplay)
const preferredLanguageInstructions =
preferredLanguage && preferredLanguage !== DEFAULT_LANGUAGE_SETTINGS
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
@@ -1711,6 +1789,8 @@ export class Task {
return `[${block.name} for creating a new task]`
case "condense":
return `[${block.name}]`
case "report_bug":
return `[${block.name}]`
case "new_rule":
return `[${block.name} for '${block.params.path}']`
}
@@ -1916,7 +1996,7 @@ export class Task {
: "other_diff_error"
// Add telemetry for diff edit failure
telemetryService.captureDiffEditFailure(this.taskId, errorType)
telemetryService.captureDiffEditFailure(this.taskId, this.api.getModel().id, errorType)
pushToolResult(
formatResponse.toolError(
@@ -3138,6 +3218,135 @@ export class Task {
break
}
}
case "report_bug": {
const title = block.params.title
const what_happened = block.params.what_happened
const steps_to_reproduce = block.params.steps_to_reproduce
const api_request_output = block.params.api_request_output
const additional_context = block.params.additional_context
try {
if (block.partial) {
await this.ask(
"report_bug",
JSON.stringify({
title: removeClosingTag("title", title),
what_happened: removeClosingTag("what_happened", what_happened),
steps_to_reproduce: removeClosingTag("steps_to_reproduce", steps_to_reproduce),
api_request_output: removeClosingTag("api_request_output", api_request_output),
additional_context: removeClosingTag("additional_context", additional_context),
}),
block.partial,
).catch(() => {})
break
} else {
if (!title) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("report_bug", "title"))
await this.saveCheckpoint()
break
}
if (!what_happened) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("report_bug", "what_happened"))
await this.saveCheckpoint()
break
}
if (!steps_to_reproduce) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("report_bug", "steps_to_reproduce"))
await this.saveCheckpoint()
break
}
if (!api_request_output) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("report_bug", "api_request_output"))
await this.saveCheckpoint()
break
}
if (!additional_context) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("report_bug", "additional_context"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
if (this.autoApprovalSettings.enabled && this.autoApprovalSettings.enableNotifications) {
showSystemNotification({
subtitle: "Cline wants to create a github issue...",
message: `Cline is suggesting to create a github issue with the title: ${title}`,
})
}
// Derive system information values algorithmically
const operatingSystem = os.platform() + " " + os.release()
const clineVersion =
vscode.extensions.getExtension("saoudrizwan.claude-dev")?.packageJSON.version || "Unknown"
const systemInfo = `VSCode: ${vscode.version}, Node.js: ${process.version}, Architecture: ${os.arch()}`
const providerAndModel = `${(await getGlobalState(this.getContext(), "apiProvider")) as string} / ${this.api.getModel().id}`
// Ask user for confirmation
const bugReportData = JSON.stringify({
title,
what_happened,
steps_to_reproduce,
api_request_output,
additional_context,
// Include derived values in the JSON for display purposes
provider_and_model: providerAndModel,
operating_system: operatingSystem,
system_info: systemInfo,
cline_version: clineVersion,
})
const { text, images } = await this.ask("report_bug", bugReportData, 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 on the Github issue generated:\n<feedback>\n${text}\n</feedback>`,
images,
),
)
} else {
// If no response, the user accepted the condensed version
pushToolResult(
formatResponse.toolResult(`The user accepted the creation of the Github issue.`),
)
try {
// Create a Map of parameters for the GitHub issue
const params = new Map<string, string>()
params.set("title", title)
params.set("operating-system", operatingSystem)
params.set("cline-version", clineVersion)
params.set("system-info", systemInfo)
params.set("additional-context", additional_context)
params.set("what-happened", what_happened)
params.set("steps", steps_to_reproduce)
params.set("provider-model", providerAndModel)
params.set("logs", api_request_output)
// Use our utility function to create and open the GitHub issue URL
// This bypasses VS Code's URI handling issues with special characters
await createAndOpenGitHubIssue("cline", "cline", "bug_report.yml", params)
} catch (error) {
console.error(`An error occurred while attempting to report the bug: ${error}`)
}
}
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("reporting bug", error)
await this.saveCheckpoint()
break
}
}
case "plan_mode_respond": {
const response: string | undefined = block.params.response
const optionsRaw: string | undefined = block.params.options
@@ -3509,17 +3718,11 @@ export class Task {
}),
)
if (isFirstRequest) {
await this.say("checkpoint_created") // no hash since we need to wait for CheckpointTracker to be initialized
}
// use this opportunity to initialize the checkpoint tracker (can be expensive to initialize in the constructor)
// FIXME: right now we're letting users init checkpoints for old tasks, but this could be a problem if opening a task in the wrong workspace
// isNewTask &&
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
// Initialize checkpoint tracker first if enabled and it's the first request
if (isFirstRequest && this.enableCheckpoints && !this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await pTimeout(
CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath),
CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath, this.enableCheckpoints),
{
milliseconds: 15_000,
message:
@@ -3533,14 +3736,22 @@ export class Task {
}
}
// Now that checkpoint tracker is initialized, update the dummy checkpoint_created message with the commit hash. (This is necessary since we use the API request loading as an opportunity to initialize the checkpoint tracker, which can take some time)
if (isFirstRequest) {
const commitHash = await this.checkpointTracker?.commit()
// Now, if it's the first request AND checkpoints are enabled AND tracker was successfully initialized,
// then say "checkpoint_created" and perform the commit.
if (isFirstRequest && this.enableCheckpoints && this.checkpointTracker) {
await this.say("checkpoint_created") // Now this is conditional
const commitHash = await this.checkpointTracker.commit() // Actual commit
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
if (lastCheckpointMessage) {
lastCheckpointMessage.lastCheckpointHash = commitHash
await this.saveClineMessagesAndUpdateHistory()
// saveClineMessagesAndUpdateHistory will be called later after API response,
// so no need to call it here unless this is the only modification to this message.
// For now, assuming it's handled later.
}
} else if (isFirstRequest && this.enableCheckpoints && !this.checkpointTracker && this.checkpointTrackerErrorMessage) {
// Checkpoints are enabled, but tracker failed to initialize.
// checkpointTrackerErrorMessage is already set and will be part of the state.
// No explicit UI message here, error message will be in ExtensionState.
}
const [parsedUserContent, environmentDetails, clinerulesError] = await this.loadContext(userContent, includeFileDetails)
@@ -21,15 +21,17 @@ class CheckpointTracker {
this.cwd = cwd
}
public static async create(taskId: string, provider?: ClineProvider): Promise<CheckpointTracker | undefined> {
public static async create(
taskId: string,
enableCheckpointsSetting: boolean,
provider?: ClineProvider,
): Promise<CheckpointTracker | undefined> {
try {
if (!provider) {
throw new Error("Provider is required to create a checkpoint tracker")
}
// Check if checkpoints are disabled in VS Code settings
const enableCheckpoints = vscode.workspace.getConfiguration("cline").get<boolean>("enableCheckpoints") ?? true
if (!enableCheckpoints) {
if (!enableCheckpointsSetting) {
return undefined // Don't create tracker when disabled
}
@@ -90,7 +90,11 @@ class CheckpointTracker {
* Configuration:
* - Respects 'cline.enableCheckpoints' VS Code setting
*/
public static async create(taskId: string, globalStoragePath: string | undefined): Promise<CheckpointTracker | undefined> {
public static async create(
taskId: string,
globalStoragePath: string | undefined,
enableCheckpointsSetting: boolean,
): Promise<CheckpointTracker | undefined> {
if (!globalStoragePath) {
throw new Error("Global storage path is required to create a checkpoint tracker")
}
@@ -98,9 +102,9 @@ class CheckpointTracker {
console.info(`Creating new CheckpointTracker for task ${taskId}`)
const startTime = performance.now()
// Check if checkpoints are disabled in VS Code settings
const enableCheckpoints = vscode.workspace.getConfiguration("cline").get<boolean>("enableCheckpoints") ?? true
if (!enableCheckpoints) {
// Check if checkpoints are disabled by setting
if (!enableCheckpointsSetting) {
console.info(`Checkpoints disabled by setting for task ${taskId}`)
return undefined // Don't create tracker when disabled
}
+58 -17
View File
@@ -26,6 +26,9 @@ export class DiffViewProvider {
private streamedLines: string[] = []
private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
private fileEncoding: string = "utf8"
private lastFirstVisibleLine: number = 0
private shouldAutoScroll: boolean = true
private scrollListener?: vscode.Disposable
constructor(private cwd: string) {}
@@ -34,6 +37,8 @@ export class DiffViewProvider {
const fileExists = this.editType === "modify"
const absolutePath = path.resolve(this.cwd, relPath)
this.isEditing = true
this.shouldAutoScroll = true
this.lastFirstVisibleLine = 0
// if the file is already open, ensure it's not dirty before getting its contents
if (fileExists) {
const existingDocument = vscode.workspace.textDocuments.find((doc) => arePathsEqual(doc.uri.fsPath, absolutePath))
@@ -79,6 +84,21 @@ export class DiffViewProvider {
this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
this.scrollEditorToLine(0) // will this crash for new files?
this.streamedLines = []
// Add scroll detection to disable auto-scrolling when user scrolls up
this.scrollListener = vscode.window.onDidChangeTextEditorVisibleRanges((e: vscode.TextEditorVisibleRangesChangeEvent) => {
if (e.textEditor === this.activeDiffEditor) {
const currentFirstVisibleLine = e.visibleRanges[0]?.start.line || 0
// If the first visible line moved upward, user scrolled up
if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
this.shouldAutoScroll = false
}
// Always update our tracking variable
this.lastFirstVisibleLine = currentFirstVisibleLine
}
})
}
async update(accumulatedContent: string, isFinal: boolean) {
@@ -128,25 +148,30 @@ export class DiffViewProvider {
this.activeLineController.setActiveLine(currentLine)
this.fadedOverlayController.updateOverlayAfterLine(currentLine, document.lineCount)
// 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))
// Scroll to the last changed line only if the user hasn't scrolled up
if (this.shouldAutoScroll) {
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
// 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)
}
// Ensure we end at the final line
this.scrollEditorToLine(currentLine)
}
}
@@ -408,6 +433,12 @@ export class DiffViewProvider {
// close editor if open?
async reset() {
// releasing memory by clearing the diff editor
try {
await this.closeAllDiffViews()
} catch (error) {
console.error("Error closing diff views:", error)
}
this.editType = undefined
this.isEditing = false
this.originalContent = undefined
@@ -418,5 +449,15 @@ export class DiffViewProvider {
this.activeLineController = undefined
this.streamedLines = []
this.preDiagnostics = []
// Clean up the scroll listener
if (this.scrollListener) {
this.scrollListener.dispose()
this.scrollListener = undefined
}
// Reset auto-scroll state
this.shouldAutoScroll = true
this.lastFirstVisibleLine = 0
}
}
+2 -1
View File
@@ -40,7 +40,8 @@ export async function extractTextFromFile(filePath: string): Promise<string> {
return extractTextFromIPYNB(filePath)
default:
const fileBuffer = await fs.readFile(filePath)
if (fileBuffer.byteLength > 300 * 1024) {
if (fileBuffer.byteLength > 20 * 1000 * 1024) {
// 20MB limit (20 * 1000 * 1024 bytes, decimal MB)
throw new Error(`File is too large to read into context.`)
}
const encoding = await detectEncoding(fileBuffer, fileExtension)
+31 -10
View File
@@ -1,6 +1,7 @@
import * as vscode from "vscode"
import fs from "fs/promises"
import * as path from "path"
import sizeOf from "image-size"
export async function selectImages(): Promise<string[]> {
const options: vscode.OpenDialogOptions = {
@@ -17,16 +18,36 @@ export async function selectImages(): Promise<string[]> {
return []
}
return await Promise.all(
fileUris.map(async (uri) => {
const imagePath = uri.fsPath
const buffer = await fs.readFile(imagePath)
const base64 = buffer.toString("base64")
const mimeType = getMimeType(imagePath)
const dataUrl = `data:${mimeType};base64,${base64}`
return dataUrl
}),
)
const processedImagePromises = fileUris.map(async (uri) => {
const imagePath = uri.fsPath
let buffer: Buffer
try {
// Read the file into a buffer first
buffer = await fs.readFile(imagePath)
// Convert Node.js Buffer to Uint8Array
const uint8Array = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
const dimensions = sizeOf(uint8Array) // Get dimensions from Uint8Array
if (dimensions.width! > 7500 || dimensions.height! > 7500) {
console.warn(`Image dimensions exceed 7500px, skipping: ${imagePath}`)
vscode.window.showErrorMessage(
`Image too large: ${path.basename(imagePath)} was skipped (dimensions exceed 7500px).`,
)
return null
}
} catch (error) {
console.error(`Error reading file or getting dimensions for ${imagePath}:`, error)
vscode.window.showErrorMessage(`Could not read dimensions for ${path.basename(imagePath)}, skipping.`)
return null
}
// If dimensions are valid, proceed to convert the existing buffer to base64
const base64 = buffer.toString("base64")
const mimeType = getMimeType(imagePath)
return `data:${mimeType};base64,${base64}`
})
const dataUrlsWithNulls = await Promise.all(processedImagePromises)
return dataUrlsWithNulls.filter((url) => url !== null) as string[] // Filter out skipped images
}
function getMimeType(filePath: string): string {
@@ -1,6 +1,7 @@
import { EventEmitter } from "events"
import { stripAnsi } from "./ansiUtils"
import * as vscode from "vscode"
import { Logger } from "@services/logging/Logger"
export interface TerminalProcessEvents {
line: [line: string]
@@ -34,7 +35,42 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
let isFirstChunk = true
let didOutputNonCommand = false
let didEmitEmptyLine = false
let firstChunkTimeout: NodeJS.Timeout
const isWindows = process.platform === "win32"
const timeoutMs = isWindows ? 5000 : 500
const onTimeout = () => {
// In rare cases (e.g. running the same command twice like `npm run build`),
// the shell integration stream enters a broken state where no data is ever emitted.
// We never even get the first chunk, which bricks the UI and locks the user out.
// Interestingly, the stream still gets created, and future commands (like `ls`) will work,
// suggesting the stream itself isn't one-shot—but certain shell states break its behavior.
// To recover, we add a timeout waiting for the first chunk.
// If it doesnt arrive in time, we assume the terminal is broken, dispose it,
// and emit an error so the user can safely retry in a clean terminal.
Logger.debug(
`[TerminalProcess.run] First chunk timeout hit — terminal likely in bad state. Terminating terminal.`,
)
try {
terminal.dispose()
} catch (err) {
Logger.debug(`[TerminalProcess.run] Failed to dispose terminal: ${String(err)}`)
}
this.emit(
"error",
new Error("The command ran successfully, but we couldn't capture its output. Please proceed accordingly."),
)
this.emit("completed")
this.emit("continue")
}
firstChunkTimeout = setTimeout(onTimeout, timeoutMs)
for await (let data of stream) {
clearTimeout(firstChunkTimeout)
// 1. Process chunk and remove artifacts
if (isFirstChunk) {
/*
+18 -4
View File
@@ -67,11 +67,25 @@ export class BrowserSession {
}
}
async getDetectedChromePath(): Promise<{ path: string; isBundled: boolean }> {
// First check VSCode config
/**
* Migrates the chromeExecutablePath setting from VSCode configuration to browserSettings
*/
private async migrateChromeExecutablePathSetting(): Promise<void> {
const config = vscode.workspace.getConfiguration("cline")
const configPath = vscode.workspace.getConfiguration("cline").get<string>("chromeExecutablePath")
if (configPath && (await fileExistsAtPath(configPath))) {
return { path: configPath, isBundled: false }
if (configPath !== undefined) {
this.browserSettings.chromeExecutablePath = configPath
// Remove from VSCode configuration
await config.update("chromeExecutablePath", undefined, true)
}
}
async getDetectedChromePath(): Promise<{ path: string; isBundled: boolean }> {
// First check browserSettings (from UI, stored in global state)
await this.migrateChromeExecutablePathSetting()
if (this.browserSettings.chromeExecutablePath && (await fileExistsAtPath(this.browserSettings.chromeExecutablePath))) {
return { path: this.browserSettings.chromeExecutablePath, isBundled: false }
}
// Then try to find system Chrome
@@ -22,7 +22,20 @@ interface Collection {
properties: any
}
/**
* Represents telemetry event categories that can be individually enabled or disabled
* When adding a new category, add it both here and to the initial values in telemetryCategoryEnabled
* Ensure `if (!this.isCategoryEnabled('<category_name>')` is added to the capture method
*/
type TelemetryCategory = "checkpoints" | "browser"
class PostHogClient {
// Map to control specific telemetry categories (event types)
private telemetryCategoryEnabled: Map<TelemetryCategory, boolean> = new Map([
["checkpoints", false], // Checkpoints telemetry disabled
["browser", true], // Browser telemetry enabled
])
// Stores events when collect=true
private collectedTasks: CollectedTasks[] = []
// Event constants for tracking user interactions and system events
@@ -271,7 +284,6 @@ class PostHogClient {
}
/**
* TODO
* Records token usage metrics for cost tracking and usage analysis
* @param taskId Unique identifier for the task
* @param tokensIn Number of input tokens consumed
@@ -365,6 +377,10 @@ class PostHogClient {
durationMs?: number,
collect: boolean = false,
) {
if (!this.isCategoryEnabled("checkpoints")) {
return
}
this.capture(
{
event: PostHogClient.EVENTS.TASK.CHECKPOINT_USED,
@@ -512,13 +528,14 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param errorType Type of error that occurred (e.g., "search_not_found", "invalid_format")
*/
public captureDiffEditFailure(taskId: string, errorType?: string, collect: boolean = false) {
public captureDiffEditFailure(taskId: string, modelId: string, errorType?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.DIFF_EDIT_FAILED,
properties: {
taskId,
errorType,
modelId,
},
},
collect,
@@ -583,6 +600,10 @@ class PostHogClient {
* @param browserSettings The browser settings being used
*/
public captureBrowserToolStart(taskId: string, browserSettings: BrowserSettings, collect: boolean = false) {
if (!this.isCategoryEnabled("browser")) {
return
}
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_START,
@@ -612,6 +633,10 @@ class PostHogClient {
},
collect: boolean = false,
) {
if (!this.isCategoryEnabled("browser")) {
return
}
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_END,
@@ -646,6 +671,10 @@ class PostHogClient {
},
collect: boolean = false,
) {
if (!this.isCategoryEnabled("browser")) {
return
}
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_ERROR,
@@ -719,10 +748,24 @@ class PostHogClient {
)
}
/**
* Checks if telemetry is enabled
* @returns Boolean indicating whether telemetry is enabled
*/
public isTelemetryEnabled(): boolean {
return this.telemetryEnabled
}
/**
* Checks if a specific telemetry category is enabled
* @param category The telemetry category to check
* @returns Boolean indicating whether the specified telemetry category is enabled
*/
public isCategoryEnabled(category: TelemetryCategory): boolean {
// Default to true if category has not been explicitly configured
return this.telemetryCategoryEnabled.get(category) ?? true
}
public async sendCollectedEvents(taskId?: string): Promise<void> {
if (this.collectedTasks.length > 0) {
if (taskId) {

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