Compare commits

...

31 Commits

Author SHA1 Message Date
github-actions[bot] 5255da936f v3.15.0 Release Notes
v3.15.0 Release Notes
2025-05-09 17:18:17 -07:00
Evan 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-dev 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 Hudson 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
Toshii 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
pashpashpash 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 Eternal 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
Ara 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
Ara 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
pashpashpash 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
pashpashpash 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
Evan 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
Wesley Smith 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
Evan 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
pashpashpash 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
95 changed files with 4407 additions and 1547 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
---
disable autocapture
-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
@@ -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
@@ -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
@@ -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": minor
---
updated OR/cline provider to automate gemini models caching
-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
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Migrate running models locally section to new docs
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
timestamp visualization
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Allow the user to scroll when Cline is editing a file by disabling auto-scroll when the user scrolls up
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add image links to 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 }}
+40
View File
@@ -1,5 +1,45 @@
# Changelog
## [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!)
+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"
]
+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.
+20 -35
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.14.0",
"version": "3.15.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.14.0",
"version": "3.15.0",
"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",
+4 -4
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.14.0",
"version": "3.15.0",
"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)"
}
@@ -314,7 +314,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",
@@ -411,11 +411,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",
+45 -1
View File
@@ -10,7 +10,51 @@ 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 (OpenRouterModels);
// Refreshes and returns OpenAI models
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
}
// 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 ModelInfo 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;
}
// Response message for OpenRouter models
message OpenRouterModels {
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-":
+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
*/
+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]
+59 -200
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,
@@ -232,15 +231,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 +274,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 +334,9 @@ 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)
@@ -603,11 +593,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()
@@ -884,18 +881,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 +1114,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) {
@@ -1200,143 +1159,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
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)
}
await this.postMessageToWebview({
type: "openRouterModels",
openRouterModels: models,
})
return models
}
async refreshRequestyModels() {
const parsePrice = (price: any) => {
if (price) {
@@ -1556,6 +1378,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) => {
@@ -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: [] })
}
}
+6
View File
@@ -5,10 +5,16 @@
import { registerMethod } from "./index"
import { getLmStudioModels } from "./getLmStudioModels"
import { getOllamaModels } from "./getOllamaModels"
import { getVsCodeLmModels } from "./getVsCodeLmModels"
import { refreshOpenAiModels } from "./refreshOpenAiModels"
import { refreshOpenRouterModels } from "./refreshOpenRouterModels"
// 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)
}
@@ -0,0 +1,40 @@
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)]
// Send models to webview
controller.postMessageToWebview({ type: "openAiModels", openAiModels: models })
return StringArray.create({ values: models })
} catch (error) {
console.error("Error fetching OpenAI models:", error)
return StringArray.create({ values: [] })
}
}
@@ -0,0 +1,178 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { OpenRouterModels, 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<OpenRouterModels> {
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 ?? "",
}
}
// Send models to webview
await controller.postMessageToWebview({
type: "openRouterModels",
openRouterModels: typedModels,
})
return OpenRouterModels.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,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
+149 -4
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
@@ -194,9 +196,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)
@@ -211,6 +211,7 @@ export class Task {
// 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()
@@ -314,6 +315,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)
@@ -853,6 +855,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 +883,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())
@@ -1711,6 +1725,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 +1932,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 +3154,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
+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,41 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
let isFirstChunk = true
let didOutputNonCommand = false
let didEmitEmptyLine = false
let firstChunkTimeout: NodeJS.Timeout
const timeoutMs = 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) {
/*
@@ -512,13 +512,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,
+2
View File
@@ -17,6 +17,7 @@ export interface AutoApprovalSettings {
// Global settings
maxRequests: number // Maximum number of auto-approved requests
enableNotifications: boolean // Show notifications for approval and task completion
favorites: string[] // IDs of actions favorited by the user for quick access
}
export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = {
@@ -34,4 +35,5 @@ export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = {
},
maxRequests: 20,
enableNotifications: false,
favorites: [],
}
+1 -2
View File
@@ -28,8 +28,6 @@ export interface ExtensionMessage {
| "requestyModels"
| "mcpServers"
| "relinquishControl"
| "vsCodeLmModels"
| "requestVsCodeLmModels"
| "authCallback"
| "mcpMarketplaceCatalog"
| "mcpDownloadDetails"
@@ -179,6 +177,7 @@ export type ClineAsk =
| "use_mcp_server"
| "new_task"
| "condense"
| "report_bug"
export type ClineSay =
| "task"
+1
View File
@@ -11,4 +11,5 @@ export type HistoryItem = {
size?: number
shadowGitConfigWorkTree?: string
conversationHistoryDeletedRange?: [number, number]
isFavorited?: boolean
}
+1 -2
View File
@@ -13,6 +13,7 @@ export interface WebviewMessage {
| "webviewDidLaunch"
| "newTask"
| "condense"
| "reportBug"
| "askResponse"
| "didShowAnnouncement"
| "selectImages"
@@ -20,9 +21,7 @@ export interface WebviewMessage {
| "openInBrowser"
| "openMention"
| "showChatView"
| "refreshOpenRouterModels"
| "refreshRequestyModels"
| "refreshOpenAiModels"
| "refreshClineRules"
| "openMcpSettings"
| "restartMcpServer"
+26 -6
View File
@@ -204,27 +204,33 @@ export const bedrockModels = {
contextWindow: 300_000,
supportsImages: true,
supportsPromptCache: false,
supportsPromptCache: true,
inputPrice: 0.8,
outputPrice: 3.2,
// cacheWritesPrice: 3.2, // not written
cacheReadsPrice: 0.2,
},
"amazon.nova-lite-v1:0": {
maxTokens: 5000,
contextWindow: 300_000,
supportsImages: true,
supportsPromptCache: false,
supportsPromptCache: true,
inputPrice: 0.06,
outputPrice: 0.24,
// cacheWritesPrice: 0.24, // not written
cacheReadsPrice: 0.015,
},
"amazon.nova-micro-v1:0": {
maxTokens: 5000,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
supportsPromptCache: true,
inputPrice: 0.035,
outputPrice: 0.14,
// cacheWritesPrice: 0.14, // not written
cacheReadsPrice: 0.00875,
},
"anthropic.claude-3-7-sonnet-20250219-v1:0": {
maxTokens: 8192,
@@ -251,10 +257,10 @@ export const bedrockModels = {
"anthropic.claude-3-5-haiku-20241022-v1:0": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: false,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 1.0,
outputPrice: 5.0,
inputPrice: 0.8,
outputPrice: 4.0,
cacheWritesPrice: 1.0,
cacheReadsPrice: 0.08,
},
@@ -436,16 +442,19 @@ export const vertexModels = {
supportsPromptCache: true,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.31,
tiers: [
{
contextWindow: 200000,
inputPrice: 1.25,
outputPrice: 10,
cacheReadsPrice: 0.31,
},
{
contextWindow: Infinity,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.625,
},
],
},
@@ -566,16 +575,19 @@ export const geminiModels = {
supportsPromptCache: true,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.31,
tiers: [
{
contextWindow: 200000,
inputPrice: 1.25,
outputPrice: 10,
cacheReadsPrice: 0.31,
},
{
contextWindow: Infinity,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.625,
},
],
},
@@ -1387,6 +1399,14 @@ export const mistralModels = {
inputPrice: 0.1,
outputPrice: 0.3,
},
"mistral-medium-latest": {
maxTokens: 128_000,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.4,
outputPrice: 2.0,
},
"mistral-small-2501": {
maxTokens: 32_000,
contextWindow: 32_000,
@@ -0,0 +1,23 @@
import { VsCodeLmModel } from "../../proto/models"
/**
* Represents a VS Code language model in the native VS Code format
*/
export interface VsCodeNativeModel {
vendor?: string
family?: string
version?: string
id?: string
}
/**
* Converts VS Code native model format to protobuf format
*/
export function convertVsCodeNativeModelsToProtoModels(models: VsCodeNativeModel[]): VsCodeLmModel[] {
return (models || []).map((model) => ({
vendor: model.vendor || "",
family: model.family || "",
version: model.version || "",
id: model.id || "",
}))
}
+724 -1
View File
@@ -5,10 +5,672 @@
// source: models.proto
/* eslint-disable */
import { StringArray, StringRequest } from "./common"
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { EmptyRequest, Metadata, StringArray, StringRequest } from "./common"
export const protobufPackage = "cline"
/** List of VS Code LM models */
export interface VsCodeLmModelsArray {
models: VsCodeLmModel[]
}
/** Structure representing a VS Code LM model */
export interface VsCodeLmModel {
vendor: string
family: string
version: string
id: string
}
/** For ModelInfo structure in OpenRouterModels */
export interface OpenRouterModelInfo {
maxTokens: number
contextWindow: number
supportsImages: boolean
supportsPromptCache: boolean
inputPrice: number
outputPrice: number
cacheWritesPrice: number
cacheReadsPrice: number
description: string
}
/** Response message for OpenRouter models */
export interface OpenRouterModels {
models: { [key: string]: OpenRouterModelInfo }
}
export interface OpenRouterModels_ModelsEntry {
key: string
value?: OpenRouterModelInfo | undefined
}
/** Request for fetching OpenAI models */
export interface OpenAiModelsRequest {
metadata?: Metadata | undefined
baseUrl: string
apiKey: string
}
function createBaseVsCodeLmModelsArray(): VsCodeLmModelsArray {
return { models: [] }
}
export const VsCodeLmModelsArray: MessageFns<VsCodeLmModelsArray> = {
encode(message: VsCodeLmModelsArray, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
for (const v of message.models) {
VsCodeLmModel.encode(v!, writer.uint32(10).fork()).join()
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): VsCodeLmModelsArray {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseVsCodeLmModelsArray()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.models.push(VsCodeLmModel.decode(reader, reader.uint32()))
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): VsCodeLmModelsArray {
return {
models: globalThis.Array.isArray(object?.models) ? object.models.map((e: any) => VsCodeLmModel.fromJSON(e)) : [],
}
},
toJSON(message: VsCodeLmModelsArray): unknown {
const obj: any = {}
if (message.models?.length) {
obj.models = message.models.map((e) => VsCodeLmModel.toJSON(e))
}
return obj
},
create<I extends Exact<DeepPartial<VsCodeLmModelsArray>, I>>(base?: I): VsCodeLmModelsArray {
return VsCodeLmModelsArray.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<VsCodeLmModelsArray>, I>>(object: I): VsCodeLmModelsArray {
const message = createBaseVsCodeLmModelsArray()
message.models = object.models?.map((e) => VsCodeLmModel.fromPartial(e)) || []
return message
},
}
function createBaseVsCodeLmModel(): VsCodeLmModel {
return { vendor: "", family: "", version: "", id: "" }
}
export const VsCodeLmModel: MessageFns<VsCodeLmModel> = {
encode(message: VsCodeLmModel, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.vendor !== "") {
writer.uint32(10).string(message.vendor)
}
if (message.family !== "") {
writer.uint32(18).string(message.family)
}
if (message.version !== "") {
writer.uint32(26).string(message.version)
}
if (message.id !== "") {
writer.uint32(34).string(message.id)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): VsCodeLmModel {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseVsCodeLmModel()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.vendor = reader.string()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.family = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.version = reader.string()
continue
}
case 4: {
if (tag !== 34) {
break
}
message.id = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): VsCodeLmModel {
return {
vendor: isSet(object.vendor) ? globalThis.String(object.vendor) : "",
family: isSet(object.family) ? globalThis.String(object.family) : "",
version: isSet(object.version) ? globalThis.String(object.version) : "",
id: isSet(object.id) ? globalThis.String(object.id) : "",
}
},
toJSON(message: VsCodeLmModel): unknown {
const obj: any = {}
if (message.vendor !== "") {
obj.vendor = message.vendor
}
if (message.family !== "") {
obj.family = message.family
}
if (message.version !== "") {
obj.version = message.version
}
if (message.id !== "") {
obj.id = message.id
}
return obj
},
create<I extends Exact<DeepPartial<VsCodeLmModel>, I>>(base?: I): VsCodeLmModel {
return VsCodeLmModel.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<VsCodeLmModel>, I>>(object: I): VsCodeLmModel {
const message = createBaseVsCodeLmModel()
message.vendor = object.vendor ?? ""
message.family = object.family ?? ""
message.version = object.version ?? ""
message.id = object.id ?? ""
return message
},
}
function createBaseOpenRouterModelInfo(): OpenRouterModelInfo {
return {
maxTokens: 0,
contextWindow: 0,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: "",
}
}
export const OpenRouterModelInfo: MessageFns<OpenRouterModelInfo> = {
encode(message: OpenRouterModelInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.maxTokens !== 0) {
writer.uint32(8).int32(message.maxTokens)
}
if (message.contextWindow !== 0) {
writer.uint32(16).int32(message.contextWindow)
}
if (message.supportsImages !== false) {
writer.uint32(24).bool(message.supportsImages)
}
if (message.supportsPromptCache !== false) {
writer.uint32(32).bool(message.supportsPromptCache)
}
if (message.inputPrice !== 0) {
writer.uint32(41).double(message.inputPrice)
}
if (message.outputPrice !== 0) {
writer.uint32(49).double(message.outputPrice)
}
if (message.cacheWritesPrice !== 0) {
writer.uint32(57).double(message.cacheWritesPrice)
}
if (message.cacheReadsPrice !== 0) {
writer.uint32(65).double(message.cacheReadsPrice)
}
if (message.description !== "") {
writer.uint32(74).string(message.description)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): OpenRouterModelInfo {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseOpenRouterModelInfo()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break
}
message.maxTokens = reader.int32()
continue
}
case 2: {
if (tag !== 16) {
break
}
message.contextWindow = reader.int32()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.supportsImages = reader.bool()
continue
}
case 4: {
if (tag !== 32) {
break
}
message.supportsPromptCache = reader.bool()
continue
}
case 5: {
if (tag !== 41) {
break
}
message.inputPrice = reader.double()
continue
}
case 6: {
if (tag !== 49) {
break
}
message.outputPrice = reader.double()
continue
}
case 7: {
if (tag !== 57) {
break
}
message.cacheWritesPrice = reader.double()
continue
}
case 8: {
if (tag !== 65) {
break
}
message.cacheReadsPrice = reader.double()
continue
}
case 9: {
if (tag !== 74) {
break
}
message.description = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): OpenRouterModelInfo {
return {
maxTokens: isSet(object.maxTokens) ? globalThis.Number(object.maxTokens) : 0,
contextWindow: isSet(object.contextWindow) ? globalThis.Number(object.contextWindow) : 0,
supportsImages: isSet(object.supportsImages) ? globalThis.Boolean(object.supportsImages) : false,
supportsPromptCache: isSet(object.supportsPromptCache) ? globalThis.Boolean(object.supportsPromptCache) : false,
inputPrice: isSet(object.inputPrice) ? globalThis.Number(object.inputPrice) : 0,
outputPrice: isSet(object.outputPrice) ? globalThis.Number(object.outputPrice) : 0,
cacheWritesPrice: isSet(object.cacheWritesPrice) ? globalThis.Number(object.cacheWritesPrice) : 0,
cacheReadsPrice: isSet(object.cacheReadsPrice) ? globalThis.Number(object.cacheReadsPrice) : 0,
description: isSet(object.description) ? globalThis.String(object.description) : "",
}
},
toJSON(message: OpenRouterModelInfo): unknown {
const obj: any = {}
if (message.maxTokens !== 0) {
obj.maxTokens = Math.round(message.maxTokens)
}
if (message.contextWindow !== 0) {
obj.contextWindow = Math.round(message.contextWindow)
}
if (message.supportsImages !== false) {
obj.supportsImages = message.supportsImages
}
if (message.supportsPromptCache !== false) {
obj.supportsPromptCache = message.supportsPromptCache
}
if (message.inputPrice !== 0) {
obj.inputPrice = message.inputPrice
}
if (message.outputPrice !== 0) {
obj.outputPrice = message.outputPrice
}
if (message.cacheWritesPrice !== 0) {
obj.cacheWritesPrice = message.cacheWritesPrice
}
if (message.cacheReadsPrice !== 0) {
obj.cacheReadsPrice = message.cacheReadsPrice
}
if (message.description !== "") {
obj.description = message.description
}
return obj
},
create<I extends Exact<DeepPartial<OpenRouterModelInfo>, I>>(base?: I): OpenRouterModelInfo {
return OpenRouterModelInfo.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<OpenRouterModelInfo>, I>>(object: I): OpenRouterModelInfo {
const message = createBaseOpenRouterModelInfo()
message.maxTokens = object.maxTokens ?? 0
message.contextWindow = object.contextWindow ?? 0
message.supportsImages = object.supportsImages ?? false
message.supportsPromptCache = object.supportsPromptCache ?? false
message.inputPrice = object.inputPrice ?? 0
message.outputPrice = object.outputPrice ?? 0
message.cacheWritesPrice = object.cacheWritesPrice ?? 0
message.cacheReadsPrice = object.cacheReadsPrice ?? 0
message.description = object.description ?? ""
return message
},
}
function createBaseOpenRouterModels(): OpenRouterModels {
return { models: {} }
}
export const OpenRouterModels: MessageFns<OpenRouterModels> = {
encode(message: OpenRouterModels, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
Object.entries(message.models).forEach(([key, value]) => {
OpenRouterModels_ModelsEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join()
})
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): OpenRouterModels {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseOpenRouterModels()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
const entry1 = OpenRouterModels_ModelsEntry.decode(reader, reader.uint32())
if (entry1.value !== undefined) {
message.models[entry1.key] = entry1.value
}
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): OpenRouterModels {
return {
models: isObject(object.models)
? Object.entries(object.models).reduce<{ [key: string]: OpenRouterModelInfo }>((acc, [key, value]) => {
acc[key] = OpenRouterModelInfo.fromJSON(value)
return acc
}, {})
: {},
}
},
toJSON(message: OpenRouterModels): unknown {
const obj: any = {}
if (message.models) {
const entries = Object.entries(message.models)
if (entries.length > 0) {
obj.models = {}
entries.forEach(([k, v]) => {
obj.models[k] = OpenRouterModelInfo.toJSON(v)
})
}
}
return obj
},
create<I extends Exact<DeepPartial<OpenRouterModels>, I>>(base?: I): OpenRouterModels {
return OpenRouterModels.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<OpenRouterModels>, I>>(object: I): OpenRouterModels {
const message = createBaseOpenRouterModels()
message.models = Object.entries(object.models ?? {}).reduce<{ [key: string]: OpenRouterModelInfo }>(
(acc, [key, value]) => {
if (value !== undefined) {
acc[key] = OpenRouterModelInfo.fromPartial(value)
}
return acc
},
{},
)
return message
},
}
function createBaseOpenRouterModels_ModelsEntry(): OpenRouterModels_ModelsEntry {
return { key: "", value: undefined }
}
export const OpenRouterModels_ModelsEntry: MessageFns<OpenRouterModels_ModelsEntry> = {
encode(message: OpenRouterModels_ModelsEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.key !== "") {
writer.uint32(10).string(message.key)
}
if (message.value !== undefined) {
OpenRouterModelInfo.encode(message.value, writer.uint32(18).fork()).join()
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): OpenRouterModels_ModelsEntry {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseOpenRouterModels_ModelsEntry()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.key = reader.string()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.value = OpenRouterModelInfo.decode(reader, reader.uint32())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): OpenRouterModels_ModelsEntry {
return {
key: isSet(object.key) ? globalThis.String(object.key) : "",
value: isSet(object.value) ? OpenRouterModelInfo.fromJSON(object.value) : undefined,
}
},
toJSON(message: OpenRouterModels_ModelsEntry): unknown {
const obj: any = {}
if (message.key !== "") {
obj.key = message.key
}
if (message.value !== undefined) {
obj.value = OpenRouterModelInfo.toJSON(message.value)
}
return obj
},
create<I extends Exact<DeepPartial<OpenRouterModels_ModelsEntry>, I>>(base?: I): OpenRouterModels_ModelsEntry {
return OpenRouterModels_ModelsEntry.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<OpenRouterModels_ModelsEntry>, I>>(object: I): OpenRouterModels_ModelsEntry {
const message = createBaseOpenRouterModels_ModelsEntry()
message.key = object.key ?? ""
message.value =
object.value !== undefined && object.value !== null ? OpenRouterModelInfo.fromPartial(object.value) : undefined
return message
},
}
function createBaseOpenAiModelsRequest(): OpenAiModelsRequest {
return { metadata: undefined, baseUrl: "", apiKey: "" }
}
export const OpenAiModelsRequest: MessageFns<OpenAiModelsRequest> = {
encode(message: OpenAiModelsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.baseUrl !== "") {
writer.uint32(18).string(message.baseUrl)
}
if (message.apiKey !== "") {
writer.uint32(26).string(message.apiKey)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): OpenAiModelsRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseOpenAiModelsRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.baseUrl = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.apiKey = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): OpenAiModelsRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
baseUrl: isSet(object.baseUrl) ? globalThis.String(object.baseUrl) : "",
apiKey: isSet(object.apiKey) ? globalThis.String(object.apiKey) : "",
}
},
toJSON(message: OpenAiModelsRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.baseUrl !== "") {
obj.baseUrl = message.baseUrl
}
if (message.apiKey !== "") {
obj.apiKey = message.apiKey
}
return obj
},
create<I extends Exact<DeepPartial<OpenAiModelsRequest>, I>>(base?: I): OpenAiModelsRequest {
return OpenAiModelsRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<OpenAiModelsRequest>, I>>(object: I): OpenAiModelsRequest {
const message = createBaseOpenAiModelsRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.baseUrl = object.baseUrl ?? ""
message.apiKey = object.apiKey ?? ""
return message
},
}
/** Service for model-related operations */
export type ModelsServiceDefinition = typeof ModelsServiceDefinition
export const ModelsServiceDefinition = {
@@ -33,5 +695,66 @@ export const ModelsServiceDefinition = {
responseStream: false,
options: {},
},
/** Fetches available models from VS Code LM API */
getVsCodeLmModels: {
name: "getVsCodeLmModels",
requestType: EmptyRequest,
requestStream: false,
responseType: VsCodeLmModelsArray,
responseStream: false,
options: {},
},
/** Refreshes and returns OpenRouter models */
refreshOpenRouterModels: {
name: "refreshOpenRouterModels",
requestType: EmptyRequest,
requestStream: false,
responseType: OpenRouterModels,
responseStream: false,
options: {},
},
/** Refreshes and returns OpenAI models */
refreshOpenAiModels: {
name: "refreshOpenAiModels",
requestType: OpenAiModelsRequest,
requestStream: false,
responseType: StringArray,
responseStream: false,
options: {},
},
},
} as const
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined
export type DeepPartial<T> = T extends Builtin
? T
: T extends globalThis.Array<infer U>
? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
function isObject(value: any): boolean {
return typeof value === "object" && value !== null
}
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter
decode(input: BinaryReader | Uint8Array, length?: number): T
fromJSON(object: any): T
toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
}
+878 -1
View File
@@ -17,6 +17,61 @@ export interface NewTaskRequest {
images: string[]
}
/** Request message for toggling task favorite status */
export interface TaskFavoriteRequest {
metadata?: Metadata | undefined
taskId: string
isFavorited: boolean
}
/** Response for task details */
export interface TaskResponse {
id: string
task: string
ts: number
isFavorited: boolean
size: number
totalCost: number
tokensIn: number
tokensOut: number
cacheWrites: number
cacheReads: number
}
/** Results returned when deleting non-favorited tasks */
export interface DeleteNonFavoritedTasksResults {
tasksPreserved: number
tasksDeleted: number
}
/** Request for getting task history with filtering */
export interface GetTaskHistoryRequest {
metadata?: Metadata | undefined
favoritesOnly: boolean
searchQuery: string
sortBy: string
}
/** Response for task history */
export interface TaskHistoryArray {
tasks: TaskItem[]
totalCount: number
}
/** Task item details for history list */
export interface TaskItem {
id: string
task: string
ts: number
isFavorited: boolean
size: number
totalCost: number
tokensIn: number
tokensOut: number
cacheWrites: number
cacheReads: number
}
function createBaseNewTaskRequest(): NewTaskRequest {
return { metadata: undefined, text: "", images: [] }
}
@@ -110,6 +165,790 @@ export const NewTaskRequest: MessageFns<NewTaskRequest> = {
},
}
function createBaseTaskFavoriteRequest(): TaskFavoriteRequest {
return { metadata: undefined, taskId: "", isFavorited: false }
}
export const TaskFavoriteRequest: MessageFns<TaskFavoriteRequest> = {
encode(message: TaskFavoriteRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.taskId !== "") {
writer.uint32(18).string(message.taskId)
}
if (message.isFavorited !== false) {
writer.uint32(24).bool(message.isFavorited)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): TaskFavoriteRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseTaskFavoriteRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.taskId = reader.string()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.isFavorited = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): TaskFavoriteRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
taskId: isSet(object.taskId) ? globalThis.String(object.taskId) : "",
isFavorited: isSet(object.isFavorited) ? globalThis.Boolean(object.isFavorited) : false,
}
},
toJSON(message: TaskFavoriteRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.taskId !== "") {
obj.taskId = message.taskId
}
if (message.isFavorited !== false) {
obj.isFavorited = message.isFavorited
}
return obj
},
create<I extends Exact<DeepPartial<TaskFavoriteRequest>, I>>(base?: I): TaskFavoriteRequest {
return TaskFavoriteRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<TaskFavoriteRequest>, I>>(object: I): TaskFavoriteRequest {
const message = createBaseTaskFavoriteRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.taskId = object.taskId ?? ""
message.isFavorited = object.isFavorited ?? false
return message
},
}
function createBaseTaskResponse(): TaskResponse {
return {
id: "",
task: "",
ts: 0,
isFavorited: false,
size: 0,
totalCost: 0,
tokensIn: 0,
tokensOut: 0,
cacheWrites: 0,
cacheReads: 0,
}
}
export const TaskResponse: MessageFns<TaskResponse> = {
encode(message: TaskResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.id !== "") {
writer.uint32(10).string(message.id)
}
if (message.task !== "") {
writer.uint32(18).string(message.task)
}
if (message.ts !== 0) {
writer.uint32(24).int64(message.ts)
}
if (message.isFavorited !== false) {
writer.uint32(32).bool(message.isFavorited)
}
if (message.size !== 0) {
writer.uint32(40).int64(message.size)
}
if (message.totalCost !== 0) {
writer.uint32(49).double(message.totalCost)
}
if (message.tokensIn !== 0) {
writer.uint32(56).int32(message.tokensIn)
}
if (message.tokensOut !== 0) {
writer.uint32(64).int32(message.tokensOut)
}
if (message.cacheWrites !== 0) {
writer.uint32(72).int32(message.cacheWrites)
}
if (message.cacheReads !== 0) {
writer.uint32(80).int32(message.cacheReads)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): TaskResponse {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseTaskResponse()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.id = reader.string()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.task = reader.string()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.ts = longToNumber(reader.int64())
continue
}
case 4: {
if (tag !== 32) {
break
}
message.isFavorited = reader.bool()
continue
}
case 5: {
if (tag !== 40) {
break
}
message.size = longToNumber(reader.int64())
continue
}
case 6: {
if (tag !== 49) {
break
}
message.totalCost = reader.double()
continue
}
case 7: {
if (tag !== 56) {
break
}
message.tokensIn = reader.int32()
continue
}
case 8: {
if (tag !== 64) {
break
}
message.tokensOut = reader.int32()
continue
}
case 9: {
if (tag !== 72) {
break
}
message.cacheWrites = reader.int32()
continue
}
case 10: {
if (tag !== 80) {
break
}
message.cacheReads = reader.int32()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): TaskResponse {
return {
id: isSet(object.id) ? globalThis.String(object.id) : "",
task: isSet(object.task) ? globalThis.String(object.task) : "",
ts: isSet(object.ts) ? globalThis.Number(object.ts) : 0,
isFavorited: isSet(object.isFavorited) ? globalThis.Boolean(object.isFavorited) : false,
size: isSet(object.size) ? globalThis.Number(object.size) : 0,
totalCost: isSet(object.totalCost) ? globalThis.Number(object.totalCost) : 0,
tokensIn: isSet(object.tokensIn) ? globalThis.Number(object.tokensIn) : 0,
tokensOut: isSet(object.tokensOut) ? globalThis.Number(object.tokensOut) : 0,
cacheWrites: isSet(object.cacheWrites) ? globalThis.Number(object.cacheWrites) : 0,
cacheReads: isSet(object.cacheReads) ? globalThis.Number(object.cacheReads) : 0,
}
},
toJSON(message: TaskResponse): unknown {
const obj: any = {}
if (message.id !== "") {
obj.id = message.id
}
if (message.task !== "") {
obj.task = message.task
}
if (message.ts !== 0) {
obj.ts = Math.round(message.ts)
}
if (message.isFavorited !== false) {
obj.isFavorited = message.isFavorited
}
if (message.size !== 0) {
obj.size = Math.round(message.size)
}
if (message.totalCost !== 0) {
obj.totalCost = message.totalCost
}
if (message.tokensIn !== 0) {
obj.tokensIn = Math.round(message.tokensIn)
}
if (message.tokensOut !== 0) {
obj.tokensOut = Math.round(message.tokensOut)
}
if (message.cacheWrites !== 0) {
obj.cacheWrites = Math.round(message.cacheWrites)
}
if (message.cacheReads !== 0) {
obj.cacheReads = Math.round(message.cacheReads)
}
return obj
},
create<I extends Exact<DeepPartial<TaskResponse>, I>>(base?: I): TaskResponse {
return TaskResponse.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<TaskResponse>, I>>(object: I): TaskResponse {
const message = createBaseTaskResponse()
message.id = object.id ?? ""
message.task = object.task ?? ""
message.ts = object.ts ?? 0
message.isFavorited = object.isFavorited ?? false
message.size = object.size ?? 0
message.totalCost = object.totalCost ?? 0
message.tokensIn = object.tokensIn ?? 0
message.tokensOut = object.tokensOut ?? 0
message.cacheWrites = object.cacheWrites ?? 0
message.cacheReads = object.cacheReads ?? 0
return message
},
}
function createBaseDeleteNonFavoritedTasksResults(): DeleteNonFavoritedTasksResults {
return { tasksPreserved: 0, tasksDeleted: 0 }
}
export const DeleteNonFavoritedTasksResults: MessageFns<DeleteNonFavoritedTasksResults> = {
encode(message: DeleteNonFavoritedTasksResults, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.tasksPreserved !== 0) {
writer.uint32(8).int32(message.tasksPreserved)
}
if (message.tasksDeleted !== 0) {
writer.uint32(16).int32(message.tasksDeleted)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): DeleteNonFavoritedTasksResults {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseDeleteNonFavoritedTasksResults()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break
}
message.tasksPreserved = reader.int32()
continue
}
case 2: {
if (tag !== 16) {
break
}
message.tasksDeleted = reader.int32()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): DeleteNonFavoritedTasksResults {
return {
tasksPreserved: isSet(object.tasksPreserved) ? globalThis.Number(object.tasksPreserved) : 0,
tasksDeleted: isSet(object.tasksDeleted) ? globalThis.Number(object.tasksDeleted) : 0,
}
},
toJSON(message: DeleteNonFavoritedTasksResults): unknown {
const obj: any = {}
if (message.tasksPreserved !== 0) {
obj.tasksPreserved = Math.round(message.tasksPreserved)
}
if (message.tasksDeleted !== 0) {
obj.tasksDeleted = Math.round(message.tasksDeleted)
}
return obj
},
create<I extends Exact<DeepPartial<DeleteNonFavoritedTasksResults>, I>>(base?: I): DeleteNonFavoritedTasksResults {
return DeleteNonFavoritedTasksResults.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<DeleteNonFavoritedTasksResults>, I>>(object: I): DeleteNonFavoritedTasksResults {
const message = createBaseDeleteNonFavoritedTasksResults()
message.tasksPreserved = object.tasksPreserved ?? 0
message.tasksDeleted = object.tasksDeleted ?? 0
return message
},
}
function createBaseGetTaskHistoryRequest(): GetTaskHistoryRequest {
return { metadata: undefined, favoritesOnly: false, searchQuery: "", sortBy: "" }
}
export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
encode(message: GetTaskHistoryRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.favoritesOnly !== false) {
writer.uint32(16).bool(message.favoritesOnly)
}
if (message.searchQuery !== "") {
writer.uint32(26).string(message.searchQuery)
}
if (message.sortBy !== "") {
writer.uint32(34).string(message.sortBy)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): GetTaskHistoryRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseGetTaskHistoryRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 16) {
break
}
message.favoritesOnly = reader.bool()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.searchQuery = reader.string()
continue
}
case 4: {
if (tag !== 34) {
break
}
message.sortBy = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): GetTaskHistoryRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
favoritesOnly: isSet(object.favoritesOnly) ? globalThis.Boolean(object.favoritesOnly) : false,
searchQuery: isSet(object.searchQuery) ? globalThis.String(object.searchQuery) : "",
sortBy: isSet(object.sortBy) ? globalThis.String(object.sortBy) : "",
}
},
toJSON(message: GetTaskHistoryRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.favoritesOnly !== false) {
obj.favoritesOnly = message.favoritesOnly
}
if (message.searchQuery !== "") {
obj.searchQuery = message.searchQuery
}
if (message.sortBy !== "") {
obj.sortBy = message.sortBy
}
return obj
},
create<I extends Exact<DeepPartial<GetTaskHistoryRequest>, I>>(base?: I): GetTaskHistoryRequest {
return GetTaskHistoryRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<GetTaskHistoryRequest>, I>>(object: I): GetTaskHistoryRequest {
const message = createBaseGetTaskHistoryRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.favoritesOnly = object.favoritesOnly ?? false
message.searchQuery = object.searchQuery ?? ""
message.sortBy = object.sortBy ?? ""
return message
},
}
function createBaseTaskHistoryArray(): TaskHistoryArray {
return { tasks: [], totalCount: 0 }
}
export const TaskHistoryArray: MessageFns<TaskHistoryArray> = {
encode(message: TaskHistoryArray, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
for (const v of message.tasks) {
TaskItem.encode(v!, writer.uint32(10).fork()).join()
}
if (message.totalCount !== 0) {
writer.uint32(16).int32(message.totalCount)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): TaskHistoryArray {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseTaskHistoryArray()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.tasks.push(TaskItem.decode(reader, reader.uint32()))
continue
}
case 2: {
if (tag !== 16) {
break
}
message.totalCount = reader.int32()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): TaskHistoryArray {
return {
tasks: globalThis.Array.isArray(object?.tasks) ? object.tasks.map((e: any) => TaskItem.fromJSON(e)) : [],
totalCount: isSet(object.totalCount) ? globalThis.Number(object.totalCount) : 0,
}
},
toJSON(message: TaskHistoryArray): unknown {
const obj: any = {}
if (message.tasks?.length) {
obj.tasks = message.tasks.map((e) => TaskItem.toJSON(e))
}
if (message.totalCount !== 0) {
obj.totalCount = Math.round(message.totalCount)
}
return obj
},
create<I extends Exact<DeepPartial<TaskHistoryArray>, I>>(base?: I): TaskHistoryArray {
return TaskHistoryArray.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<TaskHistoryArray>, I>>(object: I): TaskHistoryArray {
const message = createBaseTaskHistoryArray()
message.tasks = object.tasks?.map((e) => TaskItem.fromPartial(e)) || []
message.totalCount = object.totalCount ?? 0
return message
},
}
function createBaseTaskItem(): TaskItem {
return {
id: "",
task: "",
ts: 0,
isFavorited: false,
size: 0,
totalCost: 0,
tokensIn: 0,
tokensOut: 0,
cacheWrites: 0,
cacheReads: 0,
}
}
export const TaskItem: MessageFns<TaskItem> = {
encode(message: TaskItem, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.id !== "") {
writer.uint32(10).string(message.id)
}
if (message.task !== "") {
writer.uint32(18).string(message.task)
}
if (message.ts !== 0) {
writer.uint32(24).int64(message.ts)
}
if (message.isFavorited !== false) {
writer.uint32(32).bool(message.isFavorited)
}
if (message.size !== 0) {
writer.uint32(40).int64(message.size)
}
if (message.totalCost !== 0) {
writer.uint32(49).double(message.totalCost)
}
if (message.tokensIn !== 0) {
writer.uint32(56).int32(message.tokensIn)
}
if (message.tokensOut !== 0) {
writer.uint32(64).int32(message.tokensOut)
}
if (message.cacheWrites !== 0) {
writer.uint32(72).int32(message.cacheWrites)
}
if (message.cacheReads !== 0) {
writer.uint32(80).int32(message.cacheReads)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): TaskItem {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseTaskItem()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.id = reader.string()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.task = reader.string()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.ts = longToNumber(reader.int64())
continue
}
case 4: {
if (tag !== 32) {
break
}
message.isFavorited = reader.bool()
continue
}
case 5: {
if (tag !== 40) {
break
}
message.size = longToNumber(reader.int64())
continue
}
case 6: {
if (tag !== 49) {
break
}
message.totalCost = reader.double()
continue
}
case 7: {
if (tag !== 56) {
break
}
message.tokensIn = reader.int32()
continue
}
case 8: {
if (tag !== 64) {
break
}
message.tokensOut = reader.int32()
continue
}
case 9: {
if (tag !== 72) {
break
}
message.cacheWrites = reader.int32()
continue
}
case 10: {
if (tag !== 80) {
break
}
message.cacheReads = reader.int32()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): TaskItem {
return {
id: isSet(object.id) ? globalThis.String(object.id) : "",
task: isSet(object.task) ? globalThis.String(object.task) : "",
ts: isSet(object.ts) ? globalThis.Number(object.ts) : 0,
isFavorited: isSet(object.isFavorited) ? globalThis.Boolean(object.isFavorited) : false,
size: isSet(object.size) ? globalThis.Number(object.size) : 0,
totalCost: isSet(object.totalCost) ? globalThis.Number(object.totalCost) : 0,
tokensIn: isSet(object.tokensIn) ? globalThis.Number(object.tokensIn) : 0,
tokensOut: isSet(object.tokensOut) ? globalThis.Number(object.tokensOut) : 0,
cacheWrites: isSet(object.cacheWrites) ? globalThis.Number(object.cacheWrites) : 0,
cacheReads: isSet(object.cacheReads) ? globalThis.Number(object.cacheReads) : 0,
}
},
toJSON(message: TaskItem): unknown {
const obj: any = {}
if (message.id !== "") {
obj.id = message.id
}
if (message.task !== "") {
obj.task = message.task
}
if (message.ts !== 0) {
obj.ts = Math.round(message.ts)
}
if (message.isFavorited !== false) {
obj.isFavorited = message.isFavorited
}
if (message.size !== 0) {
obj.size = Math.round(message.size)
}
if (message.totalCost !== 0) {
obj.totalCost = message.totalCost
}
if (message.tokensIn !== 0) {
obj.tokensIn = Math.round(message.tokensIn)
}
if (message.tokensOut !== 0) {
obj.tokensOut = Math.round(message.tokensOut)
}
if (message.cacheWrites !== 0) {
obj.cacheWrites = Math.round(message.cacheWrites)
}
if (message.cacheReads !== 0) {
obj.cacheReads = Math.round(message.cacheReads)
}
return obj
},
create<I extends Exact<DeepPartial<TaskItem>, I>>(base?: I): TaskItem {
return TaskItem.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<TaskItem>, I>>(object: I): TaskItem {
const message = createBaseTaskItem()
message.id = object.id ?? ""
message.task = object.task ?? ""
message.ts = object.ts ?? 0
message.isFavorited = object.isFavorited ?? false
message.size = object.size ?? 0
message.totalCost = object.totalCost ?? 0
message.tokensIn = object.tokensIn ?? 0
message.tokensOut = object.tokensOut ?? 0
message.cacheWrites = object.cacheWrites ?? 0
message.cacheReads = object.cacheReads ?? 0
return message
},
}
export type TaskServiceDefinition = typeof TaskServiceDefinition
export const TaskServiceDefinition = {
name: "TaskService",
@@ -156,7 +995,7 @@ export const TaskServiceDefinition = {
name: "showTaskWithId",
requestType: StringRequest,
requestStream: false,
responseType: Empty,
responseType: TaskResponse,
responseStream: false,
options: {},
},
@@ -169,6 +1008,33 @@ export const TaskServiceDefinition = {
responseStream: false,
options: {},
},
/** Toggles the favorite status of a task */
toggleTaskFavorite: {
name: "toggleTaskFavorite",
requestType: TaskFavoriteRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
/** Deletes all non-favorited tasks */
deleteNonFavoritedTasks: {
name: "deleteNonFavoritedTasks",
requestType: EmptyRequest,
requestStream: false,
responseType: DeleteNonFavoritedTasksResults,
responseStream: false,
options: {},
},
/** Gets filtered task history */
getTaskHistory: {
name: "getTaskHistory",
requestType: GetTaskHistoryRequest,
requestStream: false,
responseType: TaskHistoryArray,
responseStream: false,
options: {},
},
},
} as const
@@ -189,6 +1055,17 @@ export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
function longToNumber(int64: { toString(): string }): number {
const num = globalThis.Number(int64.toString())
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER")
}
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER")
}
return num
}
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
+208
View File
@@ -0,0 +1,208 @@
/**
* github-url-utils.ts
*
* Portable utility functions for creating and opening GitHub issue URLs
* with proper URL encoding that bypasses VS Code's URI handling issues.
*
* This utility addresses a longstanding issue in VS Code's URI handling:
* https://github.com/microsoft/vscode/issues/85930
*
* The issue causes URLs with special characters in query parameters to be incorrectly
* encoded when opened through VS Code's standard APIs (vscode.Uri.parse followed by
* vscode.env.openExternal). This particularly affects GitHub issue URLs with pre-filled
* fields containing special characters.
*/
import * as vscode from "vscode"
import * as cp from "child_process"
import * as os from "os"
import * as util from "util"
/**
* Creates a properly encoded GitHub issue URL.
*
* This function manually encodes each parameter value using encodeURIComponent()
* to ensure consistent and correct encoding of all special characters. This is
* necessary because VS Code's URI handling (vscode.Uri.parse) has issues with
* encoding/decoding URL parameters, as documented in:
* https://github.com/microsoft/vscode/issues/85930
*
* Specifically, VS Code's URI handling:
* - Double-encodes some characters like # (hash) becoming %2523 instead of %23
* - Inconsistently handles other characters like & (ampersand) and + (plus)
* - Can corrupt query parameters containing special characters
*
* @param baseUrl The base GitHub repository URL (e.g., 'https://github.com/owner/repo/issues/new')
* @param params Map of parameter names to values for the issue form
* @returns The properly encoded full URL
*/
export function createGitHubIssueUrl(baseUrl: string, params: Map<string, string>): string {
// Build query string manually with proper encoding
const queryParts: string[] = []
for (const [key, value] of params.entries()) {
const encodedKey = encodeURIComponent(key)
const encodedValue = encodeURIComponent(value)
queryParts.push(`${encodedKey}=${encodedValue}`)
}
// Determine the proper separator (? or &) based on whether baseUrl already has parameters
const separator = baseUrl.includes("?") ? "&" : "?"
// Join all parts to create the final URL
const queryString = queryParts.join("&")
return `${baseUrl}${separator}${queryString}`
}
/**
* Opens a URL using platform-specific commands to bypass VS Code's URI handling issues.
*
* IMPORTANT: This function intentionally avoids using VS Code's built-in URI handling
* (vscode.Uri.parse() and vscode.env.openExternal()) due to known encoding issues with URLs
* that contain special characters in query parameters. See:
* https://github.com/microsoft/vscode/issues/85930
*
* The specific issues with VS Code's URI handling include:
* 1. Double-encoding of certain characters (e.g., # becomes %23 then %2523)
* 2. Inconsistent handling where some characters are encoded and others are decoded
* 3. Issues with parameters in the query string being incorrectly processed
*
* Instead, this function:
* - Uses direct OS commands to open the browser with the URL
* - Preserves the exact encoding of the URL as provided
* - Provides multiple fallback approaches if the primary method fails
*
* @param url The URL to open
* @returns A promise that resolves when an attempt to open the URL has completed
*/
export async function openUrlInBrowser(url: string): Promise<void> {
// For debugging
console.log(`Opening URL: ${url}`)
// Always copy to clipboard as a fallback
try {
await vscode.env.clipboard.writeText(url)
console.log("URL copied to clipboard as backup")
} catch (error) {
console.error(`Failed to copy URL to clipboard: ${error}`)
}
// Try to open the URL using platform-specific commands
try {
const platform = os.platform()
console.log(`Detected platform: ${platform}`)
// Use promisify for better async error handling
const execPromise = util.promisify(cp.exec)
// Use platform-specific commands
if (platform === "win32") {
// Windows - try multiple approaches
try {
await execPromise(`start "" "${url}"`)
console.log("Opened URL with Windows 'start' command")
return
} catch (winError) {
console.error(`Error with Windows 'start' command: ${winError}`)
try {
await execPromise(`powershell.exe -Command "Start-Process '${url}'"`)
console.log("Opened URL with PowerShell command")
return
} catch (psError) {
console.error(`Error with PowerShell command: ${psError}`)
// Fall through to the fallbacks
}
}
} else if (platform === "darwin") {
// macOS
await execPromise(`open "${url}"`)
console.log("Opened URL with macOS 'open' command")
return
} else {
// Linux and others - try multiple commands
const linuxCommands = ["xdg-open", "gnome-open", "kde-open", "wslview"]
for (const cmd of linuxCommands) {
try {
await execPromise(`${cmd} "${url}"`)
console.log(`Opened URL with '${cmd}' command`)
return
} catch (cmdError) {
console.error(`Error with '${cmd}' command: ${cmdError}`)
// Try next command
}
}
}
// If we got here, none of the OS commands worked
throw new Error("All OS commands failed")
} catch (error) {
console.error(`OS commands failed: ${error}`)
// First fallback: Try VS Code's openExternal
// Note: This will likely have encoding issues per https://github.com/microsoft/vscode/issues/85930
// but we include it as a fallback in case OS commands completely fail
try {
// The 'true' parameter might help preserve some encodings, but this is not guaranteed
await vscode.env.openExternal(vscode.Uri.parse(url, true))
console.log("Opened URL with vscode.env.openExternal (note: URL encoding may be affected)")
return
} catch (vscodeError) {
console.error(`Error with vscode.env.openExternal: ${vscodeError}`)
// Last fallback: Show a message with instructions
vscode.window
.showInformationMessage(
"Couldn't open the URL automatically. It has been copied to your clipboard.",
"Copy URL Again",
)
.then((selection) => {
if (selection === "Copy URL Again") {
vscode.env.clipboard.writeText(url)
}
})
}
}
}
/**
* Utility function to create and open a GitHub issue with the specified parameters.
*
* This is a high-level function that combines URL creation and opening while
* working around VS Code's URI handling limitations (issue #85930). It provides
* a simple API for the common use case of opening GitHub issue templates with
* pre-filled fields.
*
* The function:
* 1. Constructs a correctly formatted GitHub issue URL
* 2. Properly encodes all special characters in parameters
* 3. Opens the URL directly using OS commands to avoid VS Code's problematic URI handling
* 4. Provides fallback options if opening fails
*
* Reference for the VS Code URI handling issue:
* https://github.com/microsoft/vscode/issues/85930
*
* @param repoOwner GitHub repository owner/organization
* @param repoName GitHub repository name
* @param issueTemplate Template name to use (e.g., 'bug_report.yml')
* @param params Map of parameter names to values for the issue form
*/
export async function createAndOpenGitHubIssue(
repoOwner: string,
repoName: string,
issueTemplate: string | null,
params: Map<string, string>,
): Promise<void> {
// Construct the base URL
let baseUrl = `https://github.com/${repoOwner}/${repoName}/issues/new`
// Add template parameter if provided
if (issueTemplate) {
params.set("template", issueTemplate)
}
// Create the URL and open it
const issueUrl = createGitHubIssueUrl(baseUrl, params)
await openUrlInBrowser(issueUrl)
}
+24
View File
@@ -0,0 +1,24 @@
import { useEffect, type ReactNode } from "react"
import { PostHogProvider } from "posthog-js/react"
import posthog from "posthog-js"
import { posthogConfig } from "@shared/services/config/posthog-config"
import { useExtensionState } from "./context/ExtensionStateContext"
export function CustomPostHogProvider({ children }: { children: ReactNode }) {
const { telemetrySetting } = useExtensionState()
const isTelemetryEnabled = telemetrySetting === "enabled"
useEffect(() => {
if (isTelemetryEnabled) {
posthog.init(posthogConfig.apiKey, {
api_host: posthogConfig.host,
autocapture: false,
disable_session_recording: true,
})
} else {
posthog.opt_out_capturing()
}
}, [isTelemetryEnabled])
return <PostHogProvider client={posthog}>{children}</PostHogProvider>
}
+4 -11
View File
@@ -1,25 +1,18 @@
import type { ReactNode } from "react"
import { type ReactNode } from "react"
import { ExtensionStateContextProvider } from "./context/ExtensionStateContext"
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { HeroUIProvider } from "@heroui/react"
import { PostHogProvider } from "posthog-js/react"
import { posthogConfig } from "@shared/services/config/posthog-config"
import posthog from "posthog-js"
posthog.init(posthogConfig.apiKey, {
api_host: posthogConfig.host,
autocapture: false,
})
import { CustomPostHogProvider } from "./CustomPostHogProvider"
export function Providers({ children }: { children: ReactNode }) {
return (
<ExtensionStateContextProvider>
<PostHogProvider client={posthog}>
<CustomPostHogProvider>
<FirebaseAuthProvider>
<HeroUIProvider>{children}</HeroUIProvider>
</FirebaseAuthProvider>
</PostHogProvider>
</CustomPostHogProvider>
</ExtensionStateContextProvider>
)
}
+11
View File
@@ -0,0 +1,11 @@
import { SVGProps } from "react"
const ClineLogoBlack = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" width="47" height="50" viewBox="0 0 47 50" fill="none" {...props}>
<path
d="M46.4075 28.1192L43.5011 22.3166V18.9747C43.5011 13.4354 39.0302 8.94931 33.5162 8.94931H28.5491C28.9086 8.21513 29.106 7.3898 29.106 6.5189C29.106 3.44039 26.6149 0.949219 23.5363 0.949219C20.4578 0.949219 17.9667 3.44039 17.9667 6.5189C17.9667 7.3898 18.1641 8.21513 18.5236 8.94931H13.5565C8.04249 8.94931 3.57155 13.4354 3.57155 18.9747V22.3166L0.604424 28.104C0.305687 28.6863 0.305687 29.3799 0.604424 29.9622L3.57155 35.6838V39.0256C3.57155 44.5649 8.04249 49.0511 13.5565 49.0511H33.5162C39.0302 49.0511 43.5011 44.5649 43.5011 39.0256V35.6838L46.4024 29.942C46.691 29.3698 46.691 28.6964 46.4075 28.1192ZM20.4983 32.8483C20.4983 35.3648 18.4578 37.4053 15.9413 37.4053C13.4248 37.4053 11.3843 35.3648 11.3843 32.8483V24.747C11.3843 22.2305 13.4248 20.19 15.9413 20.19C18.4578 20.19 20.4983 22.2305 20.4983 24.747V32.8483ZM35.182 32.8483C35.182 35.3648 33.1415 37.4053 30.625 37.4053C28.1085 37.4053 26.068 35.3648 26.068 32.8483V24.747C26.068 22.2305 28.1085 20.19 30.625 20.19C33.1415 20.19 35.182 22.2305 35.182 24.747V32.8483Z"
fill="black"
/>
</svg>
)
export default ClineLogoBlack
@@ -0,0 +1,21 @@
import { SVGProps } from "react"
/**
* ClineLogoVariable component renders the Cline logo with automatic theme adaptation.
*
* This component uses the VS Code theme variable `--vscode-icon-foreground` for the fill color,
* which automatically adjusts based on the active VS Code theme (light, dark, high contrast)
* to ensure optimal contrast with the background.
*
* @param {SVGProps<SVGSVGElement>} props - Standard SVG props including className, style, etc.
* @returns {JSX.Element} SVG Cline logo that adapts to VS Code themes
*/
const ClineLogoVariable = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" width="47" height="50" viewBox="0 0 47 50" fill="none" {...props}>
<path
d="M46.4075 28.1192L43.5011 22.3166V18.9747C43.5011 13.4354 39.0302 8.94931 33.5162 8.94931H28.5491C28.9086 8.21513 29.106 7.3898 29.106 6.5189C29.106 3.44039 26.6149 0.949219 23.5363 0.949219C20.4578 0.949219 17.9667 3.44039 17.9667 6.5189C17.9667 7.3898 18.1641 8.21513 18.5236 8.94931H13.5565C8.04249 8.94931 3.57155 13.4354 3.57155 18.9747V22.3166L0.604424 28.104C0.305687 28.6863 0.305687 29.3799 0.604424 29.9622L3.57155 35.6838V39.0256C3.57155 44.5649 8.04249 49.0511 13.5565 49.0511H33.5162C39.0302 49.0511 43.5011 44.5649 43.5011 39.0256V35.6838L46.4024 29.942C46.691 29.3698 46.691 28.6964 46.4075 28.1192ZM20.4983 32.8483C20.4983 35.3648 18.4578 37.4053 15.9413 37.4053C13.4248 37.4053 11.3843 35.3648 11.3843 32.8483V24.747C11.3843 22.2305 13.4248 20.19 15.9413 20.19C18.4578 20.19 20.4983 22.2305 20.4983 24.747V32.8483ZM35.182 32.8483C35.182 35.3648 33.1415 37.4053 30.625 37.4053C28.1085 37.4053 26.068 35.3648 26.068 32.8483V24.747C26.068 22.2305 28.1085 20.19 30.625 20.19C33.1415 20.19 35.182 22.2305 35.182 24.747V32.8483Z"
fill="var(--vscode-icon-foreground)"
/>
</svg>
)
export default ClineLogoVariable
+20 -69
View File
@@ -44,22 +44,23 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
</h3>
<ul style={ulStyle}>
<li>
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price tracking for
Gemini models.
<b>Task Timeline:</b> See the history of your coding journey with a visual timeline of checkpoints, letting
you understand what Cline did at a glance.
</li>
<li>
<b>Copy Buttons:</b> Buttons were added to Markdown and Code blocks that allow you to copy their contents
easily.
<b>UX Improvements:</b> Type while Cline works, smarter auto-scrolling, new copy buttons for task headers and
messages, and a simplified home interface for a smoother experience.
</li>
<li>
<b>/newrule command:</b> New slash command to have cline write your .clinerules for you based on your
workflow.
<b>Commit Message Generation:</b> Let Cline help craft meaningful commit messages based on your changes.
</li>
<li>
<b>Drag and drop improvements:</b> Don't forget to hold shift while dragging files!
<b>Quote Replies:</b> Easily reference previous messages with new quote reply support for clearer
conversations.
</li>
<li>
<b>Auto Caching for Gemini:</b> Native support for Gemini's recently released Implicit Caching.
</li>
<li>Added more checkpoints across the task, allowing you to restore from more than just file changes.</li>
<li>Added support for rendering LaTeX in message responses. (Try asking Cline to show the quadratic formula)</li>
</ul>
<Accordion isCompact className="pl-0">
<AccordionItem
@@ -74,74 +75,24 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
}}>
<ul style={ulStyle}>
<li>
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price
tracking for Gemini models.
</li>
<li>
<b>Copy Buttons:</b> Buttons were added to Markdown and Code blocks that allow you to copy their
contents easily.
</li>
<li>
<b>Global Cline Rules:</b> Store multiple rules files in Documents/Cline/Rules to share between
projects.
</li>
<li>
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files
to plug and play specific rules for the task
</li>
<li>
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a
new task (more coming soon!)
</li>
<li>
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally
restore your project when the message was sent!
new task.
</li>
</ul>
</AccordionItem>
</Accordion>
{/*
// Leave this here for an example of how to structure the announcement
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
so I recommend trying them out.
<br />
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink
href={getOpenRouterAuthUrl(vscodeUriScheme)}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Get OpenRouter API Key
</VSCodeButtonLink>
)}
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
<VSCodeButton
onClick={() => {
vscode.postMessage({
type: "apiConfiguration",
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
})
}}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Switch to OpenRouter
</VSCodeButton>
)}
</li>
<li>
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
</li>
<li>
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
</li>
<li>
When Cline runs commands, you can now type directly in the terminal (+ support for Python
environments)
</li>
</ul>*/}
<div style={hrStyle} />
<p style={linkContainerStyle}>
Join us on{" "}
@@ -1,488 +0,0 @@
import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useMemo, useRef, useState } from "react"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { vscode } from "@/utils/vscode"
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { useClickAway } from "react-use"
interface AutoApproveMenuProps {
style?: React.CSSProperties
}
const SubOptionAnimateIn = styled.div<{ show: boolean }>`
max-height: ${(props) => (props.show ? "100px" : "0")};
opacity: ${(props) => (props.show ? "1" : "0")};
overflow: hidden;
transition:
max-height 0.2s ease-in-out,
opacity 0.2s ease-in-out;
`
const ACTION_METADATA: {
id: keyof AutoApprovalSettings["actions"]
label: string
shortName: string
description: string
}[] = [
{
id: "readFiles",
label: "Read project files",
shortName: "Read Local",
description: "Allows Cline to read files within your workspace.",
},
{
id: "readFilesExternally",
label: "Read all files",
shortName: "Read (all)",
description: "Allows Cline to read any file on your computer.",
},
{
id: "editFiles",
label: "Edit project files",
shortName: "Edit",
description: "Allows Cline to modify files within your workspace.",
},
{
id: "editFilesExternally",
label: "Edit all files",
shortName: "Edit (all)",
description: "Allows Cline to modify any file on your computer.",
},
{
id: "executeSafeCommands",
label: "Execute safe commands",
shortName: "Safe Commands",
description:
"Allows Cline to execute of safe terminal commands. If the model determines a command is potentially destructive, it will still require approval.",
},
{
id: "executeAllCommands",
label: "Execute all commands",
shortName: "All Commands",
description: "Allows Cline to execute all terminal commands. Use at your own risk.",
},
{
id: "useBrowser",
label: "Use the browser",
shortName: "Browser",
description: "Allows Cline to launch and interact with any website in a browser.",
},
{
id: "useMcp",
label: "Use MCP servers",
shortName: "MCP",
description: "Allows Cline to use configured MCP servers which may modify filesystem or interact with APIs.",
},
]
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
const { autoApprovalSettings } = useExtensionState()
const [isExpanded, setIsExpanded] = useState(false)
const [isHoveringCollapsibleSection, setIsHoveringCollapsibleSection] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
// Careful not to use partials to mutate since spread operator only does shallow copy
const enabledActions = ACTION_METADATA.filter((action) => autoApprovalSettings.actions[action.id])
const enabledActionsList = useMemo(() => {
// When nested auto-approve options are used, display the more permissive one (file reads, edits, and commands)
const readFilesEnabled = enabledActions.some((action) => action.id === "readFiles")
const readFilesExternallyEnabled = enabledActions.some((action) => action.id === "readFilesExternally")
const editFilesEnabled = enabledActions.some((action) => action.id === "editFiles")
const editFilesExternallyEnabled = enabledActions.some((action) => action.id === "editFilesExternally") ?? false
const safeCommandsEnabled = enabledActions.some((action) => action.id === "executeSafeCommands")
const allCommandsEnabled = enabledActions.some((action) => action.id === "executeAllCommands") ?? false
// Filter out the potentially nested options so we don't display them twice
const otherActions = enabledActions
.filter(
(action) =>
action.id !== "readFiles" &&
action.id !== "readFilesExternally" &&
action.id !== "editFiles" &&
action.id !== "editFilesExternally" &&
action.id !== "executeSafeCommands" &&
action.id !== "executeAllCommands",
)
.map((action) => action.shortName)
const labels = []
// Handle read editing labels
if ((readFilesExternallyEnabled ?? false) && readFilesEnabled) {
labels.push("Read (All)")
} else if (readFilesEnabled) {
labels.push("Read")
}
// Handle file editing labels
if ((editFilesExternallyEnabled ?? false) && editFilesEnabled) {
labels.push("Edit (All)")
} else if (editFilesEnabled) {
labels.push("Edit")
}
// Handle command execution labels
if ((allCommandsEnabled ?? false) && safeCommandsEnabled) {
labels.push("All Commands")
} else if (safeCommandsEnabled) {
labels.push("Safe Commands")
}
// Add remaining actions
return [...labels, ...otherActions].join(", ")
}, [enabledActions])
// This value is used to determine if the auto-approve menu should show 'Auto-approve: None'
// Note: we should use better logic to determine the state where no auto approve actions are in effect, regardless of the state of sub-auto-approve options
const hasEnabledActions = useMemo(() => {
let enabledActionsCount = enabledActions.length
if (!autoApprovalSettings.actions.readFiles && autoApprovalSettings.actions.readFilesExternally) {
enabledActionsCount--
}
if (!autoApprovalSettings.actions.editFiles && autoApprovalSettings.actions.editFilesExternally) {
enabledActionsCount--
}
if (!autoApprovalSettings.actions.executeSafeCommands && autoApprovalSettings.actions.executeAllCommands) {
enabledActionsCount--
}
return enabledActionsCount > 0
}, [enabledActions, autoApprovalSettings.actions])
// Get the full extension state to ensure we have the most up-to-date settings
const extensionState = useExtensionState()
const updateEnabled = useCallback(
(enabled: boolean) => {
const currentSettings = extensionState.autoApprovalSettings
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
enabled,
},
})
},
[extensionState.autoApprovalSettings],
)
const updateAction = useCallback(
(actionId: keyof AutoApprovalSettings["actions"], value: boolean) => {
const currentSettings = extensionState.autoApprovalSettings
// Calculate what the new actions state will be
const newActions = {
...currentSettings.actions,
[actionId]: value,
}
// Check if this will result in any enabled actions
const willHaveEnabledActions = Object.values(newActions).some(Boolean)
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
actions: newActions,
// If no actions will be enabled, ensure the main toggle is off
enabled: willHaveEnabledActions ? currentSettings.enabled : false,
},
})
},
[extensionState.autoApprovalSettings],
)
const updateMaxRequests = useCallback(
(maxRequests: number) => {
const currentSettings = extensionState.autoApprovalSettings
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
maxRequests,
},
})
},
[extensionState.autoApprovalSettings],
)
const updateNotifications = useCallback(
(enableNotifications: boolean) => {
const currentSettings = extensionState.autoApprovalSettings
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
enableNotifications,
},
})
},
[extensionState.autoApprovalSettings],
)
// Handle clicks outside the menu to close it
useClickAway(menuRef, () => {
if (isExpanded) {
setIsExpanded(false)
}
})
return (
<div
ref={menuRef}
style={{
padding: "0 15px",
userSelect: "none",
borderTop: isExpanded
? `0.5px solid color-mix(in srgb, ${getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND)} 20%, transparent)`
: "none",
overflowY: "auto",
backgroundColor: isExpanded ? CODE_BLOCK_BG_COLOR : "transparent",
...style,
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: isExpanded ? "8px 0" : "8px 0 0 0",
cursor: !hasEnabledActions ? "pointer" : "default",
}}
onMouseEnter={() => {
if (!hasEnabledActions) {
setIsHoveringCollapsibleSection(true)
}
}}
onMouseLeave={() => {
if (!hasEnabledActions) {
setIsHoveringCollapsibleSection(false)
}
}}
onClick={() => {
if (!hasEnabledActions) {
setIsExpanded((prev) => !prev)
}
}}>
<VSCodeCheckbox
style={{
pointerEvents: hasEnabledActions ? "auto" : "none",
}}
checked={hasEnabledActions && autoApprovalSettings.enabled}
disabled={!hasEnabledActions}
// onChange={(e) => {
// const checked = (e.target as HTMLInputElement).checked
// updateEnabled(checked)
// }}
onClick={(e) => {
/*
vscode web toolkit bug: when changing the value of a vscodecheckbox programmatically, it will call its onChange with stale state. This led to updateEnabled being called with an old version of autoApprovalSettings, effectively undoing the state change that was triggered by the last action being unchecked. A simple workaround is to just not use onChange and instead use onClick. We are lucky this is a checkbox and the newvalue is simply opposite of current state.
*/
if (!hasEnabledActions) return
e.stopPropagation() // stops click from bubbling up to the parent, in this case stopping the expanding/collapsing
updateEnabled(!autoApprovalSettings.enabled)
}}
/>
<CollapsibleSection
isHovered={isHoveringCollapsibleSection}
style={{ cursor: "pointer" }}
onClick={() => {
// to prevent this from counteracting parent
if (hasEnabledActions) {
setIsExpanded((prev) => !prev)
}
}}>
<span
style={{
color: getAsVar(VSC_FOREGROUND),
whiteSpace: "nowrap",
}}>
Auto-approve:
</span>
<span
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}>
{!hasEnabledActions ? "None" : enabledActionsList}
</span>
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
style={{
flexShrink: 0,
marginLeft: isExpanded ? "2px" : "-2px",
}}
/>
</CollapsibleSection>
</div>
{isExpanded && (
<div style={{ padding: "0" }}>
<div
style={{
marginBottom: "10px",
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
}}>
Auto-approve allows Cline to perform the following actions without asking for permission. Please use with
caution and only enable if you understand the risks.
</div>
{ACTION_METADATA.map((action) => {
// Handle readFilesExternally, editFilesExternally, and executeAllCommands as animated sub-options
if (
action.id === "executeAllCommands" ||
action.id === "editFilesExternally" ||
action.id === "readFilesExternally"
) {
const parentAction =
action.id === "executeAllCommands"
? "executeSafeCommands"
: action.id === "readFilesExternally"
? "readFiles"
: "editFiles"
return (
<SubOptionAnimateIn key={action.id} show={autoApprovalSettings.actions[parentAction] ?? false}>
<div
style={{
margin: "3px 0",
marginLeft: "28px",
}}>
<VSCodeCheckbox
checked={autoApprovalSettings.actions[action.id]}
onChange={(e) => {
const checked = (e.target as HTMLInputElement).checked
updateAction(action.id, checked)
}}>
{action.label}
</VSCodeCheckbox>
<div
style={{
marginLeft: "28px",
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
}}>
{action.description}
</div>
</div>
</SubOptionAnimateIn>
)
}
return (
<div
key={action.id}
style={{
margin: "6px 0",
}}>
<VSCodeCheckbox
checked={autoApprovalSettings.actions[action.id]}
onChange={(e) => {
const checked = (e.target as HTMLInputElement).checked
updateAction(action.id, checked)
}}>
{action.label}
</VSCodeCheckbox>
<div
style={{
marginLeft: "28px",
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
}}>
{action.description}
</div>
</div>
)
})}
<div
style={{
height: "0.5px",
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
margin: "15px 0",
opacity: 0.2,
}}
/>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginTop: "10px",
marginBottom: "8px",
color: getAsVar(VSC_FOREGROUND),
}}>
<span style={{ flexShrink: 1, minWidth: 0 }}>Max Requests:</span>
<VSCodeTextField
// placeholder={DEFAULT_AUTO_APPROVAL_SETTINGS.maxRequests.toString()}
value={autoApprovalSettings.maxRequests.toString()}
onInput={(e) => {
const input = e.target as HTMLInputElement
// Remove any non-numeric characters
input.value = input.value.replace(/[^0-9]/g, "")
const value = parseInt(input.value)
if (!isNaN(value) && value > 0) {
updateMaxRequests(value)
}
}}
onKeyDown={(e) => {
// Prevent non-numeric keys (except for backspace, delete, arrows)
if (!/^\d$/.test(e.key) && !["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(e.key)) {
e.preventDefault()
}
}}
style={{ flex: 1 }}
/>
</div>
<div
style={{
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
marginBottom: "10px",
}}>
Cline will automatically make this many API requests before asking for approval to proceed with the task.
</div>
<div style={{ margin: "6px 0" }}>
<VSCodeCheckbox
checked={autoApprovalSettings.enableNotifications}
onChange={(e) => {
const checked = (e.target as HTMLInputElement).checked
updateNotifications(checked)
}}>
Enable Notifications
</VSCodeCheckbox>
<div
style={{
marginLeft: "28px",
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
}}>
Receive system notifications when Cline requires approval to proceed or when a task is completed.
</div>
</div>
</div>
)}
</div>
)
}
const CollapsibleSection = styled.div<{ isHovered?: boolean }>`
display: flex;
align-items: center;
gap: 4px;
color: ${(props) => (props.isHovered ? getAsVar(VSC_FOREGROUND) : getAsVar(VSC_DESCRIPTION_FOREGROUND))};
flex: 1;
min-width: 0;
&:hover {
color: ${getAsVar(VSC_FOREGROUND)};
}
`
export default AutoApproveMenu
+119 -40
View File
@@ -1,4 +1,4 @@
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import { VSCodeBadge, VSCodeProgressRing, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import deepEqual from "fast-deep-equal"
import React, { memo, useCallback, useEffect, useMemo, useRef, useState, MouseEvent } from "react"
@@ -20,6 +20,62 @@ import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils
import { vscode } from "@/utils/vscode"
import { FileServiceClient } from "@/services/grpc-client"
import { CheckmarkControl } from "@/components/common/CheckmarkControl"
interface CopyButtonProps {
textToCopy: string | undefined
}
const CopyButtonStyled = styled(VSCodeButton)`
position: absolute;
bottom: 2px;
right: 2px;
z-index: 1;
opacity: 0;
`
interface WithCopyButtonProps {
children: React.ReactNode
textToCopy?: string
style?: React.CSSProperties
ref?: React.Ref<HTMLDivElement>
onMouseUp?: (event: MouseEvent<HTMLDivElement>) => void
}
const StyledContainer = styled.div`
position: relative;
&:hover ${CopyButtonStyled} {
opacity: 1;
}
`
const WithCopyButton = React.forwardRef<HTMLDivElement, WithCopyButtonProps>(
({ children, textToCopy, style, onMouseUp, ...props }, ref) => {
const [copied, setCopied] = useState(false)
const handleCopy = () => {
if (!textToCopy) return
navigator.clipboard.writeText(textToCopy).then(() => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 1500)
})
}
return (
<StyledContainer ref={ref} onMouseUp={onMouseUp} style={style} {...props}>
{children}
{textToCopy && (
<CopyButtonStyled appearance="icon" onClick={handleCopy} aria-label={copied ? "Copied" : "Copy"}>
<span className={`codicon codicon-${copied ? "check" : "copy"}`}></span>
</CopyButtonStyled>
)}
</StyledContainer>
)
},
)
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
@@ -33,6 +89,7 @@ import { highlightText } from "./TaskHeader"
import SuccessButton from "@/components/common/SuccessButton"
import TaskFeedbackButtons from "@/components/chat/TaskFeedbackButtons"
import NewTaskPreview from "./NewTaskPreview"
import ReportBugPreview from "./ReportBugPreview"
import McpResourceRow from "@/components/mcp/configuration/tabs/installed/server-row/McpResourceRow"
import UserMessage from "./UserMessage"
import QuoteButton from "./QuoteButton"
@@ -90,6 +147,7 @@ const Markdown = memo(({ markdown }: { markdown?: string }) => {
overflowWrap: "anywhere",
marginBottom: -15,
marginTop: -15,
overflow: "hidden", // contain child margins so that parent diff matches height of children
}}>
<MarkdownBlock markdown={markdown} />
</div>
@@ -901,7 +959,7 @@ export const ChatRowContent = ({
return <McpResponseDisplay responseText={message.text || ""} />
case "text":
return (
<div ref={contentRef} onMouseUp={handleMouseUp} style={{ position: "relative" }}>
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={message.text}>
<Markdown markdown={message.text} />
{quoteButtonState.visible && (
<QuoteButton
@@ -912,7 +970,7 @@ export const ChatRowContent = ({
}}
/>
)}
</div>
</WithCopyButton>
)
case "reasoning":
return (
@@ -1132,13 +1190,13 @@ export const ChatRowContent = ({
}}
/>
</div>
<div
ref={contentRef} // Added ref
onMouseUp={handleMouseUp} // Added handler
<WithCopyButton
ref={contentRef}
onMouseUp={handleMouseUp}
textToCopy={text}
style={{
color: "var(--vscode-charts-green)",
paddingTop: 10,
position: "relative", // Added position
}}>
<Markdown markdown={text} />
{quoteButtonState.visible && (
@@ -1148,7 +1206,7 @@ export const ChatRowContent = ({
onClick={handleQuoteClick}
/>
)}
</div>
</WithCopyButton>
{message.partial !== true && hasChanges && (
<div style={{ paddingTop: 17 }}>
<SuccessButton
@@ -1295,13 +1353,13 @@ export const ChatRowContent = ({
}}
/>
</div>
<div
ref={contentRef} // Added ref
onMouseUp={handleMouseUp} // Added handler
<WithCopyButton
ref={contentRef}
onMouseUp={handleMouseUp}
textToCopy={text}
style={{
color: "var(--vscode-charts-green)",
paddingTop: 10,
position: "relative", // Added position
}}>
<Markdown markdown={text} />
{quoteButtonState.visible && (
@@ -1311,30 +1369,30 @@ export const ChatRowContent = ({
onClick={handleQuoteClick}
/>
)}
{message.partial !== true && hasChanges && (
<div style={{ marginTop: 15 }}>
<SuccessButton
appearance="secondary"
disabled={seeNewChangesDisabled}
onClick={() => {
setSeeNewChangesDisabled(true)
vscode.postMessage({
type: "taskCompletionViewChanges",
number: message.ts,
})
}}>
<i
className="codicon codicon-new-file"
style={{
marginRight: 6,
cursor: seeNewChangesDisabled ? "wait" : "pointer",
}}
/>
See new changes
</SuccessButton>
</div>
)}
</div>
</WithCopyButton>
{message.partial !== true && hasChanges && (
<div style={{ marginTop: 15 }}>
<SuccessButton
appearance="secondary"
disabled={seeNewChangesDisabled}
onClick={() => {
setSeeNewChangesDisabled(true)
vscode.postMessage({
type: "taskCompletionViewChanges",
number: message.ts,
})
}}>
<i
className="codicon codicon-new-file"
style={{
marginRight: 6,
cursor: seeNewChangesDisabled ? "wait" : "pointer",
}}
/>
See new changes
</SuccessButton>
</div>
)}
</div>
)
} else {
@@ -1362,7 +1420,11 @@ export const ChatRowContent = ({
{title}
</div>
)}
<div ref={contentRef} onMouseUp={handleMouseUp} style={{ position: "relative", paddingTop: 10 }}>
<WithCopyButton
ref={contentRef}
onMouseUp={handleMouseUp}
textToCopy={question}
style={{ paddingTop: 10 }}>
<Markdown markdown={question} />
<OptionsButtons
options={options}
@@ -1379,7 +1441,7 @@ export const ChatRowContent = ({
}}
/>
)}
</div>
</WithCopyButton>
</>
)
case "new_task":
@@ -1414,6 +1476,23 @@ export const ChatRowContent = ({
<NewTaskPreview context={message.text || ""} />
</>
)
case "report_bug":
return (
<>
<div style={headerStyle}>
<span
className="codicon codicon-new-file"
style={{
color: normalColor,
marginBottom: "-1.5px",
}}></span>
<span style={{ color: normalColor, fontWeight: "bold" }}>
Cline wants to create a Github issue:
</span>
</div>
<ReportBugPreview data={message.text || ""} />
</>
)
case "plan_mode_respond": {
let response: string | undefined
let options: string[] | undefined
@@ -1428,7 +1507,7 @@ export const ChatRowContent = ({
response = message.text
}
return (
<div ref={contentRef} onMouseUp={handleMouseUp} style={{ position: "relative" }}>
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={response}>
<Markdown markdown={response} />
<OptionsButtons
options={options}
@@ -1445,7 +1524,7 @@ export const ChatRowContent = ({
}}
/>
)}
</div>
</WithCopyButton>
)
}
default:
+98 -19
View File
@@ -40,11 +40,29 @@ import { ChatSettings } from "@shared/ChatSettings"
import ServersToggleModal from "./ServersToggleModal"
import ClineRulesToggleModal from "../cline-rules/ClineRulesToggleModal"
const getImageDimensions = (dataUrl: string): Promise<{ width: number; height: number }> => {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
if (img.naturalWidth > 7500 || img.naturalHeight > 7500) {
reject(new Error("Image dimensions exceed maximum allowed size of 7500px."))
} else {
resolve({ width: img.naturalWidth, height: img.naturalHeight })
}
}
img.onerror = (err) => {
console.error("Failed to load image for dimension check:", err)
reject(new Error("Failed to load image to check dimensions."))
}
img.src = dataUrl
})
}
interface ChatTextAreaProps {
inputValue: string
activeQuote: string | null
setInputValue: (value: string) => void
textAreaDisabled: boolean
sendingDisabled: boolean
placeholderText: string
selectedImages: string[]
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>
@@ -229,7 +247,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
inputValue,
activeQuote,
setInputValue,
textAreaDisabled,
sendingDisabled,
placeholderText,
selectedImages,
setSelectedImages,
@@ -276,6 +294,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const shiftHoldTimerRef = useRef<NodeJS.Timeout | null>(null)
const [showUnsupportedFileError, setShowUnsupportedFileError] = useState(false)
const unsupportedFileTimerRef = useRef<NodeJS.Timeout | null>(null)
const [showDimensionError, setShowDimensionError] = useState(false)
const dimensionErrorTimerRef = useRef<NodeJS.Timeout | null>(null)
const [fileSearchResults, setFileSearchResults] = useState<SearchResult[]>([])
const [searchLoading, setSearchLoading] = useState(false)
@@ -519,8 +539,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const isComposing = event.nativeEvent?.isComposing ?? false
if (event.key === "Enter" && !event.shiftKey && !isComposing) {
event.preventDefault()
setIsTextAreaFocused(false)
onSend()
if (!sendingDisabled) {
setIsTextAreaFocused(false)
onSend()
}
}
if (event.key === "Backspace" && !isComposing) {
@@ -604,6 +627,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
selectedSlashCommandsIndex,
slashCommandsQuery,
handleSlashCommandsSelect,
sendingDisabled,
],
)
@@ -729,6 +753,17 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
onFocusChange?.(false) // Call prop on blur
}, [isMouseDownOnMenu, onFocusChange])
const showDimensionErrorMessage = useCallback(() => {
setShowDimensionError(true)
if (dimensionErrorTimerRef.current) {
clearTimeout(dimensionErrorTimerRef.current)
}
dimensionErrorTimerRef.current = setTimeout(() => {
setShowDimensionError(false)
dimensionErrorTimerRef.current = null
}, 3000)
}, [])
const handlePaste = useCallback(
async (e: React.ClipboardEvent) => {
const items = e.clipboardData.items
@@ -774,13 +809,24 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
return
}
const reader = new FileReader()
reader.onloadend = () => {
reader.onloadend = async () => {
if (reader.error) {
console.error("Error reading file:", reader.error)
resolve(null)
} else {
const result = reader.result
resolve(typeof result === "string" ? result : null)
if (typeof result === "string") {
try {
await getImageDimensions(result)
resolve(result)
} catch (error) {
console.warn((error as Error).message)
showDimensionErrorMessage()
resolve(null)
}
} else {
resolve(null)
}
}
}
reader.readAsDataURL(blob)
@@ -796,7 +842,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
}
},
[shouldDisableImages, setSelectedImages, cursorPosition, setInputValue, inputValue],
[shouldDisableImages, setSelectedImages, cursorPosition, setInputValue, inputValue, showDimensionErrorMessage],
)
const handleThumbnailsHeightChange = useCallback((height: number) => {
@@ -916,8 +962,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
useShortcut("Meta+Shift+a", onModeToggle, { disableTextInputs: false }) // important that we don't disable the text input here
const handleContextButtonClick = useCallback(() => {
if (textAreaDisabled) return
// Focus the textarea first
textAreaRef.current?.focus()
@@ -956,7 +1000,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
} as React.ChangeEvent<HTMLTextAreaElement>
handleInputChange(event)
updateHighlights()
}, [inputValue, textAreaDisabled, handleInputChange, updateHighlights])
}, [inputValue, handleInputChange, updateHighlights])
// Use an effect to detect menu close
useEffect(() => {
@@ -1228,13 +1272,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
(file) =>
new Promise<string | null>((resolve) => {
const reader = new FileReader()
reader.onloadend = () => {
reader.onloadend = async () => {
// Make async
if (reader.error) {
console.error("Error reading file:", reader.error)
resolve(null)
} else {
const result = reader.result
resolve(typeof result === "string" ? result : null)
if (typeof result === "string") {
try {
await getImageDimensions(result) // Check dimensions
resolve(result)
} catch (error) {
console.warn((error as Error).message)
showDimensionErrorMessage() // Show error to user
resolve(null) // Don't add this image
}
} else {
resolve(null)
}
}
}
reader.readAsDataURL(file)
@@ -1248,7 +1304,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
<div
style={{
padding: "10px 15px",
opacity: textAreaDisabled ? 0.5 : 1,
opacity: 1,
position: "relative",
display: "flex",
// Drag-over styles moved to DynamicTextArea
@@ -1258,6 +1314,31 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
onDragOver={onDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}>
{showDimensionError && (
<div
style={{
position: "absolute",
inset: "10px 15px",
backgroundColor: "rgba(var(--vscode-errorForeground-rgb), 0.1)",
border: "2px solid var(--vscode-errorForeground)",
borderRadius: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 10, // Ensure it's above other elements
pointerEvents: "none",
}}>
<span
style={{
color: "var(--vscode-errorForeground)",
fontWeight: "bold",
fontSize: "12px",
textAlign: "center",
}}>
Image dimensions exceed 7500px
</span>
</div>
)}
{showUnsupportedFileError && (
<div
style={{
@@ -1358,7 +1439,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
textAreaRef.current = el
}}
value={inputValue}
disabled={textAreaDisabled}
onChange={(e) => {
handleInputChange(e)
updateHighlights()
@@ -1379,7 +1459,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
onHeightChange?.(height)
}}
placeholder={showUnsupportedFileError ? "" : placeholderText}
placeholder={showUnsupportedFileError || showDimensionError ? "" : placeholderText}
maxRows={10}
autoFocus={true}
style={{
@@ -1408,7 +1488,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
// Instead of using boxShadow, we use a div with a border to better replicate the behavior when the textarea is focused
// boxShadow: "0px 0px 0px 1px var(--vscode-input-border)",
padding: "9px 28px 3px 9px",
cursor: textAreaDisabled ? "not-allowed" : undefined,
cursor: "text",
flex: 1,
zIndex: 1,
outline:
@@ -1466,9 +1546,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
/> */}
<div
data-testid="send-button"
className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`}
className={`input-icon-button ${sendingDisabled ? "disabled" : ""} codicon codicon-send`}
onClick={() => {
if (!textAreaDisabled) {
if (!sendingDisabled) {
setIsTextAreaFocused(false)
onSend()
}
@@ -1504,7 +1584,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
data-testid="context-button"
appearance="icon"
aria-label="Add Context"
disabled={textAreaDisabled}
onClick={handleContextButtonClick}
style={{ padding: "0px 0px", height: "20px" }}>
<ButtonContainer>
+88 -69
View File
@@ -22,7 +22,7 @@ import { TaskServiceClient } from "@/services/grpc-client"
import HistoryPreview from "@/components/history/HistoryPreview"
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import Announcement from "@/components/chat/Announcement"
import AutoApproveMenu from "@/components/chat/AutoApproveMenu"
import AutoApproveMenu from "@/components/chat/auto-approve-menu/AutoApproveMenu"
import BrowserSessionRow from "@/components/chat/BrowserSessionRow"
import ChatRow from "@/components/chat/ChatRow"
import ChatTextArea from "@/components/chat/ChatTextArea"
@@ -33,6 +33,7 @@ import { unified } from "unified"
import remarkStringify from "remark-stringify"
import rehypeRemark from "rehype-remark"
import rehypeParse from "rehype-parse"
import HomeHeader from "../welcome/HomeHeader"
interface ChatViewProps {
isHidden: boolean
@@ -41,6 +42,27 @@ interface ChatViewProps {
showHistoryView: () => void
}
// Function to clean up markdown escape characters
function cleanupMarkdownEscapes(markdown: string): string {
return (
markdown
// Handle underscores and asterisks (single or multiple)
.replace(/\\([_*]+)/g, "$1")
// Handle angle brackets (for generics and XML)
.replace(/\\([<>])/g, "$1")
// Handle backticks (for code)
.replace(/\\(`)/g, "$1")
// Handle other common markdown special characters
.replace(/\\([[\]()#.!])/g, "$1")
// Fix multiple consecutive backslashes
.replace(/\\{2,}([_*`<>[\]()#.!])/g, "$1")
)
}
async function convertHtmlToMarkdown(html: string) {
// Process the HTML to Markdown
const result = await unified()
@@ -55,10 +77,14 @@ async function convertHtmlToMarkdown(html: string) {
rule: "-", // Use - for horizontal rules
ruleSpaces: false, // No spaces in horizontal rules
fences: true,
escape: false,
entities: false,
})
.process(html)
return String(result)
const md = String(result)
// Apply comprehensive cleanup of escape characters
return cleanupMarkdownEscapes(md)
}
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
@@ -90,7 +116,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
const [activeQuote, setActiveQuote] = useState<string | null>(null)
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
const textAreaRef = useRef<HTMLTextAreaElement>(null)
const [textAreaDisabled, setTextAreaDisabled] = useState(false)
const [sendingDisabled, setSendingDisabled] = useState(false)
const [selectedImages, setSelectedImages] = useState<string[]>([])
// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
@@ -108,6 +134,16 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
useEffect(() => {
const handleCopy = async (e: ClipboardEvent) => {
const targetElement = e.target as HTMLElement | null
// If the copy event originated from an input or textarea,
// let the default browser behavior handle it.
if (
targetElement &&
(targetElement.tagName === "INPUT" || targetElement.tagName === "TEXTAREA" || targetElement.isContentEditable)
) {
return
}
if (window.getSelection) {
const selection = window.getSelection()
if (selection && selection.rangeCount > 0) {
@@ -120,7 +156,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
// Convert HTML to Markdown
const markdown = await convertHtmlToMarkdown(selectedHtml)
vscode.postMessage({ type: "copyToClipboard", text: markdown })
e.preventDefault()
}
@@ -147,42 +182,42 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
const isPartial = lastMessage.partial === true
switch (lastMessage.ask) {
case "api_req_failed":
setTextAreaDisabled(true)
setSendingDisabled(true)
setClineAsk("api_req_failed")
setEnableButtons(true)
setPrimaryButtonText("Retry")
setSecondaryButtonText("Start New Task")
break
case "mistake_limit_reached":
setTextAreaDisabled(false)
setSendingDisabled(false)
setClineAsk("mistake_limit_reached")
setEnableButtons(true)
setPrimaryButtonText("Proceed Anyways")
setSecondaryButtonText("Start New Task")
break
case "auto_approval_max_req_reached":
setTextAreaDisabled(true)
setSendingDisabled(true)
setClineAsk("auto_approval_max_req_reached")
setEnableButtons(true)
setPrimaryButtonText("Proceed")
setSecondaryButtonText("Start New Task")
break
case "followup":
setTextAreaDisabled(isPartial)
setSendingDisabled(isPartial)
setClineAsk("followup")
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
break
case "plan_mode_respond":
setTextAreaDisabled(isPartial)
setSendingDisabled(isPartial)
setClineAsk("plan_mode_respond")
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
break
case "tool":
setTextAreaDisabled(isPartial)
setSendingDisabled(isPartial)
setClineAsk("tool")
setEnableButtons(!isPartial)
const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
@@ -199,28 +234,28 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}
break
case "browser_action_launch":
setTextAreaDisabled(isPartial)
setSendingDisabled(isPartial)
setClineAsk("browser_action_launch")
setEnableButtons(!isPartial)
setPrimaryButtonText("Approve")
setSecondaryButtonText("Reject")
break
case "command":
setTextAreaDisabled(isPartial)
setSendingDisabled(isPartial)
setClineAsk("command")
setEnableButtons(!isPartial)
setPrimaryButtonText("Run Command")
setSecondaryButtonText("Reject")
break
case "command_output":
setTextAreaDisabled(false)
setSendingDisabled(false)
setClineAsk("command_output")
setEnableButtons(true)
setPrimaryButtonText("Proceed While Running")
setSecondaryButtonText(undefined)
break
case "use_mcp_server":
setTextAreaDisabled(isPartial)
setSendingDisabled(isPartial)
setClineAsk("use_mcp_server")
setEnableButtons(!isPartial)
setPrimaryButtonText("Approve")
@@ -228,14 +263,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
break
case "completion_result":
// extension waiting for feedback. but we can just present a new task button
setTextAreaDisabled(isPartial)
setSendingDisabled(isPartial)
setClineAsk("completion_result")
setEnableButtons(!isPartial)
setPrimaryButtonText("Start New Task")
setSecondaryButtonText(undefined)
break
case "resume_task":
setTextAreaDisabled(false)
setSendingDisabled(false)
setClineAsk("resume_task")
setEnableButtons(true)
setPrimaryButtonText("Resume Task")
@@ -243,7 +278,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setDidClickCancel(false) // special case where we reset the cancel button state
break
case "resume_completed_task":
setTextAreaDisabled(false)
setSendingDisabled(false)
setClineAsk("resume_completed_task")
setEnableButtons(true)
setPrimaryButtonText("Start New Task")
@@ -251,19 +286,26 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setDidClickCancel(false)
break
case "new_task":
setTextAreaDisabled(isPartial)
setSendingDisabled(isPartial)
setClineAsk("new_task")
setEnableButtons(!isPartial)
setPrimaryButtonText("Start New Task with Context")
setSecondaryButtonText(undefined)
break
case "condense":
setTextAreaDisabled(isPartial)
setSendingDisabled(isPartial)
setClineAsk("condense")
setEnableButtons(!isPartial)
setPrimaryButtonText("Condense Conversation")
setSecondaryButtonText(undefined)
break
case "report_bug":
setSendingDisabled(isPartial)
setClineAsk("report_bug")
setEnableButtons(!isPartial)
setPrimaryButtonText("Report GitHub issue")
setSecondaryButtonText(undefined)
break
}
break
case "say":
@@ -273,7 +315,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
if (secondLastMessage?.ask === "command_output") {
// if the last ask is a command_output, and we receive an api_req_started, then that means the command has finished and we don't need input from the user anymore (in every other case, the user has to interact with input field or buttons to continue, which does the following automatically)
setInputValue("")
setTextAreaDisabled(true)
setSendingDisabled(true)
setSelectedImages([])
setClineAsk(undefined)
setEnableButtons(false)
@@ -310,7 +352,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
useEffect(() => {
if (messages.length === 0) {
setTextAreaDisabled(false)
setSendingDisabled(false)
setClineAsk(undefined)
setEnableButtons(false)
setPrimaryButtonText("Approve")
@@ -392,12 +434,20 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
images,
})
break
case "report_bug":
vscode.postMessage({
type: "askResponse",
askResponse: "messageResponse",
text: messageToSend,
images,
})
break
// there is no other case that a textfield should be enabled
}
}
setInputValue("")
setActiveQuote(null) // Clear quote when sending message
setTextAreaDisabled(true)
setSendingDisabled(true)
setSelectedImages([])
setClineAsk(undefined)
setEnableButtons(false)
@@ -466,8 +516,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
text: lastMessage?.text,
})
break
case "report_bug":
vscode.postMessage({
type: "reportBug",
text: lastMessage?.text,
})
break
}
setTextAreaDisabled(true)
setSendingDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
// setPrimaryButtonText(undefined)
@@ -516,7 +572,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSelectedImages([])
break
}
setTextAreaDisabled(true)
setSendingDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
// setPrimaryButtonText(undefined)
@@ -542,8 +598,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
vscode.postMessage({ type: "selectImages" })
}, [])
const shouldDisableImages =
!selectedModelInfo.supportsImages || textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
const shouldDisableImages = !selectedModelInfo.supportsImages || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
const handleMessage = useCallback(
(e: MessageEvent) => {
@@ -552,7 +607,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "action":
switch (message.action!) {
case "didBecomeVisible":
if (!isHidden && !textAreaDisabled && !enableButtons) {
if (!isHidden && !sendingDisabled && !enableButtons) {
textAreaRef.current?.focus()
}
break
@@ -601,7 +656,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}
// textAreaRef.current is not explicitly required here since react guarantees that ref will be stable across re-renders, and we're not using its value but its reference.
},
[isHidden, textAreaDisabled, enableButtons, handleSendMessage, handlePrimaryButtonClick, handleSecondaryButtonClick],
[isHidden, sendingDisabled, enableButtons, handleSendMessage, handlePrimaryButtonClick, handleSecondaryButtonClick],
)
useEvent("message", handleMessage)
@@ -613,14 +668,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
useEffect(() => {
const timer = setTimeout(() => {
if (!isHidden && !textAreaDisabled && !enableButtons) {
if (!isHidden && !sendingDisabled && !enableButtons) {
textAreaRef.current?.focus()
}
}, 50)
return () => {
clearTimeout(timer)
}
}, [isHidden, textAreaDisabled, enableButtons])
}, [isHidden, sendingDisabled, enableButtons])
const visibleMessages = useMemo(() => {
return modifiedMessages.filter((message) => {
@@ -948,47 +1003,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px", flexShrink: 0 }}>
<h2>What can I do for you?</h2>
<p>
Thanks to{" "}
<VSCodeLink href="https://www.anthropic.com/claude/sonnet" style={{ display: "inline" }}>
Claude 3.7 Sonnet's
</VSCodeLink>
agentic coding capabilities, I can handle complex software development tasks step-by-step. With tools
that let me create & edit files, explore complex projects, use a browser, and execute terminal
commands (after you grant permission), I can assist you in ways that go beyond code completion or tech
support. I can even use MCP to create new tools and extend my own capabilities.
</p>
</div>
<HomeHeader />
{taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
</div>
)}
{/*
// Flex layout explanation:
// 1. Content div above uses flex: "1 1 0" to:
// - Grow to fill available space (flex-grow: 1)
// - Shrink when AutoApproveMenu needs space (flex-shrink: 1)
// - Start from zero size (flex-basis: 0) to ensure proper distribution
// minHeight: 0 allows it to shrink below its content height
//
// 2. AutoApproveMenu uses flex: "0 1 auto" to:
// - Not grow beyond its content (flex-grow: 0)
// - Shrink when viewport is small (flex-shrink: 1)
// - Use its content size as basis (flex-basis: auto)
// This ensures it takes its natural height when there's space
// but becomes scrollable when the viewport is too small
*/}
{!task && (
<AutoApproveMenu
style={{
marginBottom: -2,
flex: "0 1 auto", // flex-grow: 0, flex-shrink: 1, flex-basis: auto
minHeight: 0,
}}
/>
)}
{!task && <AutoApproveMenu />}
{task && (
<>
@@ -1078,7 +1098,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
</>
)}
{(() => {
console.log("[ChatView] Rendering - activeQuote:", activeQuote) // Log here
return activeQuote ? (
<div style={{ marginBottom: "-12px", marginTop: "10px" }}>
<QuotedMessagePreview
@@ -1096,7 +1115,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
activeQuote={activeQuote}
inputValue={inputValue}
setInputValue={setInputValue}
textAreaDisabled={textAreaDisabled}
sendingDisabled={sendingDisabled}
placeholderText={placeholderText}
selectedImages={selectedImages}
setSelectedImages={setSelectedImages}
@@ -0,0 +1,84 @@
import React from "react"
import MarkdownBlock from "../common/MarkdownBlock"
interface ReportBugPreviewProps {
data: string
}
const ReportBugPreview: React.FC<ReportBugPreviewProps> = ({ data }) => {
// Parse the JSON data from the context string
const bugData = React.useMemo(() => {
try {
return JSON.parse(data || "{}")
} catch (e) {
console.error("Failed to parse bug report data", e)
return {}
}
}, [data])
return (
<div className="bg-[var(--vscode-badge-background)] text-[var(--vscode-badge-foreground)] rounded-[3px] p-[14px]">
<h3 className="font-bold text-base mb-3 mt-0">{bugData.title || "Bug Report"}</h3>
<div className="space-y-3 text-sm">
{bugData.what_happened && (
<div>
<div className="font-semibold">What Happened?</div>
<MarkdownBlock markdown={bugData.what_happened} />
</div>
)}
{bugData.steps_to_reproduce && (
<div>
<div className="font-semibold">Steps to Reproduce</div>
<MarkdownBlock markdown={bugData.steps_to_reproduce} />
</div>
)}
{bugData.api_request_output && (
<div>
<div className="font-semibold">Relevant API Request Output</div>
<MarkdownBlock markdown={bugData.api_request_output} />
</div>
)}
{bugData.provider_and_model && (
<div>
<div className="font-semibold">Provider/Model</div>
<MarkdownBlock markdown={bugData.provider_and_model} />
</div>
)}
{bugData.operating_system && (
<div>
<div className="font-semibold">Operating System</div>
<MarkdownBlock markdown={bugData.operating_system} />
</div>
)}
{bugData.system_info && (
<div>
<div className="font-semibold">System Info</div>
<MarkdownBlock markdown={bugData.system_info} />
</div>
)}
{bugData.cline_version && (
<div>
<div className="font-semibold">Cline Version</div>
<MarkdownBlock markdown={bugData.cline_version} />
</div>
)}
{bugData.additional_context && (
<div>
<div className="font-semibold">Additional Context</div>
<MarkdownBlock markdown={bugData.additional_context} />
</div>
)}
</div>
</div>
)
}
export default ReportBugPreview
+38 -2
View File
@@ -400,7 +400,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
</HeroTooltip>
</div>
{!shouldShowPromptCacheInfo() && (
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
<div className="flex items-center flex-wrap">
<CopyButton taskText={task.text} />
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
</div>
)}
</div>
{shouldShowPromptCacheInfo() && (
@@ -452,7 +455,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
</HeroTooltip>
)}
</div>
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
<div className="flex items-center flex-wrap">
<CopyButton taskText={task.text} />
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
</div>
</div>
)}
<div className="flex flex-col">
@@ -619,6 +625,36 @@ export const highlightText = (text?: string, withShadow = true) => {
return [text]
}
const CopyButton: React.FC<{
taskText?: string
}> = ({ taskText }) => {
const [copied, setCopied] = useState(false)
const handleCopy = () => {
if (!taskText) return
navigator.clipboard.writeText(taskText).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
}
return (
<HeroTooltip content="Copy Task">
<VSCodeButton
appearance="icon"
onClick={handleCopy}
style={{ padding: "0px 0px" }}
className="p-0"
aria-label="Copy Task">
<div className="flex items-center gap-[3px] text-[8px] font-bold opacity-60">
<i className={`codicon codicon-${copied ? "check" : "copy"}`} />
</div>
</VSCodeButton>
</HeroTooltip>
)
}
const DeleteButton: React.FC<{
taskSize: string
taskId?: string
@@ -0,0 +1,478 @@
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import AutoApproveMenuItem from "./AutoApproveMenuItem"
import { vscode } from "@/utils/vscode"
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { useClickAway } from "react-use"
import HeroTooltip from "@/components/common/HeroTooltip"
const breakpoint = 500
interface AutoApproveMenuProps {
style?: React.CSSProperties
}
export interface ActionMetadata {
id: keyof AutoApprovalSettings["actions"] | "enableNotifications" | "enableAll"
label: string
shortName: string
description: string
icon: string
subAction?: ActionMetadata
sub?: boolean
parentActionId?: string
}
const ACTION_METADATA: ActionMetadata[] = [
{
id: "readFiles",
label: "Read project files",
shortName: "Read",
description: "Allows Cline to read files within your workspace.",
icon: "codicon-search",
subAction: {
id: "readFilesExternally",
label: "Read all files",
shortName: "Read (all)",
description: "Allows Cline to read any file on your computer.",
icon: "codicon-folder-opened",
parentActionId: "readFiles",
},
},
{
id: "editFiles",
label: "Edit project files",
shortName: "Edit",
description: "Allows Cline to modify files within your workspace.",
icon: "codicon-edit",
subAction: {
id: "editFilesExternally",
label: "Edit all files",
shortName: "Edit (all)",
description: "Allows Cline to modify any file on your computer.",
icon: "codicon-files",
parentActionId: "editFiles",
},
},
{
id: "executeSafeCommands",
label: "Execute safe commands",
shortName: "Safe Commands",
description:
"Allows Cline to execute safe terminal commands. If the model determines a command is potentially destructive, it will still require approval.",
icon: "codicon-terminal",
subAction: {
id: "executeAllCommands",
label: "Execute all commands",
shortName: "All Commands",
description: "Allows Cline to execute all terminal commands. Use at your own risk.",
icon: "codicon-terminal-bash",
parentActionId: "executeSafeCommands",
},
},
{
id: "useBrowser",
label: "Use the browser",
shortName: "Browser",
description: "Allows Cline to launch and interact with any website in a browser.",
icon: "codicon-globe",
},
{
id: "useMcp",
label: "Use MCP servers",
shortName: "MCP",
description: "Allows Cline to use configured MCP servers which may modify filesystem or interact with APIs.",
icon: "codicon-server",
},
{
id: "enableAll",
label: "Enable all",
shortName: "All",
description: "Enable all actions.",
icon: "codicon-checklist",
},
{
id: "enableNotifications",
label: "Enable notifications",
shortName: "Notifications",
description: "Receive system notifications when Cline requires approval to proceed or when a task is completed.",
icon: "codicon-bell",
},
]
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
const { autoApprovalSettings } = useExtensionState()
const [isExpanded, setIsExpanded] = useState(false)
const [containerWidth, setContainerWidth] = useState(0)
// Favorites are now derived from autoApprovalSettings
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
const menuRef = useRef<HTMLDivElement>(null)
const itemsContainerRef = useRef<HTMLDivElement>(null)
// Track container width for responsive layout
useEffect(() => {
if (!isExpanded) return
const updateWidth = () => {
if (itemsContainerRef.current) {
setContainerWidth(itemsContainerRef.current.offsetWidth)
}
}
// Initial measurement
updateWidth()
// Set up resize observer
const resizeObserver = new ResizeObserver(updateWidth)
if (itemsContainerRef.current) {
resizeObserver.observe(itemsContainerRef.current)
}
// Clean up
return () => {
resizeObserver.disconnect()
}
}, [isExpanded])
const toggleFavorite = useCallback(
(actionId: string) => {
const currentFavorites = autoApprovalSettings.favorites || []
let newFavorites: string[]
if (currentFavorites.includes(actionId)) {
newFavorites = currentFavorites.filter((id) => id !== actionId)
} else {
newFavorites = [...currentFavorites, actionId]
}
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...autoApprovalSettings,
version: (autoApprovalSettings.version ?? 1) + 1,
favorites: newFavorites,
},
})
},
[autoApprovalSettings],
)
const updateAction = useCallback(
(action: ActionMetadata, value: boolean) => {
const actionId = action.id
const subActionId = action.subAction?.id
if (actionId === "enableAll" || subActionId === "enableAll") {
toggleAll(action, value)
return
}
if (actionId === "enableNotifications" || subActionId === "enableNotifications") {
updateNotifications(action, value)
return
}
let newActions = {
...autoApprovalSettings.actions,
[actionId]: value,
}
if (value === false && subActionId) {
newActions[subActionId] = false
}
if (value === true && action.parentActionId) {
newActions[action.parentActionId as keyof AutoApprovalSettings["actions"]] = true
}
// Check if this will result in any enabled actions
const willHaveEnabledActions = Object.values(newActions).some(Boolean)
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...autoApprovalSettings,
version: (autoApprovalSettings.version ?? 1) + 1,
actions: newActions,
enabled: willHaveEnabledActions,
},
})
},
[autoApprovalSettings],
)
const updateMaxRequests = useCallback(
(maxRequests: number) => {
const currentSettings = autoApprovalSettings
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
maxRequests,
},
})
},
[autoApprovalSettings],
)
const updateNotifications = useCallback(
(action: ActionMetadata, checked: boolean) => {
if (action.id === "enableNotifications") {
const currentSettings = autoApprovalSettings
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
enableNotifications: checked,
},
})
}
},
[autoApprovalSettings],
)
const toggleAll = useCallback(
(action: ActionMetadata, checked: boolean) => {
let actions = { ...autoApprovalSettings.actions }
for (const action of Object.keys(actions)) {
actions[action as keyof AutoApprovalSettings["actions"]] = checked
}
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...autoApprovalSettings,
version: (autoApprovalSettings.version ?? 1) + 1,
actions,
},
})
},
[autoApprovalSettings],
)
// Handle clicks outside the menu to close it
useClickAway(menuRef, () => {
if (isExpanded) {
setIsExpanded(false)
}
})
// Render a favorited item with a checkbox
const renderFavoritedItem = (favId: string) => {
// Regular action item
const action = ACTION_METADATA.flatMap((a) => [a, a.subAction]).find((a) => a?.id === favId)
if (!action) return null
return (
<AutoApproveMenuItem
action={action}
isChecked={isChecked}
isFavorited={isFavorited}
onToggle={updateAction}
condensed={true}
/>
)
}
const isChecked = (action: ActionMetadata): boolean => {
if (action.id === "enableNotifications") {
return autoApprovalSettings.enableNotifications
}
if (action.id === "enableAll") {
return Object.values(autoApprovalSettings.actions).every(Boolean)
}
return autoApprovalSettings.actions[action.id] ?? false
}
const isFavorited = (action: ActionMetadata): boolean => {
return favorites.includes(action.id)
}
return (
<div
ref={menuRef}
style={{
padding: "0 10px",
margin: "0 5px",
userSelect: "none",
borderTop: `0.5px solid color-mix(in srgb, ${getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND)} 20%, transparent)`,
overflowY: "auto",
borderRadius: "10px 10px 0 0",
backgroundColor: isExpanded ? CODE_BLOCK_BG_COLOR : "transparent",
...style,
}}>
{/* Collapsed view with favorited items */}
{!isExpanded && (
<div
onClick={() => setIsExpanded(true)}
style={{
cursor: "pointer",
paddingTop: "6px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "8px",
}}>
{favorites.length > 0 ? (
<div
style={{
display: "flex",
flexWrap: "nowrap",
alignItems: "center",
overflowX: "auto",
msOverflowStyle: "none",
scrollbarWidth: "none",
WebkitOverflowScrolling: "touch",
gap: "4px",
whiteSpace: "nowrap", // Prevent text wrapping
}}>
{favorites.map((favId) => renderFavoritedItem(favId))}
</div>
) : (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
cursor: "pointer",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<HeroTooltip
content="Auto-approve allows Cline to perform the following actions without asking for permission. Please use with caution and only enable if you understand the risks."
placement="top">
<span style={{ color: getAsVar(VSC_FOREGROUND), left: "0" }}>Auto-approve</span>
</HeroTooltip>
</div>
</div>
)}
<span className="codicon codicon-chevron-right" />
</div>
)}
{/* Expanded view */}
<div
style={{
maxHeight: isExpanded ? "1000px" : favorites.length > 0 ? "40px" : "22px", // Large enough to fit content
opacity: isExpanded ? 1 : 0,
overflow: "hidden",
transition: "max-height 0.3s ease-in-out, opacity 0.3s ease-in-out", // Removed padding to transition
}}>
{isExpanded && ( // Re-added conditional rendering for content
<>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "8px 0",
cursor: "pointer",
position: "relative", // Added for positioning context
}}
onClick={() => setIsExpanded(false)}>
<HeroTooltip
content="Auto-approve allows Cline to perform the following actions without asking for permission. Please use with caution and only enable if you understand the risks."
placement="top">
<span style={{ color: getAsVar(VSC_FOREGROUND) }}>Auto-approve</span>
</HeroTooltip>
<span className="codicon codicon-chevron-down" />
</div>
<div
ref={itemsContainerRef}
style={{
display: containerWidth > breakpoint ? "grid" : "flex",
gridTemplateColumns: containerWidth > breakpoint ? "1fr 1fr" : "1fr",
gridAutoRows: "min-content",
flexDirection: "column",
gap: "4px",
margin: "8px 0",
position: "relative", // For absolute positioning of the separator
}}>
{/* Vertical separator line - only visible in two-column mode */}
{containerWidth > breakpoint && (
<div
style={{
position: "absolute",
left: "50%",
top: "0",
bottom: "0",
width: "0.5px",
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
opacity: 0.2,
transform: "translateX(-50%)", // Center the line
}}
/>
)}
{/* All items in a single list - CSS Grid will handle the column distribution */}
{ACTION_METADATA.map((action) => (
<div key={action.id} style={{ breakInside: "avoid" }}>
<AutoApproveMenuItem
action={action}
isChecked={isChecked}
isFavorited={isFavorited}
onToggle={updateAction}
onToggleFavorite={toggleFavorite}
/>
</div>
))}
</div>
<div
style={{
height: "0.5px",
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
margin: "10px 0",
opacity: 0.2,
}}
/>
<HeroTooltip
content="Cline will automatically make this many API requests before asking for approval to proceed with the task."
placement="top">
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
width: "100%",
paddingBottom: "10px",
}}>
<span className="codicon codicon-settings" style={{ color: "#CCCCCC", fontSize: "14px" }} />
<span style={{ color: "#CCCCCC", fontSize: "12px", fontWeight: 500 }}>Max Requests:</span>
<VSCodeTextField
style={{ flex: "1", width: "100%" }}
value={autoApprovalSettings.maxRequests.toString()}
onInput={(e) => {
const input = e.target as HTMLInputElement
// Remove any non-numeric characters
input.value = input.value.replace(/[^0-9]/g, "")
const value = parseInt(input.value)
if (!isNaN(value) && value > 0) {
updateMaxRequests(value)
}
}}
onKeyDown={(e) => {
// Prevent non-numeric keys (except for backspace, delete, arrows)
if (
!/^\d$/.test(e.key) &&
!["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(e.key)
) {
e.preventDefault()
}
}}
/>
</div>
</HeroTooltip>
</>
)}
</div>
</div>
)
}
export default AutoApproveMenu
@@ -0,0 +1,131 @@
import React, { type ChangeEvent, type ChangeEventHandler } from "react"
import styled from "styled-components"
import HeroTooltip from "@/components/common/HeroTooltip"
import { ActionMetadata } from "./AutoApproveMenu"
import { useState } from "react"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
interface AutoApproveMenuItemProps {
action: ActionMetadata
isChecked: (action: ActionMetadata) => boolean
isFavorited?: (action: ActionMetadata) => boolean
onToggle: (action: ActionMetadata, checked: boolean) => void
onToggleFavorite?: (actionId: string) => void
condensed?: boolean
}
const CheckboxContainer = styled.div<{
isFavorited?: boolean
onClick?: (e: MouseEvent) => void
onMouseDown?: (e: React.MouseEvent) => void
}>`
display: flex;
align-items: center;
justify-content: space-between; /* Push content to edges */
padding: 0 4px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: var(--vscode-textBlockQuote-background);
}
.left-content {
display: flex;
align-items: center;
gap: 8px;
}
.icon {
color: var(--vscode-foreground);
font-size: 14px;
}
.label {
color: var(--vscode-foreground);
font-size: 12px;
font-weight: 500;
}
.star {
color: ${(props) => (props.isFavorited ? "var(--vscode-terminal-ansiYellow)" : "var(--vscode-descriptionForeground)")};
opacity: ${(props) => (props.isFavorited ? 1 : 0.6)};
font-size: 12px;
}
`
const SubOptionAnimateIn = styled.div<{ show: boolean }>`
position: relative;
transform: ${(props) => (props.show ? "scaleY(1)" : "scaleY(0)")};
transform-origin: top;
padding-left: 24px;
opacity: ${(props) => (props.show ? "1" : "0")};
height: ${(props) => (props.show ? "auto" : "0")}; /* Manage height for layout */
overflow: visible; /* Allow tooltips to escape */
transition: transform 0.2s ease-in-out;
`
const ActionButtonContainer = styled.div`
margin: 4px;
`
const AutoApproveMenuItem = ({
action,
isChecked,
isFavorited,
onToggle,
onToggleFavorite,
condensed = false,
}: AutoApproveMenuItemProps) => {
const [isSubOptionOpen, setIsSubOptionOpen] = useState(isChecked(action))
const checked = isChecked(action)
const favorited = isFavorited?.(action)
const onChange = (e: Event) => {
e.stopPropagation()
const newChecked = !checked
setIsSubOptionOpen(newChecked)
onToggle(action, newChecked)
}
const content = (
<div>
<ActionButtonContainer>
<HeroTooltip content={action.description} delay={200}>
<CheckboxContainer isFavorited={favorited} onClick={onChange}>
<div className="left-content">
<VSCodeCheckbox checked={checked} />
<span className={`codicon ${action.icon} icon`}></span>
<span className="label">{condensed ? action.shortName : action.label}</span>
</div>
{onToggleFavorite && !condensed && (
<span
className={`codicon codicon-${favorited ? "star-full" : "star-empty"} star`}
onClick={(e) => {
e.stopPropagation()
onToggleFavorite?.(action.id)
}}
/>
)}
</CheckboxContainer>
</HeroTooltip>
</ActionButtonContainer>
{action.subAction && !condensed && (
<SubOptionAnimateIn show={isSubOptionOpen}>
<AutoApproveMenuItem
action={action.subAction}
isChecked={isChecked}
isFavorited={isFavorited}
onToggle={onToggle}
onToggleFavorite={onToggleFavorite}
/>
</SubOptionAnimateIn>
)}
</div>
)
return content
}
export default AutoApproveMenuItem
@@ -92,6 +92,18 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
{formatDate(item.ts)}
</span>
</div>
{item.isFavorited && (
<div
style={{
position: "absolute",
top: "12px",
right: "12px",
color: "var(--vscode-button-background)",
}}>
<span className="codicon codicon-star-full" aria-label="Favorited" />
</div>
)}
<div
style={{
fontSize: "var(--vscode-font-size)",
+185 -39
View File
@@ -24,6 +24,76 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
const [deleteAllDisabled, setDeleteAllDisabled] = useState(false)
const [selectedItems, setSelectedItems] = useState<string[]>([])
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false)
// Keep track of pending favorite toggle operations
const [pendingFavoriteToggles, setPendingFavoriteToggles] = useState<Record<string, boolean>>({})
// Load filtered task history with gRPC
const [filteredTasks, setFilteredTasks] = useState<any[]>([])
// Load and refresh task history
const loadTaskHistory = useCallback(async () => {
try {
const response = await TaskServiceClient.getTaskHistory({
favoritesOnly: showFavoritesOnly,
searchQuery: searchQuery || undefined,
sortBy: sortOption,
})
setFilteredTasks(response.tasks || [])
} catch (error) {
console.error("Error loading task history:", error)
// Fallback to client-side filtering
setFilteredTasks(
taskHistory.filter((item) => {
const valid = item.ts && item.task
return valid && (!showFavoritesOnly || item.isFavorited)
}),
)
}
}, [showFavoritesOnly, searchQuery, sortOption, taskHistory])
// Load when filters change
useEffect(() => {
loadTaskHistory()
}, [loadTaskHistory])
const toggleFavorite = useCallback(
async (taskId: string, currentValue: boolean) => {
// Optimistic UI update
setPendingFavoriteToggles((prev) => ({ ...prev, [taskId]: !currentValue }))
try {
await TaskServiceClient.toggleTaskFavorite({
taskId,
isFavorited: !currentValue,
})
// Refresh if favorites filter is active
if (showFavoritesOnly) {
loadTaskHistory()
}
} catch (err) {
console.error(`[FAVORITE_TOGGLE_UI] Error for task ${taskId}:`, err)
// Revert optimistic update
setPendingFavoriteToggles((prev) => {
const updated = { ...prev }
delete updated[taskId]
return updated
})
} finally {
// Clean up pending state after 1 second
setTimeout(() => {
setPendingFavoriteToggles((prev) => {
const updated = { ...prev }
delete updated[taskId]
return updated
})
}, 1000)
}
},
[showFavoritesOnly, loadTaskHistory],
)
const handleMessage = useCallback((event: MessageEvent<ExtensionMessage>) => {
if (event.data.type === "relinquishControl") {
@@ -87,9 +157,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
.toUpperCase()
}, [])
const presentableTasks = useMemo(() => {
return taskHistory.filter((item) => item.ts && item.task)
}, [taskHistory])
const presentableTasks = useMemo(() => filteredTasks, [filteredTasks])
const fuse = useMemo(() => {
return new Fuse(presentableTasks, {
@@ -252,7 +320,48 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
<VSCodeRadio value="mostRelevant" disabled={!searchQuery} style={{ opacity: searchQuery ? 1 : 0.5 }}>
Most Relevant
</VSCodeRadio>
<div
onClick={() => setShowFavoritesOnly(!showFavoritesOnly)}
style={{
display: "flex",
alignItems: "center",
marginLeft: "6px",
cursor: "pointer",
}}>
<div
style={{
width: "14px",
height: "14px",
borderRadius: "50%",
border: "1px solid var(--vscode-checkbox-border)",
backgroundColor: showFavoritesOnly ? "var(--vscode-checkbox-background)" : "transparent",
position: "relative",
display: "flex",
justifyContent: "center",
alignItems: "center",
marginRight: "6px",
}}>
{showFavoritesOnly && (
<div
style={{
width: "6px",
height: "6px",
borderRadius: "50%",
backgroundColor: "var(--vscode-checkbox-foreground)",
}}
/>
)}
</div>
<span style={{ display: "flex", alignItems: "center", gap: "6px", userSelect: "none" }}>
<div
className="codicon codicon-star-full"
style={{ color: "var(--vscode-button-background)", fontSize: "14px" }}
/>
Favorites
</span>
</div>
</VSCodeRadioGroup>
<div style={{ display: "flex", justifyContent: "flex-end", gap: "10px" }}>
<VSCodeButton
onClick={() => {
@@ -337,43 +446,80 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
}}>
{formatDate(item.ts)}
</span>
<VSCodeButton
appearance="icon"
onClick={(e) => {
e.stopPropagation()
handleDeleteHistoryItem(item.id)
}}
className="delete-button"
style={{ padding: "0px 0px" }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "3px",
fontSize: "11px",
// fontWeight: "bold",
}}>
<span className="codicon codicon-trash"></span>
{formatSize(item.size)}
</div>
</VSCodeButton>
<div style={{ display: "flex", gap: "4px" }}>
{/* only show delete button if task not favorited */}
{!(pendingFavoriteToggles[item.id] ?? item.isFavorited) && (
<VSCodeButton
appearance="icon"
onClick={(e) => {
e.stopPropagation()
handleDeleteHistoryItem(item.id)
}}
className="delete-button"
style={{ padding: "0px 0px" }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "3px",
fontSize: "11px",
}}>
<span className="codicon codicon-trash"></span>
{formatSize(item.size)}
</div>
</VSCodeButton>
)}
<VSCodeButton
appearance="icon"
onClick={(e) => {
e.stopPropagation()
toggleFavorite(item.id, item.isFavorited || false)
}}
style={{ padding: "0px" }}>
<div
className={`codicon ${
pendingFavoriteToggles[item.id] !== undefined
? pendingFavoriteToggles[item.id]
? "codicon-star-full"
: "codicon-star-empty"
: item.isFavorited
? "codicon-star-full"
: "codicon-star-empty"
}`}
style={{
color:
(pendingFavoriteToggles[item.id] ?? item.isFavorited)
? "var(--vscode-button-background)"
: "inherit",
opacity: (pendingFavoriteToggles[item.id] ?? item.isFavorited) ? 1 : 0.7,
display:
(pendingFavoriteToggles[item.id] ?? item.isFavorited)
? "block"
: undefined,
}}
/>
</VSCodeButton>
</div>
</div>
<div style={{ marginBottom: "8px", position: "relative" }}>
<div
style={{
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-foreground)",
display: "-webkit-box",
WebkitLineClamp: 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}
dangerouslySetInnerHTML={{
__html: item.task,
}}
/>
</div>
<div
style={{
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-foreground)",
display: "-webkit-box",
WebkitLineClamp: 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}
dangerouslySetInnerHTML={{
__html: item.task,
}}
/>
<div
style={{
display: "flex",
@@ -8,7 +8,7 @@ import {
VSCodeRadioGroup,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
import { useEvent, useInterval } from "react-use"
import styled from "styled-components"
@@ -209,7 +209,15 @@ const ApiOptions = ({
setLmStudioModels([])
}
} else if (selectedProvider === "vscode-lm") {
vscode.postMessage({ type: "requestVsCodeLmModels" })
try {
const response = await ModelsServiceClient.getVsCodeLmModels({})
if (response && response.models) {
setVsCodeLmModels(response.models)
}
} catch (error) {
console.error("Failed to fetch VS Code LM models:", error)
setVsCodeLmModels([])
}
}
}, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl])
useEffect(() => {
@@ -222,14 +230,6 @@ const ApiOptions = ({
selectedProvider === "ollama" || selectedProvider === "lmstudio" || selectedProvider === "vscode-lm" ? 2000 : null,
)
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
if (message.type === "vsCodeLmModels" && message.vsCodeLmModels) {
setVsCodeLmModels(message.vsCodeLmModels)
}
}, [])
useEvent("message", handleMessage)
/*
VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected.
https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433
@@ -262,6 +262,34 @@ const ApiOptions = ({
)
}
// Debounced function to refresh OpenAI models (prevents excessive API calls while typing)
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
}
}, [])
const debouncedRefreshOpenAiModels = useCallback((baseUrl?: string, apiKey?: string) => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
if (baseUrl && apiKey) {
debounceTimerRef.current = setTimeout(() => {
ModelsServiceClient.refreshOpenAiModels({
baseUrl,
apiKey,
}).catch((error) => {
console.error("Failed to refresh OpenAI models:", error)
})
}, 500)
}
}, [])
return (
<div style={{ display: "flex", flexDirection: "column", gap: 5, marginBottom: isPopup ? -10 : 0 }}>
<DropdownContainer className="dropdown-container">
@@ -822,10 +850,7 @@ const ApiOptions = ({
color: "var(--vscode-descriptionForeground)",
}}>
Select "Custom" when using the Application Inference Profile in Bedrock. Enter the Application
Inference Profile ID in the Model ID field. However, be sure to encode the / in the ARN as %2F.
<br />
Example: arn:aws:bedrock:us-west-2:&lt;AWS Account
ID&gt;:application-inference-profile%2Fxxxxxxxxxxxx
Inference Profile ARN in the Model ID field.
</p>
<label htmlFor="bedrock-model-input">
<span style={{ fontWeight: 500 }}>Model ID</span>
@@ -1002,7 +1027,12 @@ const ApiOptions = ({
value={apiConfiguration?.openAiBaseUrl || ""}
style={{ width: "100%", marginBottom: 10 }}
type="url"
onInput={handleInputChange("openAiBaseUrl")}
onInput={(e: any) => {
const baseUrl = e.target.value
handleInputChange("openAiBaseUrl")({ target: { value: baseUrl } })
debouncedRefreshOpenAiModels(baseUrl, apiConfiguration?.openAiApiKey)
}}
placeholder={"Enter base URL..."}>
<span style={{ fontWeight: 500 }}>Base URL</span>
</VSCodeTextField>
@@ -1010,7 +1040,12 @@ const ApiOptions = ({
value={apiConfiguration?.openAiApiKey || ""}
style={{ width: "100%", marginBottom: 10 }}
type="password"
onInput={handleInputChange("openAiApiKey")}
onInput={(e: any) => {
const apiKey = e.target.value
handleInputChange("openAiApiKey")({ target: { value: apiKey } })
debouncedRefreshOpenAiModels(apiConfiguration?.openAiBaseUrl, apiKey)
}}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
@@ -6,6 +6,7 @@ import { useMount } from "react-use"
import styled from "styled-components"
import { openRouterDefaultModelId } from "@shared/api"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelsServiceClient } from "@/services/grpc-client"
import { vscode } from "@/utils/vscode"
import { highlight } from "../history/HistoryView"
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
@@ -84,7 +85,9 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
}, [apiConfiguration])
useMount(() => {
vscode.postMessage({ type: "refreshOpenRouterModels" })
ModelsServiceClient.refreshOpenRouterModels({}).catch((error: Error) =>
console.error("Failed to refresh OpenRouter models:", error),
)
})
useEffect(() => {
@@ -10,7 +10,6 @@ import { useEvent } from "react-use"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import BrowserSettingsSection from "./BrowserSettingsSection"
import TerminalSettingsSection from "./TerminalSettingsSection"
import { useFeatureFlag } from "@/hooks/useFeatureFlag"
import { FEATURE_FLAGS } from "@shared/services/feature-flags/feature-flags"
const { IS_DEV } = process.env
@@ -146,8 +145,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
handleSubmit(true)
}
const showCustomInstructions = useFeatureFlag(FEATURE_FLAGS.CUSTOM_INSTRUCTIONS)
return (
<div className="fixed top-0 left-0 right-0 bottom-0 pt-[10px] pr-0 pb-0 pl-5 flex flex-col overflow-hidden">
<div className="flex justify-between items-center mb-[13px] pr-[17px]">
@@ -186,24 +183,20 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
/>
)}
{showCustomInstructions && (
<div className="mb-[5px]">
<VSCodeTextArea
value={customInstructions ?? ""}
className="w-full"
resize="vertical"
rows={4}
placeholder={
'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
}
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
<span className="font-medium">Custom Instructions</span>
</VSCodeTextArea>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
These instructions are added to the end of the system prompt sent with every request.
</p>
</div>
)}
<div className="mb-[5px]">
<VSCodeTextArea
value={customInstructions ?? ""}
className="w-full"
resize="vertical"
rows={4}
placeholder={'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'}
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
<span className="font-medium">Custom Instructions</span>
</VSCodeTextArea>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
These instructions are added to the end of the system prompt sent with every request.
</p>
</div>
<div className="mb-[5px]">
<VSCodeCheckbox
@@ -0,0 +1,28 @@
import ClineLogoVariable from "@/assets/ClineLogoVariable"
import HeroTooltip from "@/components/common/HeroTooltip"
const HomeHeader = () => {
return (
<div className="flex flex-col items-center mb-5">
<div className="my-5">
<ClineLogoVariable className="size-16" />
</div>
<div className="text-center flex items-center justify-center">
<h2 className="m-0 text-[var(--vscode-font-size)]">{"What can I do for you?"}</h2>
<HeroTooltip
placement="bottom"
className="max-w-[300px]"
content={
"I can develop software step-by-step by editing files, exploring projects, running commands, and using browsers. I can even extend my capabilities with MCP tools to assist beyond basic code completion."
}>
<span
className="codicon codicon-info ml-2 cursor-pointer"
style={{ fontSize: "14px", color: "var(--vscode-textLink-foreground)" }}
/>
</HeroTooltip>
</div>
</div>
)
}
export default HomeHeader
+17 -4
View File
@@ -1,10 +1,23 @@
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useFeatureFlagPayload } from "posthog-js/react"
export const useFeatureFlag = (flagName: string): boolean => {
const payload = useFeatureFlagPayload(flagName) as { enabled: boolean }
if (payload && payload.enabled) {
return payload.enabled
const { telemetrySetting } = useExtensionState()
try {
const payload = useFeatureFlagPayload(flagName) as { enabled: boolean }
if (payload && typeof payload === "object") {
// Check if the enabled property exists and is a boolean
if ("enabled" in payload && typeof payload.enabled === "boolean") {
return payload.enabled
}
}
if (telemetrySetting === "enabled") {
console.warn(`Feature flag ${flagName} not found or missing enabled property.`)
}
} catch (error) {
console.error(`Error retrieving feature flag "${flagName}":`, error)
}
console.warn(`Feature flag ${flagName} not found or missing enabled property.`)
return false
}
-2
View File
@@ -22,8 +22,6 @@ https://github.com/gitkraken/vscode-gitlens/blob/b1d71d4844523e8b2ef16f9e007068e
html {
height: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
+4
View File
@@ -16,6 +16,10 @@ export const SUPPORTED_SLASH_COMMANDS: SlashCommand[] = [
name: "newrule",
description: "Create a new Cline rule based on your conversation",
},
{
name: "reportbug",
description: "Create a Github issue with Cline",
},
]
// Regex for detecting slash commands in text