mirror of
https://github.com/cline/cline.git
synced 2026-09-06 12:28:08 +08:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5255da936f | |||
| 248871d770 | |||
| c634bf6368 | |||
| f5dbfaf234 | |||
| aa4d97f05d | |||
| c20a513b70 | |||
| 738c03ff3e | |||
| e8a68c49ce | |||
| 961400fdca | |||
| 8827b167ca | |||
| e1389a62c7 | |||
| 7b416ccc70 | |||
| 29f3cfa894 | |||
| 978f34e30b | |||
| 445e25221a | |||
| 489a05117c | |||
| e572ee44f9 | |||
| f4e14bfe3b | |||
| bddc1b5e96 | |||
| cb0de8f17e | |||
| e1a0b244de | |||
| a9d5411bf0 | |||
| 16af9125ec | |||
| 2792e7698f | |||
| d02e5a89e5 | |||
| 20f19917d3 | |||
| 7e5cd52864 | |||
| 4622ad767b | |||
| 96048d5ac5 | |||
| facec93082 | |||
| c040be9eb1 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add the o4-mini model in the isOminiModel
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Allow option to collect events to send them in a bundle to avoid sending too many events
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add open ai cache to ui
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
disable autocapture
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Moved rule file conversions
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
suggested model exists again
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
getRelativePaths protobus migration
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
getOllamaModels protobus migration
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add FeatureFlagProvider service for the Node.js extension side
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add confirmation dialog to Delete All History button
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
breakpoint just in system prompt for gemini for OR and cline provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
fetch cache details from generation endpoint
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Bump ollama from 0.5.13 to 0.5.15
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add npm script for issue creation
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate more info section to new docs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Extend ReasoningEffort to non-o3-mini reasoning models for all providers
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Re-enable tests in workflow
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
updated gemini caching for OR and cline provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
searchFiles protobus migration
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add ability to generate commit message via cline
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
updated OR/cline provider to automate gemini models caching
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
getLmStudioModels protobus migration
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
adding quote reply support
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add enterprise section to new docs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Make Previous Updates in the Announcement a dropdown
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate prompting section to new docs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate running models locally section to new docs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
timestamp visualization
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add image links to docs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate MCP section to new docs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate custom model config section to new docs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add ui for windsurf and cursor rules
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Batch selection and deletion of tasks in history
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate getting-started section to new docs
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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!)
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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.
|
||||
Generated
+20
-35
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
@@ -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: [] })
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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
|
||||
`
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 doesn’t 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,
|
||||
|
||||
@@ -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: [],
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -11,4 +11,5 @@ export type HistoryItem = {
|
||||
size?: number
|
||||
shadowGitConfigWorkTree?: string
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
isFavorited?: boolean
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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)",
|
||||
|
||||
@@ -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:<AWS Account
|
||||
ID>: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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user