mirror of
https://github.com/cline/cline.git
synced 2026-09-09 06:45:53 +08:00
Compare commits
61
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56a6881cec | ||
|
|
4a230ad878 | ||
|
|
94c432f3f3 | ||
|
|
d88c07c932 | ||
|
|
5ee5577010 | ||
|
|
01877c1629 | ||
|
|
f8a7b563aa | ||
|
|
915555f80f | ||
|
|
26eafd96dd | ||
|
|
e504b9d414 | ||
|
|
312777ddc5 | ||
|
|
c79acf5ffe | ||
|
|
7d5d347cdd | ||
|
|
95cc15a142 | ||
|
|
f7d464a51d | ||
|
|
a6c4c0c0ea | ||
|
|
976a8fa85e | ||
|
|
33413e91c6 | ||
|
|
801c59e75e | ||
|
|
df9c8e2e80 | ||
|
|
4d480ea3fe | ||
|
|
7e26d1117a | ||
|
|
b04810c480 | ||
|
|
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 | ||
|
|
8d3cf53289 | ||
|
|
83c4a82e6d | ||
|
|
6ad11badf3 | ||
|
|
39c7da301c | ||
|
|
5275f2eabc | ||
|
|
7cf68ff279 | ||
|
|
b8af02ebaa |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
convert condense command to use grpc
|
||||
@@ -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
|
||||
---
|
||||
|
||||
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
|
||||
@@ -2,4 +2,4 @@
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Task Timeline
|
||||
Add Fireworks API Provider
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
adding activation events so cline is activated when vs code opens
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add ability to generate commit message via cline
|
||||
@@ -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": minor
|
||||
---
|
||||
|
||||
Allow the user to scroll when Cline is editing a file by disabling auto-scroll when the user scrolls up
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
prevent IME composition Enter from auto‑sending edited message
|
||||
@@ -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
|
||||
@@ -209,7 +209,7 @@ class Task {
|
||||
switch (chunk.type) {
|
||||
case "text":
|
||||
// Parse into content blocks
|
||||
this.assistantMessageContent = parseAssistantMessage(chunk.text)
|
||||
this.assistantMessageContent = parseAssistantMessageV2(chunk.text)
|
||||
// Present blocks to user
|
||||
await this.presentAssistantMessage()
|
||||
break
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
Vendored
+22
@@ -16,6 +16,28 @@
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (Fresh Install Mode)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--profile-temp",
|
||||
"--sync",
|
||||
"off",
|
||||
"--disable-extensions",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "clean-sandbox",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"postDebugTask": "stop",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -185,6 +185,12 @@
|
||||
"label": "stop",
|
||||
"command": "echo ${input:terminate}",
|
||||
"type": "shell"
|
||||
},
|
||||
{
|
||||
"label": "clean-sandbox",
|
||||
"type": "shell",
|
||||
"dependsOn": ["watch"],
|
||||
"command": "rm -rf .vscode-dev"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
|
||||
@@ -1,5 +1,60 @@
|
||||
# Changelog
|
||||
|
||||
## [3.15.2]
|
||||
|
||||
- Added details to auto approve menu and more sensible default controls
|
||||
- Add detailed configuration options for LiteLLM provider
|
||||
- Add webview telemetry for users who have opted in to telemetry
|
||||
- Update Gemini in OpenRouter/Cline providers to use implicit caching
|
||||
- Fix freezing issues during rendering of large streaming text
|
||||
- Fix grey screen webview crashes by releasing memory after every diff edit
|
||||
- Fix breaking out of diff auto-scroll
|
||||
- Fix IME composition Enter auto‑sending edited message
|
||||
|
||||
## [3.15.1]
|
||||
|
||||
- Fix bug where PowerShell commands weren't given enough time before giving up and showing an error
|
||||
|
||||
## [3.15.0]
|
||||
|
||||
- Add Task Timeline visualization to tasks (Thanks eomcaleb!)
|
||||
- Add cache to ui for OpenAi provider
|
||||
- Add FeatureFlagProvider service for the Node.js extension side
|
||||
- Add copy buttons to task header and assistant messages
|
||||
- Add a more simplified home header was added
|
||||
- Add ability to favorite a task, allowing it to be kept when clearing all tasks
|
||||
- Add npm script for issue creation (Thanks DaveFres!)
|
||||
- Add confirmation dialog to Delete All History button
|
||||
- Add ability to allow the user to type their next message into the chat while Cline is taking action
|
||||
- Add ability to generate commit message via cline (Thanks zapp88!)
|
||||
- Add improvements to caching for gemini models on OpenRouter and Cline providers
|
||||
- Add improvements to allow scrolling the file being edited.
|
||||
- Add ui for windsurf and cursor rules
|
||||
- Add mistral medium-3 model
|
||||
- Add option to collect events to send them in a bundle to avoid sending too many events
|
||||
- Add support to quote a previous message in chat
|
||||
- Add support for Gemini Implicit Caching
|
||||
- Add support for batch selection and deletion of tasks in history (Thanks danix800!)
|
||||
- Update change suggested models
|
||||
- Update fetch cache details from generation endpoint
|
||||
- Update converted docs to Mintlify
|
||||
- Update the isOminiModel to include o4-mini model (Thanks PeterDaveHello!)
|
||||
- Update file size that can be read by Cline, allowing larger files
|
||||
- Update defaults for bedrock API models (Thanks Watany!)
|
||||
- Update to extend ReasoningEffort to non-o3-mini reasoning models for all providers (Thanks PeterDaveHello!)
|
||||
- Update to give error when a user tries to upload an image larger than 7500x7500 pixels
|
||||
- Update announcement so that previous updates are in a dropdown
|
||||
- Update UI for auto approve with favorited settings
|
||||
- Fix bug where certain terminal commands would lock you out of a task
|
||||
- Fix only initialize posthog in the webview if the user has opted into telemetry
|
||||
- Fix bug where autocapture was on for front-end telemetry
|
||||
- Fix for markdown copy excessively escaping characters (Thanks weshoke!)
|
||||
- Fix an issue where loading never finished when using an application inference profile for the model ID (Thanks WinterYukky!)
|
||||
|
||||
## [3.14.1]
|
||||
|
||||
- Disables autocaptures when initializing feature flags
|
||||
|
||||
## [3.14.0]
|
||||
|
||||
- Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile (Thanks @clicube!)
|
||||
|
||||
@@ -36,7 +36,7 @@ Cline offers the option of utilizing AWS credentials or AWS profiles to access A
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="/assets/robot_panel_dark.png"
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-aws-setup-markup%20(1).png"
|
||||
alt="AWS Bedrock configuration in Cline settings showing profile authentication setup"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
@@ -76,7 +76,10 @@ Vertex AI supports eight regions. Select a region that meets your latency, compl
|
||||
- Search for **Cline** and install the extension
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Cline extension in VS Code" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-extension-arrow.png"
|
||||
alt="Cline extension in VS Code"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
#### 3.2 Configure Cline Settings
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"getting-started/installing-dev-essentials",
|
||||
"getting-started/model-selection-guide",
|
||||
"getting-started/our-favorite-tech-stack",
|
||||
"getting-started/task-management",
|
||||
"getting-started/understanding-context-management",
|
||||
"getting-started/what-is-cline"
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@ Cline supports major cloud providers like AWS Bedrock and Google's Cloud Vertex;
|
||||
|
||||
For the purpose of this document, we assume your organization will use cloud-based frontier models. Cloud inference providers offer cutting-edge capabilities and the flexibility to select models which best suit your needs.
|
||||
|
||||
Certain scenarios may warrant using local models, including handling highly sensitive data, applications requiring consistent low-latency responses, or compliance with strict data sovereignty requirements. If your team needs to utilize local models, see [Running Local Models ](/running-local-models/read-me-first.mdx)with Cline.
|
||||
Certain scenarios may warrant using local models, including handling highly sensitive data, applications requiring consistent low-latency responses, or compliance with strict data sovereignty requirements. If your team needs to utilize local models, see [Running Local Models ](/running-models-locally/read-me-first.mdx)with Cline.
|
||||
|
||||
---
|
||||
|
||||
@@ -14,9 +14,9 @@ Certain scenarios may warrant using local models, including handling highly sens
|
||||
|
||||
#### [IAM Security Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) (For administrators)
|
||||
|
||||
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/custom-model-configs/aws-bedrock.mdx)
|
||||
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/custom-model-configs/aws-bedrock-with-credentials-authentication.mdx)
|
||||
|
||||
#### [AWS Bedrock setup for SSO token (AWS Profile)](/custom-model-configs/aws-bedrock-w-profile-authentication.mdx)
|
||||
#### [AWS Bedrock setup for SSO token (AWS Profile)](/custom-model-configs/aws-bedrock-with-profile-authentication.mdx)
|
||||
|
||||
#### VPC Endpoint Setup
|
||||
|
||||
@@ -28,12 +28,12 @@ To protect your team's data, Cline supports VPC (Virtual Private Cloud) endpoint
|
||||
2. Follow the directions for [creating a VPC endpoint](https://docs.aws.amazon.com/vpc/latest/privatelink/create-interface-endpoint.html#create-interface-endpoint-aws) in the AWS console. The image below pertains to steps 4 and 5 of the AWS guide linked above.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="VPC Console" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/vpc-console.png" alt="VPC Console" />
|
||||
</Frame>
|
||||
|
||||
3. Note the IP address of your VPC endpoint, open Cline's settings menu, and select `AWS Bedrock`from the API Provider dropdown.
|
||||
4. Click the `Use Custom VPC endpoint`checkbox and enter the IP address of your VPC endpoint
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="VPC Settings Menu" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/vpc-settings-menu.png" alt="VPC Settings Menu" />
|
||||
</Frame>
|
||||
|
||||
@@ -13,7 +13,10 @@ title: "Security Concerns"
|
||||
Cline operates exclusively as a client-side VSCode extension with zero server-side components. This fundamental design choice ensures that your code and data remain within your secure environment at all times. Unlike traditional AI assistants that send data to external servers for processing, Cline connects directly to your chosen cloud provider's AI endpoints, keeping all sensitive information within your infrastructure boundaries.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Cline's relationship to local and remote assets" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-arch.png"
|
||||
alt="Cline's relationship to local and remote assets"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
### Data Privacy Commitment
|
||||
|
||||
@@ -28,7 +28,10 @@ After each tool use, you can:
|
||||
2. Click the "Restore" button to open restore options
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Checkpoint comparison and restore options" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(13).png"
|
||||
alt="Checkpoint comparison and restore options"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
#### Rolling Back
|
||||
@@ -60,7 +63,7 @@ Checkpoints let you be more experimental with Cline. While human coding is often
|
||||
- Ideal for exploring different design patterns or architectural approaches
|
||||
|
||||
<Frame caption="In this case, I didn't like the changes Cline made to my robot dog-walking website (still working on the robots) and I wanted to revert both the codebase and the task to before any changes were made so I could start fresh.">
|
||||
<img src="/assets/robot_panel_dark.png" alt="Checkpoint restore demo" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/checkpointsDemo.gif" alt="Checkpoint restore demo" />
|
||||
</Frame>
|
||||
|
||||
### ✨ Best Practices
|
||||
@@ -104,5 +107,8 @@ Perhaps you didn't get the results you wanted, thought of a better way to phrase
|
||||
- Shift + Enter: Insert new line / line break
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Message editing interface" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/message-editing.png"
|
||||
alt="Message editing interface"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
+16
-7
@@ -7,7 +7,10 @@ title: "Plan & Act Modes: A Guide to Effective AI Development"
|
||||
Plan & Act modes represent Cline's approach to structured AI development, emphasizing thoughtful planning before implementation. This dual-mode system helps developers create more maintainable, accurate code while reducing iteration time.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Use Plan to gather context before using Act to implement the plan" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/planningThenActing%20(1).gif"
|
||||
alt="Use Plan to gather context before using Act to implement the plan"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
### Understanding the Modes
|
||||
@@ -27,7 +30,7 @@ Plan & Act modes represent Cline's approach to structured AI development, emphas
|
||||
- Can execute changes to your codebase
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Act mode capabilities" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(5).png" alt="Act mode capabilities" />
|
||||
</Frame>
|
||||
|
||||
### Workflow Guide
|
||||
@@ -39,7 +42,7 @@ Begin every significant development task in Plan mode:
|
||||
In this mode:
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Plan mode workflow" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(5)%20(1).png" alt="Plan mode workflow" />
|
||||
</Frame>
|
||||
|
||||
- Share your requirements
|
||||
@@ -48,7 +51,10 @@ In this mode:
|
||||
- Develop implementation strategy
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Planning phase" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(2)%20(1)%20(1)%20(1).png"
|
||||
alt="Planning phase"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
#### 2. Switch to Act Mode
|
||||
@@ -56,7 +62,7 @@ In this mode:
|
||||
Once you have a clear plan, switch to Act mode:
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Switching to Act mode" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/switching-to-act.gif" alt="Switching to Act mode" />
|
||||
</Frame>
|
||||
|
||||
Act mode allows Cline to:
|
||||
@@ -90,7 +96,10 @@ Complex projects often require multiple plan-act cycles:
|
||||
4. Document significant decisions
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Implementation best practices" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(3)%20(1).png"
|
||||
alt="Implementation best practices"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
### Power User Tips
|
||||
@@ -119,7 +128,7 @@ Complex projects often require multiple plan-act cycles:
|
||||
- Executing test cases
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Mode usage patterns" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(6).png" alt="Mode usage patterns" />
|
||||
</Frame>
|
||||
|
||||
### Contributing
|
||||
|
||||
@@ -22,7 +22,10 @@ Follow these steps to get Cline up and running:
|
||||
4. **Search for 'Cline':** In the Extensions search bar, type `Cline`.
|
||||
|
||||
<Frame caption="VS Code marketplace with Cline extension ready to install">
|
||||
<img src="/assets/robot_panel_dark.png" alt="VS Code marketplace showing Cline extension" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(20).png"
|
||||
alt="VS Code marketplace showing Cline extension"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
1. **Install the Extension:** Click the "Install" button next to the Cline extension.
|
||||
|
||||
@@ -28,7 +28,10 @@ Cline helps you manage this limitation with its Context Window Progress Bar, whi
|
||||
- The total capacity for your chosen model
|
||||
|
||||
<Frame caption="Visual representation of the context window usage in Cline">
|
||||
<img src="/assets/robot_panel_light.png" alt="Context window progress bar example" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(11).png"
|
||||
alt="Context window progress bar example"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
This visibility helps you work more effectively with Cline by letting you know when you might need to start fresh or break tasks into smaller chunks.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: "Task Management in Cline"
|
||||
description: "Learn how to effectively manage your task history, use favorites, and organize your work in Cline."
|
||||
---
|
||||
|
||||
# Task Management
|
||||
|
||||
As you use Cline, you'll accumulate many tasks over time. The task management system helps you organize, filter, search, and clean up your task history to keep your workspace efficient.
|
||||
|
||||
## Accessing Task History
|
||||
|
||||
You can access your task history by:
|
||||
|
||||
1. Clicking on the "History" button in the Cline sidebar
|
||||
2. Using the command palette to search for "Cline: Show Task History"
|
||||
|
||||
## Task History Features
|
||||
|
||||
The task history view provides several powerful features:
|
||||
|
||||
### Searching and Filtering
|
||||
|
||||
- **Search Bar**: Use the fuzzy search at the top to quickly find tasks by content
|
||||
- **Sort Options**: Sort tasks by:
|
||||
- Newest (default)
|
||||
- Oldest
|
||||
- Most Expensive (highest API cost)
|
||||
- Most Tokens (highest token usage)
|
||||
- Most Relevant (when searching)
|
||||
- **Favorites Filter**: Toggle to show only favorited tasks
|
||||
|
||||
### Task Actions
|
||||
|
||||
Each task in the history view has several actions available:
|
||||
|
||||
- **Open**: Click on a task to reopen it in the Cline chat
|
||||
- **Favorite**: Click the star icon to mark a task as a favorite
|
||||
- **Delete**: Remove individual tasks (favorites are protected from deletion)
|
||||
- **Export**: Export a task's conversation to markdown
|
||||
|
||||
## ⭐ Task Favorites
|
||||
|
||||
The favorites feature allows you to mark important tasks that you want to preserve and find quickly.
|
||||
|
||||
### How Favorites Work
|
||||
|
||||
- **Marking Favorites**: Click the star icon next to any task to toggle its favorite status
|
||||
- **Protection**: Favorited tasks are protected from individual and bulk deletion operations (can be overridden)
|
||||
- **Filtering**: Use the favorites filter to quickly access your important tasks
|
||||
|
||||
## Batch Operations
|
||||
|
||||
The task history view supports several batch operations:
|
||||
|
||||
- **Select Multiple**: Use the checkboxes to select multiple tasks
|
||||
- **Select All/None**: Quickly select or deselect all tasks
|
||||
- **Delete Selected**: Remove all selected tasks
|
||||
- **Delete All**: Remove all tasks from history (favorites are preserved unless you choose to include them)
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Favorite Important Tasks**: Mark reference tasks or frequently accessed conversations as favorites
|
||||
2. **Regular Cleanup**: Periodically remove old or unused tasks to improve performance
|
||||
3. **Use Search**: Leverage the fuzzy search to quickly find specific conversations
|
||||
4. **Export Valuable Tasks**: Export important tasks to markdown for external reference
|
||||
|
||||
Task management helps you maintain an organized workflow when using Cline, allowing you to quickly find past conversations, preserve important work, and keep your history clean and efficient.
|
||||
@@ -14,7 +14,7 @@ description: "Context is key to getting the most out of Cline"
|
||||
|
||||
<Frame caption="In a world of infinite context, the context window is what Cline currently has available">
|
||||
<img
|
||||
src="/assets/robot_panel_dark.png"
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(2).png"
|
||||
alt="In a world of infinite context, the context window is what Cline currently has available"
|
||||
/>
|
||||
</Frame>
|
||||
@@ -65,7 +65,10 @@ Think of context like a whiteboard you and Cline share:
|
||||
Cline provides a visual way to monitor your context window usage through a progress bar:
|
||||
|
||||
<Frame caption="Visual representation of the context window usage">
|
||||
<img src="/assets/robot_panel_light.png" alt="Context window progress bar" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1)%20(1).png"
|
||||
alt="Context window progress bar"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
### Reading the Bar
|
||||
|
||||
@@ -11,7 +11,10 @@ Utilizing MCP servers will increase your token usage. Cline offers the ability t
|
||||
3. Cline will open a new settings window. find `Cline>Mcp:Mode` and make your selection from the dropdown menu.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="MCP settings edit" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/MCP-settings-edit%20(1).png"
|
||||
alt="MCP settings edit"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
## Managing Individual MCP Servers
|
||||
@@ -22,7 +25,10 @@ Each MCP server has its own configuration panel where you can modify settings, m
|
||||
2. Locate the MCP server you want to manage in the list, and open it by clicking on its name.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="MCP settings individual" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/MCP-settings-individual.png"
|
||||
alt="MCP settings individual"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
### Deleting a Server
|
||||
@@ -45,10 +51,6 @@ To set the maximum time to wait for a response after a tool call to the MCP serv
|
||||
|
||||
1. Click the `Network Timeout` dropdown at the bottom of the individual MCP server's config box and change the time. Default is 1 minute but it can be set between 30 seconds and 1 hour.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Network Timeout pulldown" />
|
||||
</Frame>
|
||||
|
||||
## Editing MCP Settings Files
|
||||
|
||||
Settings for all installed MCP servers are located in the `cline_mcp_settings.json` file:
|
||||
|
||||
@@ -14,7 +14,7 @@ Model Context Protocol is an open protocol that standardizes how applications pr
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="/assets/robot_panel_dark.png"
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/mcp-diagram.png"
|
||||
alt="MCP diagram showing how MCP servers connect LLMs to external tools and data sources"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
@@ -242,7 +242,10 @@ Let's walk through the development process of our AlphaAdvantage MCP server, whi
|
||||
### Planning Phase
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Planning phase demonstration" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/planning-phase.gif"
|
||||
alt="Planning phase demonstration"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
During the planning phase, we:
|
||||
@@ -267,7 +270,10 @@ During the planning phase, we:
|
||||
### Implementation
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Building MCP plugin demonstration" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/building-mcp-plugin.gif"
|
||||
alt="Building MCP plugin demonstration"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
We began by bootstrapping the project:
|
||||
|
||||
@@ -26,9 +26,9 @@ For complete transparency, you can inspect our [telemetry implementation](https:
|
||||
|
||||
### How to Opt Out
|
||||
|
||||
Telemetry in Cline is entirely optional and requires your explicit consent:
|
||||
Telemetry in Cline is entirely optional:
|
||||
|
||||
- When you update or install our VS Code extension, you'll see a simple prompt: "Help Improve Cline" with Allow or Deny options
|
||||
- When you update or install our VS Code extension, you'll see a message about our anonymous telemetry
|
||||
- You can change your preference anytime in settings
|
||||
|
||||
Cline also respects VS Code's global telemetry settings. If you've disabled telemetry at the VS Code level, Cline's telemetry will automatically be disabled as well.
|
||||
|
||||
@@ -13,7 +13,7 @@ To get started with Cline Memory Bank:
|
||||
3. **Paste into Cline** - Add as custom instructions or in a .clinerules file
|
||||
4. **Initialize** - Ask Cline to "initialize memory bank"
|
||||
|
||||
[See detailed setup instructions](cline-memory-bank.md#getting-started-with-memory-bank)
|
||||
[See detailed setup instructions](#getting-started-with-memory-bank)
|
||||
|
||||
### Cline Memory Bank Custom Instructions \[COPY THIS]
|
||||
|
||||
@@ -152,7 +152,7 @@ The Memory Bank is a structured documentation system that allows Cline to mainta
|
||||
The Memory Bank isn't a Cline-specific feature - it's a methodology for managing AI context through structured documentation. When you instruct Cline to "follow custom instructions," it reads the Memory Bank files to rebuild its understanding of your project.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Workflow" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(15).png" alt="Memory Bank Workflow" />
|
||||
</Frame>
|
||||
|
||||
#### Understanding the Files
|
||||
@@ -162,7 +162,7 @@ Memory Bank files are simply markdown files you create in your project. They're
|
||||
Files are organized in a hierarchical structure that builds up a complete picture of your project:
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Memory Bank File Structure" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(16).png" alt="Memory Bank File Structure" />
|
||||
</Frame>
|
||||
|
||||
### Memory Bank Files Explained
|
||||
@@ -223,7 +223,7 @@ Create additional files when needed to organize:
|
||||
3. Ask Cline to "initialize memory bank"
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Setup" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(17).png" alt="Memory Bank Setup" />
|
||||
</Frame>
|
||||
|
||||
#### Project Brief Tips
|
||||
@@ -287,7 +287,7 @@ As you work with Cline, your context window will eventually fill up (note the pr
|
||||
This workflow ensures that important context is preserved in your Memory Bank files before the context window is cleared, allowing you to continue seamlessly in a fresh conversation.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Context Window" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(18).png" alt="Memory Bank Context Window" />
|
||||
</Frame>
|
||||
|
||||
#### How often should I update the memory bank?
|
||||
|
||||
@@ -22,7 +22,7 @@ To add custom instructions:
|
||||
4. Paste your instructions
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Cline Logo" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
|
||||
</Frame>
|
||||
|
||||
Custom instructions are powerful for:
|
||||
@@ -207,7 +207,7 @@ Located conveniently under the chat input field, this popover allows you to:
|
||||
This UI significantly simplifies switching contexts and managing different sets of instructions without needing to manually edit files or configurations during a conversation.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Cline Logo" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
|
||||
</Frame>
|
||||
|
||||
## .clineignore File Guide
|
||||
|
||||
@@ -20,7 +20,7 @@ Run AI models locally using LM Studio with Cline.
|
||||
- Download and install for your operating system
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="LM Studio download page" />
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(7).png" alt="LM Studio download page" />
|
||||
</Frame>
|
||||
|
||||
#### 2. Launch LM Studio
|
||||
@@ -29,7 +29,10 @@ Run AI models locally using LM Studio with Cline.
|
||||
- You'll see four tabs on the left: **Chat**, **Developer** (where you will start the server), **My Models** (where your downloaded models are stored), **Discover** (add new models)
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="LM Studio interface overview" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(10).png"
|
||||
alt="LM Studio interface overview"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
#### 3. Download a Model
|
||||
@@ -39,7 +42,10 @@ Run AI models locally using LM Studio with Cline.
|
||||
- Wait for download to complete
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Downloading a model in LM Studio" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/lm-studio-download-model.gif"
|
||||
alt="Downloading a model in LM Studio"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
#### 4. Start the Server
|
||||
@@ -49,7 +55,10 @@ Run AI models locally using LM Studio with Cline.
|
||||
- Note: The server will run at `http://localhost:1234`
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Starting the LM Studio server" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/lm-studio-starting-server.gif"
|
||||
alt="Starting the LM Studio server"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
#### 5. Configure Cline
|
||||
@@ -60,7 +69,10 @@ Run AI models locally using LM Studio with Cline.
|
||||
4. Select your model from the available options
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Configuring Cline with LM Studio" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/lm-studio-select-model-cline.gif"
|
||||
alt="Configuring Cline with LM Studio"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
### ⚠️ Important Notes
|
||||
|
||||
@@ -16,7 +16,10 @@ description: "A quick guide to setting up Ollama for local AI model execution wi
|
||||
- Download and install for your operating system
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Ollama download page" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(2)%20(1)%20(1).png"
|
||||
alt="Ollama download page"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
#### 2. Choose and Download a Model
|
||||
@@ -29,7 +32,10 @@ description: "A quick guide to setting up Ollama for local AI model execution wi
|
||||
```
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Selecting a model in Ollama" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/ollama-model-grab%20(2).gif"
|
||||
alt="Selecting a model in Ollama"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
- Open your Terminal and run the command:
|
||||
@@ -41,7 +47,10 @@ description: "A quick guide to setting up Ollama for local AI model execution wi
|
||||
```
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Running Ollama in terminal" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/starting-ollama-terminal%20(2).gif"
|
||||
alt="Running Ollama in terminal"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
**✨ Your model is now ready to use within Cline!**
|
||||
@@ -56,7 +65,10 @@ description: "A quick guide to setting up Ollama for local AI model execution wi
|
||||
- Select the model from your available options
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Configuring Cline with Ollama" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/selecting-ollama-model-cline%20(3).gif"
|
||||
alt="Configuring Cline with Ollama"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
### ⚠️ Important Notes
|
||||
|
||||
@@ -20,7 +20,10 @@ Local models are created by training a smaller model to imitate a larger one, bu
|
||||
Think of it like running your development environment on a calculator instead of a computer – it might handle basic tasks, but complex operations become unreliable or impossible.
|
||||
|
||||
<Frame>
|
||||
<img src="/assets/robot_panel_dark.png" alt="Local model comparison diagram" />
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(4).png"
|
||||
alt="Local model comparison diagram"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
### What Actually Happens
|
||||
|
||||
Generated
+20
-35
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.14.0",
|
||||
"version": "3.15.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.14.0",
|
||||
"version": "3.15.2",
|
||||
"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",
|
||||
|
||||
+9
-39
@@ -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.2",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -40,6 +40,8 @@
|
||||
"llama"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onLanguage",
|
||||
"onStartupFinished",
|
||||
"workspaceContains:evals.env"
|
||||
],
|
||||
"main": "./dist/extension.js",
|
||||
@@ -121,7 +123,7 @@
|
||||
},
|
||||
{
|
||||
"command": "cline.generateGitCommitMessage",
|
||||
"title": "✨ Generate Commit Message with Cline",
|
||||
"title": "Generate Commit Message with Cline",
|
||||
"category": "Cline",
|
||||
"icon": "$(robot)"
|
||||
}
|
||||
@@ -235,45 +237,11 @@
|
||||
"configuration": {
|
||||
"title": "Cline",
|
||||
"properties": {
|
||||
"cline.vsCodeLmModelSelector": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"type": "string",
|
||||
"description": "The vendor of the language model (e.g. copilot)"
|
||||
},
|
||||
"family": {
|
||||
"type": "string",
|
||||
"description": "The family of the language model (e.g. gpt-4)"
|
||||
}
|
||||
},
|
||||
"description": "Settings for VSCode Language Model API"
|
||||
},
|
||||
"cline.enableCheckpoints": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which may not work well with large workspaces."
|
||||
},
|
||||
"cline.disableBrowserTool": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Disables extension from spawning browser session."
|
||||
},
|
||||
"cline.modelSettings.o3Mini.reasoningEffort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
],
|
||||
"default": "medium",
|
||||
"description": "Controls the reasoning effort when using an OpenAI reasoning model. Higher values may result in more thorough but slower responses."
|
||||
},
|
||||
"cline.chromeExecutablePath": {
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"description": "Path to Chrome executable for browser use functionality. If not set, the extension will attempt to find or download it automatically."
|
||||
},
|
||||
"cline.preferredLanguage": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -314,7 +282,7 @@
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
||||
"protos": "node proto/build-proto.js && prettier src/shared/proto --write && prettier src/core/controller --write",
|
||||
"protos": "node proto/build-proto.js && prettier src/shared/proto src/core/controller webview-ui/src/services --write",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
@@ -336,7 +304,9 @@
|
||||
"prepare": "husky",
|
||||
"changeset": "changeset",
|
||||
"version-packages": "changeset version",
|
||||
"docs:preview": "cd docs && mintlify dev",
|
||||
"docs": "cd docs && mintlify dev",
|
||||
"docs:check-links": "cd docs && mintlify broken-links",
|
||||
"docs:rename-file": "cd docs && mintlify rename",
|
||||
"report-issue": "node scripts/report-issue.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -409,11 +379,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",
|
||||
|
||||
@@ -40,6 +40,8 @@ message BrowserSettings {
|
||||
Viewport viewport = 1;
|
||||
optional string remote_browser_host = 2;
|
||||
optional bool remote_browser_enabled = 3;
|
||||
optional string chrome_executable_path = 4;
|
||||
optional bool disable_tool_use = 5;
|
||||
}
|
||||
|
||||
message UpdateBrowserSettingsRequest {
|
||||
@@ -47,4 +49,6 @@ message UpdateBrowserSettingsRequest {
|
||||
Viewport viewport = 2;
|
||||
optional string remote_browser_host = 3;
|
||||
optional bool remote_browser_enabled = 4;
|
||||
optional string chrome_executable_path = 5;
|
||||
optional bool disable_tool_use = 6;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ const serviceNameMap = {
|
||||
task: "cline.TaskService",
|
||||
web: "cline.WebService",
|
||||
models: "cline.ModelsService",
|
||||
slash: "cline.SlashService",
|
||||
// Add new services here - no other code changes needed!
|
||||
}
|
||||
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
|
||||
|
||||
+46
-1
@@ -10,7 +10,52 @@ import "common.proto";
|
||||
service ModelsService {
|
||||
// Fetches available models from Ollama
|
||||
rpc getOllamaModels(StringRequest) returns (StringArray);
|
||||
|
||||
// Fetches available models from LM Studio
|
||||
rpc getLmStudioModels(StringRequest) returns (StringArray);
|
||||
// Fetches available models from VS Code LM API
|
||||
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
|
||||
// Refreshes and returns OpenRouter models
|
||||
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns OpenAI models
|
||||
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
|
||||
// Refreshes and returns Requesty models
|
||||
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
message VsCodeLmModelsArray {
|
||||
repeated VsCodeLmModel models = 1;
|
||||
}
|
||||
|
||||
// Structure representing a VS Code LM model
|
||||
message VsCodeLmModel {
|
||||
string vendor = 1;
|
||||
string family = 2;
|
||||
string version = 3;
|
||||
string id = 4;
|
||||
}
|
||||
|
||||
// For OpenRouterCompatibleModelInfo structure in OpenRouterModels
|
||||
message OpenRouterModelInfo {
|
||||
int32 max_tokens = 1;
|
||||
int32 context_window = 2;
|
||||
bool supports_images = 3;
|
||||
bool supports_prompt_cache = 4;
|
||||
double input_price = 5;
|
||||
double output_price = 6;
|
||||
double cache_writes_price = 7;
|
||||
double cache_reads_price = 8;
|
||||
string description = 9;
|
||||
}
|
||||
|
||||
// Shared response message for model information
|
||||
message OpenRouterCompatibleModelInfo {
|
||||
map<string, OpenRouterModelInfo> models = 1;
|
||||
}
|
||||
|
||||
// Request for fetching OpenAI models
|
||||
message OpenAiModelsRequest {
|
||||
Metadata metadata = 1;
|
||||
string baseUrl = 2;
|
||||
string apiKey = 3;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
// SlashService provides methods for managing slash
|
||||
service SlashService {
|
||||
// Sends button click message
|
||||
rpc reportBug(StringRequest) returns (Empty);
|
||||
rpc condense(StringRequest) returns (Empty);
|
||||
}
|
||||
+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;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { DoubaoHandler } from "./providers/doubao"
|
||||
import { VsCodeLmHandler } from "./providers/vscode-lm"
|
||||
import { ClineHandler } from "./providers/cline"
|
||||
import { LiteLlmHandler } from "./providers/litellm"
|
||||
import { FireworksHandler } from "./providers/fireworks"
|
||||
import { AskSageHandler } from "./providers/asksage"
|
||||
import { XAIHandler } from "./providers/xai"
|
||||
import { SambanovaHandler } from "./providers/sambanova"
|
||||
@@ -58,6 +59,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
return new DeepSeekHandler(options)
|
||||
case "requesty":
|
||||
return new RequestyHandler(options)
|
||||
case "fireworks":
|
||||
return new FireworksHandler(options)
|
||||
case "together":
|
||||
return new TogetherHandler(options)
|
||||
case "qwen":
|
||||
|
||||
@@ -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-":
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from ".."
|
||||
import {
|
||||
ApiHandlerOptions,
|
||||
DeepSeekModelId,
|
||||
ModelInfo,
|
||||
deepSeekDefaultModelId,
|
||||
deepSeekModels,
|
||||
openAiModelInfoSaneDefaults,
|
||||
} from "../../shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
export class FireworksHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.fireworks.ai/inference/v1",
|
||||
apiKey: this.options.fireworksApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const modelId = this.options.fireworksModelId ?? ""
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelId,
|
||||
...(this.options.fireworksModelMaxCompletionTokens
|
||||
? { max_completion_tokens: this.options.fireworksModelMaxCompletionTokens }
|
||||
: {}),
|
||||
...(this.options.fireworksModelMaxTokens ? { max_tokens: this.options.fireworksModelMaxTokens } : {}),
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
let reasoning: string | null = null
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (reasoning || delta?.content?.includes("<think>")) {
|
||||
reasoning = (reasoning || "") + (delta.content ?? "")
|
||||
}
|
||||
|
||||
if (delta?.content && !reasoning) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (reasoning || ("reasoning_content" in delta && delta.reasoning_content)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: delta.content || ((delta as any).reasoning_content as string | undefined) || "",
|
||||
}
|
||||
if (reasoning?.includes("</think>")) {
|
||||
// Reset so the next chunk is regular content
|
||||
reasoning = null
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0, // (deepseek reports total input AND cache reads/writes, see context caching: https://api-docs.deepseek.com/guides/kv_cache) where the input tokens is the sum of the cache hits/misses, while anthropic reports them as separate tokens. This is important to know for 1) context management truncation algorithm, and 2) cost calculation (NOTE: we report both input and cache stats but for now set input price to 0 since all the cost calculation will be done using cache hits/misses)
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.fireworksModelId ?? "",
|
||||
info: openAiModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
+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
|
||||
*/
|
||||
|
||||
@@ -65,7 +65,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
const reasoningOn = budgetTokens !== 0 ? true : false
|
||||
const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined
|
||||
|
||||
let temperature: number | undefined = 0
|
||||
let temperature: number | undefined = this.options.liteLlmModelInfo?.temperature ?? 0
|
||||
|
||||
if (isOminiModel && reasoningOn) {
|
||||
temperature = undefined // Thinking mode doesn't support temperature
|
||||
@@ -169,7 +169,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
getModel() {
|
||||
return {
|
||||
id: this.options.liteLlmModelId || liteLlmDefaultModelId,
|
||||
info: liteLlmModelInfoSaneDefaults,
|
||||
info: this.options.liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ export class OpenAiHandler implements ApiHandler {
|
||||
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
|
||||
if (
|
||||
this.options.azureApiVersion ||
|
||||
(this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") &&
|
||||
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
|
||||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
|
||||
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
|
||||
) {
|
||||
this.client = new AzureOpenAI({
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function createOpenRouterStream(
|
||||
|
||||
// prompt caching: https://openrouter.ai/docs/prompt-caching
|
||||
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
|
||||
// includes custom support for gemini which does not have iterative caching
|
||||
// handles direct model.id match logic
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
@@ -71,75 +71,6 @@ export async function createOpenRouterStream(
|
||||
}
|
||||
})
|
||||
break
|
||||
case "google/gemini-2.5-pro-preview-03-25":
|
||||
case "google/gemini-2.0-flash-001":
|
||||
case "google/gemini-flash-1.5":
|
||||
case "google/gemini-pro-1.5":
|
||||
// gemini only uses the last breakpoint for caching, so the others will be ignored
|
||||
openAiMessages[0] = {
|
||||
role: "system",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: systemPrompt,
|
||||
// @ts-ignore-next-line
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// for safety, but this should always be the case
|
||||
if (openAiMessages.length >= 2) {
|
||||
const msg = openAiMessages[1]
|
||||
|
||||
if (msg) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = [{ type: "text", text: msg.content }]
|
||||
}
|
||||
if (Array.isArray(msg.content)) {
|
||||
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
|
||||
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||
|
||||
if (!lastTextPart) {
|
||||
lastTextPart = { type: "text", text: "..." }
|
||||
msg.content.push(lastTextPart)
|
||||
}
|
||||
// @ts-ignore-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// it doesn't make sense to alter breakpoints at all with the gemini cache implementation at this time
|
||||
/*const GEMINI_CACHE_USER_MESSAGE_INTERVAL = 4 // add new breakpoint every 4 turns
|
||||
const userMessages = openAiMessages.filter((msg) => msg.role === "user")
|
||||
|
||||
const userMessageCount = userMessages.length
|
||||
const targetUserMessageNumber =
|
||||
Math.floor(userMessageCount / GEMINI_CACHE_USER_MESSAGE_INTERVAL) * GEMINI_CACHE_USER_MESSAGE_INTERVAL
|
||||
|
||||
if (targetUserMessageNumber > 0) {
|
||||
// otherwise dont need to add a breakpoint
|
||||
const msg = userMessages[targetUserMessageNumber - 1]
|
||||
|
||||
if (msg) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = [{ type: "text", text: msg.content }]
|
||||
}
|
||||
if (Array.isArray(msg.content)) {
|
||||
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
|
||||
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||
|
||||
if (!lastTextPart) {
|
||||
lastTextPart = { type: "text", text: "..." }
|
||||
msg.content.push(lastTextPart)
|
||||
}
|
||||
// @ts-ignore-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
}
|
||||
}
|
||||
}*/
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type AssistantMessageContent = TextContent | ToolUse
|
||||
|
||||
export { parseAssistantMessage } from "./parse-assistant-message"
|
||||
export { parseAssistantMessageV1, parseAssistantMessageV2 } from "./parse-assistant-message"
|
||||
|
||||
export interface TextContent {
|
||||
type: "text"
|
||||
@@ -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]
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "."
|
||||
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "." // Assuming types are defined in index.ts or a similar file
|
||||
|
||||
export function parseAssistantMessage(assistantMessage: string) {
|
||||
/**
|
||||
* @description **Version 1**
|
||||
* Parses an assistant message string potentially containing mixed text and tool usage blocks
|
||||
* marked with XML-like tags into an array of structured content objects.
|
||||
*
|
||||
* This version iterates through the message character by character, building an accumulator string.
|
||||
* It maintains state to track whether it's currently parsing text, a tool use block, or a specific tool parameter.
|
||||
* It detects the start and end of tool uses and parameters by checking if the accumulator ends with
|
||||
* the corresponding opening or closing tags.
|
||||
* Special handling is included for `write_to_file` and `new_rule` tool uses to correctly parse
|
||||
* the `content` parameter, which might contain the closing tag itself, by looking for the *last*
|
||||
* occurrence of the closing tag.
|
||||
* If the input string ends mid-tag or mid-content, the last block (text or tool use) is marked as partial.
|
||||
*
|
||||
* @param assistantMessage The raw string output from the assistant.
|
||||
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
|
||||
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
|
||||
*/
|
||||
export function parseAssistantMessageV1(assistantMessage: string): AssistantMessageContent[] {
|
||||
const contentBlocks: AssistantMessageContent[] = []
|
||||
let currentTextContent: TextContent | undefined = undefined
|
||||
let currentTextContentStartIndex = 0
|
||||
@@ -14,46 +32,56 @@ export function parseAssistantMessage(assistantMessage: string) {
|
||||
const char = assistantMessage[i]
|
||||
accumulator += char
|
||||
|
||||
// --- State: Parsing a Tool Parameter ---
|
||||
// there should not be a param without a tool use
|
||||
if (currentToolUse && currentParamName) {
|
||||
const currentParamValue = accumulator.slice(currentParamValueStartIndex)
|
||||
const paramClosingTag = `</${currentParamName}>`
|
||||
if (currentParamValue.endsWith(paramClosingTag)) {
|
||||
// end of param value
|
||||
// End of param value found
|
||||
currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim()
|
||||
currentParamName = undefined
|
||||
continue
|
||||
currentParamName = undefined // Go back to parsing tool content or looking for next param
|
||||
continue // Move to next character
|
||||
} else {
|
||||
// partial param value is accumulating
|
||||
continue
|
||||
// Partial param value is accumulating
|
||||
continue // Move to next character
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing a Tool Use (but not a specific parameter) ---
|
||||
// no currentParamName
|
||||
|
||||
if (currentToolUse) {
|
||||
const currentToolValue = accumulator.slice(currentToolUseStartIndex)
|
||||
const toolUseClosingTag = `</${currentToolUse.name}>`
|
||||
|
||||
if (currentToolValue.endsWith(toolUseClosingTag)) {
|
||||
// end of a tool use
|
||||
// End of a tool use found
|
||||
currentToolUse.partial = false
|
||||
contentBlocks.push(currentToolUse)
|
||||
currentToolUse = undefined
|
||||
continue
|
||||
currentToolUse = undefined // Go back to parsing text or looking for next tool
|
||||
// Reset text start index in case text follows immediately
|
||||
currentTextContentStartIndex = i + 1
|
||||
continue // Move to next character
|
||||
} else {
|
||||
// Check if starting a new parameter within the current tool use
|
||||
const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`)
|
||||
let foundParamStart = false
|
||||
for (const paramOpeningTag of possibleParamOpeningTags) {
|
||||
if (accumulator.endsWith(paramOpeningTag)) {
|
||||
// start of a new parameter
|
||||
// Start of a new parameter found
|
||||
currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
|
||||
currentParamValueStartIndex = accumulator.length
|
||||
foundParamStart = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (foundParamStart) {
|
||||
continue // Move to next character
|
||||
}
|
||||
|
||||
// there's no current param, and not starting a new param
|
||||
|
||||
// special case for write_to_file where file contents could contain the closing tag, in which case the param would have closed and we end up with the rest of the file contents here. To work around this, we get the string between the starting content tag and the LAST content tag.
|
||||
// Special case for write_to_file/new_rule content param allowing nested tags
|
||||
// Check if a </content> tag appears, potentially indicating the end of the content param
|
||||
// even if the main tool closing tag hasn't been seen yet.
|
||||
const contentParamName: ToolParamName = "content"
|
||||
if (
|
||||
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
|
||||
@@ -63,73 +91,385 @@ export function parseAssistantMessage(assistantMessage: string) {
|
||||
const contentStartTag = `<${contentParamName}>`
|
||||
const contentEndTag = `</${contentParamName}>`
|
||||
const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length
|
||||
// Use lastIndexOf to handle cases where </content> might appear within the content itself
|
||||
const contentEndIndex = toolContent.lastIndexOf(contentEndTag)
|
||||
if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) {
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
|
||||
// Ensure we found valid start/end tags and end is after start
|
||||
if (
|
||||
contentStartIndex !== -1 &&
|
||||
contentEndIndex !== -1 &&
|
||||
contentEndIndex > contentStartIndex - contentStartTag.length // Ensure end tag is after start tag begins
|
||||
) {
|
||||
// Check if this content param was already being parsed. If so, update it.
|
||||
// If not, and we just found the closing tag, assign it.
|
||||
// This handles cases where the </content> detection might fire before
|
||||
// the <content> tag detection logic, or if the content is very short.
|
||||
if (currentParamName === contentParamName) {
|
||||
// Already parsing content, now we found the end tag
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
currentParamName = undefined // Finished with this param
|
||||
} else if (currentParamName === undefined) {
|
||||
// Not parsing a param, but found </content>. Assume it closes the content block.
|
||||
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
|
||||
// We stay in the "parsing tool use" state, looking for more params or the tool end tag.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// partial tool value is accumulating
|
||||
continue
|
||||
// If none of the above, partial tool value is accumulating
|
||||
continue // Move to next character
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing Text (or looking for start of a tool use) ---
|
||||
// no currentToolUse
|
||||
|
||||
let didStartToolUse = false
|
||||
const possibleToolUseOpeningTags = toolUseNames.map((name) => `<${name}>`)
|
||||
for (const toolUseOpeningTag of possibleToolUseOpeningTags) {
|
||||
if (accumulator.endsWith(toolUseOpeningTag)) {
|
||||
// start of a new tool use
|
||||
// Start of a new tool use found
|
||||
const toolName = toolUseOpeningTag.slice(1, -1) as ToolUseName
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolUseOpeningTag.slice(1, -1) as ToolUseName,
|
||||
name: toolName,
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
currentToolUseStartIndex = accumulator.length
|
||||
// this also indicates the end of the current text content
|
||||
|
||||
// This also indicates the end of the current text content block (if any)
|
||||
if (currentTextContent) {
|
||||
currentTextContent.partial = false
|
||||
// remove the partially accumulated tool use tag from the end of text (<tool)
|
||||
currentTextContent.content = currentTextContent.content
|
||||
.slice(0, -toolUseOpeningTag.slice(0, -1).length)
|
||||
.trim()
|
||||
contentBlocks.push(currentTextContent)
|
||||
// Extract text content, removing the part that formed the tool opening tag
|
||||
const textEndIndex = accumulator.length - toolUseOpeningTag.length
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
|
||||
// Only add if there's actual content
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
currentTextContent = undefined
|
||||
} else {
|
||||
// Check if there was text before this tool use started
|
||||
const textEndIndex = accumulator.length - toolUseOpeningTag.length
|
||||
const potentialText = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
|
||||
if (potentialText.length > 0) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
content: potentialText,
|
||||
partial: false, // Ended because tool use started
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
didStartToolUse = true
|
||||
break
|
||||
break // Found tool start, stop checking for others
|
||||
}
|
||||
}
|
||||
|
||||
if (!didStartToolUse) {
|
||||
// no tool use, so it must be text either at the beginning or between tools
|
||||
// No tool use started, so it must be text content accumulating
|
||||
// (or continuing after a closed tool use)
|
||||
if (currentTextContent === undefined) {
|
||||
currentTextContentStartIndex = i
|
||||
}
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: accumulator.slice(currentTextContentStartIndex).trim(),
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start of a new text block
|
||||
currentTextContentStartIndex = i - (accumulator.length - currentTextContentStartIndex - 1) // Adjust start index based on how much we've accumulated since the last block ended or the beginning
|
||||
// If accumulator starts from 0, start index is i
|
||||
if (contentBlocks.length === 0 && currentToolUse === undefined) {
|
||||
currentTextContentStartIndex = accumulator.length - 1 // i
|
||||
} else {
|
||||
// Re-calculate based on the actual start of the current text segment
|
||||
// Find the end of the last block
|
||||
let lastBlockEndIndex = 0
|
||||
if (contentBlocks.length > 0) {
|
||||
const lastBlock = contentBlocks[contentBlocks.length - 1]
|
||||
// Approximation: find where the accumulator matches the end of the message string representation of the last block. This is complex.
|
||||
// Simpler: Assume text starts right after the last block ended implicitly at index i.
|
||||
lastBlockEndIndex = i // Where the loop *was* when the last block finished processing
|
||||
// Need a more robust way to track the end index of the *raw string* corresponding to the last block.
|
||||
// Let's stick to the accumulator slice approach for simplicity in this version.
|
||||
// The start index should be where the current *unmatched* text began.
|
||||
let lastProcessedIndex = -1
|
||||
if (contentBlocks.length > 0) {
|
||||
// This requires knowing the raw string length of the previous block, which V1 doesn't explicitly track easily.
|
||||
// We'll approximate based on the current accumulator and start index logic.
|
||||
// The issue arises if a tool tag was just closed. accumulator contains everything up to i.
|
||||
// lastBlockEndIndex should point to the character *after* the closing tag of the last block.
|
||||
}
|
||||
// Reset start index to the beginning of the *current* potential text block
|
||||
currentTextContentStartIndex = accumulator.length - 1 // Start accumulating from the current character `i`
|
||||
}
|
||||
|
||||
// If we just closed a tool, text starts *after* its closing tag
|
||||
// The logic needs refinement here for accurate start index after a tool closure.
|
||||
// Let's assume for now the start index logic inside the loop handles it via slicing.
|
||||
}
|
||||
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: "", // Content will be filled by slicing accumulator
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
// Update text content based on the accumulator from its start index
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trimStart() // Trim start to avoid leading space if text follows tool
|
||||
}
|
||||
} // End of loop
|
||||
|
||||
// --- Finalization after loop ---
|
||||
|
||||
// If a tool use was open at the end
|
||||
if (currentToolUse) {
|
||||
// stream did not complete tool call, add it as partial
|
||||
// If a parameter was open within that tool use
|
||||
if (currentParamName) {
|
||||
// tool call has a parameter that was not completed
|
||||
// The remaining accumulator content belongs to this partial parameter
|
||||
currentToolUse.params[currentParamName] = accumulator.slice(currentParamValueStartIndex).trim()
|
||||
}
|
||||
// Add the potentially partial tool use block
|
||||
contentBlocks.push(currentToolUse)
|
||||
}
|
||||
// If text content was being accumulated at the end
|
||||
// Note: Only one of currentToolUse or currentTextContent can be defined here,
|
||||
// as starting a tool use finalizes the preceding text block.
|
||||
else if (currentTextContent) {
|
||||
// Update content one last time
|
||||
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trim()
|
||||
// Add the potentially partial text block only if it contains content
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
}
|
||||
|
||||
// Note: it doesn't matter if check for currentToolUse or currentTextContent, only one of them will be defined since only one can be partial at a time
|
||||
if (currentTextContent) {
|
||||
// stream did not complete text content, add it as partial
|
||||
contentBlocks.push(currentTextContent)
|
||||
return contentBlocks
|
||||
}
|
||||
|
||||
/**
|
||||
* @description **Version 2**
|
||||
* Parses an assistant message string potentially containing mixed text and tool usage blocks
|
||||
* marked with XML-like tags into an array of structured content objects.
|
||||
*
|
||||
* This version aims for efficiency by avoiding the character-by-character accumulator of V1.
|
||||
* It iterates through the string using an index `i`. At each position, it checks if the substring
|
||||
* *ending* at `i` matches any known opening or closing tags for tools or parameters using `startsWith`
|
||||
* with an offset.
|
||||
* It uses pre-computed Maps (`toolUseOpenTags`, `toolParamOpenTags`) for quick tag lookups.
|
||||
* State is managed using indices (`currentTextContentStart`, `currentToolUseStart`, `currentParamValueStart`)
|
||||
* pointing to the start of the current block within the original `assistantMessage` string.
|
||||
* Slicing is used to extract content only when a block (text, parameter, or tool use) is completed.
|
||||
* Special handling for `write_to_file` and `new_rule` content parameters is included, using `indexOf`
|
||||
* and `lastIndexOf` on the relevant slice to handle potentially nested closing tags.
|
||||
* If the input string ends mid-block, the last open block is added and marked as partial.
|
||||
*
|
||||
* @param assistantMessage The raw string output from the assistant.
|
||||
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
|
||||
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
|
||||
*/
|
||||
export function parseAssistantMessageV2(assistantMessage: string): AssistantMessageContent[] {
|
||||
const contentBlocks: AssistantMessageContent[] = []
|
||||
let currentTextContentStart = 0 // Index where the current text block started
|
||||
let currentTextContent: TextContent | undefined = undefined
|
||||
let currentToolUseStart = 0 // Index *after* the opening tag of the current tool use
|
||||
let currentToolUse: ToolUse | undefined = undefined
|
||||
let currentParamValueStart = 0 // Index *after* the opening tag of the current param
|
||||
let currentParamName: ToolParamName | undefined = undefined
|
||||
|
||||
// Precompute tags for faster lookups
|
||||
const toolUseOpenTags = new Map<string, ToolUseName>()
|
||||
const toolParamOpenTags = new Map<string, ToolParamName>()
|
||||
for (const name of toolUseNames) {
|
||||
toolUseOpenTags.set(`<${name}>`, name)
|
||||
}
|
||||
for (const name of toolParamNames) {
|
||||
toolParamOpenTags.set(`<${name}>`, name)
|
||||
}
|
||||
|
||||
const len = assistantMessage.length
|
||||
for (let i = 0; i < len; i++) {
|
||||
const currentCharIndex = i
|
||||
|
||||
// --- State: Parsing a Tool Parameter ---
|
||||
if (currentToolUse && currentParamName) {
|
||||
const closeTag = `</${currentParamName}>`
|
||||
// Check if the string *ending* at index `i` matches the closing tag
|
||||
if (
|
||||
currentCharIndex >= closeTag.length - 1 &&
|
||||
assistantMessage.startsWith(
|
||||
closeTag,
|
||||
currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag
|
||||
)
|
||||
) {
|
||||
// Found the closing tag for the parameter
|
||||
const value = assistantMessage
|
||||
.slice(
|
||||
currentParamValueStart, // Start after the opening tag
|
||||
currentCharIndex - closeTag.length + 1, // End before the closing tag
|
||||
)
|
||||
.trim()
|
||||
currentToolUse.params[currentParamName] = value
|
||||
currentParamName = undefined // Go back to parsing tool content
|
||||
// We don't continue loop here, need to check for tool close or other params at index i
|
||||
} else {
|
||||
continue // Still inside param value, move to next char
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing a Tool Use (but not a specific parameter) ---
|
||||
if (currentToolUse && !currentParamName) {
|
||||
// Ensure we are not inside a parameter already
|
||||
// Check if starting a new parameter
|
||||
let startedNewParam = false
|
||||
for (const [tag, paramName] of toolParamOpenTags.entries()) {
|
||||
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
|
||||
currentParamName = paramName
|
||||
currentParamValueStart = currentCharIndex + 1 // Value starts after the tag
|
||||
startedNewParam = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (startedNewParam) {
|
||||
continue // Handled start of param, move to next char
|
||||
}
|
||||
|
||||
// Check if closing the current tool use
|
||||
const toolCloseTag = `</${currentToolUse.name}>`
|
||||
if (
|
||||
currentCharIndex >= toolCloseTag.length - 1 &&
|
||||
assistantMessage.startsWith(toolCloseTag, currentCharIndex - toolCloseTag.length + 1)
|
||||
) {
|
||||
// End of the tool use found
|
||||
// Special handling for content params *before* finalizing the tool
|
||||
const toolContentSlice = assistantMessage.slice(
|
||||
currentToolUseStart, // From after the tool opening tag
|
||||
currentCharIndex - toolCloseTag.length + 1, // To before the tool closing tag
|
||||
)
|
||||
|
||||
// Check if content parameter needs special handling (write_to_file/new_rule)
|
||||
// This check is important if the closing </content> tag was missed by the parameter parsing logic
|
||||
// (e.g., if content is empty or parsing logic prioritizes tool close)
|
||||
const contentParamName: ToolParamName = "content"
|
||||
if (
|
||||
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
|
||||
!(contentParamName in currentToolUse.params) && // Only if not already parsed
|
||||
toolContentSlice.includes(`<${contentParamName}>`) // Check if tag exists
|
||||
) {
|
||||
const contentStartTag = `<${contentParamName}>`
|
||||
const contentEndTag = `</${contentParamName}>`
|
||||
const contentStart = toolContentSlice.indexOf(contentStartTag)
|
||||
// Use lastIndexOf for robustness against nested tags
|
||||
const contentEnd = toolContentSlice.lastIndexOf(contentEndTag)
|
||||
|
||||
if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) {
|
||||
const contentValue = toolContentSlice.slice(contentStart + contentStartTag.length, contentEnd).trim()
|
||||
currentToolUse.params[contentParamName] = contentValue
|
||||
}
|
||||
}
|
||||
|
||||
currentToolUse.partial = false // Mark as complete
|
||||
contentBlocks.push(currentToolUse)
|
||||
currentToolUse = undefined // Reset state
|
||||
currentTextContentStart = currentCharIndex + 1 // Potential text starts after this tag
|
||||
continue // Move to next char
|
||||
}
|
||||
// If not starting a param and not closing the tool, continue accumulating tool content implicitly
|
||||
continue
|
||||
}
|
||||
|
||||
// --- State: Parsing Text / Looking for Tool Start ---
|
||||
if (!currentToolUse) {
|
||||
// Check if starting a new tool use
|
||||
let startedNewTool = false
|
||||
for (const [tag, toolName] of toolUseOpenTags.entries()) {
|
||||
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
|
||||
// End current text block if one was active
|
||||
if (currentTextContent) {
|
||||
currentTextContent.content = assistantMessage
|
||||
.slice(
|
||||
currentTextContentStart, // From where text started
|
||||
currentCharIndex - tag.length + 1, // To before the tool tag starts
|
||||
)
|
||||
.trim()
|
||||
currentTextContent.partial = false // Ended because tool started
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
currentTextContent = undefined
|
||||
} else {
|
||||
// Check for any text between the last block and this tag
|
||||
const potentialText = assistantMessage
|
||||
.slice(
|
||||
currentTextContentStart, // From where text *might* have started
|
||||
currentCharIndex - tag.length + 1, // To before the tool tag starts
|
||||
)
|
||||
.trim()
|
||||
if (potentialText.length > 0) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
content: potentialText,
|
||||
partial: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Start the new tool use
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolName,
|
||||
params: {},
|
||||
partial: true, // Assume partial until closing tag is found
|
||||
}
|
||||
currentToolUseStart = currentCharIndex + 1 // Tool content starts after the opening tag
|
||||
startedNewTool = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (startedNewTool) {
|
||||
continue // Handled start of tool, move to next char
|
||||
}
|
||||
|
||||
// If not starting a tool, it must be text content
|
||||
if (!currentTextContent) {
|
||||
// Start a new text block if we aren't already in one
|
||||
currentTextContentStart = currentCharIndex // Text starts at the current character
|
||||
// Check if the current char is the start of potential text *immediately* after a tag
|
||||
// This needs the previous state - simpler to let slicing handle it later.
|
||||
// Resetting start index accurately is key.
|
||||
// It should be the index *after* the last processed tag.
|
||||
// The logic managing currentTextContentStart after closing tags handles this.
|
||||
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: "", // Will be determined by slicing at the end or when a tool starts
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
// Continue accumulating text implicitly; content is extracted later.
|
||||
}
|
||||
} // End of loop
|
||||
|
||||
// --- Finalization after loop ---
|
||||
|
||||
// Finalize any open parameter within an open tool use
|
||||
if (currentToolUse && currentParamName) {
|
||||
currentToolUse.params[currentParamName] = assistantMessage
|
||||
.slice(currentParamValueStart) // From param start to end of string
|
||||
.trim()
|
||||
// Tool use remains partial
|
||||
}
|
||||
|
||||
// Finalize any open tool use (which might contain the finalized partial param)
|
||||
if (currentToolUse) {
|
||||
// Tool use is partial because the loop finished before its closing tag
|
||||
contentBlocks.push(currentToolUse)
|
||||
}
|
||||
// Finalize any trailing text content
|
||||
// Only possible if a tool use wasn't open at the very end
|
||||
else if (currentTextContent) {
|
||||
currentTextContent.content = assistantMessage
|
||||
.slice(currentTextContentStart) // From text start to end of string
|
||||
.trim()
|
||||
// Text is partial because the loop finished
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
}
|
||||
|
||||
return contentBlocks
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as vscode from "vscode"
|
||||
import crypto from "crypto"
|
||||
import { Controller } from "../index"
|
||||
import { storeSecret } from "../../storage/state"
|
||||
import { EmptyRequest, String } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Handles the user clicking the login link in the UI.
|
||||
@@ -11,7 +12,7 @@ import { storeSecret } from "../../storage/state"
|
||||
* @param controller The controller instance.
|
||||
* @returns The login URL as a string.
|
||||
*/
|
||||
export async function accountLoginClicked(controller: Controller): Promise<String> {
|
||||
export async function accountLoginClicked(controller: Controller, unused: EmptyRequest): Promise<String> {
|
||||
// Generate nonce for state validation
|
||||
const nonce = crypto.randomBytes(32).toString("hex")
|
||||
await storeSecret(controller.context, "authNonce", nonce)
|
||||
@@ -25,6 +26,8 @@ export async function accountLoginClicked(controller: Controller): Promise<Strin
|
||||
const authUrl = vscode.Uri.parse(
|
||||
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
|
||||
)
|
||||
vscode.env.openExternal(authUrl)
|
||||
return authUrl.toString()
|
||||
await vscode.env.openExternal(authUrl)
|
||||
return {
|
||||
value: authUrl.toString(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { UpdateBrowserSettingsRequest } from "../../../shared/proto/browser"
|
||||
import { Boolean } from "../../../shared/proto/common"
|
||||
import { Controller } from "../index"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
import { BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings"
|
||||
import { updateGlobalState, getGlobalState } from "../../storage/state"
|
||||
import { BrowserSettings as SharedBrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../../shared/BrowserSettings"
|
||||
|
||||
/**
|
||||
* Update browser settings
|
||||
@@ -12,23 +12,39 @@ import { BrowserSettings as SharedBrowserSettings } from "../../../shared/Browse
|
||||
*/
|
||||
export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise<Boolean> {
|
||||
try {
|
||||
// Convert from protobuf format to shared format
|
||||
const browserSettings: SharedBrowserSettings = {
|
||||
// Get current browser settings to preserve fields not in the request
|
||||
const currentSettings = (await getGlobalState(controller.context, "browserSettings")) as SharedBrowserSettings | undefined
|
||||
const mergedWithDefaults = { ...DEFAULT_BROWSER_SETTINGS, ...currentSettings }
|
||||
|
||||
// Convert from protobuf format to shared format, merging with existing settings
|
||||
const newBrowserSettings: SharedBrowserSettings = {
|
||||
...mergedWithDefaults, // Start with existing settings (and defaults)
|
||||
viewport: {
|
||||
width: request.viewport?.width || 900,
|
||||
height: request.viewport?.height || 600,
|
||||
// Apply updates from request
|
||||
width: request.viewport?.width || mergedWithDefaults.viewport.width,
|
||||
height: request.viewport?.height || mergedWithDefaults.viewport.height,
|
||||
},
|
||||
remoteBrowserEnabled: request.remoteBrowserEnabled || false,
|
||||
remoteBrowserHost: request.remoteBrowserHost || undefined,
|
||||
// Explicitly handle optional boolean and string fields from the request
|
||||
remoteBrowserEnabled:
|
||||
request.remoteBrowserEnabled === undefined
|
||||
? mergedWithDefaults.remoteBrowserEnabled
|
||||
: request.remoteBrowserEnabled,
|
||||
remoteBrowserHost:
|
||||
request.remoteBrowserHost === undefined ? mergedWithDefaults.remoteBrowserHost : request.remoteBrowserHost,
|
||||
chromeExecutablePath:
|
||||
// If chromeExecutablePath is explicitly in the request (even as ""), use it.
|
||||
// Otherwise, fall back to mergedWithDefaults.
|
||||
"chromeExecutablePath" in request ? request.chromeExecutablePath : mergedWithDefaults.chromeExecutablePath,
|
||||
disableToolUse: request.disableToolUse === undefined ? mergedWithDefaults.disableToolUse : request.disableToolUse,
|
||||
}
|
||||
|
||||
// Update global state with new settings
|
||||
await updateGlobalState(controller.context, "browserSettings", browserSettings)
|
||||
await updateGlobalState(controller.context, "browserSettings", newBrowserSettings)
|
||||
|
||||
// Update task browser settings if task exists
|
||||
if (controller.task) {
|
||||
controller.task.browserSettings = browserSettings
|
||||
controller.task.browserSession.browserSettings = browserSettings
|
||||
controller.task.browserSettings = newBrowserSettings
|
||||
controller.task.browserSession.browserSettings = newBrowserSettings
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
|
||||
@@ -12,6 +12,7 @@ import { handleStateServiceRequest, handleStateServiceStreamingRequest } from ".
|
||||
import { handleTaskServiceRequest, handleTaskServiceStreamingRequest } from "./task/index"
|
||||
import { handleWebServiceRequest, handleWebServiceStreamingRequest } from "./web/index"
|
||||
import { handleModelsServiceRequest, handleModelsServiceStreamingRequest } from "./models/index"
|
||||
import { handleSlashServiceRequest, handleSlashServiceStreamingRequest } from "./slash/index"
|
||||
|
||||
/**
|
||||
* Configuration for a service handler
|
||||
@@ -67,4 +68,8 @@ export const serviceHandlers: Record<string, ServiceHandlerConfig> = {
|
||||
requestHandler: handleModelsServiceRequest,
|
||||
streamingHandler: handleModelsServiceStreamingRequest,
|
||||
},
|
||||
"cline.SlashService": {
|
||||
requestHandler: handleSlashServiceRequest,
|
||||
streamingHandler: handleSlashServiceStreamingRequest,
|
||||
},
|
||||
}
|
||||
|
||||
+62
-256
@@ -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()
|
||||
}
|
||||
@@ -250,7 +249,7 @@ export class Controller {
|
||||
// If user already opted in to telemetry, enable telemetry service
|
||||
this.getStateToPostToWebview().then((state) => {
|
||||
const { telemetrySetting } = state
|
||||
const isOptedIn = telemetrySetting === "enabled"
|
||||
const isOptedIn = telemetrySetting !== "disabled"
|
||||
telemetryService.updateTelemetryState(isOptedIn)
|
||||
})
|
||||
break
|
||||
@@ -272,9 +271,6 @@ export class Controller {
|
||||
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
|
||||
await this.initTask(message.text, message.images)
|
||||
break
|
||||
case "condense":
|
||||
this.task?.handleWebviewAskResponse("yesButtonClicked")
|
||||
break
|
||||
case "apiConfiguration":
|
||||
if (message.apiConfiguration) {
|
||||
await updateApiConfiguration(this.context, message.apiConfiguration)
|
||||
@@ -332,21 +328,6 @@ export class Controller {
|
||||
case "resetState":
|
||||
await this.resetState()
|
||||
break
|
||||
case "requestVsCodeLmModels":
|
||||
const vsCodeLmModels = await this.getVsCodeLmModels()
|
||||
this.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
|
||||
break
|
||||
case "refreshOpenRouterModels":
|
||||
await this.refreshOpenRouterModels()
|
||||
break
|
||||
case "refreshRequestyModels":
|
||||
await this.refreshRequestyModels()
|
||||
break
|
||||
case "refreshOpenAiModels":
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
const openAiModels = await this.getOpenAiModels(apiConfiguration.openAiBaseUrl, apiConfiguration.openAiApiKey)
|
||||
this.postMessageToWebview({ type: "openAiModels", openAiModels })
|
||||
break
|
||||
case "refreshClineRules":
|
||||
await refreshClineRulesToggles(this.context, cwd)
|
||||
await refreshExternalRulesToggles(this.context, cwd)
|
||||
@@ -603,11 +584,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()
|
||||
@@ -679,7 +667,7 @@ export class Controller {
|
||||
|
||||
async updateTelemetrySetting(telemetrySetting: TelemetrySetting) {
|
||||
await updateGlobalState(this.context, "telemetrySetting", telemetrySetting)
|
||||
const isOptedIn = telemetrySetting === "enabled"
|
||||
const isOptedIn = telemetrySetting !== "disabled"
|
||||
telemetryService.updateTelemetryState(isOptedIn)
|
||||
}
|
||||
|
||||
@@ -759,6 +747,7 @@ export class Controller {
|
||||
break
|
||||
case "litellm":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
|
||||
break
|
||||
case "requesty":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
|
||||
@@ -812,7 +801,8 @@ export class Controller {
|
||||
await updateGlobalState(this.context, "lmStudioModelId", newModelId)
|
||||
break
|
||||
case "litellm":
|
||||
await updateGlobalState(this.context, "liteLlmModelId", newModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
|
||||
break
|
||||
case "requesty":
|
||||
await updateGlobalState(this.context, "requestyModelId", newModelId)
|
||||
@@ -884,18 +874,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 +1107,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAi
|
||||
|
||||
async getOpenAiModels(baseUrl?: string, apiKey?: string) {
|
||||
try {
|
||||
if (!baseUrl) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!URL.canParse(baseUrl)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const config: AxiosRequestConfig = {}
|
||||
if (apiKey) {
|
||||
config["headers"] = { Authorization: `Bearer ${apiKey}` }
|
||||
}
|
||||
|
||||
const response = await axios.get(`${baseUrl}/models`, config)
|
||||
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
|
||||
const models = [...new Set<string>(modelsArray)]
|
||||
return models
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// OpenRouter
|
||||
|
||||
async handleOpenRouterCallback(code: string) {
|
||||
@@ -1190,6 +1142,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
// Read OpenRouter models from disk cache
|
||||
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
|
||||
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
|
||||
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
|
||||
@@ -1200,190 +1153,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
return undefined
|
||||
}
|
||||
|
||||
async refreshOpenRouterModels() {
|
||||
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
|
||||
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
try {
|
||||
const response = await axios.get("https://openrouter.ai/api/v1/models")
|
||||
/*
|
||||
{
|
||||
"id": "anthropic/claude-3.5-sonnet",
|
||||
"name": "Anthropic: Claude 3.5 Sonnet",
|
||||
"created": 1718841600,
|
||||
"description": "Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: Autonomously writes, edits, and runs code with reasoning and troubleshooting\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal",
|
||||
"context_length": 200000,
|
||||
"architecture": {
|
||||
"modality": "text+image-\u003Etext",
|
||||
"tokenizer": "Claude",
|
||||
"instruct_type": null
|
||||
},
|
||||
"pricing": {
|
||||
"prompt": "0.000003",
|
||||
"completion": "0.000015",
|
||||
"image": "0.0048",
|
||||
"request": "0"
|
||||
},
|
||||
"top_provider": {
|
||||
"context_length": 200000,
|
||||
"max_completion_tokens": 8192,
|
||||
"is_moderated": true
|
||||
},
|
||||
"per_request_limits": null
|
||||
},
|
||||
*/
|
||||
if (response.data?.data) {
|
||||
const rawModels = response.data.data
|
||||
const parsePrice = (price: any) => {
|
||||
if (price) {
|
||||
return parseFloat(price) * 1_000_000
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
for (const rawModel of rawModels) {
|
||||
const modelInfo: ModelInfo = {
|
||||
maxTokens: rawModel.top_provider?.max_completion_tokens,
|
||||
contextWindow: rawModel.context_length,
|
||||
supportsImages: rawModel.architecture?.modality?.includes("image"),
|
||||
supportsPromptCache: false,
|
||||
inputPrice: parsePrice(rawModel.pricing?.prompt),
|
||||
outputPrice: parsePrice(rawModel.pricing?.completion),
|
||||
description: rawModel.description,
|
||||
}
|
||||
|
||||
switch (rawModel.id) {
|
||||
case "anthropic/claude-3-7-sonnet":
|
||||
case "anthropic/claude-3-7-sonnet:beta":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
case "anthropic/claude-3.7-sonnet:thinking":
|
||||
case "anthropic/claude-3.5-sonnet":
|
||||
case "anthropic/claude-3.5-sonnet:beta":
|
||||
// NOTE: this needs to be synced with api.ts/openrouter default model info
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 3.75
|
||||
modelInfo.cacheReadsPrice = 0.3
|
||||
break
|
||||
case "anthropic/claude-3.5-sonnet-20240620":
|
||||
case "anthropic/claude-3.5-sonnet-20240620:beta":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 3.75
|
||||
modelInfo.cacheReadsPrice = 0.3
|
||||
break
|
||||
case "anthropic/claude-3-5-haiku":
|
||||
case "anthropic/claude-3-5-haiku:beta":
|
||||
case "anthropic/claude-3-5-haiku-20241022":
|
||||
case "anthropic/claude-3-5-haiku-20241022:beta":
|
||||
case "anthropic/claude-3.5-haiku":
|
||||
case "anthropic/claude-3.5-haiku:beta":
|
||||
case "anthropic/claude-3.5-haiku-20241022":
|
||||
case "anthropic/claude-3.5-haiku-20241022:beta":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 1.25
|
||||
modelInfo.cacheReadsPrice = 0.1
|
||||
break
|
||||
case "anthropic/claude-3-opus":
|
||||
case "anthropic/claude-3-opus:beta":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 18.75
|
||||
modelInfo.cacheReadsPrice = 1.5
|
||||
break
|
||||
case "anthropic/claude-3-haiku":
|
||||
case "anthropic/claude-3-haiku:beta":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 0.3
|
||||
modelInfo.cacheReadsPrice = 0.03
|
||||
break
|
||||
case "deepseek/deepseek-chat":
|
||||
modelInfo.supportsPromptCache = true
|
||||
// see api.ts/deepSeekModels for more info
|
||||
modelInfo.inputPrice = 0
|
||||
modelInfo.cacheWritesPrice = 0.14
|
||||
modelInfo.cacheReadsPrice = 0.014
|
||||
break
|
||||
case "google/gemini-2.5-pro-preview-03-25":
|
||||
case "google/gemini-2.0-flash-001":
|
||||
case "google/gemini-flash-1.5":
|
||||
case "google/gemini-pro-1.5":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
|
||||
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
|
||||
break
|
||||
default:
|
||||
if (rawModel.id.startsWith("openai/")) {
|
||||
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
|
||||
if (modelInfo.cacheReadsPrice) {
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
|
||||
// openrouter charges no cache write pricing for openAI models
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid response from OpenRouter API")
|
||||
}
|
||||
await fs.writeFile(openRouterModelsFilePath, JSON.stringify(models))
|
||||
console.log("OpenRouter models fetched and saved", models)
|
||||
} catch (error) {
|
||||
console.error("Error fetching OpenRouter models:", error)
|
||||
}
|
||||
|
||||
await this.postMessageToWebview({
|
||||
type: "openRouterModels",
|
||||
openRouterModels: models,
|
||||
})
|
||||
return models
|
||||
}
|
||||
|
||||
async refreshRequestyModels() {
|
||||
const parsePrice = (price: any) => {
|
||||
if (price) {
|
||||
return parseFloat(price) * 1_000_000
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
try {
|
||||
const apiKey = await getSecret(this.context, "requestyApiKey")
|
||||
const headers = {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
}
|
||||
const response = await axios.get("https://router.requesty.ai/v1/models", { headers })
|
||||
if (response.data?.data) {
|
||||
for (const model of response.data.data) {
|
||||
const modelInfo: ModelInfo = {
|
||||
maxTokens: model.max_output_tokens || undefined,
|
||||
contextWindow: model.context_window,
|
||||
supportsImages: model.supports_vision || undefined,
|
||||
supportsPromptCache: model.supports_caching || undefined,
|
||||
inputPrice: parsePrice(model.input_price),
|
||||
outputPrice: parsePrice(model.output_price),
|
||||
cacheWritesPrice: parsePrice(model.caching_price),
|
||||
cacheReadsPrice: parsePrice(model.cached_price),
|
||||
description: model.description,
|
||||
}
|
||||
models[model.id] = modelInfo
|
||||
}
|
||||
console.log("Requesty models fetched", models)
|
||||
} else {
|
||||
console.error("Invalid response from Requesty API")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Requesty models:", error)
|
||||
}
|
||||
|
||||
await this.postMessageToWebview({
|
||||
type: "requestyModels",
|
||||
requestyModels: models,
|
||||
})
|
||||
return models
|
||||
}
|
||||
|
||||
// Context menus and code actions
|
||||
|
||||
getFileMentionFromPath(filePath: string) {
|
||||
@@ -1558,6 +1327,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,18 @@
|
||||
import { registerMethod } from "./index"
|
||||
import { getLmStudioModels } from "./getLmStudioModels"
|
||||
import { getOllamaModels } from "./getOllamaModels"
|
||||
import { getVsCodeLmModels } from "./getVsCodeLmModels"
|
||||
import { refreshOpenAiModels } from "./refreshOpenAiModels"
|
||||
import { refreshOpenRouterModels } from "./refreshOpenRouterModels"
|
||||
import { refreshRequestyModels } from "./refreshRequestyModels"
|
||||
|
||||
// Register all models service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("getLmStudioModels", getLmStudioModels)
|
||||
registerMethod("getOllamaModels", getOllamaModels)
|
||||
registerMethod("getVsCodeLmModels", getVsCodeLmModels)
|
||||
registerMethod("refreshOpenAiModels", refreshOpenAiModels)
|
||||
registerMethod("refreshOpenRouterModels", refreshOpenRouterModels)
|
||||
registerMethod("refreshRequestyModels", refreshRequestyModels)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Controller } from ".."
|
||||
import { OpenAiModelsRequest } from "../../../shared/proto/models"
|
||||
import { StringArray } from "../../../shared/proto/common"
|
||||
import axios from "axios"
|
||||
import type { AxiosRequestConfig } from "axios"
|
||||
|
||||
/**
|
||||
* Fetches available models from the OpenAI API
|
||||
* @param controller The controller instance
|
||||
* @param request Request containing the base URL and API key
|
||||
* @returns Array of model names
|
||||
*/
|
||||
export async function refreshOpenAiModels(controller: Controller, request: OpenAiModelsRequest): Promise<StringArray> {
|
||||
try {
|
||||
if (!request.baseUrl) {
|
||||
return StringArray.create({ values: [] })
|
||||
}
|
||||
|
||||
if (!URL.canParse(request.baseUrl)) {
|
||||
return StringArray.create({ values: [] })
|
||||
}
|
||||
|
||||
const config: AxiosRequestConfig = {}
|
||||
if (request.apiKey) {
|
||||
config["headers"] = { Authorization: `Bearer ${request.apiKey}` }
|
||||
}
|
||||
|
||||
const response = await axios.get(`${request.baseUrl}/models`, config)
|
||||
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
|
||||
const models = [...new Set<string>(modelsArray)]
|
||||
|
||||
return StringArray.create({ values: models })
|
||||
} catch (error) {
|
||||
console.error("Error fetching OpenAI models:", error)
|
||||
return StringArray.create({ values: [] })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
|
||||
import axios from "axios"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
|
||||
/**
|
||||
* Refreshes the OpenRouter models and returns the updated model list
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request object
|
||||
* @returns Response containing the OpenRouter models
|
||||
*/
|
||||
export async function refreshOpenRouterModels(
|
||||
controller: Controller,
|
||||
request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
|
||||
|
||||
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
|
||||
try {
|
||||
const response = await axios.get("https://openrouter.ai/api/v1/models")
|
||||
|
||||
if (response.data?.data) {
|
||||
const rawModels = response.data.data
|
||||
const parsePrice = (price: any) => {
|
||||
if (price) {
|
||||
return parseFloat(price) * 1_000_000
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
for (const rawModel of rawModels) {
|
||||
const modelInfo: Partial<OpenRouterModelInfo> = {
|
||||
maxTokens: rawModel.top_provider?.max_completion_tokens,
|
||||
contextWindow: rawModel.context_length,
|
||||
supportsImages: rawModel.architecture?.modality?.includes("image"),
|
||||
supportsPromptCache: false,
|
||||
inputPrice: parsePrice(rawModel.pricing?.prompt),
|
||||
outputPrice: parsePrice(rawModel.pricing?.completion),
|
||||
description: rawModel.description,
|
||||
}
|
||||
|
||||
switch (rawModel.id) {
|
||||
case "anthropic/claude-3-7-sonnet":
|
||||
case "anthropic/claude-3-7-sonnet:beta":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
case "anthropic/claude-3.7-sonnet:thinking":
|
||||
case "anthropic/claude-3.5-sonnet":
|
||||
case "anthropic/claude-3.5-sonnet:beta":
|
||||
// NOTE: this needs to be synced with api.ts/openrouter default model info
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 3.75
|
||||
modelInfo.cacheReadsPrice = 0.3
|
||||
break
|
||||
case "anthropic/claude-3.5-sonnet-20240620":
|
||||
case "anthropic/claude-3.5-sonnet-20240620:beta":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 3.75
|
||||
modelInfo.cacheReadsPrice = 0.3
|
||||
break
|
||||
case "anthropic/claude-3-5-haiku":
|
||||
case "anthropic/claude-3-5-haiku:beta":
|
||||
case "anthropic/claude-3-5-haiku-20241022":
|
||||
case "anthropic/claude-3-5-haiku-20241022:beta":
|
||||
case "anthropic/claude-3.5-haiku":
|
||||
case "anthropic/claude-3.5-haiku:beta":
|
||||
case "anthropic/claude-3.5-haiku-20241022":
|
||||
case "anthropic/claude-3.5-haiku-20241022:beta":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 1.25
|
||||
modelInfo.cacheReadsPrice = 0.1
|
||||
break
|
||||
case "anthropic/claude-3-opus":
|
||||
case "anthropic/claude-3-opus:beta":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 18.75
|
||||
modelInfo.cacheReadsPrice = 1.5
|
||||
break
|
||||
case "anthropic/claude-3-haiku":
|
||||
case "anthropic/claude-3-haiku:beta":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 0.3
|
||||
modelInfo.cacheReadsPrice = 0.03
|
||||
break
|
||||
case "deepseek/deepseek-chat":
|
||||
modelInfo.supportsPromptCache = true
|
||||
// see api.ts/deepSeekModels for more info
|
||||
modelInfo.inputPrice = 0
|
||||
modelInfo.cacheWritesPrice = 0.14
|
||||
modelInfo.cacheReadsPrice = 0.014
|
||||
break
|
||||
default:
|
||||
if (rawModel.id.startsWith("openai/")) {
|
||||
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
|
||||
if (modelInfo.cacheReadsPrice) {
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
|
||||
// openrouter charges no cache write pricing for openAI models
|
||||
}
|
||||
} else if (rawModel.id.startsWith("google/")) {
|
||||
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
|
||||
if (modelInfo.cacheReadsPrice) {
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid response from OpenRouter API")
|
||||
}
|
||||
await fs.writeFile(openRouterModelsFilePath, JSON.stringify(models))
|
||||
console.log("OpenRouter models fetched and saved", models)
|
||||
} catch (error) {
|
||||
console.error("Error fetching OpenRouter models:", error)
|
||||
|
||||
// If we failed to fetch models, try to read cached models
|
||||
const cachedModels = await readOpenRouterModels(controller)
|
||||
if (cachedModels) {
|
||||
models = cachedModels
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the Record<string, Partial<OpenRouterModelInfo>> to Record<string, OpenRouterModelInfo>
|
||||
// by filling in any missing required fields with defaults
|
||||
const typedModels: Record<string, OpenRouterModelInfo> = {}
|
||||
for (const [key, model] of Object.entries(models)) {
|
||||
typedModels[key] = {
|
||||
maxTokens: model.maxTokens ?? 0,
|
||||
contextWindow: model.contextWindow ?? 0,
|
||||
supportsImages: model.supportsImages ?? false,
|
||||
supportsPromptCache: model.supportsPromptCache ?? false,
|
||||
inputPrice: model.inputPrice ?? 0,
|
||||
outputPrice: model.outputPrice ?? 0,
|
||||
cacheWritesPrice: model.cacheWritesPrice ?? 0,
|
||||
cacheReadsPrice: model.cacheReadsPrice ?? 0,
|
||||
description: model.description ?? "",
|
||||
}
|
||||
}
|
||||
|
||||
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads cached OpenRouter models from disk
|
||||
*/
|
||||
async function readOpenRouterModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
|
||||
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
|
||||
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
|
||||
if (fileExists) {
|
||||
try {
|
||||
const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8")
|
||||
return JSON.parse(fileContents)
|
||||
} catch (error) {
|
||||
console.error("Error reading cached OpenRouter models:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the cache directory exists and returns its path
|
||||
*/
|
||||
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
|
||||
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
return cacheDir
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
|
||||
import axios from "axios"
|
||||
import { getSecret } from "@core/storage/state"
|
||||
|
||||
/**
|
||||
* Refreshes the Requesty models and returns the updated model list
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request object
|
||||
* @returns Response containing the Requesty models
|
||||
*/
|
||||
export async function refreshRequestyModels(
|
||||
controller: Controller,
|
||||
request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const parsePrice = (price: any) => {
|
||||
if (price) {
|
||||
return parseFloat(price) * 1_000_000
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
let models: Record<string, OpenRouterModelInfo> = {}
|
||||
try {
|
||||
const apiKey = await getSecret(controller.context, "requestyApiKey")
|
||||
const headers = {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
}
|
||||
const response = await axios.get("https://router.requesty.ai/v1/models", { headers })
|
||||
if (response.data?.data) {
|
||||
for (const model of response.data.data) {
|
||||
const modelInfo: OpenRouterModelInfo = {
|
||||
maxTokens: model.max_output_tokens || undefined,
|
||||
contextWindow: model.context_window,
|
||||
supportsImages: model.supports_vision || undefined,
|
||||
supportsPromptCache: model.supports_caching || undefined,
|
||||
inputPrice: parsePrice(model.input_price) || 0,
|
||||
outputPrice: parsePrice(model.output_price) || 0,
|
||||
cacheWritesPrice: parsePrice(model.caching_price) || 0,
|
||||
cacheReadsPrice: parsePrice(model.cached_price) || 0,
|
||||
description: model.description,
|
||||
}
|
||||
models[model.id] = modelInfo
|
||||
}
|
||||
console.log("Requesty models fetched", models)
|
||||
} else {
|
||||
console.error("Invalid response from Requesty API")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Requesty models:", error)
|
||||
}
|
||||
|
||||
return OpenRouterCompatibleModelInfo.create({ models })
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Controller } from ".."
|
||||
import { StringRequest, Empty } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Command slash command logic
|
||||
*/
|
||||
export async function condense(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
|
||||
import { StreamingResponseHandler } from "../grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create slash service registry
|
||||
const slashService = createServiceRegistry("slash")
|
||||
|
||||
// Export the method handler types and registration function
|
||||
export type SlashMethodHandler = ServiceMethodHandler
|
||||
export type SlashStreamingMethodHandler = StreamingMethodHandler
|
||||
export const registerMethod = slashService.registerMethod
|
||||
|
||||
// Export the request handlers
|
||||
export const handleSlashServiceRequest = slashService.handleRequest
|
||||
export const handleSlashServiceStreamingRequest = slashService.handleStreamingRequest
|
||||
export const isStreamingMethod = slashService.isStreamingMethod
|
||||
|
||||
// Register all slash methods
|
||||
registerAllMethods()
|
||||
@@ -0,0 +1,14 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { condense } from "./condense"
|
||||
import { reportBug } from "./reportBug"
|
||||
|
||||
// Register all slash service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("condense", condense)
|
||||
registerMethod("reportBug", reportBug)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Controller } from ".."
|
||||
import { StringRequest, Empty } from "../../../shared/proto/common"
|
||||
|
||||
/**
|
||||
* Report bug slash command logic
|
||||
*/
|
||||
export async function reportBug(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@ export type SecretKey =
|
||||
| "deepSeekApiKey"
|
||||
| "requestyApiKey"
|
||||
| "togetherApiKey"
|
||||
| "fireworksApiKey"
|
||||
| "qwenApiKey"
|
||||
| "doubaoApiKey"
|
||||
| "mistralApiKey"
|
||||
@@ -67,7 +68,11 @@ export type GlobalStateKey =
|
||||
| "previousModeModelInfo"
|
||||
| "liteLlmBaseUrl"
|
||||
| "liteLlmModelId"
|
||||
| "liteLlmModelInfo"
|
||||
| "liteLlmUsePromptCache"
|
||||
| "fireworksModelId"
|
||||
| "fireworksModelMaxCompletionTokens"
|
||||
| "fireworksModelMaxTokens"
|
||||
| "qwenApiLine"
|
||||
| "requestyModelId"
|
||||
| "requestyModelInfo"
|
||||
|
||||
@@ -107,7 +107,12 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
vsCodeLmModelSelector,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmModelInfo,
|
||||
liteLlmUsePromptCache,
|
||||
fireworksApiKey,
|
||||
fireworksModelId,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
userInfo,
|
||||
previousModeApiProvider,
|
||||
previousModeModelId,
|
||||
@@ -186,7 +191,12 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
|
||||
getGlobalState(context, "liteLlmBaseUrl") as Promise<string | undefined>,
|
||||
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "liteLlmUsePromptCache") as Promise<boolean | undefined>,
|
||||
getSecret(context, "fireworksApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "fireworksModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "fireworksModelMaxCompletionTokens") as Promise<number | undefined>,
|
||||
getGlobalState(context, "fireworksModelMaxTokens") as Promise<number | undefined>,
|
||||
getGlobalState(context, "userInfo") as Promise<UserInfo | undefined>,
|
||||
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "previousModeModelId") as Promise<string | undefined>,
|
||||
@@ -304,8 +314,13 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
reasoningEffort,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmModelInfo,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
fireworksApiKey,
|
||||
fireworksModelId,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
@@ -386,6 +401,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
vsCodeLmModelSelector,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmModelInfo,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
@@ -444,6 +460,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
await updateGlobalState(context, "vsCodeLmModelSelector", vsCodeLmModelSelector)
|
||||
await updateGlobalState(context, "liteLlmBaseUrl", liteLlmBaseUrl)
|
||||
await updateGlobalState(context, "liteLlmModelId", liteLlmModelId)
|
||||
await updateGlobalState(context, "liteLlmModelInfo", liteLlmModelInfo)
|
||||
await updateGlobalState(context, "liteLlmUsePromptCache", liteLlmUsePromptCache)
|
||||
await updateGlobalState(context, "qwenApiLine", qwenApiLine)
|
||||
await updateGlobalState(context, "requestyModelId", requestyModelId)
|
||||
@@ -480,6 +497,7 @@ export async function resetExtensionState(context: vscode.ExtensionContext) {
|
||||
"mistralApiKey",
|
||||
"clineApiKey",
|
||||
"liteLlmApiKey",
|
||||
"fireworksApiKey",
|
||||
"asksageApiKey",
|
||||
"xaiApiKey",
|
||||
"sambanovaApiKey",
|
||||
|
||||
+167
-7
@@ -57,9 +57,10 @@ 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"
|
||||
import { AssistantMessageContent, parseAssistantMessageV2, ToolParamName, ToolUseName } from "@core/assistant-message"
|
||||
import { constructNewFileContent } from "@core/assistant-message/diff"
|
||||
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
|
||||
import { parseMentions } from "@core/mentions"
|
||||
@@ -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())
|
||||
@@ -1428,13 +1442,28 @@ export class Task {
|
||||
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the disableBrowserTool setting from VSCode configuration to browserSettings
|
||||
*/
|
||||
private async migrateDisableBrowserToolSetting(): Promise<void> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool")
|
||||
|
||||
if (disableBrowserTool !== undefined) {
|
||||
this.browserSettings.disableToolUse = disableBrowserTool
|
||||
// Remove from VSCode configuration
|
||||
await config.update("disableBrowserTool", undefined, true)
|
||||
}
|
||||
}
|
||||
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
// Wait for MCP servers to be connected before generating system prompt
|
||||
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
|
||||
console.error("MCP servers failed to connect in time")
|
||||
})
|
||||
|
||||
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool") ?? false
|
||||
await this.migrateDisableBrowserToolSetting()
|
||||
const disableBrowserTool = this.browserSettings.disableToolUse ?? false
|
||||
// cline browser tool uses image recognition for navigation (requires model image support).
|
||||
const modelSupportsBrowserUse = this.api.getModel().info.supportsImages ?? false
|
||||
|
||||
@@ -1711,6 +1740,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 +1947,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 +3169,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 did not submit the bug, and provided feedback on the Github issue generated instead:\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
|
||||
@@ -3699,7 +3859,7 @@ export class Task {
|
||||
assistantMessage += chunk.text
|
||||
// parse raw assistant message into content blocks
|
||||
const prevLength = this.assistantMessageContent.length
|
||||
this.assistantMessageContent = parseAssistantMessage(assistantMessage)
|
||||
this.assistantMessageContent = parseAssistantMessageV2(assistantMessage)
|
||||
if (this.assistantMessageContent.length > prevLength) {
|
||||
this.userMessageContentReady = false // new content we need to present, reset to false in case previous content set this to true
|
||||
}
|
||||
|
||||
@@ -91,9 +91,9 @@ export class DiffViewProvider {
|
||||
const currentFirstVisibleLine = e.visibleRanges[0]?.start.line || 0
|
||||
|
||||
// If the first visible line moved upward, user scrolled up
|
||||
if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
|
||||
this.shouldAutoScroll = false
|
||||
}
|
||||
// if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
|
||||
// this.shouldAutoScroll = false
|
||||
// }
|
||||
|
||||
// Always update our tracking variable
|
||||
this.lastFirstVisibleLine = currentFirstVisibleLine
|
||||
@@ -433,6 +433,12 @@ export class DiffViewProvider {
|
||||
|
||||
// close editor if open?
|
||||
async reset() {
|
||||
// releasing memory by clearing the diff editor
|
||||
try {
|
||||
await this.closeAllDiffViews()
|
||||
} catch (error) {
|
||||
console.error("Error closing diff views:", error)
|
||||
}
|
||||
this.editType = undefined
|
||||
this.isEditing = false
|
||||
this.originalContent = undefined
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user