mirror of
https://github.com/cline/cline.git
synced 2026-09-16 21:01:52 +08:00
Compare commits
121
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51f4c4fb30 | ||
|
|
d3c8fbbf1d | ||
|
|
1b06633253 | ||
|
|
4ab8559fce | ||
|
|
259368e0a3 | ||
|
|
9b7839efcd | ||
|
|
47a2ae83de | ||
|
|
1b5590e26c | ||
|
|
9e493341d2 | ||
|
|
1d4cd3187b | ||
|
|
32f0f9618c | ||
|
|
3001f883c2 | ||
|
|
a64e60b8f6 | ||
|
|
3a0e6a471b | ||
|
|
c10f4e0a66 | ||
|
|
3e11271cf8 | ||
|
|
29ae2c286d | ||
|
|
82aee44a9a | ||
|
|
031604ddf6 | ||
|
|
f2101e375f | ||
|
|
8a65f0c68b | ||
|
|
32b8fa44cb | ||
|
|
0d067f7470 | ||
|
|
de6166392c | ||
|
|
5f21a9162a | ||
|
|
4de991f1b0 | ||
|
|
6e5d4a3f9e | ||
|
|
95af95badf | ||
|
|
61dcbd390c | ||
|
|
6255ac0a51 | ||
|
|
616800fcb9 | ||
|
|
df3826a59f | ||
|
|
873917810d | ||
|
|
e3c966f4e9 | ||
|
|
7eeb43ab41 | ||
|
|
7620f177ac | ||
|
|
67bab94911 | ||
|
|
eb91bfd738 | ||
|
|
b8227c19c3 | ||
|
|
8fee09f09e | ||
|
|
1c026c26d2 | ||
|
|
5bc4e5a4a0 | ||
|
|
2cfce5734e | ||
|
|
e2045bf5c3 | ||
|
|
0d933e804f | ||
|
|
16f73532f4 | ||
|
|
88bea8eeb4 | ||
|
|
1a570e98ba | ||
|
|
d86b7dd036 | ||
|
|
0178c3fa90 | ||
|
|
a107f45c6a | ||
|
|
3dd2ed9161 | ||
|
|
9a6603fdfb | ||
|
|
6d5c3e6aa4 | ||
|
|
24b9e821bb | ||
|
|
9960a3c57c | ||
|
|
88947592f0 | ||
|
|
ef4d11df19 | ||
|
|
23dec509bc | ||
|
|
07ab6b19b8 | ||
|
|
0ddef94d1f | ||
|
|
b9ae83b1cd | ||
|
|
e4eaf34827 | ||
|
|
6d3ed43c74 | ||
|
|
cbb67b48f2 | ||
|
|
a5f6a97be8 | ||
|
|
f309b062e7 | ||
|
|
768df130ab | ||
|
|
9980cb0938 | ||
|
|
aca4f842fa | ||
|
|
3fc91e2afe | ||
|
|
5f4700ce95 | ||
|
|
dbaf5e3ee3 | ||
|
|
576176c24f | ||
|
|
c8abcbfdf9 | ||
|
|
8e984f2d98 | ||
|
|
81564faa4e | ||
|
|
f8b5f1fd72 | ||
|
|
2eb57384ab | ||
|
|
c80bae504a | ||
|
|
80f955be9e | ||
|
|
7435ffcd2f | ||
|
|
a05d438612 | ||
|
|
b4b7512d9f | ||
|
|
e08c65618e | ||
|
|
d653f1cc27 | ||
|
|
c3a97c3eda | ||
|
|
0e56272d65 | ||
|
|
fdc2e2655a | ||
|
|
6050413b8b | ||
|
|
c54f0da737 | ||
|
|
6cbfb2b8b0 | ||
|
|
22788f0f12 | ||
|
|
708b785a97 | ||
|
|
099bc44d42 | ||
|
|
b9f4678dba | ||
|
|
f7d17384f6 | ||
|
|
bb5a64afb3 | ||
|
|
61224734f8 | ||
|
|
a28b995ab1 | ||
|
|
a91878efc6 | ||
|
|
65c21e7b7d | ||
|
|
56e388c90f | ||
|
|
586d804a01 | ||
|
|
85fbbcbe3f | ||
|
|
25c5310383 | ||
|
|
19ef843d4f | ||
|
|
2b3dd14271 | ||
|
|
721e7ae305 | ||
|
|
f6273e0661 | ||
|
|
68d0af2afc | ||
|
|
4286f301d2 | ||
|
|
28536084bf | ||
|
|
dfbac3eefd | ||
|
|
04f0710a03 | ||
|
|
195e15db32 | ||
|
|
19cb70de55 | ||
|
|
e2a2ecde44 | ||
|
|
1a466c0a44 | ||
|
|
31cbf489c4 | ||
|
|
c667d34f27 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed issue with sap ai core client credentials storage
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Support sending context to active webview when editor panels are opened.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
remove unused parseAssistantmessageV1
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix Qwen Api option inconsistency between UI and API layer
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix credit balance out of sync issue on account switching
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix Claude Code CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix LiteLLM Proxy Provider Cost Tracking
|
||||
@@ -716,7 +716,7 @@ The Controller class manages MCP servers through the McpHub service:
|
||||
class Controller {
|
||||
mcpHub?: McpHub
|
||||
|
||||
constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, webviewProvider: WebviewProvider) {
|
||||
constructor(context: vscode.ExtensionContext, webviewProvider: WebviewProvider) {
|
||||
this.mcpHub = new McpHub(this)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Important:** All bug reports must be reproducible using Claude 3.5 Sonnet. Cline uses complex prompts so less capable models may not work as expected.
|
||||
**Important:** All bug reports must be reproducible using Claude 4 Sonnet. Cline uses complex prompts so less capable models may not work as expected.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
@@ -24,7 +24,7 @@ body:
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
@@ -39,20 +39,19 @@ body:
|
||||
placeholder: "e.g., cline:anthropic/claude-3.7-sonnet, gemini:gemini-2.5-pro-exp-03-25"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: operating-system
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: What operating system are you using?
|
||||
placeholder: "e.g., Windows 11, macOS Sonoma, Ubuntu 22.04"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System Info
|
||||
description: What system information is relevant to the issue?
|
||||
placeholder: "e.g., CPU: Intel Core i7-11700K, GPU: NVIDIA GeForce RTX 3070, RAM: 32GB DDR4"
|
||||
label: System Information
|
||||
description: What operating system and hardware are you using?
|
||||
placeholder: |
|
||||
Operating System: Windows 11, macOS Sonoma, Ubuntu 22.04, etc.
|
||||
Hardware: CPU, GPU, RAM specifications if relevant
|
||||
e.g.,
|
||||
OS: Windows 11
|
||||
CPU: Intel Core i7-11700K
|
||||
GPU: NVIDIA GeForce RTX 3070
|
||||
RAM: 32GB DDR4
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -63,8 +62,3 @@ body:
|
||||
placeholder: "e.g., 1.2.3"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here, such as screenshots or related issues.
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
name: 💡 Feature Proposal & Contribution
|
||||
description: Propose a new feature or improvement, and optionally offer to implement feature as a contributor
|
||||
labels: ["proposal"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Feature Proposal & Contribution for Cline**
|
||||
|
||||
Thank you for proposing a feature or improvement for Cline! This template helps us understand the problem, evaluate the solution, and coordinate implementation.
|
||||
|
||||
**For detailed proposals:** Please provide comprehensive information to enable fast prioritization and discussion.
|
||||
**For contribution offers:** You can indicate your willingness to implement the feature yourself.
|
||||
|
||||
Before submitting:
|
||||
- Search existing [Issues](https://github.com/cline/cline/issues) and [Discussions](https://github.com/cline/cline/discussions) to avoid duplicates
|
||||
- Read the [Contributing Guide](https://github.com/cline/cline/blob/main/CONTRIBUTING.md) if you plan to contribute
|
||||
- Don't start implementation until the proposal is reviewed and approved
|
||||
|
||||
- type: textarea
|
||||
id: problem-description
|
||||
attributes:
|
||||
label: What problem does this solve?
|
||||
description: |
|
||||
Describe the problem clearly from a user's point of view. Focus on why this matters, who it affects, and when it occurs.
|
||||
|
||||
✅ Good examples:
|
||||
- "LLM provider returns 400 error when nearing the context window instead of truncating"
|
||||
- "Submit button is invisible in dark mode"
|
||||
- "Users can't easily share their Cline configurations with team members"
|
||||
|
||||
❌ Avoid vague descriptions:
|
||||
- "Performance is bad"
|
||||
- "UI needs work"
|
||||
|
||||
Your description should include:
|
||||
- Who is affected?
|
||||
- When does it happen?
|
||||
- What's the current vs expected behavior?
|
||||
- What is the impact?
|
||||
placeholder: Be specific about the problem, who it affects, and the impact.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposed-solution
|
||||
attributes:
|
||||
label: What's the proposed solution?
|
||||
description: |
|
||||
Describe how the problem should be solved. Be specific about UX, system behavior, and any flows that would change.
|
||||
|
||||
✅ Good examples:
|
||||
- "Add error handling immediately after attempting to create the llm stream and retry after manually truncating"
|
||||
- "Update button styling to ensure contrast in all themes"
|
||||
- "Add export/import functionality in settings with JSON format"
|
||||
|
||||
❌ Avoid vague solutions:
|
||||
- "Improve performance"
|
||||
- "Fix the bug"
|
||||
|
||||
Your solution should include:
|
||||
- What exactly will change?
|
||||
- How will users interact with it?
|
||||
- What's the expected outcome?
|
||||
placeholder: Describe the proposed changes and how they solve the problem.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: contribution-intent
|
||||
attributes:
|
||||
label: Are you interested in implementing this?
|
||||
description: Let us know if you'd like to contribute to this feature
|
||||
options:
|
||||
- "No, just proposing the idea"
|
||||
- "Yes, I'd like to implement this myself"
|
||||
- "Yes, I'd like to collaborate with others"
|
||||
- "Maybe, depending on complexity and guidance"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: implementation-approach
|
||||
attributes:
|
||||
label: Implementation approach (if contributing)
|
||||
description: |
|
||||
**Only fill this out if you selected "Yes" above.**
|
||||
|
||||
How do you plan to implement this? Include:
|
||||
- High-level technical approach
|
||||
- Files/components that would be affected
|
||||
- Any new dependencies required
|
||||
- Potential challenges or considerations you've identified
|
||||
|
||||
This helps us provide better guidance and ensures alignment before you start coding.
|
||||
placeholder: "My implementation approach would be..."
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Proposal checklist
|
||||
options:
|
||||
- label: I've checked for existing issues or related proposals
|
||||
required: true
|
||||
- label: I understand this needs review before implementation can start
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: contribution-checklist
|
||||
attributes:
|
||||
label: Contribution checklist (if contributing)
|
||||
description: Only check these if you plan to contribute
|
||||
options:
|
||||
- label: I've read the [Contributing Guide](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)
|
||||
- label: I'm willing to make changes based on feedback
|
||||
- label: I understand the code review process and requirements
|
||||
@@ -2,15 +2,14 @@
|
||||
Thank you for contributing to Cline!
|
||||
|
||||
⚠️ Important: Before submitting this PR, please ensure you have:
|
||||
- Opened an issue and discussed your proposed changes with the community / contributors
|
||||
- Received approval from a core Cline contributor prior to proceeding with the implementation
|
||||
- Link the associated issue in the "Related Issue" section
|
||||
- For feature requests: Created a discussion in our Feature Requests discussions board https://github.com/cline/cline/discussions/categories/feature-requests and received approval from core maintainers before implementation
|
||||
- For all changes: Link the associated issue/discussion in the "Related Issue" section below
|
||||
|
||||
Limited exceptions:
|
||||
Small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality may be submitted directly.
|
||||
Small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality may be submitted directly without prior discussion.
|
||||
|
||||
Why this requirement?
|
||||
We deeply appreciate all community contributions - they are the core reason we're able to operate successfully and keep innovating! We welcome community input and want to make it as easy as possible for people to submit quality work. This process helps our core maintainers review new ideas faster and saves contributor time by ensuring you have the go-ahead before spending time on implementation.
|
||||
We deeply appreciate all community contributions - they are essential to Cline's success! To ensure the best use of everyone's time and maintain project direction, we use our Feature Requests discussions board to gauge community interest and validate feature ideas before implementation begins. This helps us focus development efforts on features that will benefit the most users.
|
||||
-->
|
||||
|
||||
### Related Issue
|
||||
|
||||
+12
-15
@@ -96,16 +96,15 @@ jobs:
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
|
||||
# Unit Tests disabled due to module system conflicts between backend and webview-ui
|
||||
# - name: Unit Tests
|
||||
# run: npm run test:unit
|
||||
- name: Unit Tests
|
||||
run: npm run test:unit
|
||||
|
||||
# Run extension tests with coverage
|
||||
- name: Extension Tests with Coverage
|
||||
- name: Extension Integration Tests with Coverage
|
||||
id: extension_coverage
|
||||
continue-on-error: true
|
||||
run: |
|
||||
node ./scripts/test-ci.js > extension_coverage.txt 2>&1
|
||||
node ./scripts/test-ci.js 2>&1 | tee extension_coverage.txt
|
||||
# Default the encoding to UTF-8 - It's not the default on Windows
|
||||
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
|
||||
|
||||
@@ -117,7 +116,7 @@ jobs:
|
||||
cd webview-ui
|
||||
# Ensure coverage dependency is installed
|
||||
npm install --no-save @vitest/coverage-v8
|
||||
npm run test:coverage > webview_coverage.txt 2>&1
|
||||
npm run test:coverage 2>&1 | tee webview_coverage.txt
|
||||
cd ..
|
||||
# Default the encoding to UTF-8 - It's not the default on Windows
|
||||
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage webview-ui/webview_coverage.txt --type=webview --github-output --verbose
|
||||
@@ -132,21 +131,19 @@ jobs:
|
||||
path: |
|
||||
extension_coverage.txt
|
||||
webview-ui/webview_coverage.txt
|
||||
retention-period: workflow # Artifacts are automatically deleted when the workflow completes
|
||||
|
||||
# Set the check as failed if any of the tests failed
|
||||
- name: Print test results and check for failures
|
||||
- name: Check for test failures
|
||||
run: |
|
||||
echo "Extension Tests Result: ${{ steps.extension_coverage.outcome }}"
|
||||
cat extension_coverage.txt
|
||||
|
||||
echo "Webview Tests Result: ${{ steps.webview_coverage.outcome }}"
|
||||
cat webview-ui/webview_coverage.txt
|
||||
|
||||
# Check if any of the test steps failed
|
||||
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context
|
||||
if [ "${{ steps.extension_coverage.outcome }}" != "success" ]; then
|
||||
echo "Extension Integration Tests failed, see previous step for test output."
|
||||
fi
|
||||
if [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
|
||||
echo "Webview Tests failed, see previous step for test output."
|
||||
fi
|
||||
if [ "${{ steps.extension_coverage.outcome }}" != "success" ] || [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
|
||||
echo "Tests failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
Vendored
+3
-5
@@ -71,7 +71,7 @@
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Run Standalone Service",
|
||||
"name": "Run cline-core service",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
@@ -82,11 +82,9 @@
|
||||
// Turns on grpc debug log.
|
||||
//"GRPC_TRACE": "all",
|
||||
//"GRPC_VERBOSITY": "DEBUG",
|
||||
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules",
|
||||
|
||||
"HOST_BRIDGE_ADDRESS": "localhost:50052"
|
||||
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
|
||||
},
|
||||
"program": "standalone.js"
|
||||
"program": "cline-core.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,78 @@
|
||||
# Changelog
|
||||
|
||||
## [3.21.0]
|
||||
|
||||
- Add support for GPT-5 model family including GPT-5, GPT-5 Mini, and GPT-5 Nano with prompt caching support and set GPT-5 as the new default model
|
||||
- Add "Take a Tour" button for new users to easily access the VSCode walkthrough and improve onboarding experience
|
||||
- Enhance plan mode response handling with better exploration parameter support
|
||||
|
||||
## [3.20.13]
|
||||
|
||||
- Fix prompt caching support for Opus 4.1 on OpenRouter/Cline
|
||||
|
||||
## [3.20.12]
|
||||
|
||||
- Add Claude Opus 4.1 model support to AWS Bedrock provider (Thanks @omercelik!)
|
||||
- Fix prompt caching and extended thinking support for Claude Opus 4.1 in Anthropic provider
|
||||
|
||||
## [3.20.11]
|
||||
|
||||
Add gpt-oss-120b as a Cerebras model
|
||||
Add Opus 4.1 through Claude Code
|
||||
|
||||
## [3.20.10]
|
||||
|
||||
- Add OpenAI's new open-source models (GPT-OSS-120B and GPT-OSS-20B) to Hugging Face and Groq providers
|
||||
|
||||
## [3.20.9]
|
||||
|
||||
- Add support for Claude Opus 4.1 model in Anthropic provider
|
||||
- Add Baseten as a new API provider with support for DeepSeek, Llama, and Kimi K2 models (Thanks @AlexKer!)
|
||||
- Fix error messages not clearing from UI when retrying failed tasks
|
||||
- Fix chat input box positioning issues
|
||||
|
||||
## [3.20.8]
|
||||
|
||||
- Add navbar tooltips on hover
|
||||
|
||||
## [3.20.7]
|
||||
|
||||
- Fix circular dependency that affect the github workflow Tests / test (pull_request)
|
||||
|
||||
## [3.20.6]
|
||||
|
||||
- Fix login check on extension restart
|
||||
|
||||
## [3.20.5]
|
||||
|
||||
- Fix authentication persistence issues that could cause users to be logged out unexpectedly
|
||||
|
||||
## [3.20.4]
|
||||
|
||||
- Add new Cerebras models
|
||||
- Update rate limits for existing Cerebras models
|
||||
- Fix for delete task dialog
|
||||
|
||||
## [3.20.3]
|
||||
|
||||
- Add Huawei Cloud MaaS Provider (Thanks @ddling!)
|
||||
- Add Cerebras Qwen 3 235B instruct model (Thanks @kevint-cerebras!)
|
||||
- Add DeepSeek R1 0528 support under Hugging Face (Thanks @0ne0rZer0!)
|
||||
- Fix Global Rules directory documentation for Linux/WSL systems
|
||||
- Fix token counting when using VSCode LM API provider
|
||||
- Fix input field stealing focus issue by only focusing on visible and active editor panels
|
||||
- Fix duplicate tool registration for claude4-experimental
|
||||
- Trim input value for URL fields
|
||||
|
||||
## [3.20.2]
|
||||
|
||||
- Fixed issue with sap ai core client credentials storage
|
||||
- Fix Qwen Api option inconsistency between UI and API layer
|
||||
- Fix credit balance out of sync issue on account switching
|
||||
- Fix Claude Code CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||
- Fix cursor state after restoring files to be disabled after checked out
|
||||
- Fix issue where checkpointing blocked UI
|
||||
|
||||
## [3.20.1]
|
||||
|
||||
- Fix for files being deleted when switching modes or closing tasks
|
||||
|
||||
+5
-8
@@ -14,14 +14,11 @@ Bug reports help make Cline better for everyone! Before creating a new issue, pl
|
||||
## Before Contributing
|
||||
|
||||
All contributions must begin with a GitHub Issue, unless the change is for small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality.
|
||||
|
||||
- **Check existing issues**: Search [GitHub Issues](https://github.com/cline/cline/issues).
|
||||
- **Create an issue**: Use appropriate templates:
|
||||
- **Contributions:** Use the "Contribution Request" template to propose what you'd like to work on.
|
||||
- **Bugs:** "Bug Report" template for reporting issues.
|
||||
- **Features:** "Detailed Feature Proposal" template for suggesting new features.
|
||||
- **Wait for approval**: A core Cline contributor must approve your contribution request before you start implementation.
|
||||
- **Claim issues**: Once approved, the issue will be assigned to you.
|
||||
**For features and contributions**:
|
||||
- First check the [Feature Requests discussions board](https://github.com/cline/cline/discussions/categories/feature-requests) for similar ideas
|
||||
- If your idea is new, create a new feature request
|
||||
- Wait for approval from core maintainers before starting implementation
|
||||
- Once approved, feel free to begin working on a PR with the help of our community!
|
||||
|
||||
**PRs without approved issues may be closed.**
|
||||
|
||||
|
||||
@@ -11,7 +11,19 @@ You can create a rule by clicking the `+` button in the Rules tab. This will ope
|
||||
Once you save the file:
|
||||
|
||||
- Your rule will be stored in the `.clinerules/` directory in your project (if it's a Workspace Rule)
|
||||
- Or in the `Documents/Cline/Rules` directory (if it's a Global Rule).
|
||||
- Or in the Global Rules directory (if it's a Global Rule):
|
||||
|
||||
### Global Rules Directory Location
|
||||
|
||||
The location of your Global Rules directory depends on your operating system:
|
||||
|
||||
| Operating System | Default Location | Notes |
|
||||
|------------------|------------------|-------|
|
||||
| **Windows** | `Documents\Cline\Rules` | Uses system Documents folder |
|
||||
| **macOS** | `~/Documents/Cline/Rules` | Uses user Documents folder |
|
||||
| **Linux/WSL** | `~/Documents/Cline/Rules` | May fall back to `~/Cline/Rules` on some systems |
|
||||
|
||||
> **Note for Linux/WSL users**: If you don't find your global rules in `~/Documents/Cline/Rules`, check `~/Cline/Rules` as the location may vary depending on your system configuration and whether the Documents directory exists.
|
||||
|
||||
You can also have Cline create a rule for you by using the [`/newrule` slash command](/features/slash-commands/new-rule) in the chat.
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ There are multiple places online to find MCP servers:
|
||||
- [mcpservers.org](https://mcpservers.org/)
|
||||
- [mcp.so](https://mcp.so/)
|
||||
- [glama.ai/mcp/servers](https://glama.ai/mcp/servers)
|
||||
- [mcp.composio.dev](https://mcp.composio.dev/)
|
||||
|
||||
These directories allow users to sort the servers by various criteria such as downloads, date, stars, and use case. Each entry provides information such as features, tools, and configuration instructions.
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@ title: "Telemetry"
|
||||
|
||||
### Overview
|
||||
|
||||
To help make Cline better for everyone, we collect anonymous usage data that helps us understand how developers are using our open-source AI coding agent. This feedback loop is crucial for improving Cline's capabilities and user experience.
|
||||
To help make Cline better for everyone, we collect usage data that helps us understand how developers are using our open-source AI coding agent. This feedback loop is crucial for improving Cline's capabilities and user experience.
|
||||
|
||||
We use PostHog, an open-source analytics platform, for data collection and analysis. Our telemetry implementation is fully transparent - you can review the [source code](https://github.com/cline/cline/blob/main/src/services/posthog/telemetry/TelemetryService.ts) to see exactly what we track.
|
||||
|
||||
### Tracking Policy
|
||||
|
||||
Privacy is our priority. All collected data is anonymized before being sent to PostHog, with no personally identifiable information (PII) included. Your code, prompts, and conversation content always remain private and are never collected.
|
||||
Privacy is our priority. By default, all collected data is anonymized. If you log in with a Cline account, your telemetry data will be associated with your account to help us improve the product and provide better support when you encounter issues. Your code, prompts, and conversation content always remain private and are never collected.
|
||||
|
||||
### What We Track
|
||||
|
||||
We collect basic anonymous usage data including:
|
||||
We collect basic usage data including:
|
||||
|
||||
**Task Interactions:** When tasks start and finish, conversation flow (without content)\
|
||||
**Mode and Tool Usage:** Switches between plan/act modes, which tools are being used\
|
||||
@@ -28,7 +28,7 @@ For complete transparency, you can inspect our [telemetry implementation](https:
|
||||
|
||||
Telemetry in Cline is entirely optional:
|
||||
|
||||
- When you update or install our VS Code extension, you'll see a message about our anonymous telemetry
|
||||
- When you update or install our VS Code extension, you'll see a message about our 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.
|
||||
|
||||
@@ -16,6 +16,7 @@ description: "Learn how to configure and use Anthropic Claude models with Cline.
|
||||
|
||||
Cline supports the following Anthropic Claude models:
|
||||
|
||||
- `claude-opus-4-1-20250805`
|
||||
- `claude-opus-4-20250514`
|
||||
- `claude-opus-4-20250514:thinking` (Extended Thinking variant)
|
||||
- `claude-sonnet-4-20250514` (Recommended)
|
||||
|
||||
@@ -52,6 +52,7 @@ If you're not sure where Claude Code is installed:
|
||||
The Claude Code provider supports these models:
|
||||
|
||||
- `claude-sonnet-4-20250514` (Recommended)
|
||||
- `claude-opus-4-1-20250805`
|
||||
- `claude-opus-4-20250514`
|
||||
- `claude-3-7-sonnet-20250219`
|
||||
- `claude-3-5-sonnet-20241022`
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
const esbuild = require("esbuild")
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
import fs from "node:fs"
|
||||
import * as esbuild from "esbuild"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const production = process.argv.includes("--production")
|
||||
const watch = process.argv.includes("--watch")
|
||||
@@ -127,10 +131,8 @@ const baseConfig = {
|
||||
sourcemap: !production,
|
||||
logLevel: "silent",
|
||||
define: production
|
||||
? {
|
||||
"process.env.IS_DEV": JSON.stringify(!production),
|
||||
}
|
||||
: undefined,
|
||||
? { "import.meta.url": "_importMetaUrl", "process.env.IS_DEV": JSON.stringify(!production) }
|
||||
: { "import.meta.url": "_importMetaUrl" },
|
||||
tsconfig: path.resolve(__dirname, "tsconfig.json"),
|
||||
plugins: [
|
||||
copyWasmFiles,
|
||||
@@ -141,6 +143,9 @@ const baseConfig = {
|
||||
format: "cjs",
|
||||
sourcesContent: false,
|
||||
platform: "node",
|
||||
banner: {
|
||||
js: "const _importMetaUrl=require('url').pathToFileURL(__filename)",
|
||||
},
|
||||
}
|
||||
|
||||
// Extension-specific configuration
|
||||
@@ -34,25 +34,30 @@ directApiRuleTester.run("no-direct-vscode-api", noDirectVscodeApiRule, {
|
||||
},
|
||||
// Should allow other vscode API calls
|
||||
{
|
||||
code: `vscode.window.showInformationMessage("Hello")`,
|
||||
filename: "test.ts",
|
||||
code: `vscode.commands.registerCommand("Hello")`,
|
||||
filename: "/foo/bar.ts",
|
||||
},
|
||||
// Should allow postMessage calls on other objects
|
||||
{
|
||||
code: `window.postMessage({ type: "test" }, "*")`,
|
||||
filename: "test.ts",
|
||||
filename: "/foo/bar.ts",
|
||||
},
|
||||
// Should allow variables named vscode but not calling postMessage
|
||||
{
|
||||
code: `const vscode = { other: "method" }; vscode.other()`,
|
||||
filename: "test.ts",
|
||||
filename: "/foo/bar.ts",
|
||||
},
|
||||
// Should allow vscode.postMessage in test files
|
||||
{
|
||||
code: `vscode.postMessage({ type: "newTask", text: message.text })`,
|
||||
filename: "/foo/bar.test.ts",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Should disallow vscode.postMessage in regular files
|
||||
{
|
||||
code: `vscode.postMessage({ type: "test", data: {} })`,
|
||||
filename: "test.ts",
|
||||
filename: "/foo/bar.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useGrpcClient",
|
||||
@@ -69,23 +74,13 @@ directApiRuleTester.run("no-direct-vscode-api", noDirectVscodeApiRule, {
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow vscode.postMessage in test files
|
||||
{
|
||||
code: `vscode.postMessage({ type: "newTask", text: message.text })`,
|
||||
filename: "test.test.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useGrpcClient",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow property access for disallowed APIs
|
||||
{
|
||||
code: `const folders = vscode.workspace.workspaceFolders;`,
|
||||
filename: "workspace.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useHostBridge",
|
||||
messageId: "useHostBridgeWorkspace",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -35,18 +35,24 @@ const disallowedApis = {
|
||||
"vscode.env.openExternal": {
|
||||
messageId: "useUtils",
|
||||
},
|
||||
// "vscode.window.showWarningMessage": {
|
||||
// messageId: "useHostBridgeShowMessage",
|
||||
// },
|
||||
"vscode.window.showWarningMessage": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
"vscode.window.showOpenDialog": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
"vscode.window.showErrorMessage": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
// "vscode.window.showInformationMessage": {
|
||||
// messageId: "useHostBridgeShowMessage",
|
||||
// },
|
||||
"vscode.window.showInformationMessage": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
"vscode.window.showInputBox": {
|
||||
messageId: "useHostBridge",
|
||||
},
|
||||
"vscode.workspace.findFiles": {
|
||||
messageId: "useNative",
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = createRule({
|
||||
@@ -87,6 +93,10 @@ module.exports = createRule({
|
||||
"Use utilities in @/utils instead of calling vscode APIs directly.\n" +
|
||||
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
|
||||
"Found: {{code}}",
|
||||
useNative:
|
||||
"Use a native Javascript API instead of calling the vscode API.\n" +
|
||||
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
|
||||
"Found: {{code}}",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
@@ -187,7 +197,7 @@ module.exports = createRule({
|
||||
if (filename.includes("/standalone/runtime-files/")) {
|
||||
return true
|
||||
}
|
||||
// Skip unit tests
|
||||
// Skip checking test files
|
||||
if (filename.endsWith(".test.ts")) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ interface RunDiffEvalOptions {
|
||||
parsingFunction: string
|
||||
diffEditFunction: string
|
||||
thinkingBudget: number
|
||||
provider: string
|
||||
parallel: boolean
|
||||
verbose: boolean
|
||||
testPath: string
|
||||
@@ -39,6 +40,8 @@ export async function runDiffEvalHandler(options: RunDiffEvalOptions) {
|
||||
options.parsingFunction,
|
||||
"--diff-edit-function",
|
||||
options.diffEditFunction,
|
||||
"--provider",
|
||||
options.provider,
|
||||
]
|
||||
|
||||
// Conditionally add the optional arguments
|
||||
|
||||
@@ -92,6 +92,7 @@ program
|
||||
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
|
||||
.option("--diff-edit-function <name>", "The diff editing function to use", "constructNewFileContentV2")
|
||||
.option("--thinking-budget <tokens>", "Set the thinking tokens budget", "0")
|
||||
.option("--provider <provider>", "API provider to use (openrouter, openai)", "openrouter")
|
||||
.option("--parallel", "Run tests in parallel", false)
|
||||
.option("--replay", "Run evaluation from a pre-recorded LLM output, skipping the API call", false)
|
||||
.option("--replay-run-id <run_id>", "The ID of the run to replay from the database")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { OpenRouterHandler } from "../../src/api/providers/openrouter"
|
||||
import { OpenAiNativeHandler } from "../../src/api/providers/openai-native"
|
||||
import { ApiHandlerOptions } from "../../src/shared/api"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
import {
|
||||
parseAssistantMessageV1,
|
||||
parseAssistantMessageV2,
|
||||
parseAssistantMessageV3,
|
||||
AssistantMessageContent,
|
||||
@@ -17,7 +17,6 @@ type ParseAssistantMessageFn = (message: string) => AssistantMessageContent[]
|
||||
type ConstructNewFileContentFn = (diff: string, original: string, strict: boolean) => Promise<string | any>
|
||||
|
||||
const parsingFunctions: Record<string, ParseAssistantMessageFn> = {
|
||||
parseAssistantMessageV1: parseAssistantMessageV1,
|
||||
parseAssistantMessageV2: parseAssistantMessageV2,
|
||||
parseAssistantMessageV3: parseAssistantMessageV3,
|
||||
}
|
||||
@@ -54,7 +53,7 @@ interface StreamResult {
|
||||
* Process the stream and return full response with timing data
|
||||
*/
|
||||
async function processStream(
|
||||
handler: OpenRouterHandler,
|
||||
handler: OpenRouterHandler | OpenAiNativeHandler,
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
): Promise<StreamResult> {
|
||||
@@ -190,19 +189,7 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
|
||||
}
|
||||
}
|
||||
|
||||
const options: ApiHandlerOptions = {
|
||||
openRouterApiKey: apiKey,
|
||||
openRouterModelId: modelId,
|
||||
thinkingBudgetTokens: thinkingBudgetTokens,
|
||||
openRouterModelInfo: {
|
||||
maxTokens: 10_000,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true, // may need to turn this on
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
}
|
||||
const provider = input.provider || "openrouter"
|
||||
|
||||
// Get the output of streaming output of this llm call
|
||||
let streamResult: StreamResult
|
||||
@@ -214,10 +201,34 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
|
||||
usage: { inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, totalCost: 0 },
|
||||
}
|
||||
} else {
|
||||
// Live mode: existing API call logic
|
||||
// Live mode: provider-specific API call logic
|
||||
try {
|
||||
const openRouterHandler = new OpenRouterHandler(options)
|
||||
streamResult = await processStream(openRouterHandler, systemPrompt, messages)
|
||||
let handler: OpenRouterHandler | OpenAiNativeHandler
|
||||
|
||||
if (provider === "openai") {
|
||||
const openAiOptions = {
|
||||
openAiNativeApiKey: apiKey,
|
||||
apiModelId: modelId,
|
||||
}
|
||||
handler = new OpenAiNativeHandler(openAiOptions)
|
||||
} else {
|
||||
const openRouterOptions = {
|
||||
openRouterApiKey: apiKey,
|
||||
openRouterModelId: modelId,
|
||||
thinkingBudgetTokens: thinkingBudgetTokens,
|
||||
openRouterModelInfo: {
|
||||
maxTokens: 10_000,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
}
|
||||
handler = new OpenRouterHandler(openRouterOptions)
|
||||
}
|
||||
|
||||
streamResult = await processStream(handler, systemPrompt, messages)
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -49,16 +49,25 @@ type TestResultSet = { [test_id: string]: (TestResult & { test_id?: string })[]
|
||||
|
||||
class NodeTestRunner {
|
||||
private apiKey: string | undefined
|
||||
private provider: string
|
||||
private currentRunId: string | null = null
|
||||
private systemPromptHash: string | null = null
|
||||
private processingFunctionsHash: string | null = null
|
||||
private caseIdMap: Map<string, string> = new Map() // test_id -> case_id mapping
|
||||
|
||||
constructor(isReplay: boolean) {
|
||||
constructor(isReplay: boolean, provider: string = "openrouter") {
|
||||
this.provider = provider
|
||||
if (!isReplay) {
|
||||
this.apiKey = process.env.OPENROUTER_API_KEY
|
||||
if (!this.apiKey) {
|
||||
throw new Error("OPENROUTER_API_KEY environment variable not set for a non-replay run.")
|
||||
if (provider === "openai") {
|
||||
this.apiKey = process.env.OPENAI_API_KEY
|
||||
if (!this.apiKey) {
|
||||
throw new Error("OPENAI_API_KEY environment variable not set for a non-replay run with OpenAI provider.")
|
||||
}
|
||||
} else {
|
||||
this.apiKey = process.env.OPENROUTER_API_KEY
|
||||
if (!this.apiKey) {
|
||||
throw new Error("OPENROUTER_API_KEY environment variable not set for a non-replay run with OpenRouter provider.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -635,6 +644,7 @@ class NodeTestRunner {
|
||||
thinkingBudgetTokens: testConfig.thinking_tokens_budget,
|
||||
originalDiffEditToolCallMessage: testConfig.replay ? testCase.original_diff_edit_tool_call_message : undefined,
|
||||
diffApplyFile: testConfig.diff_apply_file,
|
||||
provider: this.provider,
|
||||
isVerbose: isVerbose,
|
||||
}
|
||||
|
||||
@@ -927,6 +937,7 @@ async function main() {
|
||||
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
|
||||
.option("--diff-edit-function <name>", "The diff editing function to use", "diff-06-26-25")
|
||||
.option("--thinking-budget <tokens>", "Set the thinking tokens budget", "0")
|
||||
.option("--provider <provider>", "API provider to use (openrouter, openai)", "openrouter")
|
||||
.option("--parallel", "Run tests in parallel", false)
|
||||
.option("--replay", "Run evaluation from a pre-recorded LLM output, skipping the API call", false)
|
||||
.option("--replay-run-id <run_id>", "The ID of the run to replay from the database")
|
||||
@@ -959,7 +970,7 @@ async function main() {
|
||||
? parseInt(options.maxAttemptsPerCase, 10)
|
||||
: validAttemptsPerCase * 10;
|
||||
|
||||
const runner = new NodeTestRunner(options.replay || !!options.replayRunId)
|
||||
const runner = new NodeTestRunner(options.replay || !!options.replayRunId, options.provider)
|
||||
|
||||
if (options.replayRunId) {
|
||||
if (!options.diffApplyFile) {
|
||||
@@ -979,7 +990,7 @@ async function main() {
|
||||
log(isVerbose, "Warning: Could not load OpenRouter model data. Context window filtering might be affected for OpenRouter models.");
|
||||
}
|
||||
|
||||
const runner = new NodeTestRunner(options.replay)
|
||||
const runner = new NodeTestRunner(options.replay, options.provider)
|
||||
let allLoadedTestCases = runner.loadTestCases(testPath, isVerbose) // Pass isVerbose
|
||||
|
||||
const allProcessedTestCasesGlobal: ProcessedTestCase[] = allLoadedTestCases.map((tc) => ({
|
||||
|
||||
@@ -331,6 +331,42 @@ def get_performance_grade(success_rate):
|
||||
else:
|
||||
return "C", "poor"
|
||||
|
||||
def get_error_description(error_enum, error_string=None):
|
||||
"""Map error enum values to user-friendly descriptions"""
|
||||
error_map = {
|
||||
1: "No tool calls - Model didn't use the replace_in_file tool",
|
||||
2: "Multiple tool calls - Model called multiple tools instead of one",
|
||||
3: "Wrong tool call - Model used wrong tool (not replace_in_file)",
|
||||
4: "Missing parameters - Tool call missing required path or diff",
|
||||
5: "Wrong file edited - Model edited different file than expected",
|
||||
6: "Wrong tool call - Model used wrong tool type",
|
||||
7: "Wrong file edited - Model targeted incorrect file path",
|
||||
8: "API/Stream error - Problem with model API connection",
|
||||
9: "Configuration error - Invalid evaluation parameters",
|
||||
10: "Function error - Invalid parsing/diff functions",
|
||||
11: "Other error - Unexpected failure"
|
||||
}
|
||||
|
||||
base_description = error_map.get(error_enum, f"Unknown error (code: {error_enum})")
|
||||
|
||||
if error_string:
|
||||
return f"{base_description}: {error_string}"
|
||||
return base_description
|
||||
|
||||
def get_error_guidance(error_enum):
|
||||
"""Provide specific guidance based on error type"""
|
||||
guidance_map = {
|
||||
1: "💡 The model provided a response but didn't use the replace_in_file tool. Check the raw output to see what the model actually said.",
|
||||
2: "💡 The model called multiple tools when it should only call replace_in_file once. Check the parsed tool call section.",
|
||||
3: "💡 The model used a different tool instead of replace_in_file. This might indicate confusion about the task.",
|
||||
4: "💡 The model called replace_in_file but didn't provide the required 'path' or 'diff' parameters.",
|
||||
5: "💡 The model tried to edit a different file than expected. Check the parsed tool call to see which file it targeted.",
|
||||
6: "💡 The model used the wrong tool type. Check the raw output to see what tool it attempted to use.",
|
||||
7: "💡 The model tried to edit a different file path than expected. This could indicate path confusion or hallucination.",
|
||||
}
|
||||
|
||||
return guidance_map.get(error_enum, "")
|
||||
|
||||
def render_hero_section(current_run, model_performance):
|
||||
"""Render the hero section with key metrics"""
|
||||
run_title = current_run['description'] if current_run['description'] else f"Run {current_run['run_id'][:8]}..."
|
||||
@@ -570,12 +606,16 @@ def render_result_detail(result):
|
||||
"""Render detailed view of a single result"""
|
||||
st.markdown("### 🔬 Result Deep Dive")
|
||||
|
||||
# Check if this is a valid result
|
||||
is_valid = (result['error_enum'] not in [1, 6, 7]) if not pd.isna(result['error_enum']) else True
|
||||
# Check if this is a valid result (only invalid if no tool calls or wrong file)
|
||||
is_valid = True
|
||||
if not pd.isna(result['error_enum']):
|
||||
# Only these specific errors make a result "invalid" for the benchmark:
|
||||
# 1 = no_tool_calls, 5 = wrong_file_edited, 7 = wrong_file_edited
|
||||
is_valid = result['error_enum'] not in [1, 5, 7]
|
||||
|
||||
# Show validity warning if needed
|
||||
if not is_valid:
|
||||
st.warning("⚠️ **This is an invalid result** - The model didn't properly call the diff edit tool or edited the wrong file. This result is excluded from success rate calculations.")
|
||||
st.warning("⚠️ **This is an invalid result** - The model didn't call the replace_in_file tool or edited the wrong file. This result is excluded from success rate calculations.")
|
||||
|
||||
# Result metadata
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
@@ -591,7 +631,10 @@ def render_result_detail(result):
|
||||
st.markdown(f"**Round Trip:** {result['time_round_trip_ms']:.0f}ms")
|
||||
|
||||
with col4:
|
||||
st.markdown(f"**Cost:** ${result['cost_usd']:.4f}")
|
||||
if pd.notna(result['cost_usd']) and result['cost_usd'] is not None:
|
||||
st.markdown(f"**Cost:** ${result['cost_usd']:.4f}")
|
||||
else:
|
||||
st.markdown(f"**Cost:** Free")
|
||||
|
||||
# Tabbed interface for different views
|
||||
tab1, tab2, tab3, tab4 = st.tabs(["📄 File & Edits", "🤖 Raw Output", "🔧 Parsed Tool Call", "📊 Metrics"])
|
||||
@@ -693,8 +736,46 @@ def render_file_and_edits_view(result):
|
||||
# Show error information
|
||||
st.error("❌ **Edit Failed**")
|
||||
|
||||
# Show detailed error reason
|
||||
if not pd.isna(result['error_enum']):
|
||||
st.markdown(f"**Error Code:** {result['error_enum']}")
|
||||
error_description = get_error_description(
|
||||
result['error_enum'],
|
||||
result.get('error_string')
|
||||
)
|
||||
st.markdown(f"**Reason:** {error_description}")
|
||||
|
||||
# Show specific guidance based on error type
|
||||
guidance = get_error_guidance(result['error_enum'])
|
||||
if guidance:
|
||||
st.info(guidance)
|
||||
|
||||
# For valid results that failed, check for diff application failures
|
||||
elif not result['succeeded']:
|
||||
# This is a valid result that failed - likely due to diff application issues
|
||||
raw_output = result.get('raw_model_output', '')
|
||||
|
||||
# Check if we have specific error information in the raw output
|
||||
if 'does not match anything in the file' in str(raw_output).lower():
|
||||
st.warning("⚠️ **Diff Application Failed**")
|
||||
st.info("💡 The SEARCH block in the diff didn't match any content in the original file. This usually means the model hallucinated code that doesn't exist.")
|
||||
elif 'malformatted' in str(raw_output).lower() or 'malformed' in str(raw_output).lower():
|
||||
st.warning("⚠️ **Diff Format Error**")
|
||||
st.info("💡 The diff format was incorrect. Check the raw tool call to see the formatting issues.")
|
||||
elif 'error:' in str(raw_output).lower():
|
||||
# Try to extract the specific error message
|
||||
lines = str(raw_output).split('\n')
|
||||
error_lines = [line for line in lines if 'error:' in line.lower()]
|
||||
if error_lines:
|
||||
error_msg = error_lines[0].strip()
|
||||
st.warning("⚠️ **Diff Application Failed**")
|
||||
st.info(f"💡 {error_msg}")
|
||||
else:
|
||||
st.warning("⚠️ **Diff Application Failed**")
|
||||
st.info("💡 The diff couldn't be applied to the original file. Check the raw output and parsed tool call for more details.")
|
||||
else:
|
||||
# Generic diff application failure
|
||||
st.warning("⚠️ **Diff Application Failed**")
|
||||
st.info("💡 The model made a valid tool call but the diff couldn't be applied to the original file. This usually indicates a mismatch between the expected and actual file content.")
|
||||
else:
|
||||
# Show successful edit information
|
||||
st.success("✅ **Edit Successful**")
|
||||
@@ -725,8 +806,25 @@ def render_file_and_edits_view(result):
|
||||
if len(edited_lines) > 50:
|
||||
st.text(f"... ({len(edited_lines) - 50} more lines)")
|
||||
|
||||
# Show parsed tool call if available
|
||||
# Show raw and parsed tool calls if available
|
||||
if not pd.isna(result['parsed_tool_call_json']):
|
||||
with st.expander("View Raw Tool Call"):
|
||||
# Extract the raw tool call text from the model output
|
||||
raw_output = result['raw_model_output'] if not pd.isna(result['raw_model_output']) else ""
|
||||
|
||||
# Try to extract just the tool call portion
|
||||
if raw_output and '<replace_in_file>' in raw_output:
|
||||
# Find the tool call block
|
||||
start_idx = raw_output.find('<replace_in_file>')
|
||||
end_idx = raw_output.find('</replace_in_file>') + len('</replace_in_file>')
|
||||
if start_idx != -1 and end_idx != -1:
|
||||
raw_tool_call = raw_output[start_idx:end_idx]
|
||||
st.code(raw_tool_call, language='xml')
|
||||
else:
|
||||
st.text("Tool call not found in raw output")
|
||||
else:
|
||||
st.text("No raw tool call available")
|
||||
|
||||
with st.expander("View Parsed Tool Call"):
|
||||
try:
|
||||
parsed_call = json.loads(result['parsed_tool_call_json'])
|
||||
@@ -795,8 +893,10 @@ def render_metrics_view(result):
|
||||
if not pd.isna(result['completion_tokens']):
|
||||
st.metric("Completion Tokens", int(result['completion_tokens']))
|
||||
|
||||
if not pd.isna(result['cost_usd']):
|
||||
if pd.notna(result['cost_usd']) and result['cost_usd'] is not None:
|
||||
st.metric("Cost", f"${result['cost_usd']:.4f}")
|
||||
else:
|
||||
st.metric("Cost", "Free")
|
||||
|
||||
if not pd.isna(result['tokens_in_context']):
|
||||
st.metric("Context Tokens", int(result['tokens_in_context']))
|
||||
|
||||
@@ -70,246 +70,7 @@ export interface ToolUse {
|
||||
partial: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
let currentToolUse: ToolUse | undefined = undefined
|
||||
let currentToolUseStartIndex = 0
|
||||
let currentParamName: ToolParamName | undefined = undefined
|
||||
let currentParamValueStartIndex = 0
|
||||
let accumulator = ""
|
||||
|
||||
for (let i = 0; i < assistantMessage.length; i++) {
|
||||
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 found
|
||||
currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim()
|
||||
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 // 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 found
|
||||
currentToolUse.partial = false
|
||||
contentBlocks.push(currentToolUse)
|
||||
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 found
|
||||
currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
|
||||
currentParamValueStartIndex = accumulator.length
|
||||
foundParamStart = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (foundParamStart) {
|
||||
continue // Move to next character
|
||||
}
|
||||
|
||||
// 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") &&
|
||||
accumulator.endsWith(`</${contentParamName}>`)
|
||||
) {
|
||||
const toolContent = accumulator.slice(currentToolUseStartIndex)
|
||||
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)
|
||||
|
||||
// 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 found
|
||||
const toolName = toolUseOpeningTag.slice(1, -1) as ToolUseName
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolName,
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
currentToolUseStartIndex = accumulator.length
|
||||
|
||||
// This also indicates the end of the current text content block (if any)
|
||||
if (currentTextContent) {
|
||||
currentTextContent.partial = false
|
||||
// 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 // Found tool start, stop checking for others
|
||||
}
|
||||
}
|
||||
|
||||
if (!didStartToolUse) {
|
||||
// No tool use started, so it must be text content accumulating
|
||||
// (or continuing after a closed tool use)
|
||||
if (currentTextContent === undefined) {
|
||||
// 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) {
|
||||
// If a parameter was open within that tool use
|
||||
if (currentParamName) {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
return contentBlocks
|
||||
}
|
||||
// parseAssistantmessageV1 removed in https://github.com/cline/cline/pull/5425
|
||||
|
||||
/**
|
||||
* @description **Version 2**
|
||||
|
||||
@@ -104,5 +104,6 @@ export interface TestInput {
|
||||
thinkingBudgetTokens: number
|
||||
originalDiffEditToolCallMessage?: string
|
||||
diffApplyFile?: string
|
||||
provider?: string
|
||||
isVerbose: boolean
|
||||
}
|
||||
|
||||
Generated
+236
-16829
File diff suppressed because it is too large
Load Diff
+11
-7
@@ -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.20.1",
|
||||
"version": "3.21.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -119,7 +119,8 @@
|
||||
{
|
||||
"type": "webview",
|
||||
"id": "claude-dev.SidebarProvider",
|
||||
"name": ""
|
||||
"name": "",
|
||||
"icon": "assets/icons/icon.svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -338,14 +339,14 @@
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run package",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||
"compile-standalone": "npm run check-types && npm run lint && node esbuild.js --standalone",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.mjs",
|
||||
"compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
|
||||
"postcompile-standalone": "node scripts/package-standalone.mjs",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:esbuild": "node esbuild.mjs --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production",
|
||||
"protos": "node scripts/build-proto.mjs && node scripts/generate-protobus-setup.mjs && node scripts/generate-host-bridge-client.mjs",
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
|
||||
"protos": "node scripts/build-proto.mjs",
|
||||
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn",
|
||||
"clean": "rimraf dist dist-standalone webview-ui/build src/generated out/",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
@@ -446,6 +447,7 @@
|
||||
"@playwright/test": "^1.53.2",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.8.2",
|
||||
@@ -490,6 +492,8 @@
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"ts-morph": "^25.0.1",
|
||||
"turndown": "^7.2.0",
|
||||
"ulid": "^2.4.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vscode-uri": "^3.1.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
"zod": "^3.24.2"
|
||||
|
||||
+21
-6
@@ -1,17 +1,32 @@
|
||||
import { defineConfig } from "@playwright/test"
|
||||
|
||||
const isGitHubAction = !!process.env.CI
|
||||
const isCI = !!process?.env?.CI
|
||||
const isWindow = process?.platform?.startsWith("win")
|
||||
|
||||
export default defineConfig({
|
||||
workers: 1,
|
||||
retries: 1,
|
||||
testDir: "src/test/e2e",
|
||||
timeout: 20000,
|
||||
timeout: isCI || isWindow ? 40000 : 20000,
|
||||
expect: {
|
||||
timeout: 20000,
|
||||
timeout: isCI || isWindow ? 5000 : 2000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
reporter: isGitHubAction ? [["github"], ["list"]] : [["list"]],
|
||||
globalSetup: require.resolve("./src/test/e2e/utils/setup"),
|
||||
globalTeardown: require.resolve("./src/test/e2e/utils/teardown"),
|
||||
reporter: isCI ? [["github"], ["list"]] : [["list"]],
|
||||
projects: [
|
||||
{
|
||||
name: "setup test environment",
|
||||
testMatch: /global\.setup\.ts/,
|
||||
teardown: "cleanup test environment",
|
||||
},
|
||||
{
|
||||
name: "cleanup test environment",
|
||||
testMatch: /global\.teardown\.ts/,
|
||||
},
|
||||
{
|
||||
name: "e2e tests",
|
||||
testMatch: /.*\.test\.ts/,
|
||||
dependencies: ["setup test environment"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -27,6 +27,8 @@ service ModelsService {
|
||||
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
|
||||
// Refreshes and returns Groq models
|
||||
rpc refreshGroqModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Baseten models
|
||||
rpc refreshBasetenModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
@@ -129,6 +131,8 @@ enum ApiProvider {
|
||||
CLAUDE_CODE = 26;
|
||||
MOONSHOT = 27;
|
||||
HUGGINGFACE = 28;
|
||||
HUAWEI_CLOUD_MAAS = 29;
|
||||
BASETEN = 30;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
@@ -229,6 +233,9 @@ message ModelsApiConfiguration {
|
||||
optional string cline_account_id = 58;
|
||||
optional string groq_api_key = 59;
|
||||
optional string hugging_face_api_key = 60;
|
||||
optional string huawei_cloud_maas_api_key = 61;
|
||||
optional string baseten_api_key = 62;
|
||||
optional string ollama_api_key = 63;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
@@ -255,6 +262,10 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo plan_mode_groq_model_info = 121;
|
||||
optional string plan_mode_hugging_face_model_id = 122;
|
||||
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 123;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 124;
|
||||
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 125;
|
||||
optional string plan_mode_baseten_model_id = 126;
|
||||
optional OpenRouterModelInfo plan_mode_baseten_model_info = 127;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
@@ -281,6 +292,10 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo act_mode_groq_model_info = 221;
|
||||
optional string act_mode_hugging_face_model_id = 222;
|
||||
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 223;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 224;
|
||||
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 225;
|
||||
optional string act_mode_baseten_model_id = 226;
|
||||
optional OpenRouterModelInfo act_mode_baseten_model_info = 227;
|
||||
|
||||
repeated string favorited_model_ids = 300;
|
||||
}
|
||||
|
||||
+28
-17
@@ -6,14 +6,14 @@ option java_multiple_files = true;
|
||||
|
||||
service StateService {
|
||||
rpc getLatestState(EmptyRequest) returns (State);
|
||||
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
|
||||
rpc updateTerminalConnectionTimeout(UpdateTerminalConnectionTimeoutRequest) returns (UpdateTerminalConnectionTimeoutResponse);
|
||||
rpc updateTerminalReuseEnabled(BooleanRequest) returns (Empty);
|
||||
rpc updateDefaultTerminalProfile(StringRequest) returns (TerminalProfileUpdateResponse);
|
||||
rpc getAvailableTerminalProfiles(EmptyRequest) returns (TerminalProfiles);
|
||||
rpc subscribeToState(EmptyRequest) returns (stream State);
|
||||
rpc toggleFavoriteModel(StringRequest) returns (Empty);
|
||||
rpc resetState(ResetStateRequest) returns (Empty);
|
||||
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Boolean);
|
||||
rpc togglePlanActModeProto(TogglePlanActModeRequest) returns (Boolean);
|
||||
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
|
||||
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
|
||||
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
|
||||
@@ -43,7 +43,7 @@ message TerminalProfileUpdateResponse {
|
||||
|
||||
message TogglePlanActModeRequest {
|
||||
Metadata metadata = 1;
|
||||
ChatSettings chat_settings = 2;
|
||||
PlanActMode mode = 2;
|
||||
optional ChatContent chat_content = 3;
|
||||
}
|
||||
|
||||
@@ -52,12 +52,6 @@ enum PlanActMode {
|
||||
ACT = 1;
|
||||
}
|
||||
|
||||
message ChatSettings {
|
||||
PlanActMode mode = 1;
|
||||
optional string preferred_language = 2;
|
||||
optional string open_ai_reasoning_effort = 3;
|
||||
}
|
||||
|
||||
message ChatContent {
|
||||
optional string message = 1;
|
||||
repeated string images = 2;
|
||||
@@ -108,12 +102,15 @@ message UpdateSettingsRequest {
|
||||
optional bool plan_act_separate_models_setting = 4;
|
||||
optional bool enable_checkpoints_setting = 5;
|
||||
optional bool mcp_marketplace_enabled = 6;
|
||||
optional ChatSettings chat_settings = 7;
|
||||
optional int64 shell_integration_timeout = 8;
|
||||
optional int32 shell_integration_timeout = 8;
|
||||
optional bool terminal_reuse_enabled = 9;
|
||||
optional bool mcp_responses_collapsed = 10;
|
||||
optional string mcp_display_mode = 11;
|
||||
optional int64 terminal_output_line_limit = 12;
|
||||
optional int32 terminal_output_line_limit = 12;
|
||||
optional PlanActMode mode = 13;
|
||||
optional string preferred_language = 14;
|
||||
optional string openai_reasoning_effort = 15;
|
||||
optional bool strict_plan_mode_enabled = 16;
|
||||
}
|
||||
|
||||
// Complete API Configuration message
|
||||
@@ -153,8 +150,8 @@ message ApiConfiguration {
|
||||
optional string requesty_api_key = 32;
|
||||
optional string together_api_key = 33;
|
||||
optional string fireworks_api_key = 34;
|
||||
optional int64 fireworks_model_max_completion_tokens = 35;
|
||||
optional int64 fireworks_model_max_tokens = 36;
|
||||
optional int32 fireworks_model_max_completion_tokens = 35;
|
||||
optional int32 fireworks_model_max_tokens = 36;
|
||||
optional string qwen_api_key = 37;
|
||||
optional string doubao_api_key = 38;
|
||||
optional string mistral_api_key = 39;
|
||||
@@ -166,7 +163,7 @@ message ApiConfiguration {
|
||||
optional string xai_api_key = 45;
|
||||
optional string sambanova_api_key = 46;
|
||||
optional string cerebras_api_key = 47;
|
||||
optional int64 request_timeout_ms = 48;
|
||||
optional int32 request_timeout_ms = 48;
|
||||
optional string sap_ai_core_client_id = 49;
|
||||
optional string sap_ai_core_client_secret = 50;
|
||||
optional string sap_ai_resource_group = 51;
|
||||
@@ -174,11 +171,13 @@ message ApiConfiguration {
|
||||
optional string sap_ai_core_base_url = 53;
|
||||
optional string moonshot_api_key = 54;
|
||||
optional string moonshot_api_line = 55;
|
||||
optional string huawei_cloud_maas_api_key = 56;
|
||||
optional string ollama_api_key = 57;
|
||||
|
||||
// Plan mode configurations
|
||||
optional string plan_mode_api_provider = 100;
|
||||
optional string plan_mode_api_model_id = 101;
|
||||
optional int64 plan_mode_thinking_budget_tokens = 102;
|
||||
optional int32 plan_mode_thinking_budget_tokens = 102;
|
||||
optional string plan_mode_reasoning_effort = 103;
|
||||
optional string plan_mode_vscode_lm_model_selector = 104; // JSON string
|
||||
optional bool plan_mode_aws_bedrock_custom_selected = 105;
|
||||
@@ -196,11 +195,13 @@ message ApiConfiguration {
|
||||
optional string plan_mode_together_model_id = 117;
|
||||
optional string plan_mode_fireworks_model_id = 118;
|
||||
optional string plan_mode_sap_ai_core_model_id = 119;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 120;
|
||||
optional string plan_mode_huawei_cloud_maas_model_info = 121;
|
||||
|
||||
// Act mode configurations
|
||||
optional string act_mode_api_provider = 200;
|
||||
optional string act_mode_api_model_id = 201;
|
||||
optional int64 act_mode_thinking_budget_tokens = 202;
|
||||
optional int32 act_mode_thinking_budget_tokens = 202;
|
||||
optional string act_mode_reasoning_effort = 203;
|
||||
optional string act_mode_vscode_lm_model_selector = 204; // JSON string
|
||||
optional bool act_mode_aws_bedrock_custom_selected = 205;
|
||||
@@ -218,6 +219,8 @@ message ApiConfiguration {
|
||||
optional string act_mode_together_model_id = 217;
|
||||
optional string act_mode_fireworks_model_id = 218;
|
||||
optional string act_mode_sap_ai_core_model_id = 219;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 220;
|
||||
optional string act_mode_huawei_cloud_maas_model_info = 221;
|
||||
|
||||
// Favorited model IDs
|
||||
repeated string favorited_model_ids = 300;
|
||||
@@ -228,3 +231,11 @@ message ApiConfiguration {
|
||||
|
||||
optional string cline_account_id = 303;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutRequest {
|
||||
optional int32 timeout_ms = 1;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutResponse {
|
||||
optional int32 timeout_ms = 1;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ service UiService {
|
||||
rpc onDidShowAnnouncement(EmptyRequest) returns (Boolean);
|
||||
|
||||
// Subscribe to addToInput events (when user adds content via context menu)
|
||||
rpc subscribeToAddToInput(EmptyRequest) returns (stream String);
|
||||
rpc subscribeToAddToInput(StringRequest) returns (stream String);
|
||||
|
||||
// Subscribe to MCP button clicked events
|
||||
rpc subscribeToMcpButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
|
||||
@@ -267,4 +267,7 @@ service UiService {
|
||||
|
||||
// Opens a URL in the default browser
|
||||
rpc openUrl(StringRequest) returns (Empty);
|
||||
|
||||
// Opens the Cline walkthrough
|
||||
rpc openWalkthrough(EmptyRequest) returns (Empty);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,4 @@ service EnvService {
|
||||
|
||||
// Reads text from the system clipboard.
|
||||
rpc clipboardReadText(cline.EmptyRequest) returns (cline.String);
|
||||
|
||||
// Opens a URL in the user's default browser or application.
|
||||
rpc openExternal(cline.StringRequest) returns (cline.Empty);
|
||||
}
|
||||
|
||||
+14
-1
@@ -14,6 +14,7 @@ service WindowService {
|
||||
rpc showMessage(ShowMessageRequest) returns (SelectedResponse);
|
||||
rpc showInputBox(ShowInputBoxRequest) returns (ShowInputBoxResponse);
|
||||
rpc showSaveDialog(ShowSaveDialogRequest) returns (ShowSaveDialogResponse);
|
||||
rpc openFile(OpenFileRequest) returns (OpenFileResponse);
|
||||
rpc getOpenTabs(GetOpenTabsRequest) returns (GetOpenTabsResponse);
|
||||
rpc getVisibleTabs(GetVisibleTabsRequest) returns (GetVisibleTabsResponse);
|
||||
}
|
||||
@@ -83,6 +84,8 @@ message ShowSaveDialogRequest {
|
||||
|
||||
message ShowSaveDialogOptions {
|
||||
optional string default_path = 1;
|
||||
// A map of file types to extensions, e.g
|
||||
// "Text Files": { "extensions": ["txt", "md"] }
|
||||
map<string, FileExtensionList> filters = 2;
|
||||
}
|
||||
|
||||
@@ -91,6 +94,7 @@ message FileExtensionList {
|
||||
}
|
||||
|
||||
message ShowSaveDialogResponse {
|
||||
// If the user cancelled the dialog, this will be empty.
|
||||
optional string selected_path = 1;
|
||||
}
|
||||
|
||||
@@ -105,6 +109,15 @@ message ShowInputBoxResponse {
|
||||
optional string response = 1;
|
||||
}
|
||||
|
||||
message OpenFileRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
string file_path = 2;
|
||||
}
|
||||
|
||||
message OpenFileResponse {
|
||||
// empty
|
||||
}
|
||||
|
||||
message GetOpenTabsRequest {
|
||||
// empty
|
||||
}
|
||||
@@ -119,4 +132,4 @@ message GetVisibleTabsRequest {
|
||||
|
||||
message GetVisibleTabsResponse {
|
||||
repeated string paths = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,12 @@ import "cline/common.proto";
|
||||
service WorkspaceService {
|
||||
// Returns a list of the top level directories of the workspace.
|
||||
rpc getWorkspacePaths(GetWorkspacePathsRequest) returns (GetWorkspacePathsResponse);
|
||||
// Saves an open document if it's dirty
|
||||
rpc saveOpenDocumentIfDirty(SaveOpenDocumentIfDirtyRequest) returns (cline.Empty);
|
||||
// Saves an open document if it's open in the editor and has unsaved changes.
|
||||
// Returns true if the document was saved, returns false if the document was not found, or did not
|
||||
// need to be saved.
|
||||
rpc saveOpenDocumentIfDirty(SaveOpenDocumentIfDirtyRequest) returns (SaveOpenDocumentIfDirtyResponse);
|
||||
// Get diagnostics from the workspace.
|
||||
rpc getDiagnostics(GetDiagnosticsRequest) returns (GetDiagnosticsResponse);
|
||||
}
|
||||
|
||||
message GetWorkspacePathsRequest {
|
||||
@@ -28,6 +32,46 @@ message GetWorkspacePathsResponse {
|
||||
}
|
||||
|
||||
message SaveOpenDocumentIfDirtyRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
string file_path = 2;
|
||||
optional string file_path = 2;
|
||||
}
|
||||
message SaveOpenDocumentIfDirtyResponse {
|
||||
// Returns true if the document was saved.
|
||||
optional bool was_saved = 1;
|
||||
}
|
||||
|
||||
message GetDiagnosticsRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
}
|
||||
|
||||
message GetDiagnosticsResponse {
|
||||
repeated FileDiagnostics file_diagnostics = 1;
|
||||
}
|
||||
|
||||
message FileDiagnostics {
|
||||
string file_path = 1;
|
||||
repeated Diagnostic diagnostics = 2;
|
||||
}
|
||||
|
||||
message Diagnostic {
|
||||
string message = 1;
|
||||
DiagnosticRange range = 2;
|
||||
DiagnosticSeverity severity = 3;
|
||||
optional string source = 4;
|
||||
}
|
||||
|
||||
message DiagnosticRange {
|
||||
DiagnosticPosition start = 1;
|
||||
DiagnosticPosition end = 2;
|
||||
}
|
||||
|
||||
message DiagnosticPosition {
|
||||
int32 line = 1;
|
||||
int32 character = 2;
|
||||
}
|
||||
|
||||
enum DiagnosticSeverity {
|
||||
DIAGNOSTIC_ERROR = 0;
|
||||
DIAGNOSTIC_WARNING = 1;
|
||||
DIAGNOSTIC_INFORMATION = 2;
|
||||
DIAGNOSTIC_HINT = 3;
|
||||
}
|
||||
|
||||
+55
-3
@@ -7,8 +7,10 @@ import { globby } from "globby"
|
||||
import { createRequire } from "module"
|
||||
import os from "os"
|
||||
import * as path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { rmrf } from "./file-utils.mjs"
|
||||
import { main as generateProtoBusSetup } from "./generate-protobus-setup.mjs"
|
||||
import { main as generateHostBridgeClient } from "./generate-host-bridge-client.mjs"
|
||||
import { loadProtoDescriptorSet } from "./proto-utils.mjs"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
|
||||
@@ -34,9 +36,14 @@ const TS_PROTO_OPTIONS = [
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.bold.blue("Compiling Protocol Buffers..."))
|
||||
|
||||
await cleanup()
|
||||
await compileProtos()
|
||||
await checkProtos()
|
||||
await generateProtoBusSetup()
|
||||
await generateHostBridgeClient()
|
||||
}
|
||||
async function compileProtos() {
|
||||
console.log(chalk.bold.blue("Compiling Protocol Buffers..."))
|
||||
|
||||
// Check for Apple Silicon compatibility before proceeding
|
||||
checkAppleSiliconCompatibility()
|
||||
@@ -180,6 +187,51 @@ function checkAppleSiliconCompatibility() {
|
||||
}
|
||||
}
|
||||
|
||||
const int64TypeNames = ["TYPE_INT64", "TYPE_UINT64", "TYPE_SINT64", "TYPE_FIXED64", "TYPE_SFIXED64"]
|
||||
|
||||
async function checkProtos() {
|
||||
const proto = await loadProtoDescriptorSet()
|
||||
const int64Fields = []
|
||||
|
||||
for (const [packageName, packageDef] of Object.entries(proto)) {
|
||||
for (const [messageName, def] of Object.entries(packageDef)) {
|
||||
// Skip service definitions
|
||||
if (def && typeof def === "object" && "service" in def) {
|
||||
continue
|
||||
}
|
||||
// Check message fields
|
||||
if (def && def.type && def.type.field) {
|
||||
for (const field of def.type.field) {
|
||||
if (int64TypeNames.includes(field.type)) {
|
||||
const name = `${packageName}.${messageName}.${field.name}`
|
||||
int64Fields.push({
|
||||
name: name,
|
||||
type: field.type,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (int64Fields.length > 0) {
|
||||
console.log(chalk.yellow(`\nWarning: Found ${int64Fields.length} fields using 64-bit integer types`))
|
||||
for (const field of int64Fields) {
|
||||
const typeNames = {
|
||||
TYPE_INT64: "int64",
|
||||
TYPE_UINT64: "uint64",
|
||||
TYPE_SINT64: "sint64",
|
||||
TYPE_FIXED64: "fixed64",
|
||||
TYPE_SFIXED64: "sfixed64",
|
||||
}
|
||||
log_verbose(chalk.yellow(` - ${field.name} (${typeNames[field.type]})`))
|
||||
}
|
||||
log_verbose(chalk.yellow("\nWARNING: 64-bit integer fields detected in proto definitions"))
|
||||
log_verbose(chalk.yellow("JavaScript cannot safely represent integers larger than 2^53-1 (Number.MAX_SAFE_INTEGER)."))
|
||||
log_verbose(chalk.yellow("Consider using string representation for large numbers or implementing BigInt support.\n"))
|
||||
}
|
||||
}
|
||||
|
||||
function log_verbose(s) {
|
||||
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
|
||||
console.log(s)
|
||||
|
||||
Regular → Executable
+1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
const { execSync } = require("child_process")
|
||||
const esbuild = require("esbuild")
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const VSCODE_CLIENT_FILE = path.resolve("src/generated/hosts/vscode/hostbridge-g
|
||||
/**
|
||||
* Main function to generate the host bridge client
|
||||
*/
|
||||
async function main() {
|
||||
export async function main() {
|
||||
const { hostServices } = await loadServicesFromProtoDescriptor()
|
||||
|
||||
await generateTypesFile(hostServices)
|
||||
@@ -234,8 +234,10 @@ const ${name}ServiceRegistry = createServiceRegistry("${name}")
|
||||
${methods}`
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red("Error:"), error)
|
||||
process.exit(1)
|
||||
})
|
||||
// Only run main if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red("Error:"), error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const STANDALONE_SERVER_SETUP_FILE = path.resolve("src/generated/hosts/standalon
|
||||
|
||||
const SCRIPT_NAME = path.relative(process.cwd(), fileURLToPath(import.meta.url))
|
||||
|
||||
async function main() {
|
||||
export async function main() {
|
||||
const { protobusServices } = await loadServicesFromProtoDescriptor()
|
||||
await generateWebviewProtobusClients(protobusServices)
|
||||
await generateVscodeServiceTypes(protobusServices)
|
||||
@@ -40,11 +40,11 @@ async function generateWebviewProtobusClients(protobusServices) {
|
||||
}
|
||||
if (!rpc.responseStream) {
|
||||
rpcs.push(` static async ${rpcName}(request: ${requestType}): Promise<${responseType}> {
|
||||
return this.makeRequest("${rpcName}", request)
|
||||
return this.makeUnaryRequest("${rpcName}", request, ${requestType}.toJSON, ${responseType}.fromJSON)
|
||||
}`)
|
||||
} else {
|
||||
rpcs.push(` static ${rpcName}(request: ${requestType}, callbacks: Callbacks<${responseType}>): ()=>void {
|
||||
return this.makeStreamingRequest("${rpcName}", request, callbacks)
|
||||
return this.makeStreamingRequest("${rpcName}", request, ${requestType}.toJSON, ${responseType}.fromJSON, callbacks)
|
||||
}`)
|
||||
}
|
||||
}
|
||||
@@ -205,4 +205,10 @@ function getDirName(serviceName) {
|
||||
return domain.charAt(0).toLowerCase() + domain.slice(1)
|
||||
}
|
||||
|
||||
main()
|
||||
// Only run main if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red("Error:"), error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
|
||||
const typeNameToFQN = new Map()
|
||||
|
||||
function addTypeNameToFqn(name, fqn) {
|
||||
if (typeNameToFQN.has(name)) {
|
||||
if (typeNameToFQN.has(name) && typeNameToFQN.get(name) !== fqn) {
|
||||
throw new Error(`Proto type ${name} redefined (${fqn}).`)
|
||||
}
|
||||
typeNameToFQN.set(name, fqn)
|
||||
@@ -23,11 +23,15 @@ export function getFqn(name) {
|
||||
return typeNameToFQN.get(name)
|
||||
}
|
||||
|
||||
export async function loadServicesFromProtoDescriptor() {
|
||||
// Load service definitions from descriptor set
|
||||
export async function loadProtoDescriptorSet() {
|
||||
const descriptorBuffer = await fs.readFile(DESCRIPTOR_SET)
|
||||
const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(descriptorBuffer)
|
||||
const proto = grpc.loadPackageDefinition(packageDefinition)
|
||||
return grpc.loadPackageDefinition(packageDefinition)
|
||||
}
|
||||
|
||||
export async function loadServicesFromProtoDescriptor() {
|
||||
// Load service definitions from descriptor set
|
||||
const proto = await loadProtoDescriptorSet()
|
||||
|
||||
// Extract host services and proto messages from the proto definition
|
||||
const hostServices = {}
|
||||
|
||||
Regular → Executable
+19
-1
@@ -29,8 +29,10 @@ import { SapAiCoreHandler } from "./providers/sapaicore"
|
||||
import { ClaudeCodeHandler } from "./providers/claude-code"
|
||||
import { MoonshotHandler } from "./providers/moonshot"
|
||||
import { GroqHandler } from "./providers/groq"
|
||||
import { Mode } from "../shared/ChatSettings"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { HuggingFaceHandler } from "./providers/huggingface"
|
||||
import { HuaweiCloudMaaSHandler } from "./providers/huawei-cloud-maas"
|
||||
import { BasetenHandler } from "./providers/baseten"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -111,6 +113,7 @@ function createHandlerForProvider(
|
||||
case "ollama":
|
||||
return new OllamaHandler({
|
||||
ollamaBaseUrl: options.ollamaBaseUrl,
|
||||
ollamaApiKey: options.ollamaApiKey,
|
||||
ollamaModelId: mode === "plan" ? options.planModeOllamaModelId : options.actModeOllamaModelId,
|
||||
ollamaApiOptionsCtxNum: options.ollamaApiOptionsCtxNum,
|
||||
requestTimeoutMs: options.requestTimeoutMs,
|
||||
@@ -256,6 +259,13 @@ function createHandlerForProvider(
|
||||
groqModelInfo: mode === "plan" ? options.planModeGroqModelInfo : options.actModeGroqModelInfo,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "baseten":
|
||||
return new BasetenHandler({
|
||||
basetenApiKey: options.basetenApiKey,
|
||||
basetenModelId: mode === "plan" ? options.planModeBasetenModelId : options.actModeBasetenModelId,
|
||||
basetenModelInfo: mode === "plan" ? options.planModeBasetenModelInfo : options.actModeBasetenModelInfo,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "sapaicore":
|
||||
return new SapAiCoreHandler({
|
||||
sapAiCoreClientId: options.sapAiCoreClientId,
|
||||
@@ -272,6 +282,14 @@ function createHandlerForProvider(
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "huawei-cloud-maas":
|
||||
return new HuaweiCloudMaaSHandler({
|
||||
huaweiCloudMaasApiKey: options.huaweiCloudMaasApiKey,
|
||||
huaweiCloudMaasModelId:
|
||||
mode === "plan" ? options.planModeHuaweiCloudMaasModelId : options.actModeHuaweiCloudMaasModelId,
|
||||
huaweiCloudMaasModelInfo:
|
||||
mode === "plan" ? options.planModeHuaweiCloudMaasModelInfo : options.actModeHuaweiCloudMaasModelInfo,
|
||||
})
|
||||
default:
|
||||
return new AnthropicHandler({
|
||||
apiKey: options.apiKey,
|
||||
|
||||
@@ -612,101 +612,102 @@ describe("AwsBedrockHandler", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("getModelId", () => {
|
||||
it("should return raw model ID for custom models", async () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId:
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
}
|
||||
const customHandler = new AwsBedrockHandler(customOptions)
|
||||
// TODO: Re-enable or remove these tests.
|
||||
// describe("getModelId", () => {
|
||||
// it("should return raw model ID for custom models", async () => {
|
||||
// const customOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId:
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// }
|
||||
// const customHandler = new AwsBedrockHandler(customOptions)
|
||||
|
||||
const modelId = await customHandler.getModelId()
|
||||
modelId.should.equal(
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
)
|
||||
})
|
||||
// const modelId = await customHandler.getModelId()
|
||||
// modelId.should.equal(
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// )
|
||||
// })
|
||||
|
||||
it("should not encode custom model IDs with slashes", async () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId: "my-namespace/my-custom-model",
|
||||
}
|
||||
const customHandler = new AwsBedrockHandler(customOptions)
|
||||
// it("should not encode custom model IDs with slashes", async () => {
|
||||
// const customOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId: "my-namespace/my-custom-model",
|
||||
// }
|
||||
// const customHandler = new AwsBedrockHandler(customOptions)
|
||||
|
||||
const modelId = await customHandler.getModelId()
|
||||
modelId.should.equal("my-namespace/my-custom-model")
|
||||
modelId.should.not.match(/%2F/)
|
||||
})
|
||||
// const modelId = await customHandler.getModelId()
|
||||
// modelId.should.equal("my-namespace/my-custom-model")
|
||||
// modelId.should.not.match(/%2F/)
|
||||
// })
|
||||
|
||||
it("should apply cross-region prefix for non-custom models when enabled", async () => {
|
||||
const crossRegionOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "us-west-2",
|
||||
}
|
||||
const crossRegionHandler = new AwsBedrockHandler(crossRegionOptions)
|
||||
// it("should apply cross-region prefix for non-custom models when enabled", async () => {
|
||||
// const crossRegionOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// awsUseCrossRegionInference: true,
|
||||
// awsRegion: "us-west-2",
|
||||
// }
|
||||
// const crossRegionHandler = new AwsBedrockHandler(crossRegionOptions)
|
||||
|
||||
const modelId = await crossRegionHandler.getModelId()
|
||||
modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
// const modelId = await crossRegionHandler.getModelId()
|
||||
// modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
// })
|
||||
|
||||
it("should apply EU cross-region prefix", async () => {
|
||||
const euOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "eu-central-1",
|
||||
}
|
||||
const euHandler = new AwsBedrockHandler(euOptions)
|
||||
// it("should apply EU cross-region prefix", async () => {
|
||||
// const euOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// awsUseCrossRegionInference: true,
|
||||
// awsRegion: "eu-central-1",
|
||||
// }
|
||||
// const euHandler = new AwsBedrockHandler(euOptions)
|
||||
|
||||
const modelId = await euHandler.getModelId()
|
||||
modelId.should.equal("eu.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
// const modelId = await euHandler.getModelId()
|
||||
// modelId.should.equal("eu.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
// })
|
||||
|
||||
it("should apply APAC cross-region prefix", async () => {
|
||||
const apacOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "ap-northeast-1",
|
||||
}
|
||||
const apacHandler = new AwsBedrockHandler(apacOptions)
|
||||
// it("should apply APAC cross-region prefix", async () => {
|
||||
// const apacOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// awsUseCrossRegionInference: true,
|
||||
// awsRegion: "ap-northeast-1",
|
||||
// }
|
||||
// const apacHandler = new AwsBedrockHandler(apacOptions)
|
||||
|
||||
const modelId = await apacHandler.getModelId()
|
||||
modelId.should.equal("apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
// const modelId = await apacHandler.getModelId()
|
||||
// modelId.should.equal("apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
// })
|
||||
|
||||
it("should not apply cross-region prefix for custom models even when enabled", async () => {
|
||||
const customCrossRegionOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
|
||||
awsUseCrossRegionInference: true,
|
||||
}
|
||||
const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
|
||||
// it("should not apply cross-region prefix for custom models even when enabled", async () => {
|
||||
// const customCrossRegionOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
|
||||
// awsUseCrossRegionInference: true,
|
||||
// }
|
||||
// const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
|
||||
|
||||
const modelId = await customCrossRegionHandler.getModelId()
|
||||
modelId.should.equal("arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model")
|
||||
})
|
||||
// const modelId = await customCrossRegionHandler.getModelId()
|
||||
// modelId.should.equal("arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model")
|
||||
// })
|
||||
|
||||
it("should handle UltraThink model ARN correctly", async () => {
|
||||
const ultraThinkOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId:
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
}
|
||||
const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
|
||||
// it("should handle UltraThink model ARN correctly", async () => {
|
||||
// const ultraThinkOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId:
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
// }
|
||||
// const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
|
||||
|
||||
const modelId = await ultraThinkHandler.getModelId()
|
||||
// Should return the raw ARN without any encoding
|
||||
modelId.should.equal(
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
)
|
||||
modelId.should.not.match(/%2F/)
|
||||
modelId.should.not.match(/%3A/)
|
||||
})
|
||||
})
|
||||
// const modelId = await ultraThinkHandler.getModelId()
|
||||
// // Should return the raw ARN without any encoding
|
||||
// modelId.should.equal(
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// )
|
||||
// modelId.should.not.match(/%2F/)
|
||||
// modelId.should.not.match(/%3A/)
|
||||
// })
|
||||
// })
|
||||
})
|
||||
|
||||
@@ -55,6 +55,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
case "claude-3-5-sonnet-20241022":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
case "claude-opus-4-20250514":
|
||||
case "claude-opus-4-1-20250805":
|
||||
case "claude-3-opus-20240229":
|
||||
case "claude-3-haiku-20240307": {
|
||||
/*
|
||||
@@ -122,6 +123,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
switch (modelId) {
|
||||
case "claude-sonnet-4-20250514":
|
||||
case "claude-opus-4-20250514":
|
||||
case "claude-opus-4-1-20250805":
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
case "claude-3-5-sonnet-20241022":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { BasetenModelId, ModelInfo, basetenDefaultModelId, basetenModels } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "../../utils/cost"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface BasetenHandlerOptions {
|
||||
basetenApiKey?: string
|
||||
basetenModelId?: string
|
||||
basetenModelInfo?: ModelInfo
|
||||
apiModelId?: string // For backward compatibility
|
||||
}
|
||||
|
||||
export class BasetenHandler implements ApiHandler {
|
||||
private options: BasetenHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: BasetenHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.basetenApiKey) {
|
||||
throw new Error("Baseten API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://inference.baseten.co/v1",
|
||||
apiKey: this.options.basetenApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Baseten client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the optimal max_tokens based on model capabilities
|
||||
*/
|
||||
private getOptimalMaxTokens(model: { id: BasetenModelId; info: ModelInfo }): number {
|
||||
// Use model-specific max tokens if available
|
||||
if (model.info.maxTokens && model.info.maxTokens > 0) {
|
||||
return model.info.maxTokens
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 8192
|
||||
}
|
||||
|
||||
getModel(): { id: BasetenModelId; info: ModelInfo } {
|
||||
// First priority: basetenModelId and basetenModelInfo
|
||||
const basetenModelId = this.options.basetenModelId
|
||||
const basetenModelInfo = this.options.basetenModelInfo
|
||||
if (basetenModelId && basetenModelInfo) {
|
||||
return { id: basetenModelId as BasetenModelId, info: basetenModelInfo }
|
||||
}
|
||||
|
||||
// Second priority: basetenModelId with static model info
|
||||
if (basetenModelId && basetenModelId in basetenModels) {
|
||||
const id = basetenModelId as BasetenModelId
|
||||
return { id, info: basetenModels[id] }
|
||||
}
|
||||
|
||||
// Third priority: apiModelId (for backward compatibility)
|
||||
const apiModelId = this.options.apiModelId
|
||||
if (apiModelId && apiModelId in basetenModels) {
|
||||
const id = apiModelId as BasetenModelId
|
||||
return { id, info: basetenModels[id] }
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return {
|
||||
id: basetenDefaultModelId,
|
||||
info: basetenModels[basetenDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
private async *yieldUsage(modelInfo: ModelInfo, usage: any): ApiStream {
|
||||
if (usage.prompt_tokens || usage.completion_tokens) {
|
||||
const cost = calculateApiCostOpenAI(modelInfo, usage.prompt_tokens || 0, usage.completion_tokens || 0)
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.prompt_tokens || 0,
|
||||
outputTokens: usage.completion_tokens || 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost: cost,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
const maxTokens = this.getOptimalMaxTokens(model)
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_tokens: maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
let didOutputUsage = false
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
// Handle reasoning field if present (for reasoning models with parsed output)
|
||||
if ((delta as any)?.reasoning) {
|
||||
const reasoningContent = (delta as any).reasoning as string
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: reasoningContent,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle content field
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle usage information - only output once
|
||||
if (!didOutputUsage && chunk.usage) {
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
didOutputUsage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current model supports vision/images
|
||||
*/
|
||||
supportsImages(): boolean {
|
||||
const model = this.getModel()
|
||||
return model.info.supportsImages === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current model supports tools
|
||||
*/
|
||||
supportsTools(): boolean {
|
||||
const model = this.getModel()
|
||||
// Baseten models support tools via OpenAI-compatible API
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,7 @@ export class CerebrasHandler implements ApiHandler {
|
||||
messages: cerebrasMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
max_tokens: this.getModel().info.maxTokens,
|
||||
})
|
||||
|
||||
// Handle streaming response
|
||||
@@ -175,9 +176,15 @@ export class CerebrasHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in cerebrasModels) {
|
||||
const id = modelId as CerebrasModelId
|
||||
const originalModelId = this.options.apiModelId
|
||||
let apiModelId = originalModelId
|
||||
if (originalModelId === "qwen-3-coder-480b-free") {
|
||||
apiModelId = "qwen-3-coder-480b"
|
||||
return { id: apiModelId, info: cerebrasModels[originalModelId as CerebrasModelId] }
|
||||
}
|
||||
|
||||
if (originalModelId && originalModelId in cerebrasModels) {
|
||||
const id = originalModelId as CerebrasModelId
|
||||
return { id, info: cerebrasModels[id] }
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -72,6 +72,10 @@ export class ClineHandler implements ApiHandler {
|
||||
this.lastGenerationId = undefined
|
||||
|
||||
let didOutputUsage: boolean = false
|
||||
let sawText = false
|
||||
let sawReasoning = false
|
||||
let sawAnyChunk = false
|
||||
let lastFinishReason: string | undefined
|
||||
|
||||
const stream = await createOpenRouterStream(
|
||||
client,
|
||||
@@ -98,6 +102,9 @@ export class ClineHandler implements ApiHandler {
|
||||
|
||||
// Check for mid-stream error via finish_reason
|
||||
const choice = chunk.choices?.[0]
|
||||
if (choice?.finish_reason) {
|
||||
lastFinishReason = String(choice.finish_reason)
|
||||
}
|
||||
// OpenRouter may return finish_reason = "error" with error details
|
||||
if ((choice?.finish_reason as string) === "error") {
|
||||
const choiceWithError = choice as any
|
||||
@@ -112,8 +119,11 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
sawAnyChunk = true
|
||||
|
||||
const delta = choice?.delta
|
||||
if (delta?.content) {
|
||||
sawText = true
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
@@ -123,6 +133,7 @@ export class ClineHandler implements ApiHandler {
|
||||
// Reasoning tokens are returned separately from the content
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
sawReasoning = true
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
@@ -167,6 +178,18 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// If stream ended without assistant text but we observed other chunks, emit a minimal synthetic text to avoid empty assistant responses.
|
||||
if (!sawText && (sawReasoning || didOutputUsage || sawAnyChunk)) {
|
||||
const note =
|
||||
lastFinishReason && /content_filter|blocked|safety/i.test(lastFinishReason)
|
||||
? "[Provider redacted final text due to safety filtering. No assistant text was returned.]"
|
||||
: "[Provider ended stream without final assistant text. Proceeding with available information.]"
|
||||
yield {
|
||||
type: "text",
|
||||
text: note,
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to generation endpoint if usage chunk not returned
|
||||
if (!didOutputUsage) {
|
||||
console.warn("Cline API did not return usage chunk, fetching from generation endpoint")
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Mock for @google/genai module to avoid ESM compatibility issues in tests
|
||||
|
||||
export class GoogleGenAI {
|
||||
constructor(options: any) {
|
||||
// Mock constructor
|
||||
}
|
||||
|
||||
models = {
|
||||
generateContentStream: async (params: any) => {
|
||||
// Mock implementation that returns an async iterator
|
||||
return {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield {
|
||||
text: "Mock response",
|
||||
candidates: [],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 100,
|
||||
candidatesTokenCount: 50,
|
||||
thoughtsTokenCount: 0,
|
||||
cachedContentTokenCount: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
countTokens: async (params: any) => {
|
||||
// Mock token counting
|
||||
return {
|
||||
totalTokens: 100,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Export mock types
|
||||
export interface GenerateContentConfig {
|
||||
httpOptions?: any
|
||||
systemInstruction?: string
|
||||
temperature?: number
|
||||
thinkingConfig?: any
|
||||
}
|
||||
|
||||
export interface GenerateContentResponseUsageMetadata {
|
||||
promptTokenCount?: number
|
||||
candidatesTokenCount?: number
|
||||
thoughtsTokenCount?: number
|
||||
cachedContentTokenCount?: number
|
||||
}
|
||||
|
||||
export interface Part {
|
||||
thought?: boolean
|
||||
text?: string
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
|
||||
import { telemetryService } from "@services/posthog/PostHogClientProvider"
|
||||
|
||||
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
|
||||
const DEFAULT_CACHE_TTL_SECONDS = 900
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { ApiHandler } from ".."
|
||||
import { huaweiCloudMaasDefaultModelId, HuaweiCloudMaasModelId, huaweiCloudMaasModels, ModelInfo } from "@shared/api"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
interface HuaweiCloudMaaSHandlerOptions {
|
||||
huaweiCloudMaasApiKey?: string
|
||||
huaweiCloudMaasModelId?: string
|
||||
huaweiCloudMaasModelInfo?: ModelInfo
|
||||
}
|
||||
|
||||
export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
private options: HuaweiCloudMaaSHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
constructor(options: HuaweiCloudMaaSHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.huaweiCloudMaasApiKey) {
|
||||
throw new Error("Huawei Cloud MaaS API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.modelarts-maas.com/v1/",
|
||||
apiKey: this.options.huaweiCloudMaasApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Huawei Cloud MaaS client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
getModel(): { id: HuaweiCloudMaasModelId; info: ModelInfo } {
|
||||
// First priority: huaweiCloudMaasModelId and huaweiCloudMaasModelInfo (like Groq does)
|
||||
const huaweiCloudMaasModelId = this.options.huaweiCloudMaasModelId
|
||||
const huaweiCloudMaasModelInfo = this.options.huaweiCloudMaasModelInfo
|
||||
if (huaweiCloudMaasModelId && huaweiCloudMaasModelInfo) {
|
||||
return { id: huaweiCloudMaasModelId as HuaweiCloudMaasModelId, info: huaweiCloudMaasModelInfo }
|
||||
}
|
||||
|
||||
// Second priority: huaweiCloudMaasModelId with static model info
|
||||
if (huaweiCloudMaasModelId && huaweiCloudMaasModelId in huaweiCloudMaasModels) {
|
||||
const id = huaweiCloudMaasModelId as HuaweiCloudMaasModelId
|
||||
return { id, info: huaweiCloudMaasModels[id] }
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return {
|
||||
id: huaweiCloudMaasDefaultModelId,
|
||||
info: huaweiCloudMaasModels[huaweiCloudMaasDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
let reasoning: string | null = null
|
||||
let didOutputUsage: boolean = false
|
||||
let finalUsage: any = null
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
// Handle reasoning content detection
|
||||
if (delta?.content) {
|
||||
if (reasoning || delta.content.includes("<think>")) {
|
||||
reasoning = (reasoning || "") + delta.content
|
||||
} else if (!reasoning) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle reasoning output
|
||||
if (reasoning || (delta && "reasoning_content" in delta && delta.reasoning_content)) {
|
||||
const reasoningContent = delta?.content || ((delta as any)?.reasoning_content as string | undefined) || ""
|
||||
if (reasoningContent.trim()) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: reasoningContent,
|
||||
}
|
||||
}
|
||||
|
||||
// Check if reasoning is complete
|
||||
if (reasoning?.includes("</think>")) {
|
||||
reasoning = null
|
||||
}
|
||||
}
|
||||
|
||||
// Store usage information for later output
|
||||
if (chunk.usage) {
|
||||
finalUsage = chunk.usage
|
||||
}
|
||||
|
||||
// Output usage when stream is finished
|
||||
if (!didOutputUsage && chunk.choices?.[0]?.finish_reason) {
|
||||
if (finalUsage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: finalUsage.prompt_tokens || 0,
|
||||
outputTokens: finalUsage.completion_tokens || 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
}
|
||||
}
|
||||
didOutputUsage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
-26
@@ -16,9 +16,29 @@ interface LiteLlmHandlerOptions {
|
||||
taskId?: string
|
||||
}
|
||||
|
||||
interface LiteLlmModelInfoResponse {
|
||||
data: Array<{
|
||||
model_name: string
|
||||
litellm_params: {
|
||||
model: string
|
||||
[key: string]: any
|
||||
}
|
||||
model_info: {
|
||||
input_cost_per_token: number
|
||||
output_cost_per_token: number
|
||||
cache_creation_input_token_cost?: number
|
||||
cache_read_input_token_cost?: number
|
||||
[key: string]: any
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export class LiteLlmHandler implements ApiHandler {
|
||||
private options: LiteLlmHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
private modelInfoCache: LiteLlmModelInfoResponse | undefined
|
||||
private modelInfoCacheTimestamp: number = 0
|
||||
private readonly modelInfoCacheTTL = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
constructor(options: LiteLlmHandlerOptions) {
|
||||
this.options = options
|
||||
@@ -41,35 +61,112 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
return this.client
|
||||
}
|
||||
|
||||
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
|
||||
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
|
||||
private async fetchModelInfo(): Promise<LiteLlmModelInfoResponse | undefined> {
|
||||
// Check if cache is still valid
|
||||
const now = Date.now()
|
||||
if (this.modelInfoCache && now - this.modelInfoCacheTimestamp < this.modelInfoCacheTTL) {
|
||||
return this.modelInfoCache
|
||||
}
|
||||
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
|
||||
// Handle base URLs that already include /v1 to avoid double /v1/v1/
|
||||
const baseUrl = client.baseURL.endsWith("/v1") ? client.baseURL : `${client.baseURL}/v1`
|
||||
const url = `${baseUrl}/model/info`
|
||||
|
||||
try {
|
||||
const response = await fetch(`${client.baseURL}/spend/calculate`, {
|
||||
method: "POST",
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.options.liteLlmApiKey}`,
|
||||
accept: "application/json",
|
||||
"x-litellm-api-key": this.options.liteLlmApiKey || "",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
completion_response: {
|
||||
model: modelId,
|
||||
usage: {
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data: { cost: number } = await response.json()
|
||||
return data.cost
|
||||
const data: LiteLlmModelInfoResponse = await response.json()
|
||||
this.modelInfoCache = data
|
||||
this.modelInfoCacheTimestamp = now
|
||||
return data
|
||||
} else {
|
||||
console.error("Error calculating spend:", response.statusText)
|
||||
return undefined
|
||||
console.warn("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
// Try with Authorization header instead
|
||||
const retryResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
Authorization: `Bearer ${this.options.liteLlmApiKey || ""}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (retryResponse.ok) {
|
||||
const data: LiteLlmModelInfoResponse = await retryResponse.json()
|
||||
this.modelInfoCache = data
|
||||
this.modelInfoCacheTimestamp = now
|
||||
return data
|
||||
} else {
|
||||
console.warn("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error fetching LiteLLM model info:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
private async getModelCostInfo(publicModelName: string): Promise<{
|
||||
inputCostPerToken: number
|
||||
outputCostPerToken: number
|
||||
cacheCreationCostPerToken?: number
|
||||
cacheReadCostPerToken?: number
|
||||
}> {
|
||||
try {
|
||||
const modelInfo = await this.fetchModelInfo()
|
||||
|
||||
if (modelInfo?.data) {
|
||||
// Find the model by public name
|
||||
const matchingModel = modelInfo.data.find((model) => model.model_name === publicModelName)
|
||||
|
||||
if (matchingModel?.model_info) {
|
||||
return {
|
||||
inputCostPerToken: matchingModel.model_info.input_cost_per_token || 0,
|
||||
outputCostPerToken: matchingModel.model_info.output_cost_per_token || 0,
|
||||
cacheCreationCostPerToken: matchingModel.model_info.cache_creation_input_token_cost,
|
||||
cacheReadCostPerToken: matchingModel.model_info.cache_read_input_token_cost,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error getting LiteLLM model cost info:", error)
|
||||
}
|
||||
|
||||
// Fallback to zero costs if we can't get the information
|
||||
return {
|
||||
inputCostPerToken: 0,
|
||||
outputCostPerToken: 0,
|
||||
}
|
||||
}
|
||||
|
||||
async calculateCost(
|
||||
prompt_tokens: number,
|
||||
completion_tokens: number,
|
||||
cache_creation_tokens?: number,
|
||||
cache_read_tokens?: number,
|
||||
): Promise<number | undefined> {
|
||||
const publicModelId = this.options.liteLlmModelId || liteLlmDefaultModelId
|
||||
|
||||
try {
|
||||
const costInfo = await this.getModelCostInfo(publicModelId)
|
||||
|
||||
// Calculate costs for different token types
|
||||
const inputCost = Math.max(0, prompt_tokens - (cache_read_tokens || 0)) * costInfo.inputCostPerToken
|
||||
const outputCost = completion_tokens * costInfo.outputCostPerToken
|
||||
const cacheCreationCost = (cache_creation_tokens || 0) * (costInfo.cacheCreationCostPerToken || 0)
|
||||
const cacheReadCost = (cache_read_tokens || 0) * (costInfo.cacheReadCostPerToken || 0)
|
||||
|
||||
const totalCost = inputCost + outputCost + cacheCreationCost + cacheReadCost
|
||||
|
||||
return totalCost
|
||||
} catch (error) {
|
||||
console.error("Error calculating spend:", error)
|
||||
return undefined
|
||||
@@ -136,9 +233,6 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
...(this.options.taskId && { litellm_session_id: `cline-${this.options.taskId}` }), // Add session ID for LiteLLM tracking
|
||||
})
|
||||
|
||||
const inputCost = (await this.calculateCost(1e6, 0)) || 0
|
||||
const outputCost = (await this.calculateCost(0, 1e6)) || 0
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
@@ -165,9 +259,6 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
|
||||
// Handle token usage information
|
||||
if (chunk.usage) {
|
||||
const totalCost =
|
||||
(inputCost * chunk.usage.prompt_tokens) / 1e6 + (outputCost * chunk.usage.completion_tokens) / 1e6
|
||||
|
||||
// Extract cache-related information if available
|
||||
// Need to use type assertion since these properties are not in the standard OpenAI types
|
||||
const usage = chunk.usage as {
|
||||
@@ -182,6 +273,15 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
const cacheWriteTokens = usage.cache_creation_input_tokens || usage.prompt_cache_miss_tokens || 0
|
||||
const cacheReadTokens = usage.cache_read_input_tokens || usage.prompt_cache_hit_tokens || 0
|
||||
|
||||
// Calculate cost using the actual token usage including cache tokens
|
||||
const totalCost =
|
||||
(await this.calculateCost(
|
||||
usage.prompt_tokens || 0,
|
||||
usage.completion_tokens || 0,
|
||||
cacheWriteTokens > 0 ? cacheWriteTokens : undefined,
|
||||
cacheReadTokens > 0 ? cacheReadTokens : undefined,
|
||||
)) || 0
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.prompt_tokens || 0,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Message, Ollama } from "ollama"
|
||||
import { Message, Ollama, Config } from "ollama"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
|
||||
import { convertToOllamaMessages } from "../transform/ollama-format"
|
||||
@@ -8,6 +8,7 @@ import { withRetry } from "../retry"
|
||||
|
||||
interface OllamaHandlerOptions {
|
||||
ollamaBaseUrl?: string
|
||||
ollamaApiKey?: string
|
||||
ollamaModelId?: string
|
||||
ollamaApiOptionsCtxNum?: string
|
||||
requestTimeoutMs?: number
|
||||
@@ -24,7 +25,18 @@ export class OllamaHandler implements ApiHandler {
|
||||
private ensureClient(): Ollama {
|
||||
if (!this.client) {
|
||||
try {
|
||||
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
|
||||
const clientOptions: Partial<Config> = {
|
||||
host: this.options.ollamaBaseUrl || "http://localhost:11434",
|
||||
}
|
||||
|
||||
// Add API key if provided (for Ollama cloud or authenticated instances)
|
||||
if (this.options.ollamaApiKey) {
|
||||
clientOptions.headers = {
|
||||
Authorization: `Bearer ${this.options.ollamaApiKey}`,
|
||||
}
|
||||
}
|
||||
|
||||
this.client = new Ollama(clientOptions)
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Ollama client: ${error.message}`)
|
||||
}
|
||||
|
||||
@@ -104,6 +104,33 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "nectarine-alpha-new-reasoning-effort-2025-07-25":
|
||||
case "gpt-5-2025-08-07":
|
||||
case "gpt-5-mini-2025-08-07":
|
||||
case "gpt-5-nano-2025-08-07":
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
temperature: 1,
|
||||
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
reasoning_effort: (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium",
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
if (chunk.usage) {
|
||||
// Only last chunk contains usage
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
}
|
||||
break
|
||||
default: {
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
|
||||
@@ -74,6 +74,10 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
)
|
||||
|
||||
let didOutputUsage: boolean = false
|
||||
let sawText = false
|
||||
let sawReasoning = false
|
||||
let sawAnyChunk = false
|
||||
let lastFinishReason: string | undefined
|
||||
|
||||
for await (const chunk of stream) {
|
||||
// openrouter returns an error object instead of the openai sdk throwing an error
|
||||
@@ -89,6 +93,9 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
// Check for error in choices[0].finish_reason
|
||||
// OpenRouter may return errors in a non-standard way within choices
|
||||
const choice = chunk.choices?.[0]
|
||||
if (choice?.finish_reason) {
|
||||
lastFinishReason = String(choice.finish_reason)
|
||||
}
|
||||
// Use type assertion since OpenRouter uses non-standard "error" finish_reason
|
||||
if ((choice?.finish_reason as string) === "error") {
|
||||
// Use type assertion since OpenRouter adds non-standard error property
|
||||
@@ -113,8 +120,11 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
this.lastGenerationId = chunk.id
|
||||
}
|
||||
|
||||
sawAnyChunk = true
|
||||
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
sawText = true
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
@@ -124,6 +134,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
// Reasoning tokens are returned separately from the content
|
||||
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
sawReasoning = true
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
@@ -158,6 +169,18 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// If stream ended without assistant text but we observed other chunks, emit a minimal synthetic text to avoid empty assistant responses.
|
||||
if (!sawText && (sawReasoning || didOutputUsage || sawAnyChunk)) {
|
||||
const note =
|
||||
lastFinishReason && /content_filter|blocked|safety/i.test(lastFinishReason)
|
||||
? "[Provider redacted final text due to safety filtering. No assistant text was returned.]"
|
||||
: "[Provider ended stream without final assistant text. Proceeding with available information.]"
|
||||
yield {
|
||||
type: "text",
|
||||
text: note,
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to generation endpoint if usage chunk not returned
|
||||
if (!didOutputUsage) {
|
||||
const apiStreamUsage = await this.getApiStreamUsage()
|
||||
|
||||
@@ -74,7 +74,10 @@ export class RequestyHandler implements ApiHandler {
|
||||
? { thinking: { type: "enabled", budget_tokens: thinkingBudget } }
|
||||
: { thinking: { type: "disabled" } }
|
||||
const thinkingArgs =
|
||||
model.id.includes("claude-3-7-sonnet") || model.id.includes("claude-sonnet-4") || model.id.includes("claude-opus-4")
|
||||
model.id.includes("claude-3-7-sonnet") ||
|
||||
model.id.includes("claude-sonnet-4") ||
|
||||
model.id.includes("claude-opus-4") ||
|
||||
model.id.includes("claude-opus-4-1")
|
||||
? thinking
|
||||
: {}
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ export class VertexHandler implements ApiHandler {
|
||||
|
||||
switch (modelId) {
|
||||
case "claude-sonnet-4@20250514":
|
||||
case "claude-opus-4-1@20250805":
|
||||
case "claude-opus-4@20250514":
|
||||
case "claude-3-7-sonnet@20250219":
|
||||
case "claude-3-5-sonnet-v2@20241022":
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import * as vscode from "vscode"
|
||||
import { ApiHandler, SingleCompletionHandler } from "../"
|
||||
import { calculateApiCostAnthropic } from "@utils/cost"
|
||||
import { ApiStream } from "@api/transform/stream"
|
||||
import { convertToVsCodeLmMessages } from "@api/transform/vscode-lm-format"
|
||||
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
|
||||
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
|
||||
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
|
||||
import { calculateApiCostAnthropic } from "@utils/cost"
|
||||
import * as vscode from "vscode"
|
||||
import { ApiHandler, SingleCompletionHandler } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
|
||||
|
||||
interface VsCodeLmHandlerOptions {
|
||||
vsCodeLmModelSelector?: any
|
||||
@@ -237,82 +237,40 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise<number> {
|
||||
// Check for required dependencies
|
||||
if (!this.client) {
|
||||
console.warn("Cline <Language Model API>: No client available for token counting")
|
||||
return 0
|
||||
}
|
||||
|
||||
if (!this.currentRequestCancellation) {
|
||||
console.warn("Cline <Language Model API>: No cancellation token available for token counting")
|
||||
return 0
|
||||
}
|
||||
|
||||
// Validate input
|
||||
if (!text) {
|
||||
console.debug("Cline <Language Model API>: Empty text provided for token counting")
|
||||
return 0
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle different input types
|
||||
let tokenCount: number
|
||||
|
||||
if (typeof text === "string") {
|
||||
tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
|
||||
} else if (text instanceof vscode.LanguageModelChatMessage) {
|
||||
// For chat messages, ensure we have content
|
||||
if (!text.content || (Array.isArray(text.content) && text.content.length === 0)) {
|
||||
console.debug("Cline <Language Model API>: Empty chat message content")
|
||||
return 0
|
||||
}
|
||||
tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
|
||||
} else {
|
||||
console.warn("Cline <Language Model API>: Invalid input type for token counting")
|
||||
return 0
|
||||
}
|
||||
|
||||
// Validate the result
|
||||
if (typeof tokenCount !== "number") {
|
||||
console.warn("Cline <Language Model API>: Non-numeric token count received:", tokenCount)
|
||||
return 0
|
||||
}
|
||||
|
||||
if (tokenCount < 0) {
|
||||
console.warn("Cline <Language Model API>: Negative token count received:", tokenCount)
|
||||
return 0
|
||||
}
|
||||
|
||||
return tokenCount
|
||||
} catch (error) {
|
||||
// Handle specific error types
|
||||
if (error instanceof vscode.CancellationError) {
|
||||
console.debug("Cline <Language Model API>: Token counting cancelled by user")
|
||||
return 0
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.warn("Cline <Language Model API>: Token counting failed:", errorMessage)
|
||||
|
||||
// Log additional error details if available
|
||||
if (error instanceof Error && error.stack) {
|
||||
console.debug("Token counting error stack:", error.stack)
|
||||
}
|
||||
|
||||
return 0 // Fallback to prevent stream interruption
|
||||
private extractTextFromMessage(message: vscode.LanguageModelChatMessage): string {
|
||||
if (Array.isArray(message.content)) {
|
||||
return message.content
|
||||
.filter((part) => part instanceof vscode.LanguageModelTextPart)
|
||||
.map((part) => (part as vscode.LanguageModelTextPart).value)
|
||||
.join("")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private async calculateTotalInputTokens(
|
||||
systemPrompt: string,
|
||||
vsCodeLmMessages: vscode.LanguageModelChatMessage[],
|
||||
): Promise<number> {
|
||||
const systemTokens: number = await this.countTokens(systemPrompt)
|
||||
private isClaudeModel(): boolean {
|
||||
return this.client?.family?.startsWith("claude") || false
|
||||
}
|
||||
|
||||
private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise<number> {
|
||||
/**
|
||||
* NOTE (intentional trade-off):
|
||||
* We use a coarse chars/4 heuristic here instead of a real tokenizer (e.g., js-tiktoken with o200k_base).
|
||||
* Rationale:
|
||||
* - Avoid pulling multi‑MB rank files and increasing the extension install/download size.
|
||||
* - Eliminate encoder lifecycle/memory concerns in long-running sessions.
|
||||
* Consequences:
|
||||
* - This is not model-accurate and can under/over-estimate tokens, especially with tool/function calls.
|
||||
* - It is “good enough” for budgeting/context checks, and we accept the inaccuracy by design.
|
||||
* If precise accounting becomes a requirement, reintroduce a tokenizer behind a feature flag or backend-only path.
|
||||
*/
|
||||
const textContent = typeof text === "string" ? text : this.extractTextFromMessage(text)
|
||||
return Math.ceil((textContent || "").length / 4)
|
||||
}
|
||||
|
||||
private async calculateTotalInputTokens(vsCodeLmMessages: vscode.LanguageModelChatMessage[]): Promise<number> {
|
||||
const messageTokens: number[] = await Promise.all(vsCodeLmMessages.map((msg) => this.countTokens(msg)))
|
||||
|
||||
return systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)
|
||||
return messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)
|
||||
}
|
||||
|
||||
private ensureCleanState(): void {
|
||||
@@ -434,7 +392,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
this.currentRequestCancellation = new vscode.CancellationTokenSource()
|
||||
|
||||
// Calculate input tokens before starting the stream
|
||||
const totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)
|
||||
const totalInputTokens: number = await this.calculateTotalInputTokens(vsCodeLmMessages)
|
||||
|
||||
// Accumulate the text and count at the end of the stream to reduce token counting overhead.
|
||||
let accumulatedText: string = ""
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function createOpenRouterStream(
|
||||
// handles direct model.id match logic
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
@@ -82,6 +83,7 @@ export async function createOpenRouterStream(
|
||||
let maxTokens: number | undefined
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
@@ -117,6 +119,7 @@ export async function createOpenRouterStream(
|
||||
let reasoning: { max_tokens: number } | undefined = undefined
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
@@ -154,6 +157,8 @@ export async function createOpenRouterStream(
|
||||
stream_options: { include_usage: true },
|
||||
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
|
||||
include_reasoning: true,
|
||||
// Force text-formatted output to reduce cases where providers emit no final content
|
||||
response_format: { type: "text" },
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type AssistantMessageContent = TextContent | ToolUse
|
||||
|
||||
export { parseAssistantMessageV1, parseAssistantMessageV2, parseAssistantMessageV3 } from "./parse-assistant-message"
|
||||
export { parseAssistantMessageV2, parseAssistantMessageV3 } from "./parse-assistant-message"
|
||||
|
||||
export interface TextContent {
|
||||
type: "text"
|
||||
@@ -60,6 +60,7 @@ export const toolParamNames = [
|
||||
"steps_to_reproduce",
|
||||
"api_request_output",
|
||||
"additional_context",
|
||||
"needs_more_exploration",
|
||||
] as const
|
||||
|
||||
export type ToolParamName = (typeof toolParamNames)[number]
|
||||
|
||||
@@ -1,245 +1,6 @@
|
||||
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "." // Assuming types are defined in index.ts or a similar file
|
||||
|
||||
/**
|
||||
* @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
|
||||
let currentToolUse: ToolUse | undefined = undefined
|
||||
let currentToolUseStartIndex = 0
|
||||
let currentParamName: ToolParamName | undefined = undefined
|
||||
let currentParamValueStartIndex = 0
|
||||
let accumulator = ""
|
||||
|
||||
for (let i = 0; i < assistantMessage.length; i++) {
|
||||
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 found
|
||||
currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim()
|
||||
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 // 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 found
|
||||
currentToolUse.partial = false
|
||||
contentBlocks.push(currentToolUse)
|
||||
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 found
|
||||
currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
|
||||
currentParamValueStartIndex = accumulator.length
|
||||
foundParamStart = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (foundParamStart) {
|
||||
continue // Move to next character
|
||||
}
|
||||
|
||||
// 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") &&
|
||||
accumulator.endsWith(`</${contentParamName}>`)
|
||||
) {
|
||||
const toolContent = accumulator.slice(currentToolUseStartIndex)
|
||||
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)
|
||||
|
||||
// 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 found
|
||||
const toolName = toolUseOpeningTag.slice(1, -1) as ToolUseName
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolName,
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
currentToolUseStartIndex = accumulator.length
|
||||
|
||||
// This also indicates the end of the current text content block (if any)
|
||||
if (currentTextContent) {
|
||||
currentTextContent.partial = false
|
||||
// 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 // Found tool start, stop checking for others
|
||||
}
|
||||
}
|
||||
|
||||
if (!didStartToolUse) {
|
||||
// No tool use started, so it must be text content accumulating
|
||||
// (or continuing after a closed tool use)
|
||||
if (currentTextContent === undefined) {
|
||||
// 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) {
|
||||
// If a parameter was open within that tool use
|
||||
if (currentParamName) {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
return contentBlocks
|
||||
}
|
||||
// parseAssistantmessageV1 removed in https://github.com/cline/cline/pull/5425
|
||||
|
||||
/**
|
||||
* @description **Version 2**
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { setVscodeHostProviderMock } from "@/test/host-provider-test-utils"
|
||||
import * as diskModule from "@core/storage/disk"
|
||||
import { expect } from "chai"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as path from "path"
|
||||
import * as sinon from "sinon"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import type { FileMetadataEntry, TaskMetadata } from "./ContextTrackerTypes"
|
||||
import { FileContextTracker } from "./FileContextTracker"
|
||||
import * as diskModule from "@core/storage/disk"
|
||||
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
import type { DiffViewProviderCreator, WebviewProviderCreator } from "@/hosts/host-provider"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
|
||||
describe("FileContextTracker", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
@@ -54,13 +53,7 @@ describe("FileContextTracker", () => {
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
|
||||
|
||||
// Reset HostProvider before initializing to avoid "already initialized" errors
|
||||
HostProvider.reset()
|
||||
HostProvider.initialize(
|
||||
((_) => {}) as WebviewProviderCreator,
|
||||
(() => {}) as DiffViewProviderCreator,
|
||||
vscodeHostBridgeClient,
|
||||
)
|
||||
setVscodeHostProviderMock()
|
||||
|
||||
// Create tracker instance
|
||||
taskId = "test-task-id"
|
||||
|
||||
@@ -16,11 +16,9 @@ export async function relaunchChromeDebugMode(controller: Controller, _: EmptyRe
|
||||
// Relaunch Chrome in debug mode
|
||||
await browserSession.relaunchChromeDebugMode(controller)
|
||||
|
||||
// The actual result will be sent via postMessageToWebview in the BrowserSession.relaunchChromeDebugMode method
|
||||
// The actual result will be sent via the ProtoBus in the BrowserSession.relaunchChromeDebugMode method
|
||||
// Here we just return a message as a placeholder
|
||||
return StringMessage.create({
|
||||
value: "Chrome relaunch initiated",
|
||||
})
|
||||
return { value: "Chrome relaunch initiated" }
|
||||
} catch (error) {
|
||||
throw new Error(`Error relaunching Chrome: ${error instanceof Error ? error.message : globalThis.String(error)}`)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import { expect } from "chai"
|
||||
import * as sinon from "sinon"
|
||||
import { handleGrpcRequest, handleGrpcRequestCancel, getRequestRegistry } from "./grpc-handler"
|
||||
import { Controller } from "@core/controller"
|
||||
import { GrpcRequest, GrpcCancel } from "@shared/WebviewMessage"
|
||||
import { serviceHandlers } from "@generated/hosts/vscode/protobus-services"
|
||||
|
||||
describe("grpc-handler", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let mockController: Controller
|
||||
let mockPostMessageToWebview: sinon.SinonStub
|
||||
|
||||
let mockUnaryHandler: sinon.SinonStub
|
||||
let mockUnaryFailingHandler: sinon.SinonStub
|
||||
let mockStreamingHandler: sinon.SinonStub
|
||||
let mockStreamingFailingHandler: sinon.SinonStub
|
||||
|
||||
const serviceName = "cline.TestService"
|
||||
const mockResponse = { result: "result-1234" }
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create a mock controller
|
||||
mockController = {} as any
|
||||
mockPostMessageToWebview = sandbox.stub().resolves()
|
||||
|
||||
// Create mock service handlers
|
||||
mockUnaryHandler = sandbox.stub().resolves(mockResponse)
|
||||
mockStreamingHandler = sandbox.stub().resolves()
|
||||
mockUnaryFailingHandler = sandbox.stub().rejects(new Error("Test error unary"))
|
||||
mockStreamingFailingHandler = sandbox.stub().rejects(new Error("Stream error"))
|
||||
serviceHandlers[serviceName] = {
|
||||
testUnary: mockUnaryHandler,
|
||||
testUnaryFailing: mockUnaryFailingHandler,
|
||||
testStreaming: mockStreamingHandler,
|
||||
testStreamingFailing: mockStreamingFailingHandler,
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe("handleGrpcRequest", () => {
|
||||
describe("Unary requests", () => {
|
||||
it("should handle successful unary requests", async () => {
|
||||
const request: GrpcRequest = {
|
||||
service: serviceName,
|
||||
method: "testUnary",
|
||||
message: { input: "test" },
|
||||
request_id: "test-123",
|
||||
is_streaming: false,
|
||||
}
|
||||
|
||||
await handleGrpcRequest(mockController, mockPostMessageToWebview, request)
|
||||
|
||||
// Verify the handler was called
|
||||
expect(mockUnaryHandler.calledOnce).to.be.true
|
||||
expect(mockUnaryHandler.firstCall.args[0]).to.equal(mockController)
|
||||
expect(mockUnaryHandler.firstCall.args[1]).to.deep.equal({ input: "test" })
|
||||
|
||||
// Verify the response was sent
|
||||
expect(mockPostMessageToWebview.calledOnce).to.be.true
|
||||
const sentMessage = mockPostMessageToWebview.firstCall.args[0]
|
||||
expect(sentMessage).to.deep.equal({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
message: mockResponse,
|
||||
request_id: "test-123",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle errors in unary requests", async () => {
|
||||
const request: GrpcRequest = {
|
||||
service: serviceName,
|
||||
method: "testUnaryFailing",
|
||||
message: { input: "test" },
|
||||
request_id: "test-456",
|
||||
is_streaming: false,
|
||||
}
|
||||
|
||||
await handleGrpcRequest(mockController, mockPostMessageToWebview, request)
|
||||
|
||||
// Verify the error response was sent
|
||||
expect(mockPostMessageToWebview.calledOnce).to.be.true
|
||||
const sentMessage = mockPostMessageToWebview.firstCall.args[0]
|
||||
expect(sentMessage).to.deep.equal({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
error: "Test error unary",
|
||||
request_id: "test-456",
|
||||
is_streaming: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle unknown service errors", async () => {
|
||||
const request: GrpcRequest = {
|
||||
service: "UnknownService",
|
||||
method: "someMethod",
|
||||
message: {},
|
||||
request_id: "test-789",
|
||||
is_streaming: false,
|
||||
}
|
||||
|
||||
await handleGrpcRequest(mockController, mockPostMessageToWebview, request)
|
||||
|
||||
// Verify the error response was sent
|
||||
expect(mockPostMessageToWebview.calledOnce).to.be.true
|
||||
const sentMessage = mockPostMessageToWebview.firstCall.args[0]
|
||||
expect(sentMessage.type).to.equal("grpc_response")
|
||||
expect(sentMessage.grpc_response?.error).to.include("Unknown service: UnknownService")
|
||||
expect(sentMessage.grpc_response?.request_id).to.equal("test-789")
|
||||
})
|
||||
|
||||
it("should handle unknown method errors", async () => {
|
||||
const request: GrpcRequest = {
|
||||
service: serviceName,
|
||||
method: "unknownMethod",
|
||||
message: {},
|
||||
request_id: "test-999",
|
||||
is_streaming: false,
|
||||
}
|
||||
|
||||
await handleGrpcRequest(mockController, mockPostMessageToWebview, request)
|
||||
|
||||
// Verify the error response was sent
|
||||
expect(mockPostMessageToWebview.calledOnce).to.be.true
|
||||
const sentMessage = mockPostMessageToWebview.firstCall.args[0]
|
||||
expect(sentMessage.type).to.equal("grpc_response")
|
||||
expect(sentMessage.grpc_response?.error).to.include("Unknown rpc: cline.TestService.unknownMethod")
|
||||
expect(sentMessage.grpc_response?.request_id).to.equal("test-999")
|
||||
})
|
||||
})
|
||||
describe("Streaming requests", () => {
|
||||
it("should handle successful streaming requests", async () => {
|
||||
// Set up a streaming handler that sends multiple responses
|
||||
const request: GrpcRequest = {
|
||||
service: serviceName,
|
||||
method: "testStreaming",
|
||||
message: { input: "stream" },
|
||||
request_id: "stream-123",
|
||||
is_streaming: true,
|
||||
}
|
||||
|
||||
// Reset the mock and set up the handler using callsFake
|
||||
mockStreamingHandler.reset()
|
||||
mockStreamingHandler.callsFake(async (controller: any, message: any, responseStream: any, requestId: string) => {
|
||||
// Simulate streaming multiple messages
|
||||
await responseStream({ value: 1 }, false, 0)
|
||||
await responseStream({ value: 2 }, false, 1)
|
||||
await responseStream({ value: 3 }, true, 2) // Last message
|
||||
})
|
||||
|
||||
await handleGrpcRequest(mockController, mockPostMessageToWebview, request)
|
||||
|
||||
// Verify the handler was called
|
||||
expect(mockStreamingHandler.calledOnce).to.be.true
|
||||
expect(mockStreamingHandler.firstCall.args[0]).to.equal(mockController)
|
||||
expect(mockStreamingHandler.firstCall.args[1]).to.deep.equal({ input: "stream" })
|
||||
expect(mockStreamingHandler.firstCall.args[3]).to.equal("stream-123")
|
||||
|
||||
// Verify all streaming responses were sent
|
||||
expect(mockPostMessageToWebview.callCount).to.equal(3)
|
||||
|
||||
// Check all responses
|
||||
expect(mockPostMessageToWebview.firstCall.args[0]).to.deep.equal({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
message: { value: 1 },
|
||||
request_id: "stream-123",
|
||||
is_streaming: true,
|
||||
sequence_number: 0,
|
||||
},
|
||||
})
|
||||
expect(mockPostMessageToWebview.secondCall.args[0]).to.deep.equal({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
message: { value: 2 },
|
||||
request_id: "stream-123",
|
||||
is_streaming: true,
|
||||
sequence_number: 1,
|
||||
},
|
||||
})
|
||||
expect(mockPostMessageToWebview.thirdCall.args[0]).to.deep.equal({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
message: { value: 3 },
|
||||
request_id: "stream-123",
|
||||
is_streaming: false, // Last message has is_streaming: false
|
||||
sequence_number: 2,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle errors in streaming requests", async () => {
|
||||
const request: GrpcRequest = {
|
||||
service: serviceName,
|
||||
method: "testStreamingFailing",
|
||||
message: { input: "stream" },
|
||||
request_id: "stream-456",
|
||||
is_streaming: true,
|
||||
}
|
||||
|
||||
await handleGrpcRequest(mockController, mockPostMessageToWebview, request)
|
||||
|
||||
// Verify the error response was sent
|
||||
expect(mockPostMessageToWebview.calledOnce).to.be.true
|
||||
const sentMessage = mockPostMessageToWebview.firstCall.args[0]
|
||||
expect(sentMessage).to.deep.equal({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
error: "Stream error",
|
||||
request_id: "stream-456",
|
||||
is_streaming: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle streaming with message, error, then another message", async () => {
|
||||
// This test simulates a scenario where:
|
||||
// 1. First message is sent successfully
|
||||
// 2. An error occurs
|
||||
// 3. Another message is attempted (which should not be sent after error)
|
||||
|
||||
const request: GrpcRequest = {
|
||||
service: serviceName,
|
||||
method: "testStreaming",
|
||||
message: { input: "stream-with-error" },
|
||||
request_id: "stream-error-mid",
|
||||
is_streaming: true,
|
||||
}
|
||||
|
||||
// Reset the mock and set up the handler to throw an error after being called
|
||||
mockStreamingHandler.reset()
|
||||
mockStreamingHandler.callsFake(async (controller: any, message: any, responseStream: any, requestId: string) => {
|
||||
// Send first message successfully
|
||||
await responseStream({ value: "first" }, false, 0)
|
||||
// Throw an error
|
||||
throw new Error("Mid-stream error")
|
||||
})
|
||||
|
||||
await handleGrpcRequest(mockController, mockPostMessageToWebview, request)
|
||||
|
||||
// Verify the handler was called
|
||||
expect(mockStreamingHandler.calledOnce).to.be.true
|
||||
|
||||
// Verify that we got the first message and then the error
|
||||
expect(mockPostMessageToWebview.callCount).to.equal(2)
|
||||
|
||||
// Check first message was sent successfully
|
||||
expect(mockPostMessageToWebview.firstCall.args[0]).to.deep.equal({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
message: { value: "first" },
|
||||
request_id: "stream-error-mid",
|
||||
is_streaming: true,
|
||||
sequence_number: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// Check error response was sent
|
||||
expect(mockPostMessageToWebview.secondCall.args[0]).to.deep.equal({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
error: "Mid-stream error",
|
||||
request_id: "stream-error-mid",
|
||||
is_streaming: false,
|
||||
},
|
||||
})
|
||||
|
||||
// Try to send another message after the error (simulating what might happen
|
||||
// if the handler tried to continue after an error)
|
||||
const responseStream = mockStreamingHandler.firstCall.args[2]
|
||||
|
||||
// This should still work as the responseStream function is still valid
|
||||
await responseStream({ value: "after-error" }, false, 1)
|
||||
|
||||
// Verify we now have 3 total calls (first message, error, after-error message)
|
||||
expect(mockPostMessageToWebview.callCount).to.equal(3)
|
||||
|
||||
// Verify the message after error was still sent
|
||||
// (In a real scenario, the handler would have stopped due to the error,
|
||||
// but this tests that the responseStream function itself still works)
|
||||
expect(mockPostMessageToWebview.thirdCall.args[0]).to.deep.equal({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
message: { value: "after-error" },
|
||||
request_id: "stream-error-mid",
|
||||
is_streaming: true,
|
||||
sequence_number: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("handleGrpcRequestCancel", () => {
|
||||
it("should cancel an active request", async () => {
|
||||
// Register a request in the registry
|
||||
const registry = getRequestRegistry()
|
||||
const cleanupStub = sandbox.stub()
|
||||
registry.registerRequest("cancel-123", cleanupStub)
|
||||
|
||||
const cancelRequest: GrpcCancel = {
|
||||
request_id: "cancel-123",
|
||||
}
|
||||
|
||||
await handleGrpcRequestCancel(mockPostMessageToWebview, cancelRequest)
|
||||
|
||||
// Verify the cleanup was called
|
||||
expect(cleanupStub.calledOnce).to.be.true
|
||||
|
||||
// Verify the cancellation confirmation was sent
|
||||
expect(mockPostMessageToWebview.calledOnce).to.be.true
|
||||
const sentMessage = mockPostMessageToWebview.firstCall.args[0]
|
||||
expect(sentMessage).to.deep.equal({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
message: { cancelled: true },
|
||||
request_id: "cancel-123",
|
||||
is_streaming: false,
|
||||
},
|
||||
})
|
||||
|
||||
// Verify the request was removed from the registry
|
||||
expect(registry.hasRequest("cancel-123")).to.be.false
|
||||
})
|
||||
|
||||
it("should handle cancellation of non-existent request", async () => {
|
||||
const cancelRequest: GrpcCancel = {
|
||||
request_id: "non-existent",
|
||||
}
|
||||
|
||||
await handleGrpcRequestCancel(mockPostMessageToWebview, cancelRequest)
|
||||
|
||||
// Verify no message was sent (request not found)
|
||||
expect(mockPostMessageToWebview.called).to.be.false
|
||||
})
|
||||
|
||||
it("should handle cleanup errors gracefully", async () => {
|
||||
// Register a request with a failing cleanup
|
||||
const registry = getRequestRegistry()
|
||||
const cleanupStub = sandbox.stub().throws(new Error("Cleanup failed"))
|
||||
registry.registerRequest("cancel-error", cleanupStub)
|
||||
|
||||
const cancelRequest: GrpcCancel = {
|
||||
request_id: "cancel-error",
|
||||
}
|
||||
|
||||
// Should not throw
|
||||
await handleGrpcRequestCancel(mockPostMessageToWebview, cancelRequest)
|
||||
|
||||
// Verify the cleanup was attempted
|
||||
expect(cleanupStub.calledOnce).to.be.true
|
||||
|
||||
// Verify the cancellation confirmation was still sent
|
||||
expect(mockPostMessageToWebview.calledOnce).to.be.true
|
||||
|
||||
// Verify the request was removed despite the error
|
||||
expect(registry.hasRequest("cancel-error")).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
describe("Concurrent requests", () => {
|
||||
it("should handle concurrent requests", async () => {
|
||||
// Set up handlers
|
||||
mockUnaryHandler.resolves({ result: "unary" })
|
||||
mockStreamingHandler.callsFake(async (_controller: any, _message: any, responseStream: any) => {
|
||||
await responseStream({ value: "stream1" }, false, 0)
|
||||
await responseStream({ value: "stream2" }, true, 1)
|
||||
})
|
||||
|
||||
// Send multiple requests concurrently
|
||||
const requests = [
|
||||
handleGrpcRequest(mockController, mockPostMessageToWebview, {
|
||||
service: serviceName,
|
||||
method: "testUnary",
|
||||
message: { id: 1 },
|
||||
request_id: "concurrent-1",
|
||||
is_streaming: false,
|
||||
}),
|
||||
handleGrpcRequest(mockController, mockPostMessageToWebview, {
|
||||
service: serviceName,
|
||||
method: "testStreaming",
|
||||
message: { id: 2 },
|
||||
request_id: "concurrent-2",
|
||||
is_streaming: true,
|
||||
}),
|
||||
handleGrpcRequest(mockController, mockPostMessageToWebview, {
|
||||
service: serviceName,
|
||||
method: "testUnary",
|
||||
message: { id: 3 },
|
||||
request_id: "concurrent-3",
|
||||
is_streaming: false,
|
||||
}),
|
||||
]
|
||||
|
||||
await Promise.all(requests)
|
||||
|
||||
// Verify all handlers were called
|
||||
expect(mockUnaryHandler.callCount).to.equal(2)
|
||||
expect(mockStreamingHandler.callCount).to.equal(1)
|
||||
|
||||
// Verify all responses were sent (2 unary + 2 streaming)
|
||||
expect(mockPostMessageToWebview.callCount).to.equal(4)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
+101
-157
@@ -1,6 +1,8 @@
|
||||
import { Controller } from "./index"
|
||||
import { serviceHandlers } from "@generated/hosts/vscode/protobus-services"
|
||||
import { GrpcRequestRegistry } from "./grpc-request-registry"
|
||||
import { GrpcCancel, GrpcRequest } from "@/shared/WebviewMessage"
|
||||
import { ExtensionMessage } from "@/shared/ExtensionMessage"
|
||||
|
||||
/**
|
||||
* Type definition for a streaming response handler
|
||||
@@ -11,183 +13,122 @@ export type StreamingResponseHandler<TResponse> = (
|
||||
sequenceNumber?: number,
|
||||
) => Promise<void>
|
||||
|
||||
/**
|
||||
* Handles gRPC requests from the webview
|
||||
*/
|
||||
export class GrpcHandler {
|
||||
constructor(private controller: Controller) {}
|
||||
|
||||
/**
|
||||
* Handle a gRPC request from the webview
|
||||
* @param service The service name
|
||||
* @param method The method name
|
||||
* @param message The request message
|
||||
* @param requestId The request ID for response correlation
|
||||
* @param isStreaming Whether this is a streaming request
|
||||
* @returns The response message or error for unary requests, void for streaming requests
|
||||
*/
|
||||
async handleRequest(
|
||||
service: string,
|
||||
method: string,
|
||||
message: any,
|
||||
requestId: string,
|
||||
isStreaming: boolean = false,
|
||||
): Promise<{
|
||||
message?: any
|
||||
error?: string
|
||||
request_id: string
|
||||
} | void> {
|
||||
try {
|
||||
// If this is a streaming request, use the streaming handler
|
||||
if (isStreaming) {
|
||||
await this.handleStreamingRequest(service, method, message, requestId)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the service handler from the config
|
||||
const handler = getHandler(service, method)
|
||||
|
||||
// Handle unary request
|
||||
return {
|
||||
message: await handler(this.controller, message),
|
||||
request_id: requestId,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
request_id: requestId,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a streaming gRPC request
|
||||
* @param service The service name
|
||||
* @param method The method name
|
||||
* @param message The request message
|
||||
* @param requestId The request ID for response correlation
|
||||
*/
|
||||
private async handleStreamingRequest(service: string, method: string, message: any, requestId: string): Promise<void> {
|
||||
// Create a response stream function
|
||||
const responseStream: StreamingResponseHandler<any> = async (
|
||||
response: any,
|
||||
isLast: boolean = false,
|
||||
sequenceNumber?: number,
|
||||
) => {
|
||||
await this.controller.postMessageToWebview({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
message: response,
|
||||
request_id: requestId,
|
||||
is_streaming: !isLast,
|
||||
sequence_number: sequenceNumber,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the service handler from the config
|
||||
const handler = getHandler(service, method)
|
||||
|
||||
// Handle streaming request and pass the requestId to all streaming handlers
|
||||
await handler(this.controller, message, responseStream, requestId)
|
||||
|
||||
// Don't send a final message here - the stream should stay open for future updates
|
||||
// The stream will be closed when the client disconnects or when the service explicitly ends it
|
||||
} catch (error) {
|
||||
// Send error response
|
||||
console.log("Protobus error:", error)
|
||||
await this.controller.postMessageToWebview({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
request_id: requestId,
|
||||
is_streaming: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Registry to track active gRPC requests and their cleanup functions
|
||||
const requestRegistry = new GrpcRequestRegistry()
|
||||
export type PostMessageToWebview = (message: ExtensionMessage) => Thenable<boolean | undefined>
|
||||
|
||||
/**
|
||||
* Handle a gRPC request from the webview
|
||||
* @param controller The controller instance
|
||||
* @param request The gRPC request
|
||||
* Handles a gRPC request from the webview.
|
||||
*/
|
||||
export async function handleGrpcRequest(
|
||||
controller: Controller,
|
||||
request: {
|
||||
service: string
|
||||
method: string
|
||||
message: any
|
||||
request_id: string
|
||||
is_streaming?: boolean
|
||||
},
|
||||
) {
|
||||
postMessageToWebview: PostMessageToWebview,
|
||||
request: GrpcRequest,
|
||||
): Promise<void> {
|
||||
if (request.is_streaming) {
|
||||
await handleStreamingRequest(controller, postMessageToWebview, request)
|
||||
} else {
|
||||
await handleUnaryRequest(controller, postMessageToWebview, request)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a gRPC unary request from the webview.
|
||||
*
|
||||
* Calls the handler using the service and method name, and then posts the result back to the webview.
|
||||
*/
|
||||
async function handleUnaryRequest(
|
||||
controller: Controller,
|
||||
postMessageToWebview: PostMessageToWebview,
|
||||
request: GrpcRequest,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const grpcHandler = new GrpcHandler(controller)
|
||||
|
||||
// For streaming requests, handleRequest handles sending responses directly
|
||||
if (request.is_streaming) {
|
||||
try {
|
||||
await grpcHandler.handleRequest(request.service, request.method, request.message, request.request_id, true)
|
||||
} finally {
|
||||
// Note: We don't automatically clean up here anymore
|
||||
// The request will be cleaned up when it completes or is cancelled
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// For unary requests, we get a response and send it back
|
||||
const response = (await grpcHandler.handleRequest(
|
||||
request.service,
|
||||
request.method,
|
||||
request.message,
|
||||
request.request_id,
|
||||
false,
|
||||
)) as {
|
||||
message?: any
|
||||
error?: string
|
||||
request_id: string
|
||||
}
|
||||
|
||||
// Send the response back to the webview
|
||||
await controller.postMessageToWebview({
|
||||
// Get the service handler from the config
|
||||
const handler = getHandler(request.service, request.method)
|
||||
// Handle unary request
|
||||
const response = await handler(controller, request.message)
|
||||
// Send response to the webview
|
||||
await postMessageToWebview({
|
||||
type: "grpc_response",
|
||||
grpc_response: response,
|
||||
grpc_response: {
|
||||
message: response,
|
||||
request_id: request.request_id,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
// Send error response
|
||||
console.log("Protobus error:", error)
|
||||
await controller.postMessageToWebview({
|
||||
await postMessageToWebview({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
request_id: request.request_id,
|
||||
is_streaming: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a gRPC request cancellation from the webview
|
||||
* Handle a streaming gRPC request from the webview.
|
||||
*
|
||||
* Calls the handler using the service and method name, and creates a streaming response handler
|
||||
* which posts results back to the webview.
|
||||
*/
|
||||
async function handleStreamingRequest(
|
||||
controller: Controller,
|
||||
postMessageToWebview: PostMessageToWebview,
|
||||
request: GrpcRequest,
|
||||
): Promise<void> {
|
||||
// Create a response stream function
|
||||
const responseStream: StreamingResponseHandler<any> = async (
|
||||
response: any,
|
||||
isLast: boolean = false,
|
||||
sequenceNumber?: number,
|
||||
) => {
|
||||
await postMessageToWebview({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
message: response,
|
||||
request_id: request.request_id,
|
||||
is_streaming: !isLast,
|
||||
sequence_number: sequenceNumber,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the service handler from the config
|
||||
const handler = getHandler(request.service, request.method)
|
||||
|
||||
// Handle streaming request and pass the requestId to all streaming handlers
|
||||
await handler(controller, request.message, responseStream, request.request_id)
|
||||
|
||||
// Don't send a final message here - the stream should stay open for future updates
|
||||
// The stream will be closed when the client disconnects or when the service explicitly ends it
|
||||
} catch (error) {
|
||||
// Send error response
|
||||
console.log("Protobus error:", error)
|
||||
await postMessageToWebview({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
request_id: request.request_id,
|
||||
is_streaming: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a gRPC request cancellation from the webview.
|
||||
* @param controller The controller instance
|
||||
* @param request The cancellation request
|
||||
*/
|
||||
export async function handleGrpcRequestCancel(
|
||||
controller: Controller,
|
||||
request: {
|
||||
request_id: string
|
||||
},
|
||||
) {
|
||||
export async function handleGrpcRequestCancel(postMessageToWebview: PostMessageToWebview, request: GrpcCancel) {
|
||||
const cancelled = requestRegistry.cancelRequest(request.request_id)
|
||||
|
||||
if (cancelled) {
|
||||
// Send a cancellation confirmation
|
||||
await controller.postMessageToWebview({
|
||||
await postMessageToWebview({
|
||||
type: "grpc_response",
|
||||
grpc_response: {
|
||||
message: { cancelled: true },
|
||||
@@ -200,6 +141,17 @@ export async function handleGrpcRequestCancel(
|
||||
}
|
||||
}
|
||||
|
||||
// Registry to track active gRPC requests and their cleanup functions
|
||||
const requestRegistry = new GrpcRequestRegistry()
|
||||
|
||||
/**
|
||||
* Get the request registry instance
|
||||
* This allows other parts of the code to access the registry
|
||||
*/
|
||||
export function getRequestRegistry(): GrpcRequestRegistry {
|
||||
return requestRegistry
|
||||
}
|
||||
|
||||
function getHandler(serviceName: string, methodName: string): any {
|
||||
// Get the service handler from the config
|
||||
const serviceConfig = serviceHandlers[serviceName]
|
||||
@@ -212,11 +164,3 @@ function getHandler(serviceName: string, methodName: string): any {
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request registry instance
|
||||
* This allows other parts of the code to access the registry
|
||||
*/
|
||||
export function getRequestRegistry(): GrpcRequestRegistry {
|
||||
return requestRegistry
|
||||
}
|
||||
|
||||
+144
-198
@@ -1,8 +1,9 @@
|
||||
import { clineEnvConfig } from "@/config"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import { PostHogClientProvider, telemetryService } from "@/services/posthog/PostHogClientProvider"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getLatestAnnouncementId } from "@/utils/announcements"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { buildApiHandler } from "@api/index"
|
||||
@@ -13,14 +14,12 @@ import { ClineAccountService } from "@services/account/ClineAccountService"
|
||||
import { McpHub } from "@services/mcp/McpHub"
|
||||
import { ApiProvider, ModelInfo } from "@shared/api"
|
||||
import { ChatContent } from "@shared/ChatContent"
|
||||
import { ChatSettings, Mode, StoredChatSettings } from "@shared/ChatSettings"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
|
||||
import { ExtensionState, Platform } from "@shared/ExtensionMessage"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { McpMarketplaceCatalog } from "@shared/mcp"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { UserInfo } from "@shared/UserInfo"
|
||||
import { WebviewMessage } from "@shared/WebviewMessage"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import axios from "axios"
|
||||
import fs from "fs/promises"
|
||||
@@ -28,13 +27,14 @@ import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { CacheService, PersistenceErrorEvent } from "../storage/CacheService"
|
||||
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
|
||||
import { getAllExtensionState, getGlobalState, getWorkspaceState, storeSecret, updateGlobalState } from "../storage/state"
|
||||
import { getAllExtensionState, getGlobalState, updateGlobalState } from "../storage/state"
|
||||
import { Task } from "../task"
|
||||
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
|
||||
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
|
||||
import { sendStateUpdate } from "./state/subscribeToState"
|
||||
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
|
||||
import { sendAddToInputEvent, sendAddToInputEventToClient } from "./ui/subscribeToAddToInput"
|
||||
import { WebviewProvider } from "../webview"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -44,42 +44,63 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
|
||||
|
||||
export class Controller {
|
||||
readonly id: string
|
||||
private postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined
|
||||
|
||||
private disposables: vscode.Disposable[] = []
|
||||
task?: Task
|
||||
|
||||
workspaceTracker: WorkspaceTracker
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
authService: AuthService
|
||||
get latestAnnouncementId(): string {
|
||||
return this.context.extension?.packageJSON?.version?.split(".").slice(0, 2).join(".") ?? ""
|
||||
}
|
||||
readonly cacheService: CacheService
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
private readonly outputChannel: vscode.OutputChannel,
|
||||
postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined,
|
||||
id: string,
|
||||
) {
|
||||
this.id = id
|
||||
this.outputChannel.appendLine("ClineProvider instantiated")
|
||||
this.postMessage = postMessage
|
||||
|
||||
HostProvider.get().logToChannel("ClineProvider instantiated")
|
||||
this.accountService = ClineAccountService.getInstance()
|
||||
this.cacheService = new CacheService(context)
|
||||
const authService = AuthService.getInstance(this)
|
||||
|
||||
// Initialize cache service asynchronously - critical for extension functionality
|
||||
this.cacheService
|
||||
.initialize()
|
||||
.then(() => {
|
||||
authService.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("CRITICAL: Failed to initialize CacheService - extension may not function properly:", error)
|
||||
})
|
||||
|
||||
// Set up persistence error recovery
|
||||
this.cacheService.onPersistenceError = async ({ error }: PersistenceErrorEvent) => {
|
||||
console.error("Cache persistence failed, recovering:", error)
|
||||
try {
|
||||
await this.cacheService.reInitialize()
|
||||
await this.postStateToWebview()
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: "Saving settings to storage failed.",
|
||||
})
|
||||
} catch (recoveryError) {
|
||||
console.error("Cache recovery failed:", recoveryError)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to save settings. Please restart the extension.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.workspaceTracker = new WorkspaceTracker()
|
||||
this.mcpHub = new McpHub(
|
||||
() => ensureMcpServersDirectoryExists(),
|
||||
() => ensureSettingsDirectoryExists(this.context),
|
||||
(msg) => this.postMessageToWebview(msg),
|
||||
this.context.extension?.packageJSON?.version ?? "1.0.0",
|
||||
)
|
||||
this.accountService = ClineAccountService.getInstance()
|
||||
this.authService = AuthService.getInstance(context)
|
||||
this.authService.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
|
||||
// Clean up legacy checkpoints
|
||||
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
|
||||
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath).catch((error) => {
|
||||
console.error("Failed to cleanup legacy checkpoints:", error)
|
||||
})
|
||||
}
|
||||
@@ -111,12 +132,18 @@ export class Controller {
|
||||
async handleSignOut() {
|
||||
try {
|
||||
// TODO: update to clineAccountId and then move clineApiKey to a clear function.
|
||||
await storeSecret(this.context, "clineAccountId", undefined)
|
||||
this.cacheService.setSecret("clineAccountId", undefined)
|
||||
await updateGlobalState(this.context, "userInfo", undefined)
|
||||
await Promise.all([
|
||||
updateGlobalState(this.context, "planModeApiProvider", "openrouter"),
|
||||
updateGlobalState(this.context, "actModeApiProvider", "openrouter"),
|
||||
])
|
||||
|
||||
// Update API providers through cache service
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
planModeApiProvider: "openrouter" as ApiProvider,
|
||||
actModeApiProvider: "openrouter" as ApiProvider,
|
||||
}
|
||||
this.cacheService.setApiConfiguration(updatedConfig)
|
||||
|
||||
await this.postStateToWebview()
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
@@ -136,11 +163,16 @@ export class Controller {
|
||||
|
||||
async initTask(task?: string, images?: string[], files?: string[], historyItem?: HistoryItem) {
|
||||
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||
|
||||
// Get API configuration from cache for immediate access
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
|
||||
const {
|
||||
apiConfiguration,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings: storedChatSettings,
|
||||
preferredLanguage,
|
||||
openaiReasoningEffort,
|
||||
mode,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
@@ -148,17 +180,9 @@ export class Controller {
|
||||
enableCheckpointsSetting,
|
||||
isNewUser,
|
||||
taskHistory,
|
||||
strictPlanModeEnabled,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
// Get current mode using helper function
|
||||
const currentMode = await this.getCurrentMode()
|
||||
|
||||
// Reconstruct ChatSettings with mode from global state and stored preferences
|
||||
const chatSettings: ChatSettings = {
|
||||
...storedChatSettings, // Spread stored preferences (preferredLanguage, openAIReasoningEffort)
|
||||
mode: currentMode, // Use mode from global state
|
||||
}
|
||||
|
||||
const NEW_USER_TASK_COUNT_THRESHOLD = 10
|
||||
|
||||
// Check if the user has completed enough tasks to no longer be considered a "new user"
|
||||
@@ -185,13 +209,17 @@ export class Controller {
|
||||
apiConfiguration,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
preferredLanguage,
|
||||
openaiReasoningEffort,
|
||||
mode,
|
||||
strictPlanModeEnabled ?? false,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled ?? true,
|
||||
terminalOutputLineLimit ?? 500,
|
||||
defaultTerminalProfile ?? "default",
|
||||
enableCheckpointsSetting ?? true,
|
||||
await getCwd(getDesktopDir()),
|
||||
this.cacheService,
|
||||
task,
|
||||
images,
|
||||
files,
|
||||
@@ -206,41 +234,6 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
// Send any JSON serializable data to the react app
|
||||
async postMessageToWebview(message: ExtensionMessage) {
|
||||
await this.postMessage(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up an event listener to listen for messages passed from the webview context and
|
||||
* executes code based on the message that is received.
|
||||
*
|
||||
* @param webview A reference to the extension webview
|
||||
*/
|
||||
async handleWebviewMessage(message: WebviewMessage) {
|
||||
switch (message.type) {
|
||||
case "fetchMcpMarketplace": {
|
||||
await this.fetchMcpMarketplace(message.bool)
|
||||
break
|
||||
}
|
||||
case "grpc_request": {
|
||||
if (message.grpc_request) {
|
||||
await handleGrpcRequest(this, message.grpc_request)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "grpc_request_cancel": {
|
||||
if (message.grpc_request_cancel) {
|
||||
await handleGrpcRequestCancel(this, message.grpc_request_cancel)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Add more switch case statements here as more webview message commands
|
||||
// are created within the webview context (i.e. inside media/main.js)
|
||||
}
|
||||
}
|
||||
|
||||
async updateTelemetrySetting(telemetrySetting: TelemetrySetting) {
|
||||
await updateGlobalState(this.context, "telemetrySetting", telemetrySetting)
|
||||
const isOptedIn = telemetrySetting !== "disabled"
|
||||
@@ -248,28 +241,25 @@ export class Controller {
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
async togglePlanActModeWithChatSettings(chatSettings: ChatSettings, chatContent?: ChatContent): Promise<boolean> {
|
||||
const didSwitchToActMode = chatSettings.mode === "act"
|
||||
async togglePlanActMode(modeToSwitchTo: Mode, chatContent?: ChatContent): Promise<boolean> {
|
||||
const didSwitchToActMode = modeToSwitchTo === "act"
|
||||
|
||||
// Store mode to global state
|
||||
await updateGlobalState(this.context, "mode", chatSettings.mode)
|
||||
await updateGlobalState(this.context, "mode", modeToSwitchTo)
|
||||
|
||||
// Capture mode switch telemetry | Capture regardless of if we know the taskId
|
||||
telemetryService.captureModeSwitch(this.task?.taskId ?? "0", chatSettings.mode)
|
||||
telemetryService.captureModeSwitch(this.task?.taskId ?? "0", modeToSwitchTo)
|
||||
|
||||
// Update API handler with new mode (buildApiHandler now selects provider based on mode)
|
||||
if (this.task) {
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
this.task.api = buildApiHandler({ ...apiConfiguration, taskId: this.task.taskId }, chatSettings.mode)
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
this.task.api = buildApiHandler({ ...apiConfiguration, taskId: this.task.taskId }, modeToSwitchTo)
|
||||
}
|
||||
|
||||
// Save only non-mode properties to global storage
|
||||
const { mode, ...persistentChatSettings }: { mode: string } & StoredChatSettings = chatSettings
|
||||
await updateGlobalState(this.context, "chatSettings", persistentChatSettings)
|
||||
await this.postStateToWebview()
|
||||
|
||||
if (this.task) {
|
||||
this.task.chatSettings = chatSettings
|
||||
this.task.updateMode(modeToSwitchTo)
|
||||
if (this.task.taskState.isAwaitingPlanResponse && didSwitchToActMode) {
|
||||
this.task.taskState.didRespondToPlanAskBySwitchingMode = true
|
||||
// Use chatContent if provided, otherwise use default message
|
||||
@@ -315,13 +305,14 @@ export class Controller {
|
||||
this.task.taskState.abandoned = true
|
||||
}
|
||||
await this.initTask(undefined, undefined, undefined, historyItem) // clears task again, so we need to abortTask manually above
|
||||
// await this.postStateToWebview() // new Cline instance will post state when it's ready. having this here sent an empty messages array to webview leading to virtuoso having to reload the entire list
|
||||
// Dont send the state to the webview, the new Cline instance will send state when it's ready.
|
||||
// Sending the state here sent an empty messages array to webview leading to virtuoso having to reload the entire list
|
||||
}
|
||||
}
|
||||
|
||||
async handleAuthCallback(customToken: string, provider: string | null = null) {
|
||||
try {
|
||||
await this.authService.handleAuthCallback(customToken, provider ? provider : "google")
|
||||
await AuthService.getInstance(this).handleAuthCallback(customToken, provider ? provider : "google")
|
||||
|
||||
const clineProvider: ApiProvider = "cline"
|
||||
|
||||
@@ -329,27 +320,26 @@ export class Controller {
|
||||
const { planActSeparateModelsSetting } = await getAllExtensionState(this.context)
|
||||
const currentMode = await this.getCurrentMode()
|
||||
|
||||
// Get current API configuration from cache
|
||||
const currentApiConfiguration = this.cacheService.getApiConfiguration()
|
||||
|
||||
const updatedConfig = { ...currentApiConfiguration }
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
// Only update the current mode's provider
|
||||
if (currentMode === "plan") {
|
||||
await updateGlobalState(this.context, "planModeApiProvider", clineProvider)
|
||||
updatedConfig.planModeApiProvider = clineProvider
|
||||
} else {
|
||||
await updateGlobalState(this.context, "actModeApiProvider", clineProvider)
|
||||
updatedConfig.actModeApiProvider = clineProvider
|
||||
}
|
||||
} else {
|
||||
// Update both modes to keep them in sync
|
||||
await Promise.all([
|
||||
updateGlobalState(this.context, "planModeApiProvider", clineProvider),
|
||||
updateGlobalState(this.context, "actModeApiProvider", clineProvider),
|
||||
])
|
||||
updatedConfig.planModeApiProvider = clineProvider
|
||||
updatedConfig.actModeApiProvider = clineProvider
|
||||
}
|
||||
|
||||
// Get the updated API configuration (now includes the updated providers)
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
apiProvider: clineProvider,
|
||||
}
|
||||
// Update the API configuration through cache service
|
||||
this.cacheService.setApiConfiguration(updatedConfig)
|
||||
|
||||
// Mark welcome view as completed since user has successfully logged in
|
||||
await updateGlobalState(this.context, "welcomeViewCompleted", true)
|
||||
@@ -456,7 +446,7 @@ export class Controller {
|
||||
|
||||
/**
|
||||
* RPC variant that silently refreshes the MCP marketplace catalog and returns the result
|
||||
* Unlike silentlyRefreshMcpMarketplace, this doesn't post a message to the webview
|
||||
* Unlike silentlyRefreshMcpMarketplace, this doesn't send a message to the webview
|
||||
* @returns MCP marketplace catalog or undefined if refresh failed
|
||||
*/
|
||||
async silentlyRefreshMcpMarketplaceRPC() {
|
||||
@@ -468,31 +458,6 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchMcpMarketplace(forceRefresh: boolean = false) {
|
||||
try {
|
||||
// Check if we have cached data
|
||||
const cachedCatalog = (await getGlobalState(this.context, "mcpMarketplaceCatalog")) as
|
||||
| McpMarketplaceCatalog
|
||||
| undefined
|
||||
if (!forceRefresh && cachedCatalog?.items) {
|
||||
await sendMcpMarketplaceCatalogEvent(cachedCatalog)
|
||||
return
|
||||
}
|
||||
|
||||
const catalog = await this.fetchMcpMarketplaceFromApi(false)
|
||||
if (catalog) {
|
||||
await sendMcpMarketplaceCatalogEvent(catalog)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to handle cached MCP marketplace:", error)
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// OpenRouter
|
||||
|
||||
async handleOpenRouterCallback(code: string) {
|
||||
@@ -511,23 +476,22 @@ export class Controller {
|
||||
|
||||
const openrouter: ApiProvider = "openrouter"
|
||||
const currentMode = await this.getCurrentMode()
|
||||
await Promise.all([
|
||||
updateGlobalState(this.context, "planModeApiProvider", openrouter),
|
||||
updateGlobalState(this.context, "actModeApiProvider", openrouter),
|
||||
])
|
||||
await storeSecret(this.context, "openRouterApiKey", apiKey)
|
||||
|
||||
// Update API configuration through cache service
|
||||
const currentApiConfiguration = this.cacheService.getApiConfiguration()
|
||||
const updatedConfig = {
|
||||
...currentApiConfiguration,
|
||||
planModeApiProvider: openrouter,
|
||||
actModeApiProvider: openrouter,
|
||||
openRouterApiKey: apiKey,
|
||||
}
|
||||
this.cacheService.setApiConfiguration(updatedConfig)
|
||||
|
||||
await this.postStateToWebview()
|
||||
if (this.task) {
|
||||
// Get the updated API configuration (now includes the updated providers)
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
openRouterApiKey: apiKey,
|
||||
taskId: this.task.taskId,
|
||||
}
|
||||
this.task.api = buildApiHandler(updatedConfig, currentMode)
|
||||
this.task.api = buildApiHandler({ ...updatedConfig, taskId: this.task.taskId }, currentMode)
|
||||
}
|
||||
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
|
||||
// Dont send settingsButtonClicked because its bad ux if user is on welcome
|
||||
}
|
||||
|
||||
private async ensureCacheDirectoryExists(): Promise<string> {
|
||||
@@ -560,10 +524,6 @@ export class Controller {
|
||||
|
||||
// 'Add to Cline' context menu in editor and code action
|
||||
async addSelectedCodeToChat(code: string, filePath: string, languageId: string, diagnostics?: vscode.Diagnostic[]) {
|
||||
// Ensure the sidebar view is visible
|
||||
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
|
||||
await setTimeoutPromise(100)
|
||||
|
||||
// Post message to webview with the selected code
|
||||
const fileMention = await this.getFileMentionFromPath(filePath)
|
||||
|
||||
@@ -573,7 +533,10 @@ export class Controller {
|
||||
input += `\nProblems:\n${problemsString}`
|
||||
}
|
||||
|
||||
await sendAddToInputEvent(input)
|
||||
const lastActiveWebview = WebviewProvider.getLastActiveInstance()
|
||||
if (lastActiveWebview) {
|
||||
await sendAddToInputEventToClient(lastActiveWebview.getClientId(), input)
|
||||
}
|
||||
|
||||
console.log("addSelectedCodeToChat", code, filePath, languageId)
|
||||
}
|
||||
@@ -583,14 +546,6 @@ export class Controller {
|
||||
// Ensure the sidebar view is visible
|
||||
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
|
||||
await setTimeoutPromise(100)
|
||||
|
||||
// Post message to webview with the selected terminal output
|
||||
// await this.postMessageToWebview({
|
||||
// type: "addSelectedTerminalOutput",
|
||||
// output,
|
||||
// terminalName
|
||||
// })
|
||||
|
||||
await sendAddToInputEvent(`Terminal output:\n\`\`\`\n${output}\n\`\`\``)
|
||||
|
||||
console.log("addSelectedTerminalOutputToChat", output, terminalName)
|
||||
@@ -699,13 +654,18 @@ export class Controller {
|
||||
}
|
||||
|
||||
async getStateToPostToWebview(): Promise<ExtensionState> {
|
||||
// Get API configuration from cache for immediate access
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
|
||||
const {
|
||||
apiConfiguration,
|
||||
lastShownAnnouncementId,
|
||||
taskHistory,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings: storedChatSettings,
|
||||
preferredLanguage,
|
||||
openaiReasoningEffort,
|
||||
mode,
|
||||
strictPlanModeEnabled,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpDisplayMode,
|
||||
@@ -721,51 +681,51 @@ export class Controller {
|
||||
welcomeViewCompleted,
|
||||
mcpResponsesCollapsed,
|
||||
terminalOutputLineLimit,
|
||||
localClineRulesToggles,
|
||||
localWindsurfRulesToggles,
|
||||
localCursorRulesToggles,
|
||||
localWorkflowToggles,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
// Get current mode using helper function
|
||||
const currentMode = await this.getCurrentMode()
|
||||
const currentTaskItem = this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined
|
||||
const checkpointTrackerErrorMessage = this.task?.taskState.checkpointTrackerErrorMessage
|
||||
const clineMessages = this.task?.messageStateHandler.getClineMessages() || []
|
||||
|
||||
// Reconstruct ChatSettings with mode from global state and stored preferences
|
||||
const chatSettings: ChatSettings = {
|
||||
...storedChatSettings, // Spread stored preferences (preferredLanguage, openAIReasoningEffort)
|
||||
mode: currentMode, // Use mode from global state
|
||||
}
|
||||
const processedTaskHistory = (taskHistory || [])
|
||||
.filter((item) => item.ts && item.task)
|
||||
.sort((a, b) => b.ts - a.ts)
|
||||
.slice(0, 100) // for now we're only getting the latest 100 tasks, but a better solution here is to only pass in 3 for recent task history, and then get the full task history on demand when going to the task history view (maybe with pagination?)
|
||||
|
||||
const localClineRulesToggles =
|
||||
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
const localWindsurfRulesToggles =
|
||||
((await getWorkspaceState(this.context, "localWindsurfRulesToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
const localCursorRulesToggles =
|
||||
((await getWorkspaceState(this.context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
const localWorkflowToggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
const latestAnnouncementId = getLatestAnnouncementId(this.context)
|
||||
const shouldShowAnnouncement = lastShownAnnouncementId !== latestAnnouncementId
|
||||
const platform = process.platform as Platform
|
||||
const distinctId = PostHogClientProvider.getInstance().distinctId
|
||||
const version = this.context.extension?.packageJSON?.version ?? ""
|
||||
const uriScheme = vscode.env.uriScheme
|
||||
|
||||
return {
|
||||
version: this.context.extension?.packageJSON?.version ?? "",
|
||||
version,
|
||||
apiConfiguration,
|
||||
uriScheme: vscode.env.uriScheme,
|
||||
currentTaskItem: this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined,
|
||||
checkpointTrackerErrorMessage: this.task?.taskState.checkpointTrackerErrorMessage,
|
||||
clineMessages: this.task?.messageStateHandler.getClineMessages() || [],
|
||||
taskHistory: (taskHistory || [])
|
||||
.filter((item) => item.ts && item.task)
|
||||
.sort((a, b) => b.ts - a.ts)
|
||||
.slice(0, 100), // for now we're only getting the latest 100 tasks, but a better solution here is to only pass in 3 for recent task history, and then get the full task history on demand when going to the task history view (maybe with pagination?)
|
||||
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
|
||||
platform: process.platform as Platform,
|
||||
uriScheme,
|
||||
currentTaskItem,
|
||||
checkpointTrackerErrorMessage,
|
||||
clineMessages,
|
||||
taskHistory: processedTaskHistory,
|
||||
shouldShowAnnouncement,
|
||||
platform,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
preferredLanguage,
|
||||
openaiReasoningEffort,
|
||||
mode,
|
||||
strictPlanModeEnabled,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpDisplayMode,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
|
||||
distinctId: telemetryService.distinctId,
|
||||
distinctId,
|
||||
globalClineRulesToggles: globalClineRulesToggles || {},
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
|
||||
@@ -840,18 +800,4 @@ export class Controller {
|
||||
await updateGlobalState(this.context, "taskHistory", history)
|
||||
return history
|
||||
}
|
||||
|
||||
// private async clearState() {
|
||||
// this.context.workspaceState.keys().forEach((key) => {
|
||||
// this.context.workspaceState.update(key, undefined)
|
||||
// })
|
||||
// this.context.globalState.keys().forEach((key) => {
|
||||
// this.context.globalState.update(key, undefined)
|
||||
// })
|
||||
// this.context.secrets.delete("apiKey")
|
||||
// }
|
||||
|
||||
// secrets
|
||||
|
||||
// dev
|
||||
}
|
||||
|
||||
@@ -66,9 +66,9 @@ export async function downloadMcp(controller: Controller, request: StringRequest
|
||||
- Once installed, demonstrate the server's capabilities by using one of its tools.
|
||||
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
|
||||
|
||||
const { chatSettings } = await controller.getStateToPostToWebview()
|
||||
if (chatSettings.mode === "plan") {
|
||||
await controller.togglePlanActModeWithChatSettings({ mode: "act" })
|
||||
const { mode } = await controller.getStateToPostToWebview()
|
||||
if (mode === "plan") {
|
||||
await controller.togglePlanActMode("act")
|
||||
}
|
||||
|
||||
// Initialize task and show chat view
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "@shared/proto/cline/models"
|
||||
import { getAllExtensionState } from "../../storage/state"
|
||||
import { basetenModels } from "../../../shared/api"
|
||||
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 Baseten models and returns the updated model list
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request object
|
||||
* @returns Response containing the Baseten models
|
||||
*/
|
||||
export async function refreshBasetenModels(
|
||||
controller: Controller,
|
||||
request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
console.log("=== refreshBasetenModels called ===")
|
||||
const basetenModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.basetenModels)
|
||||
|
||||
// Get the Baseten API key from the controller's state
|
||||
const { apiConfiguration } = await getAllExtensionState(controller.context)
|
||||
const basetenApiKey = apiConfiguration?.basetenApiKey
|
||||
|
||||
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
|
||||
try {
|
||||
if (!basetenApiKey) {
|
||||
console.log("No Baseten API key found, using static models as fallback")
|
||||
// Don't throw an error, just use static models
|
||||
for (const [modelId, modelInfo] of Object.entries(basetenModels)) {
|
||||
models[modelId] = {
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
supportsImages: modelInfo.supportsImages,
|
||||
supportsPromptCache: modelInfo.supportsPromptCache,
|
||||
inputPrice: modelInfo.inputPrice,
|
||||
outputPrice: modelInfo.outputPrice,
|
||||
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
|
||||
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
|
||||
description: (modelInfo as any).description || `${modelId} model`,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ensure the API key is properly formatted
|
||||
const cleanApiKey = basetenApiKey.trim()
|
||||
if (!cleanApiKey) {
|
||||
throw new Error("Invalid Baseten API key format")
|
||||
}
|
||||
|
||||
console.log("Fetching Baseten models with API key:", cleanApiKey.substring(0, 10) + "...")
|
||||
|
||||
const response = await axios.get("https://inference.baseten.co/v1/models", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${cleanApiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Cline-VSCode-Extension",
|
||||
},
|
||||
timeout: 10000, // 10 second timeout
|
||||
})
|
||||
|
||||
if (response.data?.data) {
|
||||
const rawModels = response.data.data
|
||||
|
||||
for (const rawModel of rawModels) {
|
||||
// Filter out non-chat models and validate model capabilities
|
||||
if (!isValidChatModel(rawModel)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only include models that are listed in the static basetenModels
|
||||
if (!(rawModel.id in basetenModels)) {
|
||||
console.log(`Skipping model ${rawModel.id} - not in static basetenModels list`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we have static pricing information for this model
|
||||
const staticModelInfo = basetenModels[rawModel.id as keyof typeof basetenModels]
|
||||
|
||||
const modelInfo: Partial<OpenRouterModelInfo> = {
|
||||
maxTokens: staticModelInfo?.maxTokens || 8192,
|
||||
contextWindow: staticModelInfo?.contextWindow || 8192,
|
||||
supportsImages: staticModelInfo?.supportsImages || false,
|
||||
supportsPromptCache: staticModelInfo?.supportsPromptCache || false,
|
||||
inputPrice: staticModelInfo?.inputPrice || 0,
|
||||
outputPrice: staticModelInfo?.outputPrice || 0,
|
||||
cacheWritesPrice: staticModelInfo?.cacheWritesPrice || 0,
|
||||
cacheReadsPrice: staticModelInfo?.cacheReadsPrice || 0,
|
||||
description: generateModelDescription(rawModel, staticModelInfo),
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid response from Baseten API")
|
||||
}
|
||||
await fs.writeFile(basetenModelsFilePath, JSON.stringify(models))
|
||||
console.log("Baseten models fetched and saved:", Object.keys(models))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Baseten models:", error)
|
||||
|
||||
// Provide more specific error messages
|
||||
let errorMessage = "Unknown error occurred"
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.status === 401) {
|
||||
errorMessage = "Invalid Baseten API key. Please check your API key in settings."
|
||||
} else if (error.response?.status === 403) {
|
||||
errorMessage = "Access forbidden. Please verify your Baseten API key has the correct permissions."
|
||||
} else if (error.response?.status === 429) {
|
||||
errorMessage = "Rate limit exceeded. Please try again later."
|
||||
} else if (error.code === "ECONNABORTED") {
|
||||
errorMessage = "Request timeout. Please check your internet connection."
|
||||
} else {
|
||||
errorMessage = `API request failed: ${error.response?.status || error.code || "Unknown error"}`
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
console.error("Baseten API Error:", errorMessage)
|
||||
|
||||
// If we failed to fetch models, try to read cached models first
|
||||
const cachedModels = await readBasetenModels(controller)
|
||||
if (cachedModels && Object.keys(cachedModels).length > 0) {
|
||||
console.log("Using cached Baseten models")
|
||||
// Filter cached models to only include those in static basetenModels
|
||||
for (const [modelId, modelInfo] of Object.entries(cachedModels)) {
|
||||
if (modelId in basetenModels) {
|
||||
models[modelId] = modelInfo
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fall back to static models from shared/api.ts
|
||||
console.log("Using static Baseten models as fallback")
|
||||
for (const [modelId, modelInfo] of Object.entries(basetenModels)) {
|
||||
models[modelId] = {
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
supportsImages: modelInfo.supportsImages,
|
||||
supportsPromptCache: modelInfo.supportsPromptCache,
|
||||
inputPrice: modelInfo.inputPrice,
|
||||
outputPrice: modelInfo.outputPrice,
|
||||
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
|
||||
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
|
||||
description: (modelInfo as any).description || `${modelId} model`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 ?? 8192,
|
||||
contextWindow: model.contextWindow ?? 8192,
|
||||
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 ?? "",
|
||||
tiers: model.tiers ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads cached Baseten models from disk
|
||||
*/
|
||||
async function readBasetenModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
|
||||
const basetenModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.basetenModels)
|
||||
const fileExists = await fileExistsAtPath(basetenModelsFilePath)
|
||||
if (fileExists) {
|
||||
try {
|
||||
const fileContents = await fs.readFile(basetenModelsFilePath, "utf8")
|
||||
return JSON.parse(fileContents)
|
||||
} catch (error) {
|
||||
console.error("Error reading cached Baseten models:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a model is suitable for chat completions
|
||||
*/
|
||||
function isValidChatModel(rawModel: any): boolean {
|
||||
// Filter out non-chat models (whisper, TTS, guard models, etc.)
|
||||
if (rawModel.id.includes("whisper") || rawModel.id.includes("tts") || rawModel.id.includes("embedding")) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if model supports chat completions
|
||||
if (rawModel.object === "model" && rawModel.id) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a descriptive name for the model
|
||||
*/
|
||||
function generateModelDescription(rawModel: any, staticModelInfo?: any): string {
|
||||
// Use static description if available
|
||||
if (staticModelInfo?.description) {
|
||||
return staticModelInfo.description
|
||||
}
|
||||
|
||||
// Generate description based on model characteristics
|
||||
const modelId = rawModel.id
|
||||
const ownedBy = rawModel.owned_by || "Unknown"
|
||||
|
||||
return `${ownedBy} model: ${modelId}`
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { telemetryService } from "@/services/posthog/PostHogClientProvider"
|
||||
|
||||
/**
|
||||
* Refreshes the Groq models and returns the updated model list
|
||||
@@ -111,7 +112,13 @@ export async function refreshGroqModels(controller: Controller, request: EmptyRe
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
console.error("Groq API Error:", errorMessage)
|
||||
telemetryService.captureProviderApiError({
|
||||
taskId: controller.task?.taskId || "",
|
||||
ulid: controller.task?.ulid || "",
|
||||
errorMessage,
|
||||
errorStatus: error.status,
|
||||
model: "groq",
|
||||
})
|
||||
|
||||
// If we failed to fetch models, try to read cached models first
|
||||
const cachedModels = await readGroqModels(controller)
|
||||
@@ -181,7 +188,7 @@ async function readGroqModels(controller: Controller): Promise<Record<string, Pa
|
||||
*/
|
||||
function isValidChatModel(rawModel: any): boolean {
|
||||
// Check if model is active (if the property exists)
|
||||
if (rawModel.hasOwnProperty("active") && !rawModel.active) {
|
||||
if (Object.hasOwn(rawModel, "active") && !rawModel.active) {
|
||||
return false
|
||||
}
|
||||
// Filter out non-chat models (whisper, TTS, guard models, etc.)
|
||||
|
||||
@@ -49,7 +49,6 @@ export async function refreshOpenRouterModels(
|
||||
|
||||
switch (rawModel.id) {
|
||||
case "anthropic/claude-sonnet-4":
|
||||
case "anthropic/claude-opus-4":
|
||||
case "anthropic/claude-3-7-sonnet":
|
||||
case "anthropic/claude-3-7-sonnet:beta":
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
@@ -62,6 +61,12 @@ export async function refreshOpenRouterModels(
|
||||
modelInfo.cacheWritesPrice = 3.75
|
||||
modelInfo.cacheReadsPrice = 0.3
|
||||
break
|
||||
case "anthropic/claude-opus-4.1":
|
||||
case "anthropic/claude-opus-4":
|
||||
modelInfo.supportsPromptCache = true
|
||||
modelInfo.cacheWritesPrice = 18.75
|
||||
modelInfo.cacheReadsPrice = 1.5
|
||||
break
|
||||
case "anthropic/claude-3.5-sonnet-20240620":
|
||||
case "anthropic/claude-3.5-sonnet-20240620:beta":
|
||||
modelInfo.supportsPromptCache = true
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Controller } from "../index"
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { UpdateApiConfigurationRequest } from "@shared/proto/cline/models"
|
||||
import { updateApiConfiguration } from "../../storage/state"
|
||||
import { buildApiHandler } from "@api/index"
|
||||
import { convertProtoToApiConfiguration } from "@shared/proto-conversions/models/api-configuration-conversion"
|
||||
|
||||
@@ -25,7 +24,7 @@ export async function updateApiConfigurationProto(
|
||||
const appApiConfiguration = convertProtoToApiConfiguration(request.apiConfiguration)
|
||||
|
||||
// Update the API configuration in storage
|
||||
await updateApiConfiguration(controller.context, appApiConfiguration)
|
||||
controller.cacheService.setApiConfiguration(appApiConfiguration)
|
||||
|
||||
// Update the task's API handler if there's an active task
|
||||
if (controller.task) {
|
||||
|
||||
@@ -19,13 +19,13 @@ export async function resetState(controller: Controller, request: ResetStateRequ
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting global state...",
|
||||
})
|
||||
await resetGlobalState(controller.context)
|
||||
await resetGlobalState(controller)
|
||||
} else {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting workspace state...",
|
||||
})
|
||||
await resetWorkspaceState(controller.context)
|
||||
await resetWorkspaceState(controller)
|
||||
}
|
||||
|
||||
if (controller.task) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import { telemetryService } from "@services/posthog/PostHogClientProvider"
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "@shared/proto/cline/common"
|
||||
import { updateGlobalState } from "@/core/storage/state"
|
||||
@@ -16,11 +16,7 @@ export async function toggleFavoriteModel(controller: Controller, request: Strin
|
||||
}
|
||||
|
||||
const modelId = request.value
|
||||
const { apiConfiguration } = await controller.getStateToPostToWebview()
|
||||
|
||||
if (!apiConfiguration) {
|
||||
throw new Error("API configuration not found")
|
||||
}
|
||||
const apiConfiguration = controller.cacheService.getApiConfiguration()
|
||||
|
||||
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
|
||||
|
||||
@@ -29,7 +25,12 @@ export async function toggleFavoriteModel(controller: Controller, request: Strin
|
||||
? favoritedModelIds.filter((id) => id !== modelId)
|
||||
: [...favoritedModelIds, modelId]
|
||||
|
||||
await updateGlobalState(controller.context, "favoritedModelIds", updatedFavorites)
|
||||
// Update the complete API configuration through cache service
|
||||
const updatedApiConfiguration = {
|
||||
...apiConfiguration,
|
||||
favoritedModelIds: updatedFavorites,
|
||||
}
|
||||
controller.cacheService.setApiConfiguration(updatedApiConfiguration)
|
||||
|
||||
// Capture telemetry for model favorite toggle
|
||||
const isFavorited = !favoritedModelIds.includes(modelId)
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Controller } from ".."
|
||||
import { Boolean } from "@shared/proto/cline/common"
|
||||
import { TogglePlanActModeRequest } from "@shared/proto/cline/state"
|
||||
import {
|
||||
convertProtoChatContentToChatContent,
|
||||
convertProtoChatSettingsToChatSettings,
|
||||
} from "@shared/proto-conversions/state/chat-settings-conversion"
|
||||
|
||||
/**
|
||||
* Toggles between Plan and Act modes
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the chat settings and optional chat content
|
||||
* @returns An empty response
|
||||
*/
|
||||
export async function togglePlanActMode(controller: Controller, request: TogglePlanActModeRequest): Promise<Boolean> {
|
||||
try {
|
||||
if (!request.chatSettings) {
|
||||
throw new Error("Chat settings are required")
|
||||
}
|
||||
|
||||
const chatSettings = convertProtoChatSettingsToChatSettings(request.chatSettings)
|
||||
const chatContent = request.chatContent ? convertProtoChatContentToChatContent(request.chatContent) : undefined
|
||||
|
||||
// Call the existing controller implementation
|
||||
const sentMessage = await controller.togglePlanActModeWithChatSettings(chatSettings, chatContent)
|
||||
|
||||
return Boolean.create({
|
||||
value: sentMessage,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle Plan/Act mode:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Controller } from ".."
|
||||
import { Boolean } from "@shared/proto/cline/common"
|
||||
import { TogglePlanActModeRequest, PlanActMode } from "@shared/proto/cline/state"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
|
||||
/**
|
||||
* Toggles between Plan and Act modes
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the chat settings and optional chat content
|
||||
* @returns An empty response
|
||||
*/
|
||||
export async function togglePlanActModeProto(controller: Controller, request: TogglePlanActModeRequest): Promise<Boolean> {
|
||||
try {
|
||||
let mode: Mode
|
||||
if (request.mode === PlanActMode.PLAN) {
|
||||
mode = "plan"
|
||||
} else if (request.mode === PlanActMode.ACT) {
|
||||
mode = "act"
|
||||
} else {
|
||||
throw new Error(`Invalid mode value: ${request.mode}`)
|
||||
}
|
||||
const chatContent = request.chatContent
|
||||
|
||||
// Call the existing controller implementation
|
||||
const sentMessage = await controller.togglePlanActMode(mode, chatContent)
|
||||
|
||||
return Boolean.create({
|
||||
value: sentMessage,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle Plan/Act mode:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { UpdateSettingsRequest } from "@shared/proto/cline/state"
|
||||
import { updateApiConfiguration } from "../../storage/state"
|
||||
import { PlanActMode, UpdateSettingsRequest } from "@shared/proto/cline/state"
|
||||
import { buildApiHandler } from "../../../api"
|
||||
import { convertProtoApiConfigurationToApiConfiguration } from "../../../shared/proto-conversions/state/settings-conversion"
|
||||
import { convertProtoChatSettingsToChatSettings } from "../../../shared/proto-conversions/state/chat-settings-conversion"
|
||||
import { TelemetrySetting } from "@/shared/TelemetrySetting"
|
||||
import { OpenaiReasoningEffort } from "@/shared/storage/types"
|
||||
|
||||
/**
|
||||
* Updates multiple extension settings in a single request
|
||||
@@ -18,7 +17,7 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
// Update API configuration
|
||||
if (request.apiConfiguration) {
|
||||
const apiConfiguration = convertProtoApiConfigurationToApiConfiguration(request.apiConfiguration)
|
||||
await updateApiConfiguration(controller.context, apiConfiguration)
|
||||
controller.cacheService.setApiConfiguration(apiConfiguration)
|
||||
|
||||
if (controller.task) {
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
@@ -56,22 +55,26 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
await controller.context.globalState.update("mcpDisplayMode", request.mcpDisplayMode)
|
||||
}
|
||||
|
||||
// Update chat settings
|
||||
if (request.chatSettings) {
|
||||
const chatSettings = convertProtoChatSettingsToChatSettings(request.chatSettings)
|
||||
|
||||
// Store mode to global state
|
||||
if (chatSettings.mode !== undefined) {
|
||||
await controller.context.globalState.update("mode", chatSettings.mode)
|
||||
}
|
||||
|
||||
// Store chat settings (excluding mode) to global state
|
||||
const { mode, ...globalChatSettings } = chatSettings
|
||||
await controller.context.globalState.update("chatSettings", globalChatSettings)
|
||||
|
||||
if (request.mode !== undefined) {
|
||||
const mode = request.mode === PlanActMode.PLAN ? "plan" : "act"
|
||||
if (controller.task) {
|
||||
controller.task.chatSettings = chatSettings
|
||||
controller.task.updateMode(mode)
|
||||
}
|
||||
await controller.context.globalState.update("mode", request.mode)
|
||||
}
|
||||
|
||||
if (request.openaiReasoningEffort !== undefined) {
|
||||
if (controller.task) {
|
||||
controller.task.openaiReasoningEffort = request.openaiReasoningEffort as OpenaiReasoningEffort
|
||||
}
|
||||
await controller.context.globalState.update("openaiReasoningEffort", request.openaiReasoningEffort)
|
||||
}
|
||||
|
||||
if (request.preferredLanguage !== undefined) {
|
||||
if (controller.task) {
|
||||
controller.task.preferredLanguage = request.preferredLanguage
|
||||
}
|
||||
await controller.context.globalState.update("preferredLanguage", request.preferredLanguage)
|
||||
}
|
||||
|
||||
// Update terminal timeout setting
|
||||
@@ -89,6 +92,14 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
await controller.context.globalState.update("terminalOutputLineLimit", Number(request.terminalOutputLineLimit))
|
||||
}
|
||||
|
||||
// Update strict plan mode setting
|
||||
if (request.strictPlanModeEnabled !== undefined) {
|
||||
if (controller.task) {
|
||||
controller.task.updateStrictPlanMode(request.strictPlanModeEnabled)
|
||||
}
|
||||
await controller.context.globalState.update("strictPlanModeEnabled", request.strictPlanModeEnabled)
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Controller } from "../index"
|
||||
import * as proto from "@/shared/proto"
|
||||
import { UpdateTerminalConnectionTimeoutRequest, UpdateTerminalConnectionTimeoutResponse } from "@shared/proto/cline/state"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
|
||||
export async function updateTerminalConnectionTimeout(
|
||||
controller: Controller,
|
||||
request: proto.cline.Int64Request,
|
||||
): Promise<proto.cline.Int64> {
|
||||
const timeoutValue = request.value
|
||||
request: UpdateTerminalConnectionTimeoutRequest,
|
||||
): Promise<UpdateTerminalConnectionTimeoutResponse> {
|
||||
const timeoutMs = request.timeoutMs
|
||||
|
||||
// Update the terminal connection timeout setting in the state
|
||||
await updateGlobalState(controller.context, "shellIntegrationTimeout", timeoutValue)
|
||||
await updateGlobalState(controller.context, "shellIntegrationTimeout", timeoutMs)
|
||||
|
||||
// Broadcast state update to all webviews
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return proto.cline.Int64.create({ value: timeoutValue })
|
||||
return { timeoutMs }
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function deleteTasksWithIds(controller: Controller, request: String
|
||||
options: { modal: true, items: ["Delete"] },
|
||||
})
|
||||
|
||||
if (userChoice === undefined) {
|
||||
if (userChoice.selectedOption !== "Delete") {
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "@shared/proto/cline/common"
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import { telemetryService } from "@services/posthog/PostHogClientProvider"
|
||||
|
||||
/**
|
||||
* Handles task feedback submission (thumbs up/down)
|
||||
|
||||
@@ -4,11 +4,12 @@ import { EmptyRequest, Empty } from "@shared/proto/cline/common"
|
||||
import { getAllExtensionState, getGlobalState, updateGlobalState } from "../../storage/state"
|
||||
import { sendOpenRouterModelsEvent } from "../models/subscribeToOpenRouterModels"
|
||||
import { sendMcpMarketplaceCatalogEvent } from "../mcp/subscribeToMcpMarketplaceCatalog"
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import { telemetryService } from "@/services/posthog/PostHogClientProvider"
|
||||
import { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
|
||||
import { McpMarketplaceCatalog } from "@shared/mcp"
|
||||
import { refreshOpenRouterModels } from "../models/refreshOpenRouterModels"
|
||||
import { refreshGroqModels } from "../models/refreshGroqModels"
|
||||
import { refreshBasetenModels } from "../models/refreshBasetenModels"
|
||||
|
||||
/**
|
||||
* Initialize webview when it launches
|
||||
@@ -32,7 +33,8 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
refreshOpenRouterModels(controller, 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, planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const apiConfiguration = controller.cacheService.getApiConfiguration()
|
||||
const { planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
@@ -42,26 +44,32 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
const modelId = apiConfiguration[modelIdField]
|
||||
|
||||
if (modelId && response.models[modelId]) {
|
||||
await updateGlobalState(controller.context, modelInfoField, response.models[modelId])
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
[modelInfoField]: response.models[modelId],
|
||||
}
|
||||
controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
} else {
|
||||
// Shared models: update both plan and act modes
|
||||
const planModelId = apiConfiguration.planModeOpenRouterModelId
|
||||
const actModelId = apiConfiguration.actModeOpenRouterModelId
|
||||
const updatedConfig = { ...apiConfiguration }
|
||||
|
||||
// Update plan mode model info if we have a model ID
|
||||
if (planModelId && response.models[planModelId]) {
|
||||
await updateGlobalState(controller.context, "planModeOpenRouterModelInfo", response.models[planModelId])
|
||||
updatedConfig.planModeOpenRouterModelInfo = response.models[planModelId]
|
||||
}
|
||||
|
||||
// Update act mode model info if we have a model ID
|
||||
if (actModelId && response.models[actModelId]) {
|
||||
await updateGlobalState(controller.context, "actModeOpenRouterModelInfo", response.models[actModelId])
|
||||
updatedConfig.actModeOpenRouterModelInfo = response.models[actModelId]
|
||||
}
|
||||
|
||||
// Post state update if we updated any model info
|
||||
if ((planModelId && response.models[planModelId]) || (actModelId && response.models[actModelId])) {
|
||||
controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
}
|
||||
@@ -71,7 +79,8 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
refreshGroqModels(controller, EmptyRequest.create()).then(async (response) => {
|
||||
if (response && response.models) {
|
||||
// Update model info in state for Groq (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, planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const apiConfiguration = controller.cacheService.getApiConfiguration()
|
||||
const { planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
@@ -81,22 +90,67 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
const modelId = apiConfiguration[modelIdField]
|
||||
|
||||
if (modelId && response.models[modelId]) {
|
||||
await updateGlobalState(controller.context, modelInfoField, response.models[modelId])
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
[modelInfoField]: response.models[modelId],
|
||||
}
|
||||
controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
} else {
|
||||
// Shared models: update both plan and act modes
|
||||
const planModelId = apiConfiguration.planModeGroqModelId
|
||||
const actModelId = apiConfiguration.actModeGroqModelId
|
||||
const updatedConfig = { ...apiConfiguration }
|
||||
|
||||
// Update plan mode model info if we have a model ID
|
||||
if (planModelId && response.models[planModelId]) {
|
||||
await updateGlobalState(controller.context, "planModeGroqModelInfo", response.models[planModelId])
|
||||
updatedConfig.planModeGroqModelInfo = response.models[planModelId]
|
||||
}
|
||||
|
||||
// Update act mode model info if we have a model ID
|
||||
if (actModelId && response.models[actModelId]) {
|
||||
await updateGlobalState(controller.context, "actModeGroqModelInfo", response.models[actModelId])
|
||||
updatedConfig.actModeGroqModelInfo = response.models[actModelId]
|
||||
}
|
||||
|
||||
// Post state update if we updated any model info
|
||||
if ((planModelId && response.models[planModelId]) || (actModelId && response.models[actModelId])) {
|
||||
controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
refreshBasetenModels(controller, EmptyRequest.create()).then(async (response) => {
|
||||
if (response && response.models) {
|
||||
// Update model info in state for Baseten (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, planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
// Separate models: update only current mode
|
||||
const modelIdField = currentMode === "plan" ? "planModeBasetenModelId" : "actModeBasetenModelId"
|
||||
const modelInfoField = currentMode === "plan" ? "planModeBasetenModelInfo" : "actModeBasetenModelInfo"
|
||||
const modelId = apiConfiguration[modelIdField]
|
||||
|
||||
if (modelId && response.models[modelId]) {
|
||||
await updateGlobalState(controller.context, modelInfoField, response.models[modelId])
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
} else {
|
||||
// Shared models: update both plan and act modes
|
||||
const planModelId = apiConfiguration.planModeBasetenModelId
|
||||
const actModelId = apiConfiguration.actModeBasetenModelId
|
||||
|
||||
// Update plan mode model info if we have a model ID
|
||||
if (planModelId && response.models[planModelId]) {
|
||||
await updateGlobalState(controller.context, "planModeBasetenModelInfo", response.models[planModelId])
|
||||
}
|
||||
|
||||
// Update act mode model info if we have a model ID
|
||||
if (actModelId && response.models[actModelId]) {
|
||||
await updateGlobalState(controller.context, "actModeBasetenModelInfo", response.models[actModelId])
|
||||
}
|
||||
|
||||
// Post state update if we updated any model info
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Boolean } from "@shared/proto/cline/common"
|
||||
import type { Controller } from "../index"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
import { getLatestAnnouncementId } from "@/utils/announcements"
|
||||
|
||||
/**
|
||||
* Marks the current announcement as shown
|
||||
@@ -12,8 +13,9 @@ import { updateGlobalState } from "../../storage/state"
|
||||
*/
|
||||
export async function onDidShowAnnouncement(controller: Controller, _request: EmptyRequest): Promise<Boolean> {
|
||||
try {
|
||||
const latestAnnouncementId = getLatestAnnouncementId(controller.context)
|
||||
// Update the lastShownAnnouncementId to the current latestAnnouncementId
|
||||
await updateGlobalState(controller.context, "lastShownAnnouncementId", controller.latestAnnouncementId)
|
||||
await updateGlobalState(controller.context, "lastShownAnnouncementId", latestAnnouncementId)
|
||||
return Boolean.create({ value: false })
|
||||
} catch (error) {
|
||||
console.error("Failed to acknowledge announcement:", error)
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as vscode from "vscode"
|
||||
import type { Controller } from "../index"
|
||||
import type { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { telemetryService } from "../../../services/posthog/telemetry/TelemetryService"
|
||||
import { telemetryService } from "@services/posthog/PostHogClientProvider"
|
||||
|
||||
/**
|
||||
* Opens the Cline walkthrough in VSCode
|
||||
|
||||
@@ -1,33 +1,42 @@
|
||||
import { Controller } from "../index"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { String as ProtoString } from "@shared/proto/cline/common"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
|
||||
import type { String as ProtoString, StringRequest } from "@shared/proto/cline/common"
|
||||
import { getRequestRegistry, type StreamingResponseHandler } from "../grpc-handler"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
// Keep track of active addToInput subscriptions
|
||||
const activeAddToInputSubscriptions = new Set<StreamingResponseHandler<ProtoString>>()
|
||||
|
||||
// Map client IDs to their subscription handlers for targeted sending
|
||||
const addToInputSubscriptions = new Map<string, StreamingResponseHandler<ProtoString>>()
|
||||
|
||||
/**
|
||||
* Subscribe to addToInput events
|
||||
* @param controller The controller instance
|
||||
* @param request The empty request
|
||||
* @param request The request containing the client ID
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request (passed by the gRPC handler)
|
||||
*/
|
||||
export async function subscribeToAddToInput(
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
request: StringRequest,
|
||||
responseStream: StreamingResponseHandler<ProtoString>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
console.log("[DEBUG] set up addToInput subscription")
|
||||
const clientId = request.value
|
||||
if (!clientId) {
|
||||
throw new Error("Client ID is required for addToInput subscription")
|
||||
}
|
||||
|
||||
// Add this subscription to the active subscriptions
|
||||
console.log("[DEBUG] set up addToInput subscription for client:", clientId)
|
||||
|
||||
// Add this subscription to both the general set and the client-specific map
|
||||
activeAddToInputSubscriptions.add(responseStream)
|
||||
addToInputSubscriptions.set(clientId, responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeAddToInputSubscriptions.delete(responseStream)
|
||||
console.log("[DEBUG] Cleaned up addToInput subscription")
|
||||
addToInputSubscriptions.delete(clientId)
|
||||
console.log("[DEBUG] Cleaned up addToInput subscription for client:", clientId)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
@@ -61,3 +70,33 @@ export async function sendAddToInputEvent(text: string): Promise<void> {
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an addToInput event to a specific webview by client ID
|
||||
* @param clientId The ID of the client to send the event to
|
||||
* @param text The text to add to the input
|
||||
*/
|
||||
export async function sendAddToInputEventToClient(clientId: string, text: string): Promise<void> {
|
||||
const responseStream = addToInputSubscriptions.get(clientId)
|
||||
if (!responseStream) {
|
||||
console.warn(`No addToInput subscription found for client ID: ${clientId}`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const event: ProtoString = {
|
||||
value: text,
|
||||
}
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
console.log("[DEBUG] sending addToInput event to client", clientId, ":", text.length, "chars")
|
||||
} catch (error) {
|
||||
console.error(`Error sending addToInput event to client ${clientId}:`, error)
|
||||
// Remove the subscription if there was an error
|
||||
addToInputSubscriptions.delete(clientId)
|
||||
// Also remove from the general set
|
||||
activeAddToInputSubscriptions.delete(responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { openExternal } from "@utils/env"
|
||||
* @param request The URL to open
|
||||
* @returns Empty response since the client doesn't need a return value
|
||||
*/
|
||||
export async function openInBrowser(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
export async function openInBrowser(_controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
try {
|
||||
if (request.value) {
|
||||
await openExternal(request.value)
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { setVscodeHostProviderMock } from "@/test/host-provider-test-utils"
|
||||
import { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
|
||||
import * as extractTextModule from "@integrations/misc/extract-text"
|
||||
import * as terminalModule from "@integrations/terminal/get-latest-output"
|
||||
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
|
||||
import * as gitModule from "@utils/git"
|
||||
import { expect } from "chai"
|
||||
import * as fs from "fs"
|
||||
import * as isBinaryFileModule from "isbinaryfile"
|
||||
import * as path from "path"
|
||||
import * as sinon from "sinon"
|
||||
import { parseMentions } from "../index"
|
||||
|
||||
describe("parseMentions", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let urlContentFetcherStub: sinon.SinonStubbedInstance<UrlContentFetcher>
|
||||
let fileContextTrackerStub: sinon.SinonStubbedInstance<FileContextTracker>
|
||||
let fsStatStub: sinon.SinonStub
|
||||
let fsReaddirStub: sinon.SinonStub
|
||||
let extractTextStub: sinon.SinonStub
|
||||
let isBinaryFileStub: sinon.SinonStub
|
||||
let getLatestTerminalOutputStub: sinon.SinonStub
|
||||
let getWorkingStateStub: sinon.SinonStub
|
||||
let getCommitInfoStub: sinon.SinonStub
|
||||
let showMessageStub: sinon.SinonStub
|
||||
|
||||
const cwd = "/test/project"
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
setVscodeHostProviderMock()
|
||||
// Create stubs for dependencies
|
||||
urlContentFetcherStub = {
|
||||
launchBrowser: sandbox.stub().resolves(),
|
||||
closeBrowser: sandbox.stub().resolves(),
|
||||
urlToMarkdown: sandbox.stub().resolves("# Example Website\n\nContent here"),
|
||||
} as any
|
||||
|
||||
fileContextTrackerStub = {
|
||||
trackFileContext: sandbox.stub().resolves(),
|
||||
} as any
|
||||
|
||||
// Stub file system operations using fs.promises
|
||||
fsStatStub = sandbox.stub(fs.promises, "stat")
|
||||
fsReaddirStub = sandbox.stub(fs.promises, "readdir")
|
||||
|
||||
// Stub other modules
|
||||
extractTextStub = sandbox.stub(extractTextModule, "extractTextFromFile")
|
||||
isBinaryFileStub = sandbox.stub(isBinaryFileModule, "isBinaryFile")
|
||||
getLatestTerminalOutputStub = sandbox.stub(terminalModule, "getLatestTerminalOutput")
|
||||
getWorkingStateStub = sandbox.stub(gitModule, "getWorkingState")
|
||||
getCommitInfoStub = sandbox.stub(gitModule, "getCommitInfo")
|
||||
showMessageStub = sandbox.stub(HostProvider.window, "showMessage")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe("File mentions", () => {
|
||||
it("should handle simple file mention", async () => {
|
||||
const text = "Check @/src/index.ts for details"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.resolves("console.log('Hello World');")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub, fileContextTrackerStub)
|
||||
|
||||
const expectedOutput = `Check 'src/index.ts' (see below for file content) for details
|
||||
|
||||
<file_content path="src/index.ts">
|
||||
console.log('Hello World');
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
expect(fileContextTrackerStub.trackFileContext.calledWith("src/index.ts", "file_mentioned")).to.be.true
|
||||
})
|
||||
|
||||
it("should handle quoted file paths with spaces", async () => {
|
||||
const text = 'Open @"/path with spaces/file.txt"'
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.resolves("console.log('Hello World');")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Open 'path with spaces/file.txt' (see below for file content)
|
||||
|
||||
<file_content path="path with spaces/file.txt">
|
||||
console.log('Hello World');
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
|
||||
it("should handle binary files", async () => {
|
||||
const text = "Check @/image.png"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(true)
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Check 'image.png' (see below for file content)
|
||||
|
||||
<file_content path="image.png">
|
||||
(Binary file, unable to display content)
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
|
||||
it("should handle file read errors", async () => {
|
||||
const text = "Check @/missing.txt"
|
||||
|
||||
fsStatStub.rejects(new Error("ENOENT: no such file or directory"))
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Check 'missing.txt' (see below for file content)
|
||||
|
||||
<file_content path="missing.txt">
|
||||
Error fetching content: Failed to access path "missing.txt": ENOENT: no such file or directory
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Folder mentions", () => {
|
||||
it("should handle folder mention", async () => {
|
||||
const text = "Look in @/src/ folder"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => false, isDirectory: () => true })
|
||||
fsReaddirStub.resolves([
|
||||
{ name: "index.ts", isFile: () => true, isDirectory: () => false },
|
||||
{ name: "utils", isFile: () => false, isDirectory: () => true },
|
||||
{ name: "README.md", isFile: () => true, isDirectory: () => false },
|
||||
])
|
||||
|
||||
// Set up file content stubs
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.withArgs(path.resolve(cwd, "src/index.ts")).resolves("export const main = () => {};")
|
||||
extractTextStub.withArgs(path.resolve(cwd, "src/README.md")).resolves("# Source Code")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Look in 'src/' (see below for folder content) folder
|
||||
|
||||
<folder_content path="src/">
|
||||
├── index.ts
|
||||
├── utils/
|
||||
└── README.md
|
||||
|
||||
<file_content path="src/index.ts">
|
||||
export const main = () => {};
|
||||
</file_content>
|
||||
|
||||
<file_content path="src/README.md">
|
||||
# Source Code
|
||||
</file_content>
|
||||
</folder_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
})
|
||||
|
||||
describe("URL mentions", () => {
|
||||
it("should handle URL mention", async () => {
|
||||
const text = "Visit @https://example.com for info"
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Visit 'https://example.com' (see below for site content) for info
|
||||
|
||||
<url_content url="https://example.com">
|
||||
# Example Website
|
||||
|
||||
Content here
|
||||
</url_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
expect(urlContentFetcherStub.launchBrowser.called).to.be.true
|
||||
expect(urlContentFetcherStub.urlToMarkdown.calledWith("https://example.com")).to.be.true
|
||||
expect(urlContentFetcherStub.closeBrowser.called).to.be.true
|
||||
})
|
||||
|
||||
it("should handle browser launch errors", async () => {
|
||||
const text = "Visit @https://example.com"
|
||||
|
||||
urlContentFetcherStub.launchBrowser.rejects(new Error("Browser launch failed"))
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Visit 'https://example.com' (see below for site content)
|
||||
|
||||
<url_content url="https://example.com">
|
||||
Error fetching content: Browser launch failed
|
||||
</url_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
expect(showMessageStub.called).to.be.true
|
||||
})
|
||||
|
||||
it("should handle URL fetch errors", async () => {
|
||||
const text = "Visit @https://example.com"
|
||||
|
||||
urlContentFetcherStub.urlToMarkdown.rejects(new Error("Network error"))
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Visit 'https://example.com' (see below for site content)
|
||||
|
||||
<url_content url="https://example.com">
|
||||
Error fetching content: Network error
|
||||
</url_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
expect(showMessageStub.called).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
describe("Special mentions", () => {
|
||||
it("should handle @terminal mention", async () => {
|
||||
const text = "See @terminal output"
|
||||
|
||||
getLatestTerminalOutputStub.resolves("$ npm test\nAll tests passed!")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `See Terminal Output (see below for output) output
|
||||
|
||||
<terminal_output>
|
||||
$ npm test
|
||||
All tests passed!
|
||||
</terminal_output>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
|
||||
it("should handle @git-changes mention", async () => {
|
||||
const text = "Review @git-changes"
|
||||
|
||||
getWorkingStateStub.resolves("M src/index.ts\nA src/new-file.ts")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Review Working directory changes (see below for details)
|
||||
|
||||
<git_working_state>
|
||||
M src/index.ts
|
||||
A src/new-file.ts
|
||||
</git_working_state>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
|
||||
it("should handle git commit hash mention", async () => {
|
||||
const text = "See commit @abcdef1234567890"
|
||||
|
||||
getCommitInfoStub.resolves("commit abcdef1234567890\nAuthor: Test\nDate: 2024-01-01\n\nInitial commit")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `See commit Git commit 'abcdef1234567890' (see below for commit info)
|
||||
|
||||
<git_commit hash="abcdef1234567890">
|
||||
commit abcdef1234567890
|
||||
Author: Test
|
||||
Date: 2024-01-01
|
||||
|
||||
Initial commit
|
||||
</git_commit>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Multiple mentions", () => {
|
||||
it("should handle multiple mentions in order", async () => {
|
||||
const text = "Check @/file1.txt and @/file2.txt"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.withArgs(path.resolve(cwd, "file1.txt")).resolves("Content 1")
|
||||
extractTextStub.withArgs(path.resolve(cwd, "file2.txt")).resolves("Content 2")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Check 'file1.txt' (see below for file content) and 'file2.txt' (see below for file content)
|
||||
|
||||
<file_content path="file1.txt">
|
||||
Content 1
|
||||
</file_content>
|
||||
|
||||
<file_content path="file2.txt">
|
||||
Content 2
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
|
||||
it("should handle duplicate mentions only once", async () => {
|
||||
const text = "Check @/file.txt and again @/file.txt"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.resolves("Content")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Check 'file.txt' (see below for file content) and again 'file.txt' (see below for file content)
|
||||
|
||||
<file_content path="file.txt">
|
||||
Content
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
|
||||
it("should handle mixed mention types", async () => {
|
||||
const text = "Check @/file.txt, and @https://example.com"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.resolves("File content")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Check 'file.txt' (see below for file content), and 'https://example.com' (see below for site content)
|
||||
|
||||
<file_content path="file.txt">
|
||||
File content
|
||||
</file_content>
|
||||
|
||||
<url_content url="https://example.com">
|
||||
# Example Website
|
||||
|
||||
Content here
|
||||
</url_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Error handling", () => {
|
||||
it("should handle errors for each mention type gracefully", async () => {
|
||||
const text = "@/error.txt @terminal @git-changes @abc1234567"
|
||||
|
||||
fsStatStub.rejects(new Error("File error"))
|
||||
getLatestTerminalOutputStub.rejects(new Error("Terminal error"))
|
||||
getWorkingStateStub.rejects(new Error("Git state error"))
|
||||
getCommitInfoStub.rejects(new Error("Commit error"))
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `'error.txt' (see below for file content) Terminal Output (see below for output) Working directory changes (see below for details) Git commit 'abc1234567' (see below for commit info)
|
||||
|
||||
<file_content path="error.txt">
|
||||
Error fetching content: Failed to access path "error.txt": File error
|
||||
</file_content>
|
||||
|
||||
<terminal_output>
|
||||
Error fetching terminal output: Terminal error
|
||||
</terminal_output>
|
||||
|
||||
<git_working_state>
|
||||
Error fetching working state: Git state error
|
||||
</git_working_state>
|
||||
|
||||
<git_commit hash="abc1234567">
|
||||
Error fetching commit info: Commit error
|
||||
</git_commit>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge cases", () => {
|
||||
it("should handle text with no mentions", async () => {
|
||||
const text = "This is plain text without any mentions"
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
expect(result).to.equal(text)
|
||||
})
|
||||
|
||||
it("should handle empty text", async () => {
|
||||
const result = await parseMentions("", cwd, urlContentFetcherStub)
|
||||
|
||||
expect(result).to.equal("")
|
||||
})
|
||||
|
||||
it("should handle mentions with trailing punctuation", async () => {
|
||||
const text = "Check @/file.txt!"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.resolves("Content")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Check 'file.txt' (see below for file content)!
|
||||
|
||||
<file_content path="file.txt">
|
||||
Content
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
})
|
||||
})
|
||||
+21
-14
@@ -6,7 +6,7 @@ import { mentionRegexGlobal } from "@shared/context-mentions"
|
||||
import fs from "fs/promises"
|
||||
import { extractTextFromFile } from "@integrations/misc/extract-text"
|
||||
import { isBinaryFile } from "isbinaryfile"
|
||||
import { diagnosticsToProblemsString } from "@integrations/diagnostics"
|
||||
import { getWorkspaceProblemsString } from "@/integrations/diagnostics"
|
||||
import { getLatestTerminalOutput } from "@integrations/terminal/get-latest-output"
|
||||
import { getCommitInfo } from "@utils/git"
|
||||
import { getWorkingState } from "@utils/git"
|
||||
@@ -14,7 +14,7 @@ import { FileContextTracker } from "../context/context-tracking/FileContextTrack
|
||||
import { getCwd } from "@/utils/path"
|
||||
import { openExternal } from "@utils/env"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
export async function openMention(mention?: string): Promise<void> {
|
||||
if (!mention) {
|
||||
@@ -26,8 +26,8 @@ export async function openMention(mention?: string): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
if (mention.startsWith("/")) {
|
||||
const relPath = mention.slice(1)
|
||||
if (isFileMention(mention)) {
|
||||
const relPath = getFilePathFromMention(mention)
|
||||
const absPath = path.resolve(cwd, relPath)
|
||||
if (mention.endsWith("/")) {
|
||||
vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath))
|
||||
@@ -54,8 +54,8 @@ export async function parseMentions(
|
||||
mentions.add(mention)
|
||||
if (mention.startsWith("http")) {
|
||||
return `'${mention}' (see below for site content)`
|
||||
} else if (mention.startsWith("/")) {
|
||||
const mentionPath = mention.slice(1) // Remove the leading '/'
|
||||
} else if (isFileMention(mention)) {
|
||||
const mentionPath = getFilePathFromMention(mention)
|
||||
return mentionPath.endsWith("/")
|
||||
? `'${mentionPath}' (see below for folder content)`
|
||||
: `'${mentionPath}' (see below for file content)`
|
||||
@@ -106,8 +106,8 @@ export async function parseMentions(
|
||||
}
|
||||
}
|
||||
parsedText += `\n\n<url_content url="${mention}">\n${result}\n</url_content>`
|
||||
} else if (mention.startsWith("/")) {
|
||||
const mentionPath = mention.slice(1)
|
||||
} else if (isFileMention(mention)) {
|
||||
const mentionPath = getFilePathFromMention(mention)
|
||||
try {
|
||||
const content = await getFileOrFolderContent(mentionPath, cwd)
|
||||
if (mention.endsWith("/")) {
|
||||
@@ -225,10 +225,17 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise
|
||||
}
|
||||
|
||||
async function getWorkspaceProblems(): Promise<string> {
|
||||
const diagnostics = vscode.languages.getDiagnostics()
|
||||
const result = diagnosticsToProblemsString(diagnostics, [vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning])
|
||||
if (!result) {
|
||||
return "No errors or warnings detected."
|
||||
}
|
||||
return result
|
||||
return await getWorkspaceProblemsString()
|
||||
}
|
||||
|
||||
function isFileMention(mention: string): boolean {
|
||||
return mention.startsWith("/") || mention.startsWith('"/')
|
||||
}
|
||||
|
||||
function getFilePathFromMention(mention: string): string {
|
||||
// Remove quotes
|
||||
const match = mention.match(/^"(.*)"$/)
|
||||
const filePath = match ? match[1] : mention
|
||||
// Remove leading slash
|
||||
return filePath.slice(1)
|
||||
}
|
||||
|
||||
@@ -337,7 +337,6 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
|
||||
accessMcpResourceToolDefinition,
|
||||
loadMcpDocumentationTool,
|
||||
newTaskToolDefinition,
|
||||
editToolDefinition,
|
||||
]
|
||||
if (supportsBrowserUse) {
|
||||
tools.push(browserActionTool)
|
||||
|
||||
@@ -268,14 +268,15 @@ Usage:
|
||||
</new_task>
|
||||
|
||||
## plan_mode_respond
|
||||
Description: Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should ONLY be used when you have already explored the relevant files and are ready to present a concrete plan. DO NOT use this tool to announce what files you're going to read - just read them first. This tool is only available in PLAN MODE. The environment_details will specify the current mode, if it is not PLAN_MODE then you should not use this tool. For example, if the user's task is to create a website, you may start by asking some clarifying questions with the ask_followup_question tool if their message was vague, explore the codebase, read files, then present a detailed plan for how you will accomplish the task given the context, and perhaps engage in a back and forth to finalize the details before the user switches you to ACT_MODE to implement the solution.
|
||||
CRITICAL: You must complete your information gathering (reading files, exploring the codebase) BEFORE using this tool. The user expects to see a well thought-out plan based on actual analysis, not intentions.
|
||||
|
||||
Description: Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should ONLY be used when you have already explored the relevant files and are ready to present a concrete plan. DO NOT use this tool to announce what files you're going to read - just read them first. This tool is only available in PLAN MODE. The environment_details will specify the current mode; if it is not PLAN_MODE then you should not use this tool.
|
||||
However, if while writing your response you realize you actually need to do more exploration before providing a complete plan, you can add the optional needs_more_exploration parameter to indicate this. This allows you to acknowledge that you should have done more exploration first, and signals that your next message will use exploration tools instead.
|
||||
Parameters:
|
||||
- response: (required) The response to provide to the user. Do not try to use tools in this parameter, this is simply a chat response. (You MUST use the response parameter, do not simply place the response text directly within <plan_mode_respond> tags.)
|
||||
- needs_more_exploration: (optional) Set to true if while formulating your response that you found you need to do more exploration with tools, for example reading files. (Remember, you can explore the project with tools like read_file in PLAN MODE without the user having to toggle to ACT MODE.) Defaults to false if not specified.
|
||||
Usage:
|
||||
<plan_mode_respond>
|
||||
<response>Your response here</response>
|
||||
<needs_more_exploration>true or false (optional, but you MUST set to true if in <response> you need to read files or use other exploration tools)</needs_more_exploration>
|
||||
</plan_mode_respond>
|
||||
|
||||
## load_mcp_documentation
|
||||
@@ -619,6 +620,7 @@ RULES
|
||||
- Be sure to consider the type of project (e.g. Python, JavaScript, web application) when determining the appropriate structure and files to include. Also consider what files may be most relevant to accomplishing the task, for example looking at a project's manifest file would help you understand the project's dependencies, which you could incorporate into any code you write.
|
||||
- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.
|
||||
- When you want to modify a file, use the replace_in_file or write_to_file tool directly with the desired changes. You do not need to display the changes before using the tool.
|
||||
- Use Markdown **only where semantically correct** (e.g., \`inline code\`, \`\`\`code fences\`\`\`, lists, tables). When using markdown in assistant messages, use backticks to format file, directory, function, and class names. Use \( and \) for inline math, \[ and \] for block math.
|
||||
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
|
||||
- You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. However if you can use the available tools to avoid having to ask the user questions, you should do so. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.
|
||||
- When the user is being vague, you should be proactive about asking clarifying questions using the ask_followup_question tool to ensure you understand their request. However, if you can infer the user's intent based on the context and available tools, you should proceed without asking unnecessary questions
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import * as diff from "diff"
|
||||
import * as path from "path"
|
||||
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreController"
|
||||
import { Mode } from "@/shared/ChatSettings"
|
||||
import { Mode } from "@/shared/storage/types"
|
||||
|
||||
export const formatResponse = {
|
||||
duplicateFileReadNotice: () =>
|
||||
|
||||
+11
-10
@@ -5,7 +5,7 @@ import { McpHub } from "@services/mcp/McpHub"
|
||||
import { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { SYSTEM_PROMPT_CLAUDE4_EXPERIMENTAL } from "@core/prompts/model_prompts/claude4-experimental"
|
||||
import { SYSTEM_PROMPT_CLAUDE4 } from "@core/prompts/model_prompts/claude4"
|
||||
import { USE_EXPERIMENTAL_CLAUDE4_FEATURES } from "@core/task/index";
|
||||
import { USE_EXPERIMENTAL_CLAUDE4_FEATURES } from "@core/task/index"
|
||||
|
||||
export const SYSTEM_PROMPT = async (
|
||||
cwd: string,
|
||||
@@ -14,14 +14,13 @@ export const SYSTEM_PROMPT = async (
|
||||
browserSettings: BrowserSettings,
|
||||
isNextGenModel: boolean = false,
|
||||
) => {
|
||||
|
||||
if (isNextGenModel && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
|
||||
return SYSTEM_PROMPT_CLAUDE4_EXPERIMENTAL(cwd, supportsBrowserUse, mcpHub, browserSettings)
|
||||
}
|
||||
|
||||
if (isNextGenModel) {
|
||||
return SYSTEM_PROMPT_CLAUDE4(cwd, supportsBrowserUse, mcpHub, browserSettings)
|
||||
}
|
||||
if (isNextGenModel) {
|
||||
return SYSTEM_PROMPT_CLAUDE4(cwd, supportsBrowserUse, mcpHub, browserSettings)
|
||||
}
|
||||
|
||||
return `You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
|
||||
|
||||
@@ -264,12 +263,15 @@ Usage:
|
||||
</new_task>
|
||||
|
||||
## plan_mode_respond
|
||||
Description: Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should be used when you need to provide a response to a question or statement from the user about how you plan to accomplish the task. This tool is only available in PLAN MODE. The environment_details will specify the current mode, if it is not PLAN MODE then you should not use this tool. Depending on the user's message, you may ask questions to get clarification about the user's request, architect a solution to the task, and to brainstorm ideas with the user. For example, if the user's task is to create a website, you may start by asking some clarifying questions, then present a detailed plan for how you will accomplish the task given the context, and perhaps engage in a back and forth to finalize the details before the user switches you to ACT MODE to implement the solution.
|
||||
Description: Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should ONLY be used when you have already explored the relevant files and are ready to present a concrete plan. DO NOT use this tool to announce what files you're going to read - just read them first. This tool is only available in PLAN MODE. The environment_details will specify the current mode; if it is not PLAN_MODE then you should not use this tool.
|
||||
However, if while writing your response you realize you actually need to do more exploration before providing a complete plan, you can add the optional needs_more_exploration parameter to indicate this. This allows you to acknowledge that you should have done more exploration first, and signals that your next message will use exploration tools instead.
|
||||
Parameters:
|
||||
- response: (required) The response to provide to the user. Do not try to use tools in this parameter, this is simply a chat response. (You MUST use the response parameter, do not simply place the response text directly within <plan_mode_respond> tags.)
|
||||
- needs_more_exploration: (optional) Set to true if while formulating your response that you found you need to do more exploration with tools, for example reading files. (Remember, you can explore the project with tools like read_file in PLAN MODE without the user having to toggle to ACT MODE.) Defaults to false if not specified.
|
||||
Usage:
|
||||
<plan_mode_respond>
|
||||
<response>Your response here</response>
|
||||
<needs_more_exploration>true or false (optional, but you MUST set to true if in <response> you need to read files or use other exploration tools)</needs_more_exploration>
|
||||
</plan_mode_respond>
|
||||
|
||||
## load_mcp_documentation
|
||||
@@ -554,7 +556,7 @@ You have access to two tools for working with files: **write_to_file** and **rep
|
||||
By thoughtfully selecting between write_to_file and replace_in_file, you can make your file editing process smoother, safer, and more efficient.
|
||||
|
||||
====
|
||||
|
||||
|
||||
ACT MODE V.S. PLAN MODE
|
||||
|
||||
In each user message, the environment_details will specify the current mode. There are two modes:
|
||||
@@ -568,8 +570,8 @@ In each user message, the environment_details will specify the current mode. The
|
||||
## What is PLAN MODE?
|
||||
|
||||
- While you are usually in ACT MODE, the user may switch to PLAN MODE in order to have a back and forth with you to plan how to best accomplish the task.
|
||||
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task.
|
||||
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task.
|
||||
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions with ask_followup_question to get a better understanding of the task.
|
||||
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task. Present the plan to the user using the plan_mode_respond tool.
|
||||
- Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
|
||||
- Finally once it seems like you've reached a good plan, ask the user to switch you back to ACT MODE to implement the solution.
|
||||
|
||||
@@ -650,7 +652,6 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
|
||||
5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`
|
||||
}
|
||||
|
||||
|
||||
export function addUserInstructions(
|
||||
globalClineRulesFileInstructions?: string,
|
||||
localClineRulesFileInstructions?: string,
|
||||
|
||||
@@ -0,0 +1,965 @@
|
||||
import { ApiConfiguration } from "@shared/api"
|
||||
import { updateGlobalState, updateWorkspaceState, getAllExtensionState, storeSecret } from "./state"
|
||||
import { SecretKey, GlobalStateKey, LocalStateKey } from "./state-keys"
|
||||
import { CACHE_SERVICE_NOT_INITIALIZED } from "./error-messages"
|
||||
import type { ExtensionContext } from "vscode"
|
||||
|
||||
/**
|
||||
* Interface for persistence error event data
|
||||
*/
|
||||
export interface PersistenceErrorEvent {
|
||||
error: Error
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory cache service for fast state access
|
||||
* Provides immediate reads/writes with async disk persistence
|
||||
*/
|
||||
export class CacheService {
|
||||
private globalStateCache: Map<GlobalStateKey, any> = new Map()
|
||||
private secretsCache: Map<SecretKey, string | undefined> = new Map()
|
||||
private workspaceStateCache: Map<LocalStateKey, any> = new Map()
|
||||
private context: ExtensionContext
|
||||
private isInitialized = false
|
||||
|
||||
// Debounced persistence state
|
||||
private pendingGlobalState = new Set<GlobalStateKey>()
|
||||
private pendingSecrets = new Set<SecretKey>()
|
||||
private pendingWorkspaceState = new Set<LocalStateKey>()
|
||||
private persistenceTimeout: NodeJS.Timeout | null = null
|
||||
private readonly PERSISTENCE_DELAY_MS = 500
|
||||
|
||||
// Callback for persistence errors
|
||||
onPersistenceError?: (event: PersistenceErrorEvent) => void
|
||||
|
||||
constructor(context: ExtensionContext) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the cache by loading data from disk
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
// Load API configuration and populate cache with component keys
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
if (apiConfiguration) {
|
||||
// Populate the caches with the API configuration component keys
|
||||
// Use populate method to avoid triggering persistence during initialization
|
||||
this.populateApiConfigurationCache(apiConfiguration)
|
||||
}
|
||||
|
||||
this.isInitialized = true
|
||||
console.log("CacheService initialized successfully")
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize CacheService:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set method for global state keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setGlobalState<T>(key: GlobalStateKey, value: T): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for instant access
|
||||
this.globalStateCache.set(key, value)
|
||||
|
||||
// Add to pending persistence set and schedule debounced write
|
||||
this.pendingGlobalState.add(key)
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch set method for global state keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setGlobalStateBatch(updates: Partial<Record<GlobalStateKey, any>>): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for all keys
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
this.globalStateCache.set(key as GlobalStateKey, value)
|
||||
this.pendingGlobalState.add(key as GlobalStateKey)
|
||||
})
|
||||
|
||||
// Schedule debounced persistence
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set method for secret keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setSecret(key: SecretKey, value: string | undefined): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for instant access
|
||||
this.secretsCache.set(key, value)
|
||||
|
||||
// Add to pending persistence set and schedule debounced write
|
||||
this.pendingSecrets.add(key)
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch set method for secret keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setSecretsBatch(updates: Partial<Record<SecretKey, string | undefined>>): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for all keys
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
this.secretsCache.set(key as SecretKey, value)
|
||||
this.pendingSecrets.add(key as SecretKey)
|
||||
})
|
||||
|
||||
// Schedule debounced persistence
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set method for workspace state keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setWorkspaceState<T>(key: LocalStateKey, value: T): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for instant access
|
||||
this.workspaceStateCache.set(key, value)
|
||||
|
||||
// Add to pending persistence set and schedule debounced write
|
||||
this.pendingWorkspaceState.add(key)
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch set method for workspace state keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setWorkspaceStateBatch(updates: Partial<Record<LocalStateKey, any>>): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for all keys
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
this.workspaceStateCache.set(key as LocalStateKey, value)
|
||||
this.pendingWorkspaceState.add(key as LocalStateKey)
|
||||
})
|
||||
|
||||
// Schedule debounced persistence
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for getting API configuration
|
||||
* Ensures cache is initialized if not already done
|
||||
*/
|
||||
getApiConfiguration(): ApiConfiguration {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Construct API configuration from cached component keys
|
||||
return this.constructApiConfigurationFromCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for setting API configuration
|
||||
*/
|
||||
setApiConfiguration(apiConfiguration: ApiConfiguration): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
const {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsBedrockApiKey,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiKey,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
geminiBaseUrl,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
fireworksApiKey,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
basetenApiKey,
|
||||
huggingFaceApiKey,
|
||||
requestTimeoutMs,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeBasetenModelId,
|
||||
planModeBasetenModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeBasetenModelId,
|
||||
actModeBasetenModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
} = apiConfiguration
|
||||
|
||||
// Batch update global state keys
|
||||
this.setGlobalStateBatch({
|
||||
// Plan mode configuration updates
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeBasetenModelId,
|
||||
planModeBasetenModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
|
||||
// Act mode configuration updates
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeBasetenModelId,
|
||||
actModeBasetenModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
|
||||
// Global state updates
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiBaseUrl,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiUrl,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
})
|
||||
|
||||
// Batch update secrets
|
||||
this.setSecretsBatch({
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineAccountId,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsBedrockApiKey,
|
||||
openAiApiKey,
|
||||
ollamaApiKey,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
liteLlmApiKey,
|
||||
fireworksApiKey,
|
||||
asksageApiKey,
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
basetenApiKey,
|
||||
huggingFaceApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method for global state keys - reads from in-memory cache
|
||||
*/
|
||||
getGlobalStateKey<T>(key: GlobalStateKey): T | undefined {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
return this.globalStateCache.get(key) as T | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method for secret keys - reads from in-memory cache
|
||||
*/
|
||||
getSecretKey(key: SecretKey): string | undefined {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
return this.secretsCache.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method for workspace state keys - reads from in-memory cache
|
||||
*/
|
||||
getWorkspaceStateKey<T>(key: LocalStateKey): T | undefined {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
return this.workspaceStateCache.get(key) as T | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitialize the cache service by clearing all state and reloading from disk
|
||||
* Used for error recovery when write operations fail
|
||||
*/
|
||||
async reInitialize(): Promise<void> {
|
||||
// Clear all cached data and pending state
|
||||
this.dispose()
|
||||
|
||||
// Reinitialize from disk
|
||||
await this.initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the cache service
|
||||
*/
|
||||
private dispose(): void {
|
||||
if (this.persistenceTimeout) {
|
||||
clearTimeout(this.persistenceTimeout)
|
||||
this.persistenceTimeout = null
|
||||
}
|
||||
|
||||
this.pendingGlobalState.clear()
|
||||
this.pendingSecrets.clear()
|
||||
this.pendingWorkspaceState.clear()
|
||||
|
||||
this.globalStateCache.clear()
|
||||
this.secretsCache.clear()
|
||||
this.workspaceStateCache.clear()
|
||||
|
||||
this.isInitialized = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule debounced persistence - simple timeout-based persistence
|
||||
*/
|
||||
private scheduleDebouncedPersistence(): void {
|
||||
// Clear existing timeout if one is pending
|
||||
if (this.persistenceTimeout) {
|
||||
clearTimeout(this.persistenceTimeout)
|
||||
}
|
||||
|
||||
// Schedule a new timeout to persist pending changes
|
||||
this.persistenceTimeout = setTimeout(async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.persistGlobalStateBatch(this.pendingGlobalState),
|
||||
this.persistSecretsBatch(this.pendingSecrets),
|
||||
this.persistWorkspaceStateBatch(this.pendingWorkspaceState),
|
||||
])
|
||||
|
||||
// Clear pending sets on successful persistence
|
||||
this.pendingGlobalState.clear()
|
||||
this.pendingSecrets.clear()
|
||||
this.pendingWorkspaceState.clear()
|
||||
this.persistenceTimeout = null
|
||||
} catch (error) {
|
||||
console.error("Failed to persist pending changes:", error)
|
||||
this.persistenceTimeout = null
|
||||
|
||||
// Call persistence error callback for error recovery
|
||||
this.onPersistenceError?.({ error: error as Error })
|
||||
}
|
||||
}, this.PERSISTENCE_DELAY_MS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to batch persist global state keys with Promise.all
|
||||
*/
|
||||
private async persistGlobalStateBatch(keys: Set<GlobalStateKey>): Promise<void> {
|
||||
try {
|
||||
await Promise.all(
|
||||
Array.from(keys).map((key) => {
|
||||
const value = this.globalStateCache.get(key)
|
||||
return this.context.globalState.update(key, value)
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to persist global state batch:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to batch persist secrets with Promise.all
|
||||
*/
|
||||
private async persistSecretsBatch(keys: Set<SecretKey>): Promise<void> {
|
||||
try {
|
||||
await Promise.all(
|
||||
Array.from(keys).map((key) => {
|
||||
const value = this.secretsCache.get(key)
|
||||
if (value) {
|
||||
return this.context.secrets.store(key, value)
|
||||
} else {
|
||||
return this.context.secrets.delete(key)
|
||||
}
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to persist secrets batch:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to batch persist workspace state keys with Promise.all
|
||||
*/
|
||||
private async persistWorkspaceStateBatch(keys: Set<LocalStateKey>): Promise<void> {
|
||||
try {
|
||||
await Promise.all(
|
||||
Array.from(keys).map((key) => {
|
||||
const value = this.workspaceStateCache.get(key)
|
||||
return this.context.workspaceState.update(key, value)
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to persist workspace state batch:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to populate API configuration cache without triggering persistence
|
||||
* Used during initialization
|
||||
*/
|
||||
private populateApiConfigurationCache(apiConfiguration: ApiConfiguration): void {
|
||||
const {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsBedrockApiKey,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiKey,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
geminiBaseUrl,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
basetenApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
fireworksApiKey,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
huggingFaceApiKey,
|
||||
requestTimeoutMs,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeBasetenModelId,
|
||||
planModeBasetenModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeBasetenModelId,
|
||||
actModeBasetenModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
} = apiConfiguration
|
||||
|
||||
// Directly populate global state cache without triggering persistence
|
||||
const globalStateUpdates = {
|
||||
// Plan mode configuration updates
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeBasetenModelId,
|
||||
planModeBasetenModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
|
||||
// Act mode configuration updates
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeBasetenModelId,
|
||||
actModeBasetenModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
|
||||
// Global state updates
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiBaseUrl,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiUrl,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
}
|
||||
|
||||
// Populate global state cache directly
|
||||
Object.entries(globalStateUpdates).forEach(([key, value]) => {
|
||||
this.globalStateCache.set(key as GlobalStateKey, value)
|
||||
})
|
||||
|
||||
// Directly populate secrets cache without triggering persistence
|
||||
const secretsUpdates = {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineAccountId,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsBedrockApiKey,
|
||||
openAiApiKey,
|
||||
ollamaApiKey,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
liteLlmApiKey,
|
||||
fireworksApiKey,
|
||||
asksageApiKey,
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
basetenApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
huggingFaceApiKey,
|
||||
}
|
||||
|
||||
// Populate secrets cache directly
|
||||
Object.entries(secretsUpdates).forEach(([key, value]) => {
|
||||
this.secretsCache.set(key as SecretKey, value)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct API configuration from cached component keys
|
||||
*/
|
||||
private constructApiConfigurationFromCache(): ApiConfiguration {
|
||||
return {
|
||||
// Secrets
|
||||
apiKey: this.secretsCache.get("apiKey"),
|
||||
openRouterApiKey: this.secretsCache.get("openRouterApiKey"),
|
||||
clineAccountId: this.secretsCache.get("clineAccountId"),
|
||||
awsAccessKey: this.secretsCache.get("awsAccessKey"),
|
||||
awsSecretKey: this.secretsCache.get("awsSecretKey"),
|
||||
awsSessionToken: this.secretsCache.get("awsSessionToken"),
|
||||
awsBedrockApiKey: this.secretsCache.get("awsBedrockApiKey"),
|
||||
openAiApiKey: this.secretsCache.get("openAiApiKey"),
|
||||
ollamaApiKey: this.secretsCache.get("ollamaApiKey"),
|
||||
geminiApiKey: this.secretsCache.get("geminiApiKey"),
|
||||
openAiNativeApiKey: this.secretsCache.get("openAiNativeApiKey"),
|
||||
deepSeekApiKey: this.secretsCache.get("deepSeekApiKey"),
|
||||
requestyApiKey: this.secretsCache.get("requestyApiKey"),
|
||||
togetherApiKey: this.secretsCache.get("togetherApiKey"),
|
||||
qwenApiKey: this.secretsCache.get("qwenApiKey"),
|
||||
doubaoApiKey: this.secretsCache.get("doubaoApiKey"),
|
||||
mistralApiKey: this.secretsCache.get("mistralApiKey"),
|
||||
liteLlmApiKey: this.secretsCache.get("liteLlmApiKey"),
|
||||
fireworksApiKey: this.secretsCache.get("fireworksApiKey"),
|
||||
asksageApiKey: this.secretsCache.get("asksageApiKey"),
|
||||
xaiApiKey: this.secretsCache.get("xaiApiKey"),
|
||||
sambanovaApiKey: this.secretsCache.get("sambanovaApiKey"),
|
||||
cerebrasApiKey: this.secretsCache.get("cerebrasApiKey"),
|
||||
groqApiKey: this.secretsCache.get("groqApiKey"),
|
||||
basetenApiKey: this.secretsCache.get("basetenApiKey"),
|
||||
moonshotApiKey: this.secretsCache.get("moonshotApiKey"),
|
||||
nebiusApiKey: this.secretsCache.get("nebiusApiKey"),
|
||||
sapAiCoreClientId: this.secretsCache.get("sapAiCoreClientId"),
|
||||
sapAiCoreClientSecret: this.secretsCache.get("sapAiCoreClientSecret"),
|
||||
huggingFaceApiKey: this.secretsCache.get("huggingFaceApiKey"),
|
||||
|
||||
// Global state
|
||||
awsRegion: this.globalStateCache.get("awsRegion"),
|
||||
awsUseCrossRegionInference: this.globalStateCache.get("awsUseCrossRegionInference"),
|
||||
awsBedrockUsePromptCache: this.globalStateCache.get("awsBedrockUsePromptCache"),
|
||||
awsBedrockEndpoint: this.globalStateCache.get("awsBedrockEndpoint"),
|
||||
awsProfile: this.globalStateCache.get("awsProfile"),
|
||||
awsUseProfile: this.globalStateCache.get("awsUseProfile"),
|
||||
awsAuthentication: this.globalStateCache.get("awsAuthentication"),
|
||||
vertexProjectId: this.globalStateCache.get("vertexProjectId"),
|
||||
vertexRegion: this.globalStateCache.get("vertexRegion"),
|
||||
openAiBaseUrl: this.globalStateCache.get("openAiBaseUrl"),
|
||||
openAiHeaders: this.globalStateCache.get("openAiHeaders") || {},
|
||||
ollamaBaseUrl: this.globalStateCache.get("ollamaBaseUrl"),
|
||||
ollamaApiOptionsCtxNum: this.globalStateCache.get("ollamaApiOptionsCtxNum"),
|
||||
lmStudioBaseUrl: this.globalStateCache.get("lmStudioBaseUrl"),
|
||||
anthropicBaseUrl: this.globalStateCache.get("anthropicBaseUrl"),
|
||||
geminiBaseUrl: this.globalStateCache.get("geminiBaseUrl"),
|
||||
azureApiVersion: this.globalStateCache.get("azureApiVersion"),
|
||||
openRouterProviderSorting: this.globalStateCache.get("openRouterProviderSorting"),
|
||||
liteLlmBaseUrl: this.globalStateCache.get("liteLlmBaseUrl"),
|
||||
liteLlmUsePromptCache: this.globalStateCache.get("liteLlmUsePromptCache"),
|
||||
qwenApiLine: this.globalStateCache.get("qwenApiLine"),
|
||||
moonshotApiLine: this.globalStateCache.get("moonshotApiLine"),
|
||||
asksageApiUrl: this.globalStateCache.get("asksageApiUrl"),
|
||||
favoritedModelIds: this.globalStateCache.get("favoritedModelIds"),
|
||||
requestTimeoutMs: this.globalStateCache.get("requestTimeoutMs"),
|
||||
fireworksModelMaxCompletionTokens: this.globalStateCache.get("fireworksModelMaxCompletionTokens"),
|
||||
fireworksModelMaxTokens: this.globalStateCache.get("fireworksModelMaxTokens"),
|
||||
sapAiCoreBaseUrl: this.globalStateCache.get("sapAiCoreBaseUrl"),
|
||||
sapAiCoreTokenUrl: this.globalStateCache.get("sapAiCoreTokenUrl"),
|
||||
sapAiResourceGroup: this.globalStateCache.get("sapAiResourceGroup"),
|
||||
claudeCodePath: this.globalStateCache.get("claudeCodePath"),
|
||||
|
||||
// Plan mode configurations
|
||||
planModeApiProvider: this.globalStateCache.get("planModeApiProvider"),
|
||||
planModeApiModelId: this.globalStateCache.get("planModeApiModelId"),
|
||||
planModeThinkingBudgetTokens: this.globalStateCache.get("planModeThinkingBudgetTokens"),
|
||||
planModeReasoningEffort: this.globalStateCache.get("planModeReasoningEffort"),
|
||||
planModeVsCodeLmModelSelector: this.globalStateCache.get("planModeVsCodeLmModelSelector"),
|
||||
planModeAwsBedrockCustomSelected: this.globalStateCache.get("planModeAwsBedrockCustomSelected"),
|
||||
planModeAwsBedrockCustomModelBaseId: this.globalStateCache.get("planModeAwsBedrockCustomModelBaseId"),
|
||||
planModeOpenRouterModelId: this.globalStateCache.get("planModeOpenRouterModelId"),
|
||||
planModeOpenRouterModelInfo: this.globalStateCache.get("planModeOpenRouterModelInfo"),
|
||||
planModeOpenAiModelId: this.globalStateCache.get("planModeOpenAiModelId"),
|
||||
planModeOpenAiModelInfo: this.globalStateCache.get("planModeOpenAiModelInfo"),
|
||||
planModeOllamaModelId: this.globalStateCache.get("planModeOllamaModelId"),
|
||||
planModeLmStudioModelId: this.globalStateCache.get("planModeLmStudioModelId"),
|
||||
planModeLiteLlmModelId: this.globalStateCache.get("planModeLiteLlmModelId"),
|
||||
planModeLiteLlmModelInfo: this.globalStateCache.get("planModeLiteLlmModelInfo"),
|
||||
planModeRequestyModelId: this.globalStateCache.get("planModeRequestyModelId"),
|
||||
planModeRequestyModelInfo: this.globalStateCache.get("planModeRequestyModelInfo"),
|
||||
planModeTogetherModelId: this.globalStateCache.get("planModeTogetherModelId"),
|
||||
planModeFireworksModelId: this.globalStateCache.get("planModeFireworksModelId"),
|
||||
planModeSapAiCoreModelId: this.globalStateCache.get("planModeSapAiCoreModelId"),
|
||||
planModeGroqModelId: this.globalStateCache.get("planModeGroqModelId"),
|
||||
planModeGroqModelInfo: this.globalStateCache.get("planModeGroqModelInfo"),
|
||||
planModeBasetenModelId: this.globalStateCache.get("planModeBasetenModelId"),
|
||||
planModeBasetenModelInfo: this.globalStateCache.get("planModeBasetenModelInfo"),
|
||||
planModeHuggingFaceModelId: this.globalStateCache.get("planModeHuggingFaceModelId"),
|
||||
planModeHuggingFaceModelInfo: this.globalStateCache.get("planModeHuggingFaceModelInfo"),
|
||||
|
||||
// Act mode configurations
|
||||
actModeApiProvider: this.globalStateCache.get("actModeApiProvider"),
|
||||
actModeApiModelId: this.globalStateCache.get("actModeApiModelId"),
|
||||
actModeThinkingBudgetTokens: this.globalStateCache.get("actModeThinkingBudgetTokens"),
|
||||
actModeReasoningEffort: this.globalStateCache.get("actModeReasoningEffort"),
|
||||
actModeVsCodeLmModelSelector: this.globalStateCache.get("actModeVsCodeLmModelSelector"),
|
||||
actModeAwsBedrockCustomSelected: this.globalStateCache.get("actModeAwsBedrockCustomSelected"),
|
||||
actModeAwsBedrockCustomModelBaseId: this.globalStateCache.get("actModeAwsBedrockCustomModelBaseId"),
|
||||
actModeOpenRouterModelId: this.globalStateCache.get("actModeOpenRouterModelId"),
|
||||
actModeOpenRouterModelInfo: this.globalStateCache.get("actModeOpenRouterModelInfo"),
|
||||
actModeOpenAiModelId: this.globalStateCache.get("actModeOpenAiModelId"),
|
||||
actModeOpenAiModelInfo: this.globalStateCache.get("actModeOpenAiModelInfo"),
|
||||
actModeOllamaModelId: this.globalStateCache.get("actModeOllamaModelId"),
|
||||
actModeLmStudioModelId: this.globalStateCache.get("actModeLmStudioModelId"),
|
||||
actModeLiteLlmModelId: this.globalStateCache.get("actModeLiteLlmModelId"),
|
||||
actModeLiteLlmModelInfo: this.globalStateCache.get("actModeLiteLlmModelInfo"),
|
||||
actModeRequestyModelId: this.globalStateCache.get("actModeRequestyModelId"),
|
||||
actModeRequestyModelInfo: this.globalStateCache.get("actModeRequestyModelInfo"),
|
||||
actModeTogetherModelId: this.globalStateCache.get("actModeTogetherModelId"),
|
||||
actModeFireworksModelId: this.globalStateCache.get("actModeFireworksModelId"),
|
||||
actModeSapAiCoreModelId: this.globalStateCache.get("actModeSapAiCoreModelId"),
|
||||
actModeGroqModelId: this.globalStateCache.get("actModeGroqModelId"),
|
||||
actModeGroqModelInfo: this.globalStateCache.get("actModeGroqModelInfo"),
|
||||
actModeBasetenModelId: this.globalStateCache.get("actModeBasetenModelId"),
|
||||
actModeBasetenModelInfo: this.globalStateCache.get("actModeBasetenModelInfo"),
|
||||
actModeHuggingFaceModelId: this.globalStateCache.get("actModeHuggingFaceModelId"),
|
||||
actModeHuggingFaceModelInfo: this.globalStateCache.get("actModeHuggingFaceModelInfo"),
|
||||
} as ApiConfiguration
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export const GlobalFileNames = {
|
||||
uiMessages: "ui_messages.json",
|
||||
openRouterModels: "openrouter_models.json",
|
||||
groqModels: "groq_models.json",
|
||||
basetenModels: "baseten_models.json",
|
||||
mcpSettings: "cline_mcp_settings.json",
|
||||
clineRules: ".clinerules",
|
||||
workflows: ".clinerules/workflows",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const CACHE_SERVICE_NOT_INITIALIZED = "CacheService must be initialized before attempting to access state."
|
||||
@@ -7,6 +7,7 @@ export type SecretKey =
|
||||
| "awsSessionToken"
|
||||
| "awsBedrockApiKey"
|
||||
| "openAiApiKey"
|
||||
| "ollamaApiKey"
|
||||
| "geminiApiKey"
|
||||
| "openAiNativeApiKey"
|
||||
| "deepSeekApiKey"
|
||||
@@ -28,6 +29,8 @@ export type SecretKey =
|
||||
| "sapAiCoreClientId"
|
||||
| "sapAiCoreClientSecret"
|
||||
| "groqApiKey"
|
||||
| "huaweiCloudMaasApiKey"
|
||||
| "basetenApiKey"
|
||||
|
||||
export type GlobalStateKey =
|
||||
| "awsRegion"
|
||||
@@ -82,8 +85,10 @@ export type GlobalStateKey =
|
||||
| "sapAiCoreBaseUrl"
|
||||
| "sapAiResourceGroup"
|
||||
| "claudeCodePath"
|
||||
| "strictPlanModeEnabled"
|
||||
// Settings around plan/act and ephemeral model configuration
|
||||
| "chatSettings"
|
||||
| "preferredLanguage"
|
||||
| "openaiReasoningEffort"
|
||||
| "mode"
|
||||
// Plan mode configurations
|
||||
| "planModeApiProvider"
|
||||
@@ -108,8 +113,12 @@ export type GlobalStateKey =
|
||||
| "planModeSapAiCoreModelId"
|
||||
| "planModeGroqModelId"
|
||||
| "planModeGroqModelInfo"
|
||||
| "planModeBasetenModelId"
|
||||
| "planModeBasetenModelInfo"
|
||||
| "planModeHuggingFaceModelId"
|
||||
| "planModeHuggingFaceModelInfo"
|
||||
| "planModeHuaweiCloudMaasModelId"
|
||||
| "planModeHuaweiCloudMaasModelInfo"
|
||||
// Act mode configurations
|
||||
| "actModeApiProvider"
|
||||
| "actModeApiModelId"
|
||||
@@ -133,7 +142,11 @@ export type GlobalStateKey =
|
||||
| "actModeSapAiCoreModelId"
|
||||
| "actModeGroqModelId"
|
||||
| "actModeGroqModelInfo"
|
||||
| "actModeBasetenModelId"
|
||||
| "actModeBasetenModelInfo"
|
||||
| "actModeHuggingFaceModelId"
|
||||
| "actModeHuggingFaceModelInfo"
|
||||
| "actModeHuaweiCloudMaasModelId"
|
||||
| "actModeHuaweiCloudMaasModelInfo"
|
||||
|
||||
export type LocalStateKey = "localClineRulesToggles" | "localCursorRulesToggles" | "localWindsurfRulesToggles" | "workflowToggles"
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ensureRulesDirectoryExists } from "./disk"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { updateGlobalState, getAllExtensionState, getGlobalState } from "./state"
|
||||
import { GlobalStateKey } from "./state-keys"
|
||||
|
||||
export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionContext) {
|
||||
// Keys to migrate from workspace storage back to global storage
|
||||
@@ -13,7 +12,6 @@ export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionC
|
||||
"apiModelId",
|
||||
"thinkingBudgetTokens",
|
||||
"reasoningEffort",
|
||||
"chatSettings",
|
||||
"vsCodeLmModelSelector",
|
||||
|
||||
// Provider-specific model keys
|
||||
@@ -136,43 +134,6 @@ export async function migrateCustomInstructionsToGlobalRules(context: vscode.Ext
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateModeFromWorkspaceStorageToControllerState(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
// Check legacy workspace storage (use raw methods since chatSettings is now global)
|
||||
const workspaceChatSettings = (await context.workspaceState.get("chatSettings")) as any
|
||||
|
||||
if (workspaceChatSettings && typeof workspaceChatSettings === "object" && "mode" in workspaceChatSettings) {
|
||||
console.log("Cleaning up mode from legacy workspace storage...")
|
||||
|
||||
// Remove mode property from chatSettings
|
||||
const { mode, ...cleanedChatSettings } = workspaceChatSettings
|
||||
|
||||
// Save cleaned chatSettings back to workspace storage (will be migrated later)
|
||||
await context.workspaceState.update("chatSettings", cleanedChatSettings)
|
||||
|
||||
console.log("Successfully removed mode from legacy workspace storage chatSettings")
|
||||
}
|
||||
|
||||
// Also check global storage for any mode cleanup needed
|
||||
const globalChatSettings = (await context.globalState.get("chatSettings")) as any
|
||||
|
||||
if (globalChatSettings && typeof globalChatSettings === "object" && "mode" in globalChatSettings) {
|
||||
console.log("Cleaning up mode from global storage...")
|
||||
|
||||
// Remove mode property from chatSettings
|
||||
const { mode, ...cleanedChatSettings } = globalChatSettings
|
||||
|
||||
// Save cleaned chatSettings back to global storage
|
||||
await updateGlobalState(context, "chatSettings", cleanedChatSettings)
|
||||
|
||||
console.log("Successfully removed mode from global storage chatSettings")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to cleanup mode from storage:", error)
|
||||
// Continue execution - migration failure shouldn't break extension startup
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateLegacyApiConfigurationToModeSpecific(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
// Check if migration is needed - if planModeApiProvider already exists, skip migration
|
||||
@@ -563,6 +524,7 @@ export async function migrateWelcomeViewCompleted(context: vscode.ExtensionConte
|
||||
config.awsRegion,
|
||||
config.vertexProjectId,
|
||||
config.openAiApiKey,
|
||||
config.ollamaApiKey,
|
||||
config.planModeOllamaModelId,
|
||||
config.planModeLmStudioModelId,
|
||||
config.actModeOllamaModelId,
|
||||
|
||||
+69
-261
@@ -1,5 +1,5 @@
|
||||
import * as vscode from "vscode"
|
||||
import { DEFAULT_CHAT_SETTINGS, Mode } from "@shared/ChatSettings"
|
||||
import { Mode, OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
|
||||
import { GlobalStateKey, LocalStateKey, SecretKey } from "./state-keys"
|
||||
@@ -7,12 +7,12 @@ import { ApiConfiguration, ApiProvider, BedrockModelId, ModelInfo } from "@share
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { StoredChatSettings } from "@shared/ChatSettings"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { UserInfo } from "@shared/UserInfo"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { DEFAULT_MCP_DISPLAY_MODE, McpDisplayMode } from "@shared/McpDisplayMode"
|
||||
import { migrateEnableCheckpointsSetting, migrateMcpMarketplaceEnableSetting } from "./state-migrations"
|
||||
import { Controller } from "../controller"
|
||||
/*
|
||||
Storage
|
||||
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
|
||||
@@ -110,7 +110,6 @@ export async function getWorkspaceState(context: vscode.ExtensionContext, key: L
|
||||
}
|
||||
|
||||
export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
const firstBatchStart = performance.now()
|
||||
const [
|
||||
isNewUser,
|
||||
welcomeViewCompleted,
|
||||
@@ -134,6 +133,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
openAiApiKey,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiKey,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
@@ -168,6 +168,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
basetenApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
huggingFaceApiKey,
|
||||
@@ -190,6 +191,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
huaweiCloudMaasApiKey,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "welcomeViewCompleted") as Promise<boolean | undefined>,
|
||||
@@ -213,6 +215,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getSecret(context, "openAiApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "openAiHeaders") as Promise<Record<string, string> | undefined>,
|
||||
getGlobalState(context, "ollamaBaseUrl") as Promise<string | undefined>,
|
||||
getSecret(context, "ollamaApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "ollamaApiOptionsCtxNum") as Promise<string | undefined>,
|
||||
getGlobalState(context, "lmStudioBaseUrl") as Promise<string | undefined>,
|
||||
getGlobalState(context, "anthropicBaseUrl") as Promise<string | undefined>,
|
||||
@@ -247,6 +250,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "cerebrasApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "groqApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "basetenApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "moonshotApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "nebiusApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "huggingFaceApiKey") as Promise<string | undefined>,
|
||||
@@ -269,14 +273,21 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "sapAiCoreTokenUrl") as Promise<string | undefined>,
|
||||
getGlobalState(context, "sapAiResourceGroup") as Promise<string | undefined>,
|
||||
getGlobalState(context, "claudeCodePath") as Promise<string | undefined>,
|
||||
getSecret(context, "huaweiCloudMaasApiKey") as Promise<string | undefined>,
|
||||
])
|
||||
|
||||
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
|
||||
const [localClineRulesToggles, localWindsurfRulesToggles, localCursorRulesToggles, localWorkflowToggles] = await Promise.all([
|
||||
getWorkspaceState(context, "localClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
getWorkspaceState(context, "localWindsurfRulesToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
getWorkspaceState(context, "localCursorRulesToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
getWorkspaceState(context, "workflowToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
])
|
||||
|
||||
const secondBatchStart = performance.now()
|
||||
const [
|
||||
chatSettings,
|
||||
currentMode,
|
||||
preferredLanguage,
|
||||
openaiReasoningEffort,
|
||||
mode,
|
||||
strictPlanModeEnabled,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
@@ -300,8 +311,12 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeBasetenModelId,
|
||||
planModeBasetenModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
planModeHuaweiCloudMaasModelId,
|
||||
planModeHuaweiCloudMaasModelInfo,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
@@ -325,11 +340,17 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeBasetenModelId,
|
||||
actModeBasetenModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
actModeHuaweiCloudMaasModelId,
|
||||
actModeHuaweiCloudMaasModelInfo,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "chatSettings") as Promise<StoredChatSettings | undefined>,
|
||||
getGlobalState(context, "preferredLanguage") as Promise<string | undefined>,
|
||||
getGlobalState(context, "openaiReasoningEffort") as Promise<OpenaiReasoningEffort | undefined>,
|
||||
getGlobalState(context, "mode") as Promise<Mode | undefined>,
|
||||
getGlobalState(context, "strictPlanModeEnabled") as Promise<boolean | undefined>,
|
||||
// Plan mode configurations
|
||||
getGlobalState(context, "planModeApiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "planModeApiModelId") as Promise<string | undefined>,
|
||||
@@ -353,8 +374,12 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "planModeSapAiCoreModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeGroqModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeGroqModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "planModeBasetenModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeBasetenModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "planModeHuggingFaceModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeHuggingFaceModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "planModeHuaweiCloudMaasModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeHuaweiCloudMaasModelInfo") as Promise<ModelInfo | undefined>,
|
||||
// Act mode configurations
|
||||
getGlobalState(context, "actModeApiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "actModeApiModelId") as Promise<string | undefined>,
|
||||
@@ -378,11 +403,14 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "actModeSapAiCoreModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeGroqModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeGroqModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "actModeBasetenModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeBasetenModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "actModeHuggingFaceModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeHuggingFaceModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "actModeHuaweiCloudMaasModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeHuaweiCloudMaasModelInfo") as Promise<ModelInfo | undefined>,
|
||||
])
|
||||
|
||||
const processingStart = performance.now()
|
||||
let apiProvider: ApiProvider
|
||||
if (planModeApiProvider) {
|
||||
apiProvider = planModeApiProvider
|
||||
@@ -442,6 +470,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
openAiApiKey,
|
||||
openAiHeaders: openAiHeaders || {},
|
||||
ollamaBaseUrl,
|
||||
ollamaApiKey,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
@@ -470,6 +499,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
basetenApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
@@ -480,6 +510,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
huggingFaceApiKey,
|
||||
huaweiCloudMaasApiKey,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider: planModeApiProvider || apiProvider,
|
||||
planModeApiModelId,
|
||||
@@ -503,8 +534,12 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeBasetenModelId,
|
||||
planModeBasetenModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
planModeHuaweiCloudMaasModelId,
|
||||
planModeHuaweiCloudMaasModelInfo,
|
||||
// Act mode configurations
|
||||
actModeApiProvider: actModeApiProvider || apiProvider,
|
||||
actModeApiModelId,
|
||||
@@ -528,8 +563,12 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeBasetenModelId,
|
||||
actModeBasetenModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
actModeHuaweiCloudMaasModelId,
|
||||
actModeHuaweiCloudMaasModelInfo,
|
||||
},
|
||||
isNewUser: isNewUser ?? true,
|
||||
welcomeViewCompleted,
|
||||
@@ -537,13 +576,11 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
taskHistory,
|
||||
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
|
||||
globalClineRulesToggles: globalClineRulesToggles || {},
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled)
|
||||
chatSettings: {
|
||||
...DEFAULT_CHAT_SETTINGS, // Apply defaults first
|
||||
...(chatSettings || {}), // Spread fetched global chatSettings, which includes preferredLanguage, and openAIReasoningEffort
|
||||
mode: currentMode || "act", // Merge mode from global state
|
||||
},
|
||||
preferredLanguage: preferredLanguage || "English",
|
||||
openaiReasoningEffort: (openaiReasoningEffort as OpenaiReasoningEffort) || "medium",
|
||||
mode: mode || "act",
|
||||
strictPlanModeEnabled: strictPlanModeEnabled ?? false,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
|
||||
mcpDisplayMode: mcpDisplayMode ?? DEFAULT_MCP_DISPLAY_MODE,
|
||||
@@ -556,256 +593,25 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
|
||||
defaultTerminalProfile: defaultTerminalProfile ?? "default",
|
||||
globalWorkflowToggles: globalWorkflowToggles || {},
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
|
||||
localCursorRulesToggles: localCursorRulesToggles || {},
|
||||
localWorkflowToggles: localWorkflowToggles || {},
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateApiConfiguration(context: vscode.ExtensionContext, apiConfiguration: ApiConfiguration) {
|
||||
const {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsBedrockApiKey,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
geminiBaseUrl,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
fireworksApiKey,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
huggingFaceApiKey,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
} = apiConfiguration
|
||||
export async function resetWorkspaceState(controller: Controller) {
|
||||
const context = controller.context
|
||||
await Promise.all(context.workspaceState.keys().map((key) => controller.context.workspaceState.update(key, undefined)))
|
||||
|
||||
// OPTIMIZED: Batch all global state updates into 2 operations instead of 47
|
||||
const batchedGlobalUpdates = {
|
||||
// Plan mode configuration updates
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
|
||||
// Act mode configuration updates
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
|
||||
// Global state updates (27 keys)
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiHeaders: openAiHeaders || {},
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiBaseUrl,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiUrl,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs: apiConfiguration.requestTimeoutMs,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
}
|
||||
|
||||
// OPTIMIZED: Batch all secret updates into 1 operation instead of 23
|
||||
const batchedSecretUpdates = {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineAccountId,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsBedrockApiKey,
|
||||
openAiApiKey,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
liteLlmApiKey,
|
||||
fireworksApiKey,
|
||||
asksageApiKey,
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
huggingFaceApiKey,
|
||||
}
|
||||
|
||||
// Execute batched operations in parallel for maximum performance
|
||||
await Promise.all([updateGlobalStateBatch(context, batchedGlobalUpdates), updateSecretsBatch(context, batchedSecretUpdates)])
|
||||
await controller.cacheService.reInitialize()
|
||||
}
|
||||
|
||||
export async function resetWorkspaceState(context: vscode.ExtensionContext) {
|
||||
for (const key of context.workspaceState.keys()) {
|
||||
await context.workspaceState.update(key, undefined)
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
export async function resetGlobalState(controller: Controller) {
|
||||
// TODO: Reset all workspace states?
|
||||
for (const key of context.globalState.keys()) {
|
||||
await context.globalState.update(key, undefined)
|
||||
}
|
||||
const context = controller.context
|
||||
|
||||
await Promise.all(context.globalState.keys().map((key) => context.globalState.update(key, undefined)))
|
||||
const secretKeys: SecretKey[] = [
|
||||
"apiKey",
|
||||
"openRouterApiKey",
|
||||
@@ -814,6 +620,7 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
"awsSessionToken",
|
||||
"awsBedrockApiKey",
|
||||
"openAiApiKey",
|
||||
"ollamaApiKey",
|
||||
"geminiApiKey",
|
||||
"openAiNativeApiKey",
|
||||
"deepSeekApiKey",
|
||||
@@ -830,11 +637,12 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
"sambanovaApiKey",
|
||||
"cerebrasApiKey",
|
||||
"groqApiKey",
|
||||
"basetenApiKey",
|
||||
"moonshotApiKey",
|
||||
"nebiusApiKey",
|
||||
"huggingFaceApiKey",
|
||||
"huaweiCloudMaasApiKey",
|
||||
]
|
||||
for (const key of secretKeys) {
|
||||
await storeSecret(context, key, undefined)
|
||||
}
|
||||
await Promise.all(secretKeys.map((key) => storeSecret(context, key, undefined)))
|
||||
await controller.cacheService.reInitialize()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { showSystemNotification } from "@/integrations/notifications"
|
||||
import { listFiles } from "@/services/glob/list-files"
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import { telemetryService } from "@/services/posthog/PostHogClientProvider"
|
||||
import { regexSearchFiles } from "@/services/ripgrep"
|
||||
import { parseSourceCodeForDefinitionsTopLevel } from "@/services/tree-sitter"
|
||||
import { findLast, findLastIndex, parsePartialArrayString } from "@/shared/array"
|
||||
@@ -32,9 +32,16 @@ import {
|
||||
COMPLETION_RESULT_CHANGES_FLAG,
|
||||
} from "@shared/ExtensionMessage"
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import { extractFileContent, FileContentResult } from "@integrations/misc/extract-file-content"
|
||||
import { COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { isClaude4ModelFamily, isGemini2dot5ModelFamily } from "@utils/model-utils"
|
||||
import {
|
||||
isClaude4ModelFamily,
|
||||
isGemini2dot5ModelFamily,
|
||||
isGrok4ModelFamily,
|
||||
modelDoesntSupportWebp,
|
||||
isNextGenModelFamily,
|
||||
} from "@utils/model-utils"
|
||||
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
|
||||
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
import os from "os"
|
||||
@@ -49,12 +56,12 @@ import { ContextManager } from "../context/context-management/ContextManager"
|
||||
import { loadMcpDocumentation } from "../prompts/loadMcpDocumentation"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { ensureTaskDirectoryExists } from "../storage/disk"
|
||||
import { getGlobalState, getWorkspaceState } from "../storage/state"
|
||||
import { CacheService } from "../storage/CacheService"
|
||||
import { TaskState } from "./TaskState"
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { AutoApprove } from "./tools/autoApprove"
|
||||
import { showNotificationForApprovalIfAutoApprovalEnabled } from "./utils"
|
||||
import { ChatSettings } from "@/shared/ChatSettings"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
|
||||
export class ToolExecutor {
|
||||
private autoApprover: AutoApprove
|
||||
@@ -85,13 +92,16 @@ export class ToolExecutor {
|
||||
private clineIgnoreController: ClineIgnoreController,
|
||||
private workspaceTracker: WorkspaceTracker,
|
||||
private contextManager: ContextManager,
|
||||
private cacheService: CacheService,
|
||||
|
||||
// Configuration & Settings
|
||||
private autoApprovalSettings: AutoApprovalSettings,
|
||||
private browserSettings: BrowserSettings,
|
||||
private cwd: string,
|
||||
private taskId: string,
|
||||
private chatSettings: ChatSettings,
|
||||
private ulid: string,
|
||||
private mode: Mode,
|
||||
private strictPlanModeEnabled: boolean,
|
||||
|
||||
// Callbacks to the Task (Entity)
|
||||
private say: (
|
||||
@@ -105,7 +115,12 @@ export class ToolExecutor {
|
||||
type: ClineAsk,
|
||||
text?: string,
|
||||
partial?: boolean,
|
||||
) => Promise<{ response: ClineAskResponse; text?: string; images?: string[]; files?: string[] }>,
|
||||
) => Promise<{
|
||||
response: ClineAskResponse
|
||||
text?: string
|
||||
images?: string[]
|
||||
files?: string[]
|
||||
}>,
|
||||
private saveCheckpoint: (isAttemptCompletionMessage?: boolean) => Promise<void>,
|
||||
private sayAndCreateMissingParamError: (toolName: ToolUseName, paramName: string, relPath?: string) => Promise<any>,
|
||||
private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>,
|
||||
@@ -122,8 +137,24 @@ export class ToolExecutor {
|
||||
this.autoApprover.updateSettings(settings)
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the tools which should be restricted in plan mode
|
||||
*/
|
||||
private isPlanModeToolRestricted(toolName: ToolUseName): boolean {
|
||||
const planModeRestrictedTools: ToolUseName[] = ["write_to_file", "replace_in_file"]
|
||||
return planModeRestrictedTools.includes(toolName)
|
||||
}
|
||||
|
||||
public updateMode(mode: Mode): void {
|
||||
this.mode = mode
|
||||
}
|
||||
|
||||
public updateStrictPlanModeEnabled(strictPlanModeEnabled: boolean): void {
|
||||
this.strictPlanModeEnabled = strictPlanModeEnabled
|
||||
}
|
||||
|
||||
private pushToolResult = (content: ToolResponse, block: ToolUse) => {
|
||||
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
|
||||
const isNextGenModel = isNextGenModelFamily(this.api)
|
||||
|
||||
if (typeof content === "string") {
|
||||
const resultText = content || "(tool did not return anything)"
|
||||
@@ -434,6 +465,15 @@ export class ToolExecutor {
|
||||
return
|
||||
}
|
||||
|
||||
// Logic for plan-model tool call restrictions
|
||||
if (this.strictPlanModeEnabled && this.mode === "plan" && block.name && this.isPlanModeToolRestricted(block.name)) {
|
||||
const errorMessage = `Tool '${block.name}' is not available in PLAN MODE. This tool is restricted to ACT MODE for file modifications. Only use tools available for PLAN MODE when in that mode.`
|
||||
await this.say("error", errorMessage)
|
||||
this.pushToolResult(formatResponse.toolError(errorMessage), block)
|
||||
await this.saveCheckpoint()
|
||||
return
|
||||
}
|
||||
|
||||
if (block.name !== "browser_action") {
|
||||
await this.browserSession.closeBrowser()
|
||||
}
|
||||
@@ -443,7 +483,7 @@ export class ToolExecutor {
|
||||
case "write_to_file":
|
||||
case "replace_in_file": {
|
||||
const relPath: string | undefined = block.params.path
|
||||
let content: string | undefined = block.params.content // for write_to_file
|
||||
const content: string | undefined = block.params.content // for write_to_file
|
||||
let diff: string | undefined = block.params.diff // for replace_in_file
|
||||
if (!relPath || (!content && !diff)) {
|
||||
// checking for content/diff ensures relPath is complete
|
||||
@@ -488,7 +528,7 @@ export class ToolExecutor {
|
||||
|
||||
const currentFullJson = block.params.diff
|
||||
// Check if we should use streaming (e.g., for specific models)
|
||||
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
|
||||
const isNextGenModel = isNextGenModelFamily(this.api)
|
||||
// Going through claude family of models
|
||||
if (isNextGenModel && USE_EXPERIMENTAL_CLAUDE4_FEATURES && currentFullJson) {
|
||||
const streamingResult = await this.handleStreamingJsonReplacement(block, relPath, currentFullJson)
|
||||
@@ -839,12 +879,18 @@ export class ToolExecutor {
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, this.api.getModel().id, false, true)
|
||||
}
|
||||
// now execute the tool like normal
|
||||
const content = await extractTextFromFile(absolutePath)
|
||||
const supportsImages = this.api.getModel().info.supportsImages ?? false
|
||||
const result = await extractFileContent(absolutePath, supportsImages)
|
||||
|
||||
// Track file read operation
|
||||
await this.fileContextTracker.trackFileContext(relPath, "read_tool")
|
||||
|
||||
this.pushToolResult(content, block)
|
||||
this.pushToolResult(result.text, block)
|
||||
|
||||
if (result.imageBlock) {
|
||||
this.taskState.userMessageContent.push(result.imageBlock)
|
||||
}
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
@@ -1175,7 +1221,9 @@ export class ToolExecutor {
|
||||
// Re-make browserSession to make sure latest settings apply
|
||||
if (this.context) {
|
||||
await this.browserSession.dispose()
|
||||
this.browserSession = new BrowserSession(this.context, this.browserSettings)
|
||||
|
||||
const useWebp = this.api ? !modelDoesntSupportWebp(this.api) : true
|
||||
this.browserSession = new BrowserSession(this.context, this.browserSettings, useWebp)
|
||||
} else {
|
||||
console.warn("no controller context available for browserSession")
|
||||
}
|
||||
@@ -1919,11 +1967,9 @@ export class ToolExecutor {
|
||||
const clineVersion =
|
||||
vscode.extensions.getExtension("saoudrizwan.claude-dev")?.packageJSON.version || "Unknown"
|
||||
const systemInfo = `VSCode: ${vscode.version}, Node.js: ${process.version}, Architecture: ${os.arch()}`
|
||||
const currentMode = this.chatSettings.mode
|
||||
const apiProvider =
|
||||
currentMode === "plan"
|
||||
? await getGlobalState(this.context, "planModeApiProvider")
|
||||
: await getGlobalState(this.context, "actModeApiProvider")
|
||||
const currentMode = this.mode
|
||||
const apiConfig = this.cacheService.getApiConfiguration()
|
||||
const apiProvider = currentMode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider
|
||||
const providerAndModel = `${apiProvider} / ${this.api.getModel().id}`
|
||||
|
||||
// Ask user for confirmation
|
||||
@@ -2099,6 +2145,7 @@ export class ToolExecutor {
|
||||
case "plan_mode_respond": {
|
||||
const response: string | undefined = block.params.response
|
||||
const optionsRaw: string | undefined = block.params.options
|
||||
const needsMoreExploration: boolean = block.params.needs_more_exploration === "true"
|
||||
const sharedMessage = {
|
||||
response: this.removeClosingTag(block, "response", response),
|
||||
options: parsePartialArrayString(this.removeClosingTag(block, "options", optionsRaw)),
|
||||
@@ -2123,6 +2170,17 @@ export class ToolExecutor {
|
||||
// })
|
||||
// }
|
||||
|
||||
// The plan_mode_respond tool tends to run into this issue where the model realizes mid-tool call that it should have called another tool before calling plan_mode_respond. And it ends the plan_mode_respond tool call with 'Proceeding to reading files...' which doesn't do anything because we restrict to 1 tool call per message. As an escape hatch for the model, we provide it the optionality to tack on a parameter at the end of its response `needs_more_exploration`, which will allow the loop to continue.
|
||||
if (needsMoreExploration) {
|
||||
this.pushToolResult(
|
||||
formatResponse.toolResult(
|
||||
`[You have indicated that you need more exploration. Proceed with calling tools to continue the planning process.]`,
|
||||
),
|
||||
block,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
// Store the number of options for telemetry
|
||||
const options = parsePartialArrayString(optionsRaw || "[]")
|
||||
|
||||
@@ -2301,7 +2359,7 @@ export class ToolExecutor {
|
||||
await this.say("completion_result", result, undefined, undefined, false)
|
||||
await this.saveCheckpoint(true)
|
||||
await addNewChangesFlagToLastCompletionResultMessage()
|
||||
telemetryService.captureTaskCompleted(this.taskId)
|
||||
telemetryService.captureTaskCompleted(this.taskId, this.ulid)
|
||||
} else {
|
||||
// we already sent a command message, meaning the complete completion message has also been sent
|
||||
await this.saveCheckpoint(true)
|
||||
@@ -2326,7 +2384,7 @@ export class ToolExecutor {
|
||||
await this.say("completion_result", result, undefined, undefined, false)
|
||||
await this.saveCheckpoint(true)
|
||||
await addNewChangesFlagToLastCompletionResultMessage()
|
||||
telemetryService.captureTaskCompleted(this.taskId)
|
||||
telemetryService.captureTaskCompleted(this.taskId, this.ulid)
|
||||
}
|
||||
|
||||
// we already sent completion_result says, an empty string asks relinquishes control over button and field
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user