mirror of
https://github.com/cline/cline.git
synced 2026-09-07 12:58:33 +08:00
Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 064dac48f8 | |||
| 82d0ac2088 | |||
| 3a1cee2faf | |||
| 8171b887ad | |||
| 203f805548 | |||
| c91ec4c97d | |||
| d85c8ceeb2 | |||
| b38994f1e8 | |||
| d846c2cce9 | |||
| 6e50778db7 | |||
| 3a325a6445 | |||
| f73172aeae | |||
| 97c25cb35e | |||
| 0fb82c1975 | |||
| 36c3f93884 | |||
| 3fb0360e01 | |||
| 084c0a73a3 | |||
| 092bd17921 | |||
| 079d05c2cc | |||
| acc795ed69 | |||
| 1aaa30ecce | |||
| f7e5398ac8 | |||
| 89f35b0800 | |||
| d7f30fdf73 | |||
| 0c6a4f9452 | |||
| c1e38e649c | |||
| b8a65a446a | |||
| 6626124bef | |||
| 80f67c3c89 | |||
| 01afe5ec53 | |||
| 83928ccecc | |||
| 38a1179e62 | |||
| e0f9eeaa79 | |||
| f10a82916f | |||
| da12437251 | |||
| fb3012f778 | |||
| 8336831d8f | |||
| ec064426b3 | |||
| 8f72bf11f7 | |||
| 915cf76f85 | |||
| f1fef24f25 | |||
| ceb0900bf6 | |||
| cc9fc9bd1f | |||
| dcf59bc29f | |||
| 5c3e7a38d4 | |||
| d6ccbcdf22 | |||
| e4e03fa0dd | |||
| 10892670be | |||
| 3c7a42c35d | |||
| abfaf6ca7a | |||
| f699f1fd80 | |||
| a6fbdcb5c1 | |||
| a4200869c9 | |||
| 9f59b6010e | |||
| 29d3175e0b | |||
| 600174322e | |||
| 9818d976dd | |||
| 3938e23cde | |||
| 86bb0c6ded | |||
| 36f57ce6c0 | |||
| ed0181a114 | |||
| 439c62935d | |||
| 9bc24ecd96 | |||
| 2150e4882e | |||
| ee347bfe9d | |||
| 08d86990e2 | |||
| f74a8ba82e | |||
| ad51b7a4e1 | |||
| 94516092b1 | |||
| adf25681cc | |||
| e2f73bce61 | |||
| fec8626291 | |||
| 6fa819a170 | |||
| 2ca3e9ac82 | |||
| 9d801a1a68 | |||
| 7bfc00b80e | |||
| a346f05e9c | |||
| 35929b6869 | |||
| ffeee7e48d | |||
| 10239f0616 | |||
| 9f1b01b561 | |||
| 7f641072d4 | |||
| cf0af8a3f0 | |||
| b9551c960a | |||
| f2ffe26aaf | |||
| fbe57b2f10 | |||
| 7f3eb926ee | |||
| 2d390b56f6 | |||
| 67f4caf2fe | |||
| 9b248ad6c2 | |||
| 45974ac925 | |||
| 2cedcc5a58 | |||
| 14a0c60550 | |||
| cdf368c21c | |||
| f3ae1340cf | |||
| 624fb8c8a1 | |||
| dbb5ac265d | |||
| 1fd137b684 | |||
| a718cc950e | |||
| cea93d9e98 | |||
| 346c1f7eff | |||
| 32f200e837 | |||
| 07a2daa827 | |||
| a22fc10a72 | |||
| 0c306121aa | |||
| 97139f713b | |||
| 77a877c6d3 | |||
| 627590ff2a | |||
| 6835870dee | |||
| a48c37ee82 | |||
| 579b1f1968 | |||
| 331da0f802 | |||
| 782ed7ff21 | |||
| c511b91a09 | |||
| efb6ae1529 | |||
| 58f80d4d16 | |||
| c3b556ddce | |||
| f16120fbfc | |||
| 02002860ca | |||
| b97e57fd4e | |||
| cd927ae279 | |||
| e2da226c10 | |||
| eae28f1cee | |||
| 47eacdc545 | |||
| b669dfbc7e | |||
| 9f192768bc |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate openSettings message to use state navigation handlers
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate refreshClineRules to protobus
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Support Streameable Http Transport for MCPs
|
||||
@@ -164,6 +164,7 @@ Key providers include:
|
||||
- **OpenRouter**: Meta-provider supporting multiple model providers
|
||||
- **AWS Bedrock**: Integration with Amazon's AI services
|
||||
- **Gemini**: Google's AI models
|
||||
- **Cerebras**: High-performance inference with Llama, Qwen, and DeepSeek models
|
||||
- **Ollama**: Local model hosting
|
||||
- **LM Studio**: Local model hosting
|
||||
- **VSCode LM**: VSCode's built-in language models
|
||||
|
||||
+3
-2
@@ -5,7 +5,7 @@
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"plugins": ["@typescript-eslint", "eslint-rules"],
|
||||
"rules": {
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"warn",
|
||||
@@ -19,7 +19,8 @@
|
||||
"eqeqeq": "warn",
|
||||
"no-throw-literal": "warn",
|
||||
"semi": "off",
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"eslint-rules/no-grpc-client-object-literals": "error"
|
||||
},
|
||||
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# This workflow will only label and/or close 30 issues at a time in order to avoid exceeding a rate limit.
|
||||
# More info: https://docs.github.com/en/actions/use-cases-and-examples/project-management/closing-inactive-issues
|
||||
name: Close inactive issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@28ca103
|
||||
with:
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: 14
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 60 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
exempt-issue-labels: "pinned,security"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Test Stale Issues Workflow
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
days-before-stale:
|
||||
description: "Days before an issue becomes stale"
|
||||
required: true
|
||||
default: "1"
|
||||
days-before-close:
|
||||
description: "Days before a stale issue is closed"
|
||||
required: true
|
||||
default: "1"
|
||||
|
||||
jobs:
|
||||
test-stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@28ca103
|
||||
with:
|
||||
days-before-issue-stale: ${{ github.event.inputs.days-before-stale }}
|
||||
days-before-issue-close: ${{ github.event.inputs.days-before-close }}
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for ${{ github.event.inputs.days-before-stale }} days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for ${{ github.event.inputs.days-before-close }} days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
exempt-issue-labels: "pinned,security"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
debug-only: true
|
||||
@@ -20,3 +20,11 @@ coverage
|
||||
!.github/scripts/coverage/
|
||||
|
||||
*evals.env
|
||||
|
||||
# Generated proto files
|
||||
src/shared/proto/*.ts
|
||||
src/core/controller/*/methods.ts
|
||||
src/core/controller/*/index.ts
|
||||
src/core/controller/grpc-service-config.ts
|
||||
webview-ui/src/services/grpc-client.ts
|
||||
src/standalone/server-setup.ts
|
||||
|
||||
@@ -3,3 +3,5 @@ node_modules
|
||||
webview-ui/build/
|
||||
*.md
|
||||
package-lock.json
|
||||
src/core/prompts/system.ts
|
||||
src/core/prompts/model_prompts/claude4.ts
|
||||
|
||||
@@ -1,5 +1,82 @@
|
||||
# Changelog
|
||||
|
||||
## [3.17.9]
|
||||
|
||||
- Aligning Cline to work with Claude 4 model family (Experimental)
|
||||
- Add task timeline scrolling feature
|
||||
- Add support for uploading CSV and XLSX files for data analysis and processing
|
||||
- Add stable Grok-3 models to xAI provider (grok-3, grok-3-fast, grok-3-mini, grok-3-mini-fast) and update default model from grok-3-beta to grok-3 (Thanks @PeterDaveHello!)
|
||||
- Add new models to Vertex AI provider
|
||||
- Add new model to Nebius AI Studio
|
||||
- Remove hard-coded temperature from LM Studio API requests and add support for reasoning_content in LM Studio responses
|
||||
- Display delay information when retrying API calls for better user feedback
|
||||
- Fix AWS Bedrock credential caching issue where externally updated credentials (e.g., by AWS Identity Manager) were not detected, requiring extension restart (Thanks @DaveFres!)
|
||||
- Fix search tool overloading conversation with massive outputs by setting maximum byte limit for responses
|
||||
- Fix checkpoints functionality
|
||||
- Fix token counting for xAI provider
|
||||
- Fix Ollama provider issues
|
||||
- Fix window title display for Windows users
|
||||
- Improve chat box UI
|
||||
|
||||
## [3.17.8]
|
||||
|
||||
- Fix bug where terminal would get stuck and output "capture failure"
|
||||
|
||||
## [3.17.7]
|
||||
|
||||
- Fix diff editing reliability for Claude 4 family models by adding constraints to prevent errors with large replacements
|
||||
|
||||
## [3.17.6]
|
||||
|
||||
- Add Cerebras as a new API provider with 5 high-performance models including reasoning-capable models (Thanks @kevint-cerebras!)
|
||||
- Add support for uploading various file types (XML, JSON, TXT, LOG, MD, DOCX, IPYNB, PDF) alongside images
|
||||
- Add improved onboarding experience for new users with guided setup
|
||||
- Add prompt cache indicator for Gemini 2.5 Flash models
|
||||
- Update SambaNova provider with new model list and documentation links (Thanks @luisfucros!)
|
||||
- Fix diff editing support for Claude 4 family of models
|
||||
- Improve telemetry and analytics for better user experience insights
|
||||
|
||||
## [3.17.5]
|
||||
|
||||
- Fix issue with Claude 4 models where after several conversation turns, it would start making invalid diff edits
|
||||
|
||||
## [3.17.4]
|
||||
|
||||
- Fix thinking budget slider for Claude 4
|
||||
|
||||
## [3.17.3]
|
||||
|
||||
- Fix diff edit errors with Claude 4 models
|
||||
|
||||
## [3.17.2]
|
||||
|
||||
- Add support for Claude 4 models (Sonnet 4 and Opus 4) in AWS Bedrock and Vertex AI providers
|
||||
- Add support for global workflows, allowing workflows to be shared across workspaces with local workflows taking precedence
|
||||
- Fix settings page z-index UI issues that caused display problems
|
||||
- Fix AWS Bedrock environment variable handling to properly restore process.env after API calls (Thanks @DaveFres!)
|
||||
|
||||
## [3.17.1]
|
||||
|
||||
- Add prompt caching for Claude 4 models on Cline and OpenRouter providers
|
||||
- Increase max tokens for Claude Opus 4 from 4096 to 8192
|
||||
|
||||
## [3.17.0]
|
||||
|
||||
- Add support for Anthropic Claude Sonnet 4 and Claude Opus 4 in both Anthropic and Vertex providers
|
||||
- Add integration with Nebius AI Studio as a new provider (Thanks @Aktsvigun!)
|
||||
- Add custom highlight and hotkey suggestion when the assistant prompts to switch to Act mode
|
||||
- Update settings page design, now split into tabs for easier navigation (Thanks Yellow Bat @dlab-anton, and Roo Team!)
|
||||
- Fix MCP Server configuration bug
|
||||
- Fix model listing for Requesty provider
|
||||
- Move all advanced settings to settings page
|
||||
|
||||
## [3.16.3]
|
||||
|
||||
- Add devstral-small-2505 to the Mistral model list, a new specialized coding model from Mistral AI (Thanks @BarreiroT!)
|
||||
- Add documentation links to rules & workflows UI
|
||||
- Add support for Streameable HTTP Transport for MCPs (Thanks @alejandropta!)
|
||||
- Improve error handling for Mistral SDK API
|
||||
|
||||
## [3.16.2]
|
||||
|
||||
- Add support for Gemini 2.5 Flash Preview 05-20 model to Vertex AI provider with massive 1M token context window (Thanks @omercelik!)
|
||||
|
||||
@@ -51,7 +51,7 @@ Thanks to [Claude 3.7 Sonnet's agentic coding capabilities](https://www.anthrop
|
||||
|
||||
### Use any API and Model
|
||||
|
||||
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, and GCP Vertex. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
|
||||
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, and Cerebras. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
|
||||
|
||||
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: "VS Code Language Model API"
|
||||
description: "Learn how to use Cline with the experimental VS Code Language Model API, enabling access to models from GitHub Copilot and other compatible extensions."
|
||||
---
|
||||
|
||||
Cline offers _experimental_ support for the [VS Code Language Model API](https://code.visualstudio.com/api/extension-guides/language-model). This API enables extensions to grant access to language models directly within the VS Code environment. Consequently, you might be able to leverage models from:
|
||||
|
||||
- **GitHub Copilot:** Provided you have an active Copilot subscription and the extension installed.
|
||||
- **Other VS Code Extensions:** Any extension that implements the Language Model API.
|
||||
|
||||
**Important Note:** This integration is currently in an experimental phase and might not perform as anticipated. Its functionality relies on other extensions correctly implementing the VS Code Language Model API.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **VS Code:** The Language Model API is accessible via VS Code (it is not currently supported by Cursor).
|
||||
- **A Language Model Provider Extension:** An extension that furnishes a language model is required. Examples include:
|
||||
- **GitHub Copilot:** With a Copilot subscription, the GitHub Copilot and GitHub Copilot Chat extensions can serve as model providers.
|
||||
- **Alternative Extensions:** Explore the VS Code Marketplace for extensions mentioning "Language Model API" or "lm". Other experimental options may be available.
|
||||
|
||||
### Configuration Steps
|
||||
|
||||
1. **Access Cline Settings:** Click the gear icon (⚙️) located in the Cline panel.
|
||||
2. **Choose Provider:** Select "VS Code LM API" from the "API Provider" dropdown menu.
|
||||
3. **Select Model:** The "Language Model" dropdown will (eventually) populate with available models. The naming convention is `vendor/family`. For instance, if Copilot is active, you might encounter options such as:
|
||||
- `copilot - claude-3.5-sonnet`
|
||||
- `copilot - o3-mini`
|
||||
- `copilot - o1-ga`
|
||||
- `copilot - gemini-2.0-flash`
|
||||
|
||||
### Current Limitations
|
||||
|
||||
- **Experimental API Status:** The VS Code Language Model API is still under active development. Anticipate potential changes and instability.
|
||||
- **Dependency on Extensions:** This feature is entirely contingent on other extensions making models available. Cline does not directly control the list of accessible models.
|
||||
- **Restricted Functionality:** The VS Code Language Model API might not encompass all features available through other API providers (e.g., image input capabilities, streaming responses, detailed usage metrics).
|
||||
- **No Direct Cost Management:** Users are subject to the pricing structures and terms of service of the extension providing the model. Cline cannot directly monitor or regulate associated costs.
|
||||
- **GitHub Copilot Rate Throttling:** When employing the VS Code LM API with GitHub Copilot, be mindful that GitHub may enforce rate limits on Copilot usage. These limitations are governed by GitHub, not Cline.
|
||||
|
||||
### Troubleshooting Tips
|
||||
|
||||
- **Models Not Appearing:**
|
||||
- Confirm that VS Code is installed.
|
||||
- Verify that a language model provider extension (e.g., GitHub Copilot, GitHub Copilot Chat) is installed and enabled.
|
||||
- If utilizing Copilot, ensure you have previously sent a Copilot Chat message using the desired model.
|
||||
- **Unexpected Operation:** Should you encounter unforeseen behavior, it is likely an issue stemming from the underlying Language Model API or the provider extension. Consider reporting the problem to the developers of the provider extension.
|
||||
+2
-1
@@ -148,7 +148,8 @@
|
||||
"custom-model-configs/aws-bedrock-with-credentials-authentication",
|
||||
"custom-model-configs/aws-bedrock-with-profile-authentication",
|
||||
"custom-model-configs/gcp-vertex-ai",
|
||||
"custom-model-configs/litellm-and-cline-using-codestral"
|
||||
"custom-model-configs/litellm-and-cline-using-codestral",
|
||||
"custom-model-configs/vscode-language-model-api"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,35 +13,13 @@ Before you jump into coding, make sure you have these essentials ready:
|
||||
|
||||
A popular, free, and powerful code editor.
|
||||
|
||||
- [Download VS Code](https://code.visualstudio.com/)
|
||||
- [<u>Download VS Code</u>](https://code.visualstudio.com/)
|
||||
|
||||
📺 **Recommended YouTube Tutorial:** [How to Install VS Code](https://www.youtube.com/watch?v=MlIzFUI1QGA)
|
||||
📺 **Recommended YouTube Tutorial:** [<u>How to Install VS Code</u>](https://www.youtube.com/watch?v=MlIzFUI1QGA)
|
||||
|
||||
> ✅ **Pro Tip:** Install VS Code in your Applications folder (macOS) or Program Files (Windows) for easy access from your dock or start menu.
|
||||
|
||||
#### 2. **Essential Development Tools**
|
||||
|
||||
Basic software required for coding efficiently:
|
||||
|
||||
- Homebrew (macOS)
|
||||
- Node.js
|
||||
- Git
|
||||
|
||||
👉 Follow our detailed guide on Installing Essential Development Tools with step-by-step help from Cline.
|
||||
|
||||
📺 **Recommended YouTube Tutorials:**
|
||||
|
||||
- **For macOS:**
|
||||
- [Install Homebrew on Mac](https://www.youtube.com/watch?v=hwGNgVbqasc)
|
||||
- [Install Git on MacOS 2024](https://www.youtube.com/watch?v=B4qsvQ5IqWk)
|
||||
- [Install Node.js on Mac (M1 | M2 | M3)](https://www.youtube.com/watch?v=I8H4wolRFBk)
|
||||
- **For Windows:**
|
||||
- [Install Git on Windows 10/11 (2024)](https://www.youtube.com/watch?v=yjxv1HuRQy0)
|
||||
- [Install Node.js in Windows 10/11](https://www.youtube.com/watch?v=uCgAuOYpJd0)
|
||||
|
||||
> ⚠️ **Note:** If you run into permission issues during installation, try running your terminal or command prompt as an administrator.
|
||||
|
||||
#### 3. **Organize Your Projects**
|
||||
#### 2. **Organize Your Projects**
|
||||
|
||||
Create a dedicated folder named `Cline` in your Documents folder for all your coding projects:
|
||||
|
||||
@@ -55,14 +33,36 @@ Inside your `Cline` folder, structure projects clearly:
|
||||
|
||||
> 💡 **Tip:** Keeping your projects organized from the start will save you time and confusion later!
|
||||
|
||||
#### 4. **Install the Cline VS Code Extension**
|
||||
#### 3. **Install the Cline VS Code Extension**
|
||||
|
||||
Enhance your coding workflow by installing the Cline extension directly within VS Code:
|
||||
|
||||
- Get Started with Cline Extension Tutorial
|
||||
|
||||
📺 **Recommended YouTube Tutorial:** [How To Install Extensions in VS Code](https://www.youtube.com/watch?v=E7trgwZa-mk)
|
||||
📺 **Recommended YouTube Tutorial:** [<u>How To Install Extensions in VS Code</u>](https://www.youtube.com/watch?v=E7trgwZa-mk)
|
||||
|
||||
> ✅ **Pro Tip:** After installing, reload VS Code to ensure the extension is activated properly.
|
||||
|
||||
#### 4. **Essential Development Tools**
|
||||
|
||||
Basic software required for coding efficiently:
|
||||
|
||||
- Homebrew (macOS)
|
||||
- Node.js
|
||||
- Git
|
||||
|
||||
👉 [<u>Follow our detailed guide on Installing Essential Development Tools with step-by-step help from Cline.</u>](https://docs.cline.bot/getting-started/installing-dev-essentials#installing-dev-essentials)
|
||||
|
||||
📺 **Recommended YouTube Tutorials for Manual Installation:**
|
||||
|
||||
- **For macOS:**
|
||||
- [<u>Install Homebrew on Mac</u>](https://www.youtube.com/watch?v=hwGNgVbqasc)
|
||||
- [<u>Install Git on macOS 2024</u>](https://www.youtube.com/watch?v=B4qsvQ5IqWk)
|
||||
- [<u>Install Node.js on Mac (M1 | M2 | M3)</u>](https://www.youtube.com/watch?v=I8H4wolRFBk)
|
||||
- **For Windows:**
|
||||
- [<u>Install Git on Windows 10/11 (2024)</u>](https://www.youtube.com/watch?v=yjxv1HuRQy0)
|
||||
- [<u>Install Node.js in Windows 10/11</u>](https://www.youtube.com/watch?v=uCgAuOYpJd0)
|
||||
|
||||
> ⚠️ **Note:** If you run into permission issues during installation, try running your terminal or command prompt as an administrator.
|
||||
|
||||
🎉 You're all set! Dive in and start coding smarter and faster with **Cline**.
|
||||
|
||||
@@ -69,9 +69,9 @@ Choose your AI assistant based on your needs:
|
||||
### Getting Started
|
||||
|
||||
1. Install the development essentials:
|
||||
- Follow our [Development Essentials Installation Guide](https://docs.cline.bot/getting-started/getting-started-new-coders/installing-dev-essentials)
|
||||
- Follow our [Development Essentials Installation Guide](https://docs.cline.bot/getting-started/installing-dev-essentials)
|
||||
2. Set up Cline's Memory Bank:
|
||||
- Follow the [Memory Bank setup instructions](https://docs.cline.bot/improving-your-prompting-skills/custom-instructions-library/cline-memory-bank)
|
||||
- Follow the [Memory Bank setup instructions](https://docs.cline.bot/prompting/cline-memory-bank)
|
||||
- Create an empty `cline_docs` folder in your project root
|
||||
- Create `projectBrief.md` in the `cline_docs` folder (see example below)
|
||||
- Tell Cline to "initialize memory bank"
|
||||
@@ -197,20 +197,20 @@ git push origin main # Upload to GitHub
|
||||
1. **Start of day**: Get latest changes
|
||||
|
||||
```bash
|
||||
bashCopygit pull origin main # Download latest code
|
||||
git pull origin main # Download latest code
|
||||
```
|
||||
|
||||
2. **During development**: Save work regularly
|
||||
|
||||
```bash
|
||||
bashCopygit add .
|
||||
git add .
|
||||
git commit -m "Clear message about changes"
|
||||
```
|
||||
|
||||
3. **End of day**: Share your progress
|
||||
|
||||
```bash
|
||||
bashCopygit push origin main # Upload to GitHub
|
||||
git push origin main # Upload to GitHub
|
||||
```
|
||||
|
||||
**Best Practices**
|
||||
|
||||
@@ -38,7 +38,7 @@ Cline actively builds context in two ways:
|
||||
- Guide focus areas
|
||||
- Share design thoughts and requirements
|
||||
|
||||
💡 **Key Point**: Cline isn't passive - it actively seeks to understand your project. You can either let it explore or guide its focus, especially in [Plan](https://docs.cline.bot/exploring-clines-tools/plan-and-act-modes-a-guide-to-effective-ai-development) mode.
|
||||
💡 **Key Point**: Cline isn't passive - it actively seeks to understand your project. You can either let it explore or guide its focus, especially in [Plan](https://docs.cline.bot/features/plan-and-act) mode.
|
||||
|
||||
### Context & Context Windows
|
||||
|
||||
@@ -93,7 +93,7 @@ Context files help maintain understanding across sessions. They serve as documen
|
||||
|
||||
#### Approaches to Context Files
|
||||
|
||||
1. **Evergreen Project Context (i.e.** [**Memory Bank**](https://docs.cline.bot/improving-your-prompting-skills/custom-instructions-library/cline-memory-bank)**)**
|
||||
1. **Evergreen Project Context (i.e.** [**Memory Bank**](https://docs.cline.bot/prompting/cline-memory-bank)**)**
|
||||
- Living documentation that evolves with your project
|
||||
- Updated as architecture and patterns emerge
|
||||
- Example: The Memory Bank pattern maintains files like `techContext.md` and `systemPatterns.md`
|
||||
@@ -151,7 +151,7 @@ Context files help maintain understanding across sessions. They serve as documen
|
||||
- Use Plan mode for complex discussions
|
||||
- Start fresh sessions when needed
|
||||
3. **Team Projects**
|
||||
- Share common context files (consider using [.clinerules](https://docs.cline.bot/improving-your-prompting-skills/prompting) files in project roots)
|
||||
- Share common context files (consider using [.clinerules](https://docs.cline.bot/features/cline-rules) files in project roots)
|
||||
- Document architectural decisions
|
||||
- Maintain consistent patterns
|
||||
- Keep documentation current
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
const { RuleTester: GrpcRuleTester } = require("eslint")
|
||||
const grpcRule = require("../no-grpc-client-object-literals")
|
||||
|
||||
const grpcRuleTester = new GrpcRuleTester({
|
||||
parser: require.resolve("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
grpcRuleTester.run("no-grpc-client-object-literals", grpcRule, {
|
||||
valid: [
|
||||
// Valid case: Using .create() method with gRPC client
|
||||
{
|
||||
code: `
|
||||
import { TogglePlanActModeRequest } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: PlanActMode.PLAN,
|
||||
preferredLanguage: 'en',
|
||||
},
|
||||
})
|
||||
);
|
||||
`,
|
||||
},
|
||||
// Valid case: Using .fromPartial() method with gRPC client
|
||||
{
|
||||
code: `
|
||||
import { TogglePlanActModeRequest, ChatSettings } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
const chatSettings = ChatSettings.fromPartial({
|
||||
mode: PlanActMode.PLAN,
|
||||
preferredLanguage: 'en',
|
||||
});
|
||||
|
||||
StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: chatSettings,
|
||||
})
|
||||
);
|
||||
`,
|
||||
},
|
||||
// Valid case: Regular function call with object literal (not a gRPC client)
|
||||
{
|
||||
code: `
|
||||
function processData(data) {
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
processData({
|
||||
id: 123,
|
||||
name: 'test',
|
||||
});
|
||||
`,
|
||||
},
|
||||
// Valid case: Using proper nested protobuf objects
|
||||
{
|
||||
code: `
|
||||
import { TogglePlanActModeRequest, ChatSettings } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
// Using proper nested protobuf objects
|
||||
const chatSettings = ChatSettings.create({
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
});
|
||||
|
||||
const request = TogglePlanActModeRequest.create({
|
||||
chatSettings: chatSettings,
|
||||
});
|
||||
|
||||
StateServiceClient.togglePlanActMode(request);
|
||||
`,
|
||||
},
|
||||
// Valid case: Object literal in second parameter (should not be checked)
|
||||
{
|
||||
code: `
|
||||
import { StateSubscribeRequest } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
const request = StateSubscribeRequest.create({
|
||||
topics: ['apiConfig', 'tasks']
|
||||
});
|
||||
|
||||
// Second parameter is an object literal but should not trigger the rule
|
||||
StateServiceClient.subscribe(request, {
|
||||
metadata: {
|
||||
userId: 123,
|
||||
sessionId: "abc-123"
|
||||
}
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Invalid case: Using object literal directly with gRPC client
|
||||
{
|
||||
code: `
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
StateServiceClient.togglePlanActMode({
|
||||
chatSettings: {
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
},
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Invalid case: Using object literal with nested properties
|
||||
{
|
||||
code: `
|
||||
import { ChatSettings } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
const chatSettings = ChatSettings.create({
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
});
|
||||
|
||||
StateServiceClient.togglePlanActMode({
|
||||
chatSettings: {
|
||||
mode: 1,
|
||||
preferredLanguage: 'fr',
|
||||
},
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Invalid case: Nested object literal in protobuf create method
|
||||
{
|
||||
code: `
|
||||
import { TogglePlanActModeRequest, ChatSettings } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
// Using nested object literal instead of ChatSettings.create()
|
||||
const request = TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
},
|
||||
});
|
||||
|
||||
StateServiceClient.togglePlanActMode(request);
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Invalid case: Object literal as first parameter to subscribe method
|
||||
{
|
||||
code: `
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
// First parameter is an object literal, which should trigger the rule
|
||||
StateServiceClient.subscribe({
|
||||
topics: ['apiConfig', 'tasks']
|
||||
}, {
|
||||
metadata: {
|
||||
userId: 123,
|
||||
sessionId: "abc-123"
|
||||
}
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
// eslint-rules/index.js
|
||||
const noGrpcClientObjectLiterals = require("./no-grpc-client-object-literals")
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
"no-grpc-client-object-literals": noGrpcClientObjectLiterals,
|
||||
},
|
||||
configs: {
|
||||
recommended: {
|
||||
plugins: ["local"],
|
||||
rules: {
|
||||
"local/no-grpc-client-object-literals": "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
const { ESLintUtils } = require("@typescript-eslint/utils")
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
|
||||
|
||||
module.exports = createRule({
|
||||
name: "no-grpc-client-object-literals",
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Enforce using .create() or .fromPartial() for gRPC service client parameters instead of object literals",
|
||||
recommended: "error",
|
||||
},
|
||||
messages: {
|
||||
useProtobufMethod:
|
||||
"Use the appropriate protobuf .create() or .fromPartial() method instead of " +
|
||||
"object literal for gRPC client parameters.\n" +
|
||||
"Found: {{code}}\n" +
|
||||
"gRPC client methods should always receive properly created protobuf objects.",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
|
||||
create(context) {
|
||||
// Check if a name matches the gRPC service client pattern using regex
|
||||
// Must start with an uppercase letter and end with ServiceClient
|
||||
const isGrpcServiceClient = (name) => {
|
||||
return typeof name === "string" && /^[A-Z].*ServiceClient$/.test(name)
|
||||
}
|
||||
|
||||
const safeObjectExpressions = new Map() // Track object expressions in create/fromPartial calls
|
||||
|
||||
return {
|
||||
// Skip object literals inside create() or fromPartial() method calls
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee &&
|
||||
node.callee.type === "MemberExpression" &&
|
||||
(node.callee.property.name === "create" || node.callee.property.name === "fromPartial") &&
|
||||
node.arguments.length > 0 &&
|
||||
node.arguments[0].type === "ObjectExpression"
|
||||
) {
|
||||
// Track this object expression as being used with create/fromPartial
|
||||
safeObjectExpressions.set(node.arguments[0], { isProblematic: false })
|
||||
}
|
||||
},
|
||||
|
||||
// Track create/fromPartial calls that contain nested object literals
|
||||
"CallExpression[callee.type='MemberExpression'][callee.property.name=/^(create|fromPartial)$/]"(node) {
|
||||
if (node.arguments.length > 0 && node.arguments[0].type === "ObjectExpression") {
|
||||
// Track problematic nested object literals
|
||||
const nestedObjectLiterals = new Map() // Map of object expressions to their containing property paths
|
||||
|
||||
// Search for nested object literals
|
||||
const queue = [
|
||||
...node.arguments[0].properties.map((prop) => ({
|
||||
property: prop,
|
||||
path: prop.key && prop.key.name ? prop.key.name : "unknown",
|
||||
})),
|
||||
]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { property, path } = queue.shift()
|
||||
|
||||
// Skip spread elements
|
||||
if (property.type !== "Property") continue
|
||||
|
||||
// If this is an object literal, mark it as problematic
|
||||
if (property.value.type === "ObjectExpression") {
|
||||
nestedObjectLiterals.set(property.value, path)
|
||||
|
||||
// Add nested properties to queue
|
||||
queue.push(
|
||||
...property.value.properties.map((prop) => ({
|
||||
property: prop,
|
||||
path: `${path}.${prop.key && prop.key.name ? prop.key.name : "unknown"}`,
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// For each problematic nested object, track it with its path
|
||||
nestedObjectLiterals.forEach((path, objectExpr) => {
|
||||
safeObjectExpressions.set(objectExpr, {
|
||||
isProblematic: true,
|
||||
path: path,
|
||||
parentNode: node,
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Check calls to gRPC service clients
|
||||
"CallExpression[callee.type='MemberExpression']"(node) {
|
||||
// Get the object (left side) of the member expression
|
||||
const callee = node.callee
|
||||
if (callee.object && callee.object.type === "Identifier") {
|
||||
const objectName = callee.object.name
|
||||
|
||||
// Check if this is a call to one of our gRPC service clients
|
||||
if (isGrpcServiceClient(objectName)) {
|
||||
// Only check the first argument of gRPC service client calls
|
||||
if (node.arguments.length > 0) {
|
||||
const arg = node.arguments[0] // Only check the first parameter
|
||||
if (arg.type === "ObjectExpression" && !safeObjectExpressions.has(arg)) {
|
||||
// This is an object literal being passed directly to a gRPC client
|
||||
const sourceCode = context.getSourceCode()
|
||||
const callText = sourceCode.getText(node).trim()
|
||||
|
||||
context.report({
|
||||
node: arg,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
code: callText,
|
||||
},
|
||||
})
|
||||
} else if (arg.type === "ObjectExpression") {
|
||||
// Search for nested object literals that aren't protected
|
||||
const queue = [...arg.properties]
|
||||
while (queue.length > 0) {
|
||||
const property = queue.shift()
|
||||
|
||||
// Skip spread elements
|
||||
if (property.type !== "Property") continue
|
||||
|
||||
// Check value
|
||||
if (
|
||||
property.value.type === "ObjectExpression" &&
|
||||
!safeObjectExpressions.has(property.value)
|
||||
) {
|
||||
// Found a nested object literal
|
||||
const sourceCode = context.getSourceCode()
|
||||
const propertyText = sourceCode.getText(property).trim()
|
||||
|
||||
context.report({
|
||||
node: property.value,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
code: `${objectName}.${callee.property.name}(... ${propertyText} ...)`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Add any nested properties to the queue
|
||||
if (property.value.type === "ObjectExpression") {
|
||||
queue.push(...property.value.properties)
|
||||
}
|
||||
}
|
||||
} else if (arg.type === "Identifier") {
|
||||
// This is a variable - check if it references a problematic protobuf object
|
||||
const varName = arg.name
|
||||
const sourceCode = context.getSourceCode()
|
||||
const scope = sourceCode.getScope(node)
|
||||
|
||||
// Find the variable declaration
|
||||
const variable = scope.variables.find((v) => v.name === varName)
|
||||
if (variable && variable.references && variable.references.length > 0) {
|
||||
// Look for definitions
|
||||
const def = variable.defs.find(
|
||||
(d) => d.node && d.node.type === "VariableDeclarator" && d.node.init,
|
||||
)
|
||||
|
||||
if (
|
||||
def &&
|
||||
def.node.init.type === "CallExpression" &&
|
||||
def.node.init.callee.type === "MemberExpression" &&
|
||||
(def.node.init.callee.property.name === "create" ||
|
||||
def.node.init.callee.property.name === "fromPartial")
|
||||
) {
|
||||
// Flag if we find problematic nested object literals in this create/fromPartial call
|
||||
const callText = sourceCode.getText(node).trim()
|
||||
const initCallText = sourceCode.getText(def.node.init).trim()
|
||||
|
||||
// Check for nested object literals in init node
|
||||
let foundNestedLiteral = false
|
||||
if (
|
||||
def.node.init.arguments.length > 0 &&
|
||||
def.node.init.arguments[0].type === "ObjectExpression"
|
||||
) {
|
||||
// Find any nested object literals
|
||||
const queue = [...def.node.init.arguments[0].properties]
|
||||
while (queue.length > 0 && !foundNestedLiteral) {
|
||||
const property = queue.shift()
|
||||
|
||||
// Skip spread elements
|
||||
if (property.type !== "Property") continue
|
||||
|
||||
if (property.value.type === "ObjectExpression") {
|
||||
foundNestedLiteral = true
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
code: `${callText} - using request created with nested object literal at: ${property.key.name}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Add any nested properties to the queue
|
||||
if (property.value.type === "ObjectExpression") {
|
||||
queue.push(...property.value.properties)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
Generated
+2479
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "eslint-plugin-eslint-rules",
|
||||
"version": "1.0.0",
|
||||
"description": "Custom ESLint rules for Cline",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "mocha --no-config --require ts-node/register __tests__/**/*.test.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"eslint",
|
||||
"eslintplugin"
|
||||
],
|
||||
"author": "Cline Bot Inc.",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^8.33.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/eslint": "^8.0.0",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/parser": "^7.14.1",
|
||||
"eslint": "^8.57.0",
|
||||
"mocha": "^10.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "dist",
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.js", "**/*.tsx", "__tests__/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
التقى Cline، مساعد الذكاء الاصطناعي الذي يمكنه استخدام **سطر الأوامر** و **محرر النصوص** الخاص بك.
|
||||
|
||||
بفضل [قدرات Claude 3.7 Sonnet على التعليمات البرمجية الوكيلة](https://www.anthropic.com/claude/sonnet)، يمكن لـ Cline التعامل مع مهام تطوير البرامج المعقدة خطوة بخطوة. مع الأدوات التي تسمح له بإنشاء وتعديل الملفات، واستكشاف المشاريع الكبيرة، واستخدام المتصفح، وتنفيذ أوامر الطرفية (بعد منحك الإذن)، يمكنه مساعدتك بطرق تتجاوز إكمال الكود أو الدعم الفني. يمكن لـ Cline أيضًا استخدام بروتوكول سياق النموذج (MCP) لإنشاء أدوات جديدة وتوسيع قدراته الخاصة. في حين تعمل النصوص البرمجية الآلية المستقلة تقليديًا في بيئات محاصرة، توفر هذه الإضافة واجهة رسومية لموافقة المستخدم على كل تغيير في الملف وأمر طرفية، مما يوفر طريقة آمنة وسهلة الاستخدام لاستكشاف إمكانات الذكاء الاصطناعي الوكيل.
|
||||
بفضل [قدرات Claude 4 Sonnet على التعليمات البرمجية الوكيلة](https://www.anthropic.com/claude/sonnet)، يمكن لـ Cline التعامل مع مهام تطوير البرامج المعقدة خطوة بخطوة. مع الأدوات التي تسمح له بإنشاء وتعديل الملفات، واستكشاف المشاريع الكبيرة، واستخدام المتصفح، وتنفيذ أوامر الطرفية (بعد منحك الإذن)، يمكنه مساعدتك بطرق تتجاوز إكمال الكود أو الدعم الفني. يمكن لـ Cline أيضًا استخدام بروتوكول سياق النموذج (MCP) لإنشاء أدوات جديدة وتوسيع قدراته الخاصة. في حين تعمل النصوص البرمجية الآلية المستقلة تقليديًا في بيئات محاصرة، توفر هذه الإضافة واجهة رسومية لموافقة المستخدم على كل تغيير في الملف وأمر طرفية، مما يوفر طريقة آمنة وسهلة الاستخدام لاستكشاف إمكانات الذكاء الاصطناعي الوكيل.
|
||||
|
||||
1. أدخل مهمتك وأضف الصور لتحويل المحاكاة إلى تطبيقات وظيفية أو إصلاح الأخطاء مع لقطات الشاشة.
|
||||
2. يبدأ Cline بتحليل هيكل الملفات الخاصة بك وشجرة التعريف المصدرية، وإجراء عمليات بحث regex، وقراءة الملفات ذات الصلة للاطلاع على المشاريع الحالية. من خلال إدارة المعلومات التي يتم إضافتها إلى السياق بعناية، يمكن لـ Cline تقديم مساعدة قيمة حتى للمشاريع الكبيرة والمعقدة دون إرهاق نافذة السياق.
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
### استخدم المتصفح
|
||||
|
||||
مع قدرة [استخدام الكمبيوتر](https://www.anthropic.com/news/3-5-models-and-computer-use) الجديدة لـ Claude 3.5 Sonnet، يمكن لـ Cline إطلاق متصفح، والنقر على العناصر، وكتابة النص، والتمرير، والتقاط لقطات الشاشة وسجلات وحدة التحكم في كل خطوة. يسمح له هذا بالتصحيح التفاعلي، واختبار نهاية إلى نهاية، وحتى الاستخدام العام للويب! يمنحه هذا الاستقلالية لإصلاح الأخطاء البصرية وأخطاء وقت التشغيل دون الحاجة إلى نسخ ولصق سجلات الأخطاء بنفسك.
|
||||
مع قدرة [استخدام الكمبيوتر](https://www.anthropic.com/news/3-5-models-and-computer-use) الجديدة لـ Claude 4 Sonnet، يمكن لـ Cline إطلاق متصفح، والنقر على العناصر، وكتابة النص، والتمرير، والتقاط لقطات الشاشة وسجلات وحدة التحكم في كل خطوة. يسمح له هذا بالتصحيح التفاعلي، واختبار نهاية إلى نهاية، وحتى الاستخدام العام للويب! يمنحه هذا الاستقلالية لإصلاح الأخطاء البصرية وأخطاء وقت التشغيل دون الحاجة إلى نسخ ولصق سجلات الأخطاء بنفسك.
|
||||
|
||||
حاول طلب من Cline "اختبار التطبيق"، وشاهده يشغل أمرًا مثل `npm run dev`، ويطلق خادم التطوير المحلي في متصفح، ويجري سلسلة من الاختبارات للتأكد من أن كل شيء يعمل. [شاهد عرضًا توضيحيًا هنا.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
Lernen Sie Cline kennen, einen KI-Assistenten, der Ihre **CLI** u**N**d **E**ditor nutzen kann.
|
||||
|
||||
Dank der [agentischen Codierungsfähigkeiten von Claude 3.7 Sonnet](https://www.anthropic.com/claude/sonnet) kann Cline komplexe Softwareentwicklungsaufgaben Schritt für Schritt bewältigen. Mit Werkzeugen, die ihm das Erstellen und Bearbeiten von Dateien, das Erkunden großer Projekte, die Nutzung des Browsers und das Ausführen von Terminalbefehlen (nach Ihrer Genehmigung) ermöglichen, kann er Ihnen auf eine Weise helfen, die über die Codevervollständigung oder technischen Support hinausgeht. Cline kann sogar das Model Context Protocol (MCP) verwenden, um neue Werkzeuge zu erstellen und seine eigenen Fähigkeiten zu erweitern. Während autonome KI-Skripte traditionell in sandboxed Umgebungen laufen, bietet diese Erweiterung eine Mensch-in-der-Schleife-GUI, um jede Dateiänderung und jeden Terminalbefehl zu genehmigen, was eine sichere und zugängliche Möglichkeit bietet, das Potenzial agentischer KI zu erkunden.
|
||||
Dank der [agentischen Codierungsfähigkeiten von Claude 4 Sonnet](https://www.anthropic.com/claude/sonnet) kann Cline komplexe Softwareentwicklungsaufgaben Schritt für Schritt bewältigen. Mit Werkzeugen, die ihm das Erstellen und Bearbeiten von Dateien, das Erkunden großer Projekte, die Nutzung des Browsers und das Ausführen von Terminalbefehlen (nach Ihrer Genehmigung) ermöglichen, kann er Ihnen auf eine Weise helfen, die über die Codevervollständigung oder technischen Support hinausgeht. Cline kann sogar das Model Context Protocol (MCP) verwenden, um neue Werkzeuge zu erstellen und seine eigenen Fähigkeiten zu erweitern. Während autonome KI-Skripte traditionell in sandboxed Umgebungen laufen, bietet diese Erweiterung eine Mensch-in-der-Schleife-GUI, um jede Dateiänderung und jeden Terminalbefehl zu genehmigen, was eine sichere und zugängliche Möglichkeit bietet, das Potenzial agentischer KI zu erkunden.
|
||||
|
||||
1. Geben Sie Ihre Aufgabe ein und fügen Sie Bilder hinzu, um Mockups in funktionale Apps zu konvertieren oder Fehler mit Screenshots zu beheben.
|
||||
2. Cline beginnt mit der Analyse Ihrer Dateistruktur und Quellcode-ASTs, führt Regex-Suchen durch und liest relevante Dateien, um sich in bestehenden Projekten zurechtzufinden. Durch sorgfältiges Management der hinzugefügten Informationen kann Cline wertvolle Unterstützung auch bei großen, komplexen Projekten bieten, ohne das Kontextfenster zu überladen.
|
||||
@@ -83,7 +83,7 @@ Alle von Cline vorgenommenen Änderungen werden in der Timeline Ihrer Datei aufg
|
||||
|
||||
### Den Browser verwenden
|
||||
|
||||
Mit der neuen [Computer Use](https://www.anthropic.com/news/3-5-models-and-computer-use) Fähigkeit von Claude 3.5 Sonnet kann Cline einen Browser starten, Elemente anklicken, Text eingeben und scrollen, dabei Screenshots und Konsolenprotokolle bei jedem Schritt erfassen. Dies ermöglicht interaktives Debugging, End-to-End-Tests und sogar allgemeine Webnutzung! Dies gibt ihm die Autonomie, visuelle Fehler und Laufzeitprobleme zu beheben, ohne dass Sie selbst Fehlerprotokolle kopieren und einfügen müssen.
|
||||
Mit der neuen [Computer Use](https://www.anthropic.com/news/3-5-models-and-computer-use) Fähigkeit von Claude 4 Sonnet kann Cline einen Browser starten, Elemente anklicken, Text eingeben und scrollen, dabei Screenshots und Konsolenprotokolle bei jedem Schritt erfassen. Dies ermöglicht interaktives Debugging, End-to-End-Tests und sogar allgemeine Webnutzung! Dies gibt ihm die Autonomie, visuelle Fehler und Laufzeitprobleme zu beheben, ohne dass Sie selbst Fehlerprotokolle kopieren und einfügen müssen.
|
||||
|
||||
Versuchen Sie, Cline zu bitten, "die App zu testen", und sehen Sie zu, wie er einen Befehl wie `npm run dev` ausführt, Ihren lokal laufenden Dev-Server in einem Browser startet und eine Reihe von Tests durchführt, um zu bestätigen, dass alles funktioniert. [Sehen Sie sich hier eine Demo an.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
Conozca a Cline, un asistente de IA que puede usar su **CLI** y **E**ditor.
|
||||
|
||||
Gracias a las [habilidades de codificación agencial de Claude 3.7 Sonnet](https://www.anthropic.com/claude/sonnet), Cline puede abordar tareas complejas de desarrollo de software paso a paso. Con herramientas que le permiten crear y editar archivos, explorar grandes proyectos, usar el navegador y ejecutar comandos de terminal (con su aprobación), puede ayudarle de una manera que va más allá de la autocompletación de código o el soporte técnico. Cline incluso puede usar el Model Context Protocol (MCP) para crear nuevas herramientas y expandir sus propias capacidades. Mientras que los scripts de IA autónomos tradicionalmente se ejecutan en entornos aislados, esta extensión ofrece una GUI con un humano en el bucle para aprobar cada cambio de archivo y comando de terminal, proporcionando una forma segura y accesible de explorar el potencial de la IA agencial.
|
||||
Gracias a las [habilidades de codificación agencial de Claude 4 Sonnet](https://www.anthropic.com/claude/sonnet), Cline puede abordar tareas complejas de desarrollo de software paso a paso. Con herramientas que le permiten crear y editar archivos, explorar grandes proyectos, usar el navegador y ejecutar comandos de terminal (con su aprobación), puede ayudarle de una manera que va más allá de la autocompletación de código o el soporte técnico. Cline incluso puede usar el Model Context Protocol (MCP) para crear nuevas herramientas y expandir sus propias capacidades. Mientras que los scripts de IA autónomos tradicionalmente se ejecutan en entornos aislados, esta extensión ofrece una GUI con un humano en el bucle para aprobar cada cambio de archivo y comando de terminal, proporcionando una forma segura y accesible de explorar el potencial de la IA agencial.
|
||||
|
||||
1. Ingrese su tarea y agregue imágenes para convertir maquetas en aplicaciones funcionales o solucionar errores con capturas de pantalla.
|
||||
2. Cline comenzará analizando su estructura de archivos y ASTs de código fuente, realizando búsquedas Regex y leyendo archivos relevantes para orientarse en proyectos existentes. Al gestionar cuidadosamente la información agregada, Cline puede proporcionar asistencia valiosa incluso en proyectos grandes y complejos sin sobrecargar la ventana de contexto.
|
||||
@@ -83,7 +83,7 @@ Todos los cambios realizados por Cline se registran en la línea de tiempo de su
|
||||
|
||||
### Usar el navegador
|
||||
|
||||
Con la nueva [habilidad de uso de computadora](https://www.anthropic.com/news/3-5-models-and-computer-use) de Claude 3.5 Sonnet, Cline puede iniciar un navegador, hacer clic en elementos, escribir texto y desplazarse, capturando capturas de pantalla y registros de consola. Esto permite la depuración interactiva, pruebas de extremo a extremo e incluso el uso general de la web. Esto le da la autonomía para solucionar errores visuales y problemas de tiempo de ejecución sin que tenga que copiar y pegar registros de errores.
|
||||
Con la nueva [habilidad de uso de computadora](https://www.anthropic.com/news/3-5-models-and-computer-use) de Claude 4 Sonnet, Cline puede iniciar un navegador, hacer clic en elementos, escribir texto y desplazarse, capturando capturas de pantalla y registros de consola. Esto permite la depuración interactiva, pruebas de extremo a extremo e incluso el uso general de la web. Esto le da la autonomía para solucionar errores visuales y problemas de tiempo de ejecución sin que tenga que copiar y pegar registros de errores.
|
||||
|
||||
Intente pedirle a Cline que "pruebe la aplicación" y observe cómo ejecuta un comando como `npm run dev`, inicia su servidor de desarrollo local en un navegador y realiza una serie de pruebas para confirmar que todo funciona. [Vea una demostración aquí.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
Clineは、**CLI**と**エディター**を使用できるAIアシスタントです。
|
||||
|
||||
[Claude 3.7 Sonnetのエージェント的コーディング機能](https://www.anthropic.com/claude/sonnet)のおかげで、Clineは複雑なソフトウェア開発タスクをステップバイステップで処理できます。ファイルの作成と編集、大規模プロジェクトの探索、ブラウザの使用、ターミナルコマンドの実行(許可後)などのツールを使用して、コード補完や技術サポートを超えた支援を提供します。Clineは、Model Context Protocol (MCP)を使用して新しいツールを作成し、自身の機能を拡張することもできます。自律的なAIスクリプトは通常サンドボックス環境で実行されますが、この拡張機能はファイル変更やターミナルコマンドを承認するための人間インターフェースを提供し、エージェント的AIの可能性を安全かつアクセスしやすい方法で探求できます。
|
||||
[Claude 4 Sonnetのエージェント的コーディング機能](https://www.anthropic.com/claude/sonnet)のおかげで、Clineは複雑なソフトウェア開発タスクをステップバイステップで処理できます。ファイルの作成と編集、大規模プロジェクトの探索、ブラウザの使用、ターミナルコマンドの実行(許可後)などのツールを使用して、コード補完や技術サポートを超えた支援を提供します。Clineは、Model Context Protocol (MCP)を使用して新しいツールを作成し、自身の機能を拡張することもできます。自律的なAIスクリプトは通常サンドボックス環境で実行されますが、この拡張機能はファイル変更やターミナルコマンドを承認するための人間インターフェースを提供し、エージェント的AIの可能性を安全かつアクセスしやすい方法で探求できます。
|
||||
|
||||
1. タスクを入力し、モックアップを機能するアプリに変換したり、スクリーンショットでバグを修正したりします。
|
||||
2. Clineは、ファイル構造とソースコードASTの分析、正規表現検索の実行、関連ファイルの読み取りから始め、既存プロジェクトに精通します。コンテキストに追加される情報を慎重に管理することで、大規模で複雑なプロジェクトでもコンテキストウィンドウを圧倒することなく貴重な支援を提供できます。
|
||||
@@ -83,7 +83,7 @@ Clineによるすべての変更はファイルのタイムラインに記録さ
|
||||
|
||||
### ブラウザの使用
|
||||
|
||||
Claude 3.5 Sonnetの新しい[コンピュータ使用](https://www.anthropic.com/news/3-5-models-and-computer-use)機能により、Clineはブラウザを起動し、要素をクリック、テキストを入力、スクロールし、各ステップでスクリーンショットとコンソールログをキャプチャできます。これにより、インタラクティブなデバッグ、エンドツーエンドテスト、さらには一般的なウェブ使用が可能になります。これにより、エラーログを手動でコピー&ペーストすることなく、視覚的なバグやランタイムの問題を自律的に修正できます。
|
||||
Claude 4 Sonnetの新しい[コンピュータ使用](https://www.anthropic.com/news/3-5-models-and-computer-use)機能により、Clineはブラウザを起動し、要素をクリック、テキストを入力、スクロールし、各ステップでスクリーンショットとコンソールログをキャプチャできます。これにより、インタラクティブなデバッグ、エンドツーエンドテスト、さらには一般的なウェブ使用が可能になります。これにより、エラーログを手動でコピー&ペーストすることなく、視覚的なバグやランタイムの問題を自律的に修正できます。
|
||||
|
||||
Clineに「アプリをテストして」と頼んでみてください。彼は`npm run dev`のようなコマンドを実行し、ローカルで実行中の開発サーバーをブラウザで起動し、一連のテストを実行してすべてが正常に動作することを確認します。[デモはこちら。](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
Cline을 만나보세요, **CLI** 및 **에디터**를 활용할 수 있는 AI 어시스턴트입니다.
|
||||
|
||||
[Claude 3.7 Sonnet의 에이전트형 코딩 기능](https://www.anthropic.com/claude/sonnet) 덕분에, Cline은 복잡한 소프트웨어 개발 작업을 단계별로 처리할 수 있습니다. 파일 생성과 편집, 대규모 프로젝트 탐색, 브라우저 사용, 터미널 명령 실행(권한 허가 필요) 등의 도구를 사용하여 단순 코드 완성이나 기술 지원을 넘어서는 도움을 제공합니다. Cline은 Model Context Protocol(MCP)를 사용하여 새로운 도구를 만들고 자신의 기능을 확장할 수도 있습니다. 자율적인 AI 스크립트는 일반적으로 샌드박스 환경에서 실행되지만, 이 확장 프로그램은 모든 파일 변경 및 터미널 명령을 승인할 수 있는 사람이 개입가능한 GUI를 제공하여, 에이전트형 AI의 잠재력을 보다 안전하고 쉽게 탐색할 수 있도록 합니다.
|
||||
[Claude 4 Sonnet의 에이전트형 코딩 기능](https://www.anthropic.com/claude/sonnet) 덕분에, Cline은 복잡한 소프트웨어 개발 작업을 단계별로 처리할 수 있습니다. 파일 생성과 편집, 대규모 프로젝트 탐색, 브라우저 사용, 터미널 명령 실행(권한 허가 필요) 등의 도구를 사용하여 단순 코드 완성이나 기술 지원을 넘어서는 도움을 제공합니다. Cline은 Model Context Protocol(MCP)를 사용하여 새로운 도구를 만들고 자신의 기능을 확장할 수도 있습니다. 자율적인 AI 스크립트는 일반적으로 샌드박스 환경에서 실행되지만, 이 확장 프로그램은 모든 파일 변경 및 터미널 명령을 승인할 수 있는 사람이 개입가능한 GUI를 제공하여, 에이전트형 AI의 잠재력을 보다 안전하고 쉽게 탐색할 수 있도록 합니다.
|
||||
|
||||
1. 작업을 입력하고, 목업을 기능하는 앱으로 변환하거나 스크린샷으로 버그를 수정합니다.
|
||||
2. Cline은 파일 구조와 소스코드 AST의 분석, 정규식 검색 실행, 관련 파일 읽기부터 시작하여 기존 프로젝트를 파악합니다. 또한, 어떤 정보를 컨텍스트에 추가할지를 신중하게 관리하여, 대규모 복잡한 프로젝트에서도 컨텍스트 윈도우를 과부하시키지 않으면서도 효과적인 지원을 제공합니다.
|
||||
@@ -78,7 +78,7 @@ Cline에 의한 모든 변경은 파일의 타임라인에 기록되어 필요
|
||||
|
||||
### 브라우저 사용
|
||||
|
||||
Claude 3.5 Sonnet의 새로운 [컴퓨터 사용](https://www.anthropic.com/news/3-5-models-and-computer-use) 기능으로 인해, Cline은 브라우저를 실행하고 요소를 클릭하고 텍스트를 입력하고 스크롤하며 각 단계에서 스크린샷과 콘솔 로그를 캡처할 수 있습니다. 이를 통해 인터랙티브한 디버깅, 엔드투엔드 테스트, 심지어 일반적인 웹 탐색까지 가능해집니다. 이로 인해 오류 로그를 수동으로 복사 & 붙여넣기 할 필요 없이 시각적 버그나 런타임 문제를 자율적으로 수정할 수 있습니다.
|
||||
Claude 4 Sonnet의 새로운 [컴퓨터 사용](https://www.anthropic.com/news/3-5-models-and-computer-use) 기능으로 인해, Cline은 브라우저를 실행하고 요소를 클릭하고 텍스트를 입력하고 스크롤하며 각 단계에서 스크린샷과 콘솔 로그를 캡처할 수 있습니다. 이를 통해 인터랙티브한 디버깅, 엔드투엔드 테스트, 심지어 일반적인 웹 탐색까지 가능해집니다. 이로 인해 오류 로그를 수동으로 복사 & 붙여넣기 할 필요 없이 시각적 버그나 런타임 문제를 자율적으로 수정할 수 있습니다.
|
||||
|
||||
Cline에게 "앱을 테스트해줘"라고 요청하면, `npm run dev`와 같은 명령을 실행하고 로컬에서 실행 중인 개발 서버를 브라우저에서 실행하여 일련의 테스트를 수행하고 모든 것이 정상적으로 작동하는지 확인합니다. [데모는 여기를 참조하세요.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
Conheça o Cline: um assistente de IA que pode usar seu **CLI** e **Editor**.
|
||||
|
||||
Graças às [habilidades avançadas do Claude 3.7 Sonnet](https://www.anthropic.com/claude/sonnet), o Cline pode lidar com tarefas complexas de desenvolvimento de software passo a passo. Com ferramentas que permitem criar e editar arquivos, explorar grandes projetos, usar o navegador e executar comandos no terminal (com sua aprovação), ele pode ajudar você de maneiras que vão além da inclusão de código ou suporte técnico. O Cline pode é capaz inclusive de usar o Model Context Protocol (MCP) para criar novas ferramentas e expandir seus próprios recursos. Embora os scripts de IA autônomas tradicionalmente sejam executados em ambientes isolados, esta extensão oferece uma GUI com um humano no circuito para aprovar cada alteração de arquivo e comando de terminal, fornecendo uma maneira segura e acessível de explorar todo o potencial da IA.
|
||||
Graças às [habilidades avançadas do Claude 4 Sonnet](https://www.anthropic.com/claude/sonnet), o Cline pode lidar com tarefas complexas de desenvolvimento de software passo a passo. Com ferramentas que permitem criar e editar arquivos, explorar grandes projetos, usar o navegador e executar comandos no terminal (com sua aprovação), ele pode ajudar você de maneiras que vão além da inclusão de código ou suporte técnico. O Cline pode é capaz inclusive de usar o Model Context Protocol (MCP) para criar novas ferramentas e expandir seus próprios recursos. Embora os scripts de IA autônomas tradicionalmente sejam executados em ambientes isolados, esta extensão oferece uma GUI com um humano no circuito para aprovar cada alteração de arquivo e comando de terminal, fornecendo uma maneira segura e acessível de explorar todo o potencial da IA.
|
||||
|
||||
1. Insira sua tarefa e adicione imagens para transformar mockups em aplicativos funcionais ou corrigir erros através de capturas de tela.
|
||||
|
||||
@@ -83,7 +83,7 @@ Todas as alterações feitas pelo Cline são registradas na Linha do tempo do ar
|
||||
|
||||
### Uso do navegador
|
||||
|
||||
Com a nova habilidade de [uso de computador](https://www.anthropic.com/news/3-5-models-and-computer-use) do Claude Sonnet 3.5, Cline pode abrir um navegador, clicar em elementos, digitar texto e rolar, capturando a tela e logs de console. Isso permite depurar de maneira interativa, testes end-to-end e até mesmo uso geral da web. Isso lhe dá autonomia para solucionar erros visuais e problemas em tempo de execução sem precisar copiar e colar logs dos erros.
|
||||
Com a nova habilidade de [uso de computador](https://www.anthropic.com/news/3-5-models-and-computer-use) do Claude Sonnet 4, Cline pode abrir um navegador, clicar em elementos, digitar texto e rolar, capturando a tela e logs de console. Isso permite depurar de maneira interativa, testes end-to-end e até mesmo uso geral da web. Isso lhe dá autonomia para solucionar erros visuais e problemas em tempo de execução sem precisar copiar e colar logs dos erros.
|
||||
|
||||
Tente pedir a Cline para "testar o aplicativo" e observe enquanto o Cline executa um comando como `npm run dev`, inicia seu servidor de desenvolvimento local em um navegador e executa uma série de testes para confirmar se tudo funciona. [Veja uma demonstração aqui.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
认识 Cline —— 一个可以使用你的 **终端** 和 **编辑器** 的 AI 助手。
|
||||
|
||||
得益于 [Claude 3.7 Sonnet 的代理式编码能力](https://www.anthropic.com/claude/sonnet),Cline 能够逐步处理复杂的软件开发任务。借助于一系列工具,他可以创建和编辑文件、浏览大型项目、使用浏览器,并在你授权后执行终端命令,从而在代码补全或技术支持之外提供更深入的帮助。Cline 甚至还能使用 Model Context Protocol(MCP)来创建新工具,并扩展自身的能力。虽然传统的自动化 AI 脚本通常运行在沙盒环境中,但这个扩展提供了一个人类参与审核的图形界面(GUI),用于审批每一次文件变更和终端命令,从而为探索代理式 AI 的潜力提供了一种安全且易于使用的方式。
|
||||
得益于 [Claude 4 Sonnet 的代理式编码能力](https://www.anthropic.com/claude/sonnet),Cline 能够逐步处理复杂的软件开发任务。借助于一系列工具,他可以创建和编辑文件、浏览大型项目、使用浏览器,并在你授权后执行终端命令,从而在代码补全或技术支持之外提供更深入的帮助。Cline 甚至还能使用 Model Context Protocol(MCP)来创建新工具,并扩展自身的能力。虽然传统的自动化 AI 脚本通常运行在沙盒环境中,但这个扩展提供了一个人类参与审核的图形界面(GUI),用于审批每一次文件变更和终端命令,从而为探索代理式 AI 的潜力提供了一种安全且易于使用的方式。
|
||||
|
||||
1. 输入你的任务,并添加图片,以将界面原型(mockup)转换为功能应用,或通过截图修复 bug。
|
||||
2. Cline 会从分析你的文件结构和源代码的抽象语法树(AST)开始,同时执行正则搜索并读取相关文件,以便尽快熟悉项目上下文。通过精细地管理上下文中引入的信息,即使面对大型复杂项目,Cline 也能在不超出上下文窗口限制的前提下提供有效协助。
|
||||
@@ -83,7 +83,7 @@ Cline 所做的所有更改都会记录在你的文件时间轴中,提供了
|
||||
|
||||
### 使用浏览器
|
||||
|
||||
借助 Claude 3.5 Sonnet 的新 [计算机使用](https://www.anthropic.com/news/3-5-models-and-computer-use) 功能,Cline 可以启动浏览器,点击元素,输入文本和滚动,在每一步捕获截图和控制台日志。这允许进行交互式调试、端到端测试,甚至是一般的网页使用!这使他能够自主修复视觉错误和运行时问题,而无需你亲自操作和复制粘贴错误日志。
|
||||
借助 Claude 4 Sonnet 的新 [计算机使用](https://www.anthropic.com/news/3-5-models-and-computer-use) 功能,Cline 可以启动浏览器,点击元素,输入文本和滚动,在每一步捕获截图和控制台日志。这允许进行交互式调试、端到端测试,甚至是一般的网页使用!这使他能够自主修复视觉错误和运行时问题,而无需你亲自操作和复制粘贴错误日志。
|
||||
|
||||
试试让 Cline “测试应用程序”,看看他如何运行 `npm run dev` 命令,在浏览器中启动你本地运行的开发服务器,并执行一系列测试以确认一切正常。[在这里查看演示。](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
認識 Cline,一個可以使用您的**命令列介面** (CLI) 和**程式編輯器** (Editor) 的 AI 助理。
|
||||
|
||||
感謝 [Claude 3.7 Sonnet 的代理式程式設計能力](https://www.anthropic.com/claude/sonnet),Cline 能夠逐步處理複雜的軟體開發任務。透過能讓他建立和編輯檔案、探索大型專案、使用瀏覽器,以及執行終端機指令(在您授權後)的工具,從而在程式碼補全或技術支援之外提供更深入的協助。Cline 甚至能使用模型上下文協定(Model Context Protocol,MCP)來建立新工具並擴展自己的功能。雖然自主 AI 腳本傳統上會在沙箱環境中執行,但這個擴充套件提供了人機互動的圖形介面,讓您可以核准每個檔案變更和終端機指令,提供一個安全且容易使用的方式來探索代理式 AI 的潛力。
|
||||
感謝 [Claude 4 Sonnet 的代理式程式設計能力](https://www.anthropic.com/claude/sonnet),Cline 能夠逐步處理複雜的軟體開發任務。透過能讓他建立和編輯檔案、探索大型專案、使用瀏覽器,以及執行終端機指令(在您授權後)的工具,從而在程式碼補全或技術支援之外提供更深入的協助。Cline 甚至能使用模型上下文協定(Model Context Protocol,MCP)來建立新工具並擴展自己的功能。雖然自主 AI 腳本傳統上會在沙箱環境中執行,但這個擴充套件提供了人機互動的圖形介面,讓您可以核准每個檔案變更和終端機指令,提供一個安全且容易使用的方式來探索代理式 AI 的潛力。
|
||||
|
||||
1. 輸入您的任務,並可以加入圖片來將設計稿轉換成功能性應用程式,或使用截圖來修正錯誤。
|
||||
2. Cline 會先分析您的檔案結構和程式碼 AST、執行正規表達式搜尋,並讀取相關檔案,以便在現有專案中快速掌握狀況。透過仔細管理加入上下文的資訊,Cline 可以在不超過上下文視窗的情況下,為大型且複雜的專案提供有價值的協助。
|
||||
@@ -84,7 +84,7 @@ Cline 可以直接在您的編輯器中建立和編輯檔案,並顯示變更
|
||||
|
||||
### 使用瀏覽器
|
||||
|
||||
透過 Claude 3.5 Sonnet 的新[電腦使用](https://www.anthropic.com/news/3-5-models-and-computer-use)功能,Cline 可以啟動瀏覽器、點選元素、輸入文字和捲動,在每個步驟擷取螢幕截圖和主控台記錄。這讓互動式除錯、端對端測試,甚至一般網頁使用成為可能!這讓他能獨立修正視覺問題和執行時錯誤,而不需要您手動複製錯誤記錄。
|
||||
透過 Claude 4 Sonnet 的新[電腦使用](https://www.anthropic.com/news/3-5-models-and-computer-use)功能,Cline 可以啟動瀏覽器、點選元素、輸入文字和捲動,在每個步驟擷取螢幕截圖和主控台記錄。這讓互動式除錯、端對端測試,甚至一般網頁使用成為可能!這讓他能獨立修正視覺問題和執行時錯誤,而不需要您手動複製錯誤記錄。
|
||||
|
||||
試著請 Cline 「測試應用程式」,觀察他如何執行 `npm run dev`、在瀏覽器中啟動您的本機開發伺服器,並執行一系列測試來確認一切正常運作。[點此觀看示範](https://x.com/sdrzn/status/1850880547825823989)。
|
||||
|
||||
@@ -108,10 +108,13 @@ Cline 可以直接在您的編輯器中建立和編輯檔案,並顯示變更
|
||||
|
||||
### 新增上下文
|
||||
|
||||
**`@url`:**貼上網址讓擴充套件擷取並轉換為 Markdown,當您想給 Cline 最新文件時很有用
|
||||
**`@problems`:**新增工作區的錯誤和警告(「問題」面板)給 Cline 修正
|
||||
**`@file`:**新增檔案內容,讓您不必浪費 API 請求來核准讀取檔案(+ 輸入以搜尋檔案)
|
||||
**`@folder`:**一次新增整個資料夾的檔案,讓您的工作流程更快速
|
||||
**`@url`**:貼上網址讓擴充套件擷取並轉換為 Markdown,當您想給 Cline 最新文件時很有用
|
||||
|
||||
**`@problems`**:新增工作區的錯誤和警告(「問題」面板)給 Cline 修正
|
||||
|
||||
**`@file`**:新增檔案內容,讓您不必浪費 API 請求來核准讀取檔案(+ 輸入以搜尋檔案)
|
||||
|
||||
**`@folder`**:一次新增整個資料夾的檔案,讓您的工作流程更快速
|
||||
|
||||
<!-- 透明像素用於浮動圖片後的換行 -->
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
Generated
+3485
-1618
File diff suppressed because it is too large
Load Diff
+22
-10
@@ -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.16.2",
|
||||
"version": "3.17.9",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -50,8 +50,15 @@
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "claude-dev-ActivityBar",
|
||||
"title": "Cline",
|
||||
"icon": "assets/icons/icon.svg"
|
||||
"title": "Cline (⌘+')",
|
||||
"icon": "assets/icons/icon.svg",
|
||||
"when": "isMac"
|
||||
},
|
||||
{
|
||||
"id": "claude-dev-ActivityBar",
|
||||
"title": "Cline (Ctrl+')",
|
||||
"icon": "assets/icons/icon.svg",
|
||||
"when": "!isMac"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -260,19 +267,19 @@
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run package",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||
"compile-standalone": "npm run protos && npm run check-types && npm run lint && node esbuild.js --standalone",
|
||||
"compile-standalone": "npm run check-types && npm run lint && node esbuild.js --standalone",
|
||||
"postcompile-standalone": "node scripts/package-standalone.mjs",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production",
|
||||
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs",
|
||||
"postprotos": "prettier src/shared/proto src/core/controller webview-ui/src/services src/standalone/server-setup.ts --write --log-level silent",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
"check-types": "tsc --noEmit",
|
||||
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run compile-standalone && npm run lint",
|
||||
"check-types": "npm run protos && tsc --noEmit",
|
||||
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && cd webview-ui && npm run lint",
|
||||
"format": "prettier . --check",
|
||||
"format:fix": "prettier . --write",
|
||||
"test": "npm-run-all test:unit test:integration",
|
||||
@@ -309,13 +316,15 @@
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@types/vscode": "^1.84.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@typescript-eslint/utils": "^8.33.0",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.4.1",
|
||||
"chai": "^4.3.10",
|
||||
"chalk": "^5.3.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-eslint-rules": "file:eslint-rules",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"husky": "^9.1.7",
|
||||
"mintlify": "^4.0.515",
|
||||
@@ -334,8 +343,9 @@
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.6.4",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.821.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
"@google/genai": "^0.13.0",
|
||||
"@grpc/grpc-js": "^1.9.15",
|
||||
@@ -349,6 +359,7 @@
|
||||
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.8.2",
|
||||
@@ -358,6 +369,7 @@
|
||||
"clone-deep": "^4.0.1",
|
||||
"default-shell": "^2.2.0",
|
||||
"diff": "^5.2.0",
|
||||
"exceljs": "^4.4.0",
|
||||
"execa": "^9.5.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"firebase": "^11.2.0",
|
||||
|
||||
@@ -16,4 +16,7 @@ service AccountService {
|
||||
// Handles the user clicking the logout button in the UI.
|
||||
// Clears API keys and user state.
|
||||
rpc accountLogoutClicked(EmptyRequest) returns (Empty);
|
||||
|
||||
// Subscribe to auth callback events (when authentication tokens are received)
|
||||
rpc subscribeToAuthCallback(EmptyRequest) returns (stream String);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,44 @@ import { fileURLToPath } from "url"
|
||||
import { execSync } from "child_process"
|
||||
import { globby } from "globby"
|
||||
import chalk from "chalk"
|
||||
import os from "os"
|
||||
|
||||
import { createRequire } from "module"
|
||||
const require = createRequire(import.meta.url)
|
||||
const protoc = path.join(require.resolve("grpc-tools"), "../bin/protoc")
|
||||
|
||||
// Check for Apple Silicon compatibility
|
||||
function checkAppleSiliconCompatibility() {
|
||||
// Only run check on macOS
|
||||
if (process.platform !== "darwin") {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if running on Apple Silicon
|
||||
const cpuArchitecture = os.arch()
|
||||
if (cpuArchitecture === "arm64") {
|
||||
try {
|
||||
// Check if Rosetta is installed
|
||||
const rosettaCheck = execSync('/usr/bin/pgrep oahd || echo "NOT_INSTALLED"').toString().trim()
|
||||
|
||||
if (rosettaCheck === "NOT_INSTALLED") {
|
||||
console.log(chalk.yellow("Detected Apple Silicon (ARM64) architecture."))
|
||||
console.log(
|
||||
chalk.red("Rosetta 2 is NOT installed. The npm version of protoc is not compatible with Apple Silicon."),
|
||||
)
|
||||
console.log(chalk.cyan("Please install Rosetta 2 using the following command:"))
|
||||
console.log(chalk.cyan(" softwareupdate --install-rosetta --agree-to-license"))
|
||||
console.log(chalk.red("Aborting build process."))
|
||||
process.exit(1)
|
||||
} else {
|
||||
console.log(chalk.green("Rosetta 2 is installed. Continuing with build."))
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(chalk.yellow("Could not determine Rosetta installation status. Proceeding anyway."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const SCRIPT_DIR = path.dirname(__filename)
|
||||
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
|
||||
@@ -42,6 +75,9 @@ const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(RO
|
||||
async function main() {
|
||||
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
|
||||
|
||||
// Check for Apple Silicon compatibility before proceeding
|
||||
checkAppleSiliconCompatibility()
|
||||
|
||||
// Define output directories
|
||||
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
|
||||
|
||||
|
||||
@@ -58,3 +58,8 @@ message Boolean {
|
||||
message StringArray {
|
||||
repeated string values = 1;
|
||||
}
|
||||
|
||||
message StringArrays {
|
||||
repeated string values1 = 1;
|
||||
repeated string values2 = 2;
|
||||
}
|
||||
|
||||
+8
-1
@@ -31,6 +31,9 @@ service FileService {
|
||||
|
||||
// Select images from the file system and return as data URLs
|
||||
rpc selectImages(EmptyRequest) returns (StringArray);
|
||||
|
||||
// Select images and other files from the file system and returns as data URLs & paths respectively
|
||||
rpc selectFiles(BooleanRequest) returns (StringArrays);
|
||||
|
||||
// Convert URIs to workspace-relative paths
|
||||
rpc getRelativePaths(RelativePathsRequest) returns (RelativePaths);
|
||||
@@ -49,6 +52,9 @@ service FileService {
|
||||
|
||||
// Refreshes all rule toggles (Cline, External, and Workflows)
|
||||
rpc refreshRules(EmptyRequest) returns (RefreshedRules);
|
||||
|
||||
// Opens a task's conversation history file on disk
|
||||
rpc openTaskHistory(StringRequest) returns (Empty);
|
||||
}
|
||||
|
||||
// Response for refreshRules operation
|
||||
@@ -57,7 +63,8 @@ message RefreshedRules {
|
||||
ClineRulesToggles local_cline_rules_toggles = 2;
|
||||
ClineRulesToggles local_cursor_rules_toggles = 3;
|
||||
ClineRulesToggles local_windsurf_rules_toggles = 4;
|
||||
ClineRulesToggles workflow_toggles = 5;
|
||||
ClineRulesToggles local_workflow_toggles = 5;
|
||||
ClineRulesToggles global_workflow_toggles = 6;
|
||||
}
|
||||
|
||||
// Request to toggle a Windsurf rule
|
||||
|
||||
@@ -15,6 +15,7 @@ service McpService {
|
||||
rpc deleteMcpServer(StringRequest) returns (McpServers);
|
||||
rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers);
|
||||
rpc refreshMcpMarketplace(EmptyRequest) returns (McpMarketplaceCatalog);
|
||||
rpc openMcpSettings(EmptyRequest) returns (Empty);
|
||||
}
|
||||
|
||||
message ToggleMcpServerRequest {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
syntax = "proto3";
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
@@ -10,6 +12,7 @@ service StateService {
|
||||
rpc resetState(EmptyRequest) returns (Empty);
|
||||
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Empty);
|
||||
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
|
||||
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
|
||||
}
|
||||
|
||||
message State {
|
||||
@@ -36,4 +39,28 @@ message ChatSettings {
|
||||
message ChatContent {
|
||||
optional string message = 1;
|
||||
repeated string images = 2;
|
||||
repeated string files = 3;
|
||||
}
|
||||
|
||||
// Message for auto approval settings
|
||||
message AutoApprovalSettingsRequest {
|
||||
Metadata metadata = 1;
|
||||
|
||||
message Actions {
|
||||
bool read_files = 1;
|
||||
bool read_files_externally = 2;
|
||||
bool edit_files = 3;
|
||||
bool edit_files_externally = 4;
|
||||
bool execute_safe_commands = 5;
|
||||
bool execute_all_commands = 6;
|
||||
bool use_browser = 7;
|
||||
bool use_mcp = 8;
|
||||
}
|
||||
|
||||
int32 version = 2;
|
||||
bool enabled = 3;
|
||||
Actions actions = 4;
|
||||
int32 max_requests = 5;
|
||||
bool enable_notifications = 6;
|
||||
repeated string favorites = 7;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ service TaskService {
|
||||
rpc cancelTask(EmptyRequest) returns (Empty);
|
||||
// Clears the current task
|
||||
rpc clearTask(EmptyRequest) returns (Empty);
|
||||
// Gets the total size of all tasks
|
||||
rpc getTotalTasksSize(EmptyRequest) returns (Int64);
|
||||
// Deletes multiple tasks with the given IDs
|
||||
rpc deleteTasksWithIds(StringArrayRequest) returns (Empty);
|
||||
// Creates a new task with the given text and optional images
|
||||
@@ -38,6 +40,7 @@ message NewTaskRequest {
|
||||
Metadata metadata = 1;
|
||||
string text = 2;
|
||||
repeated string images = 3;
|
||||
repeated string files = 4;
|
||||
}
|
||||
|
||||
// Request message for toggling task favorite status
|
||||
@@ -102,4 +105,5 @@ message AskResponseRequest {
|
||||
string response_type = 2;
|
||||
string text = 3;
|
||||
repeated string images = 4;
|
||||
repeated string files = 5;
|
||||
}
|
||||
|
||||
+24
-1
@@ -6,9 +6,32 @@ option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
// Enum for webview provider types
|
||||
enum WebviewProviderType {
|
||||
SIDEBAR = 0;
|
||||
TAB = 1;
|
||||
}
|
||||
|
||||
// Define a new message type for webview provider info
|
||||
message WebviewProviderTypeRequest {
|
||||
Metadata metadata = 1;
|
||||
WebviewProviderType providerType = 2;
|
||||
}
|
||||
|
||||
// UiService provides methods for managing UI interactions
|
||||
service UiService {
|
||||
// Scrolls to a specific settings section in the settings view
|
||||
rpc scrollToSettings(StringRequest) returns (Empty);
|
||||
|
||||
// Marks the current announcement as shown and returns whether an announcement should still be shown
|
||||
rpc onDidShowAnnouncement(EmptyRequest) returns (Boolean);
|
||||
|
||||
// Subscribe to addToInput events (when user adds content via context menu)
|
||||
rpc subscribeToAddToInput(EmptyRequest) returns (stream String);
|
||||
|
||||
// Subscribe to MCP button clicked events
|
||||
rpc subscribeToMcpButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to history button click events
|
||||
rpc subscribeToHistoryButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import "common.proto";
|
||||
service WebService {
|
||||
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
|
||||
rpc fetchOpenGraphData(StringRequest) returns (OpenGraphData);
|
||||
rpc openInBrowser(StringRequest) returns (Empty);
|
||||
}
|
||||
|
||||
message IsImageUrl {
|
||||
|
||||
@@ -3,7 +3,8 @@ set -eu
|
||||
|
||||
DIR=${1:-src/}
|
||||
DEST_DIR=dist-standalone
|
||||
DEST=dist-standalone/vscode-uses.txt
|
||||
SDK_DEST=$DEST_DIR/vscode-sdk-uses.txt
|
||||
CSS_DEST=$DEST_DIR/vscode-css-uses.txt
|
||||
mkdir -p $DEST_DIR
|
||||
|
||||
{
|
||||
@@ -11,8 +12,16 @@ git grep -h 'vscode\.' $DIR |
|
||||
grep -Ev '//.*vscode' | # remove commented out code
|
||||
sed 's|.*vscode\.|vscode.|'| # remove everything before vscode.
|
||||
sed 's/[^a-zA-Z0-9_.].*$//' | # remove everything after last identifier
|
||||
sort | uniq > $DEST
|
||||
sort | uniq > $SDK_DEST
|
||||
}
|
||||
echo Wrote uses of the vscode SDK to $(realpath $SDK_DEST)
|
||||
|
||||
echo Done, wrote uses of the vscode SDK to $(realpath $DEST)
|
||||
{
|
||||
grep -rh -- --vscode- webview-ui/build/ |
|
||||
sed 's/--vscode/\n--vscode/g' | # One var per line
|
||||
grep -- --vscode | # Remove lines that don't have vars.
|
||||
sed 's/[),"\\].*$//' | # remove from the end of the var name to the end of the line.
|
||||
sort | uniq > $CSS_DEST
|
||||
}
|
||||
echo Wrote vscode vars used to $(realpath $CSS_DEST)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
|
||||
import { DeepSeekHandler } from "./providers/deepseek"
|
||||
import { RequestyHandler } from "./providers/requesty"
|
||||
import { TogetherHandler } from "./providers/together"
|
||||
import { NebiusHandler } from "./providers/nebius"
|
||||
import { QwenHandler } from "./providers/qwen"
|
||||
import { MistralHandler } from "./providers/mistral"
|
||||
import { DoubaoHandler } from "./providers/doubao"
|
||||
@@ -23,6 +24,7 @@ import { FireworksHandler } from "./providers/fireworks"
|
||||
import { AskSageHandler } from "./providers/asksage"
|
||||
import { XAIHandler } from "./providers/xai"
|
||||
import { SambanovaHandler } from "./providers/sambanova"
|
||||
import { CerebrasHandler } from "./providers/cerebras"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -75,12 +77,16 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
return new ClineHandler(options)
|
||||
case "litellm":
|
||||
return new LiteLlmHandler(options)
|
||||
case "nebius":
|
||||
return new NebiusHandler(options)
|
||||
case "asksage":
|
||||
return new AskSageHandler(options)
|
||||
case "xai":
|
||||
return new XAIHandler(options)
|
||||
case "sambanova":
|
||||
return new SambanovaHandler(options)
|
||||
case "cerebras":
|
||||
return new CerebrasHandler(options)
|
||||
default:
|
||||
return new AnthropicHandler(options)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import "should"
|
||||
import { AwsBedrockHandler } from "../bedrock"
|
||||
import { ApiHandlerOptions } from "@shared/api"
|
||||
|
||||
describe("AwsBedrockHandler", () => {
|
||||
describe("withTempEnv", () => {
|
||||
// Store original env vars for cleanup
|
||||
const originalEnv: Record<string, string | undefined> = {}
|
||||
|
||||
beforeEach(() => {
|
||||
// Store original values before each test
|
||||
originalEnv.TEST_VAR = process.env.TEST_VAR
|
||||
originalEnv.ANOTHER_VAR = process.env.ANOTHER_VAR
|
||||
originalEnv.VAR1 = process.env.VAR1
|
||||
originalEnv.VAR2 = process.env.VAR2
|
||||
originalEnv.VAR3 = process.env.VAR3
|
||||
originalEnv.UNDEFINED_VAR = process.env.UNDEFINED_VAR
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original values after each test
|
||||
Object.entries(originalEnv).forEach(([key, value]) => {
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it("should restore original environment variables after operation", async () => {
|
||||
// Set initial environment
|
||||
process.env.TEST_VAR = "original"
|
||||
process.env.ANOTHER_VAR = "another"
|
||||
|
||||
// Store original values
|
||||
const originalTestVar = process.env.TEST_VAR
|
||||
const originalAnotherVar = process.env.ANOTHER_VAR
|
||||
|
||||
await AwsBedrockHandler["withTempEnv"](
|
||||
() => {
|
||||
process.env.TEST_VAR = "modified"
|
||||
delete process.env.ANOTHER_VAR
|
||||
},
|
||||
async () => {
|
||||
// Verify environment is modified
|
||||
process.env.TEST_VAR!.should.equal("modified")
|
||||
should.not.exist(process.env.ANOTHER_VAR)
|
||||
return "test"
|
||||
},
|
||||
)
|
||||
|
||||
// Verify environment is restored
|
||||
process.env.TEST_VAR!.should.equal(originalTestVar)
|
||||
process.env.ANOTHER_VAR!.should.equal(originalAnotherVar)
|
||||
})
|
||||
|
||||
it("should handle undefined environment variables", async () => {
|
||||
await AwsBedrockHandler["withTempEnv"](
|
||||
() => {
|
||||
delete process.env.UNDEFINED_VAR
|
||||
},
|
||||
async () => {
|
||||
should.not.exist(process.env.UNDEFINED_VAR)
|
||||
return "test"
|
||||
},
|
||||
)
|
||||
|
||||
// Verify undefined variable is not present
|
||||
should.not.exist(process.env.UNDEFINED_VAR)
|
||||
})
|
||||
|
||||
it("should handle errors and still restore environment", async () => {
|
||||
// Set initial environment
|
||||
process.env.TEST_VAR = "original"
|
||||
|
||||
try {
|
||||
await AwsBedrockHandler["withTempEnv"](
|
||||
() => {
|
||||
process.env.TEST_VAR = "modified"
|
||||
},
|
||||
async () => {
|
||||
throw new Error("Test error")
|
||||
},
|
||||
)
|
||||
should.fail(null, null, "Expected error was not thrown", "throw")
|
||||
} catch (error) {
|
||||
;(error as Error).message.should.equal("Test error")
|
||||
}
|
||||
|
||||
// Verify environment is restored even after error
|
||||
process.env.TEST_VAR!.should.equal("original")
|
||||
})
|
||||
|
||||
it("should handle multiple environment variable changes", async () => {
|
||||
// Set initial environment
|
||||
process.env.VAR1 = "original1"
|
||||
process.env.VAR2 = "original2"
|
||||
process.env.VAR3 = "original3"
|
||||
|
||||
// Store original values
|
||||
const originalVar1 = process.env.VAR1
|
||||
const originalVar2 = process.env.VAR2
|
||||
const originalVar3 = process.env.VAR3
|
||||
|
||||
await AwsBedrockHandler["withTempEnv"](
|
||||
() => {
|
||||
process.env.VAR1 = "modified1"
|
||||
process.env.VAR2 = "modified2"
|
||||
delete process.env.VAR3
|
||||
},
|
||||
async () => {
|
||||
// Verify environment is modified
|
||||
process.env.VAR1!.should.equal("modified1")
|
||||
process.env.VAR2!.should.equal("modified2")
|
||||
should.not.exist(process.env.VAR3)
|
||||
return "test"
|
||||
},
|
||||
)
|
||||
|
||||
// Verify environment is restored
|
||||
process.env.VAR1!.should.equal(originalVar1)
|
||||
process.env.VAR2!.should.equal(originalVar2)
|
||||
process.env.VAR3!.should.equal(originalVar3)
|
||||
})
|
||||
|
||||
it("should work with AWS_PROFILE", async () => {
|
||||
process.env["AWS_PROFILE"] = "test-profile"
|
||||
|
||||
const preAWSProfile = process.env["AWS_PROFILE"]
|
||||
|
||||
await AwsBedrockHandler["withTempEnv"](
|
||||
() => {
|
||||
delete process.env["AWS_PROFILE"]
|
||||
},
|
||||
async () => {
|
||||
should.not.exist(process.env["AWS_PROFILE"])
|
||||
return "test"
|
||||
},
|
||||
)
|
||||
|
||||
process.env["AWS_PROFILE"]!.should.equal(preAWSProfile)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -24,13 +24,15 @@ export class AnthropicHandler implements ApiHandler {
|
||||
const modelId = model.id
|
||||
|
||||
const budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
const reasoningOn = (modelId.includes("3-7") || modelId.includes("4-")) && budget_tokens !== 0 ? true : false
|
||||
|
||||
switch (modelId) {
|
||||
// 'latest' alias does not support cache_control
|
||||
case "claude-sonnet-4-20250514":
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
case "claude-3-5-sonnet-20241022":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
case "claude-opus-4-20250514":
|
||||
case "claude-3-opus-20240229":
|
||||
case "claude-3-haiku-20240307": {
|
||||
/*
|
||||
@@ -96,6 +98,8 @@ export class AnthropicHandler implements ApiHandler {
|
||||
// https://github.com/anthropics/anthropic-sdk-typescript?tab=readme-ov-file#default-headers
|
||||
// https://github.com/anthropics/anthropic-sdk-typescript/commit/c920b77fc67bd839bfeb6716ceab9d7c9bbe7393
|
||||
switch (modelId) {
|
||||
case "claude-sonnet-4-20250514":
|
||||
case "claude-opus-4-20250514":
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
case "claude-3-5-sonnet-20241022":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
@@ -129,7 +133,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
switch (chunk?.type) {
|
||||
case "message_start":
|
||||
// tells us cache reads/writes/input/output
|
||||
const usage = chunk.message.usage
|
||||
|
||||
@@ -47,7 +47,11 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = baseModelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
const reasoningOn =
|
||||
(baseModelId.includes("3-7") || baseModelId.includes("sonnet-4") || baseModelId.includes("opus-4")) &&
|
||||
budget_tokens !== 0
|
||||
? true
|
||||
: false
|
||||
|
||||
// Get model info and message indices for caching
|
||||
const userMsgIndices = messages.reduce((acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), [] as number[])
|
||||
@@ -58,59 +62,65 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
// initialization, and allowing for session renewal if necessary as well
|
||||
const client = await this.getAnthropicClient()
|
||||
|
||||
// AWS SDK prioritizes AWS_PROFILE over AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY pair
|
||||
// If this is set as an env variable already (ie. from ~/.zshrc) it will override credentials configured by Cline
|
||||
const previousEnv = process.env
|
||||
delete process.env["AWS_PROFILE"]
|
||||
const stream = await client.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
temperature: reasoningOn ? undefined : 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
...(this.options.awsBedrockUsePromptCache === true && {
|
||||
cache_control: { type: "ephemeral" },
|
||||
}),
|
||||
},
|
||||
],
|
||||
messages: messages.map((message, index) => {
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
...(this.options.awsBedrockUsePromptCache === true && {
|
||||
cache_control: { type: "ephemeral" },
|
||||
}),
|
||||
},
|
||||
]
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? {
|
||||
...content,
|
||||
// Use withTempEnv to ensure environment variables are properly restored
|
||||
const stream = await AwsBedrockHandler.withTempEnv(
|
||||
() => {
|
||||
// AWS SDK prioritizes AWS_PROFILE over AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY pair
|
||||
// If this is set as an env variable already (ie. from ~/.zshrc) it will override credentials configured by Cline
|
||||
// Temporarily remove AWS_PROFILE to ensure our credentials are used
|
||||
delete process.env["AWS_PROFILE"]
|
||||
},
|
||||
async () => {
|
||||
return await client.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
temperature: reasoningOn ? undefined : 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
...(this.options.awsBedrockUsePromptCache === true && {
|
||||
cache_control: { type: "ephemeral" },
|
||||
}),
|
||||
},
|
||||
],
|
||||
messages: messages.map((message, index) => {
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
...(this.options.awsBedrockUsePromptCache === true && {
|
||||
cache_control: { type: "ephemeral" },
|
||||
}),
|
||||
}
|
||||
: content,
|
||||
),
|
||||
}
|
||||
}
|
||||
return message
|
||||
}),
|
||||
stream: true,
|
||||
})
|
||||
process.env = previousEnv
|
||||
},
|
||||
]
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? {
|
||||
...content,
|
||||
...(this.options.awsBedrockUsePromptCache === true && {
|
||||
cache_control: { type: "ephemeral" },
|
||||
}),
|
||||
}
|
||||
: content,
|
||||
),
|
||||
}
|
||||
}
|
||||
return message
|
||||
}),
|
||||
stream: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
switch (chunk?.type) {
|
||||
case "message_start":
|
||||
const usage = chunk.message.usage
|
||||
yield {
|
||||
@@ -213,8 +223,19 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
secretAccessKey: string
|
||||
sessionToken?: string
|
||||
}> {
|
||||
// Configure provider options
|
||||
const providerOptions: any = {}
|
||||
if (this.options.awsUseProfile) {
|
||||
// For profile-based auth, always use ignoreCache to detect credential file changes
|
||||
// This solves the AWS Identity Manager issue where credential files change externally
|
||||
providerOptions.ignoreCache = true
|
||||
if (this.options.awsProfile) {
|
||||
providerOptions.profile = this.options.awsProfile
|
||||
}
|
||||
}
|
||||
|
||||
// Create AWS credentials by executing an AWS provider chain
|
||||
const providerChain = fromNodeProviderChain()
|
||||
const providerChain = fromNodeProviderChain(providerOptions)
|
||||
return await AwsBedrockHandler.withTempEnv(
|
||||
() => {
|
||||
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
|
||||
@@ -297,13 +318,23 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
private static async withTempEnv<R>(updateEnv: () => void, fn: () => Promise<R>): Promise<R> {
|
||||
const previousEnv = { ...process.env }
|
||||
const previousEnv = Object.assign({}, process.env)
|
||||
|
||||
try {
|
||||
updateEnv()
|
||||
return await fn()
|
||||
} finally {
|
||||
process.env = previousEnv
|
||||
// Restore the previous environment
|
||||
// First clear any new variables that might have been added
|
||||
for (const key in process.env) {
|
||||
if (!(key in previousEnv)) {
|
||||
delete process.env[key]
|
||||
}
|
||||
}
|
||||
// Then restore all previous values
|
||||
for (const key in previousEnv) {
|
||||
process.env[key] = previousEnv[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import Cerebras from "@cerebras/cerebras_cloud_sdk"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandlerOptions, ModelInfo, CerebrasModelId, cerebrasDefaultModelId, cerebrasModels } from "@shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiStream } from "@api/transform/stream"
|
||||
|
||||
export class CerebrasHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: Cerebras
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
|
||||
// Clean and validate the API key
|
||||
const cleanApiKey = this.options.cerebrasApiKey?.trim()
|
||||
|
||||
if (!cleanApiKey) {
|
||||
throw new Error("Cerebras API key is required")
|
||||
}
|
||||
|
||||
this.client = new Cerebras({
|
||||
apiKey: cleanApiKey,
|
||||
timeout: 30000, // 30 second timeout
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
// Convert Anthropic messages to Cerebras format
|
||||
const cerebrasMessages: Array<{
|
||||
role: "system" | "user" | "assistant"
|
||||
content: string
|
||||
}> = [{ role: "system", content: systemPrompt }]
|
||||
|
||||
// Convert Anthropic messages to Cerebras format
|
||||
for (const message of messages) {
|
||||
if (message.role === "user") {
|
||||
const content = Array.isArray(message.content)
|
||||
? message.content
|
||||
.map((block) => {
|
||||
if (block.type === "text") {
|
||||
return block.text
|
||||
} else if (block.type === "image") {
|
||||
return "[Image content not supported in Cerebras]"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
.join("\n")
|
||||
: message.content
|
||||
cerebrasMessages.push({ role: "user", content })
|
||||
} else if (message.role === "assistant") {
|
||||
const content = Array.isArray(message.content)
|
||||
? message.content
|
||||
.map((block) => {
|
||||
if (block.type === "text") {
|
||||
return block.text
|
||||
}
|
||||
return ""
|
||||
})
|
||||
.join("\n")
|
||||
: message.content || ""
|
||||
cerebrasMessages.push({ role: "assistant", content })
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: this.getModel().id,
|
||||
messages: cerebrasMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
})
|
||||
|
||||
// Handle streaming response
|
||||
let reasoning: string | null = null // Track reasoning content for models that support thinking
|
||||
const modelId = this.getModel().id
|
||||
const isReasoningModel = modelId.includes("qwen") || modelId.includes("deepseek-r1-distill")
|
||||
|
||||
for await (const chunk of stream as any) {
|
||||
// Type assertion for the streaming chunk
|
||||
const streamChunk = chunk as any
|
||||
|
||||
if (streamChunk.choices?.[0]?.delta?.content) {
|
||||
const content = streamChunk.choices[0].delta.content
|
||||
|
||||
// Handle reasoning models (Qwen and DeepSeek R1 Distill) that use <think> tags
|
||||
if (isReasoningModel) {
|
||||
// Check if we're entering or continuing reasoning mode
|
||||
if (reasoning || content.includes("<think>")) {
|
||||
reasoning = (reasoning || "") + content
|
||||
|
||||
// Clean the content by removing think tags for display
|
||||
let cleanContent = content.replace(/<think>/g, "").replace(/<\/think>/g, "")
|
||||
|
||||
// Only yield reasoning content if there's actual content after cleaning
|
||||
if (cleanContent.trim()) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: cleanContent,
|
||||
}
|
||||
}
|
||||
|
||||
// Check if reasoning is complete
|
||||
if (reasoning.includes("</think>")) {
|
||||
reasoning = null
|
||||
}
|
||||
} else {
|
||||
// Regular content outside of thinking tags
|
||||
yield {
|
||||
type: "text",
|
||||
text: content,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-reasoning models - just yield text content
|
||||
yield {
|
||||
type: "text",
|
||||
text: content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle usage information from Cerebras API
|
||||
// Usage is typically only available in the final chunk
|
||||
if (streamChunk.usage) {
|
||||
const totalCost = this.calculateCost({
|
||||
inputTokens: streamChunk.usage.prompt_tokens || 0,
|
||||
outputTokens: streamChunk.usage.completion_tokens || 0,
|
||||
})
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: streamChunk.usage.prompt_tokens || 0,
|
||||
outputTokens: streamChunk.usage.completion_tokens || 0,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in cerebrasModels) {
|
||||
const id = modelId as CerebrasModelId
|
||||
return { id, info: cerebrasModels[id] }
|
||||
}
|
||||
return {
|
||||
id: cerebrasDefaultModelId,
|
||||
info: cerebrasModels[cerebrasDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
private calculateCost({ inputTokens, outputTokens }: { inputTokens: number; outputTokens: number }): number {
|
||||
const model = this.getModel()
|
||||
const inputPrice = model.info.inputPrice || 0
|
||||
const outputPrice = model.info.outputPrice || 0
|
||||
|
||||
const inputCost = (inputPrice / 1_000_000) * inputTokens
|
||||
const outputCost = (outputPrice / 1_000_000) * outputTokens
|
||||
|
||||
return inputCost + outputCost
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ export class LmStudioHandler implements ApiHandler {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: this.getModel().id,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
})
|
||||
for await (const chunk of stream) {
|
||||
@@ -38,6 +37,12 @@ export class LmStudioHandler implements ApiHandler {
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: (delta.reasoning_content as string | undefined) || "",
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// LM Studio doesn't return an error code/body for now
|
||||
|
||||
@@ -19,13 +19,23 @@ export class MistralHandler implements ApiHandler {
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const stream = await this.client.chat.stream({
|
||||
model: this.getModel().id,
|
||||
// max_completion_tokens: this.getModel().info.maxTokens,
|
||||
temperature: 0,
|
||||
messages: [{ role: "system", content: systemPrompt }, ...convertToMistralMessages(messages)],
|
||||
stream: true,
|
||||
})
|
||||
const stream = await this.client.chat
|
||||
.stream({
|
||||
model: this.getModel().id,
|
||||
// max_completion_tokens: this.getModel().info.maxTokens,
|
||||
temperature: 0,
|
||||
messages: [{ role: "system", content: systemPrompt }, ...convertToMistralMessages(messages)],
|
||||
stream: true,
|
||||
})
|
||||
.catch((err) => {
|
||||
// The Mistal SDK uses statusCode instead of status
|
||||
// However, if they introduce status for something, I don't want to override it
|
||||
if ("statusCode" in err && !("status" in err)) {
|
||||
err.status = err.statusCode
|
||||
}
|
||||
|
||||
throw err
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.data.choices[0]?.delta
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../index"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { nebiusDefaultModelId, nebiusModels, type ModelInfo, type ApiHandlerOptions, type NebiusModelId } from "../../shared/api"
|
||||
|
||||
export class NebiusHandler implements ApiHandler {
|
||||
private client: OpenAI
|
||||
|
||||
constructor(private readonly options: ApiHandlerOptions) {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.studio.nebius.ai/v1",
|
||||
apiKey: this.options.nebiusApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = model.id.includes("DeepSeek-R1")
|
||||
? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: (delta.reasoning_content as string | undefined) || "",
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
|
||||
if (modelId !== undefined && modelId in nebiusModels) {
|
||||
return { id: modelId, info: nebiusModels[modelId as NebiusModelId] }
|
||||
}
|
||||
return { id: nebiusDefaultModelId, info: nebiusModels[nebiusDefaultModelId] }
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,9 @@ export class OllamaHandler implements ApiHandler {
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.ollamaModelId || "",
|
||||
info: openAiModelInfoSaneDefaults,
|
||||
info: this.options.ollamaApiOptionsCtxNum
|
||||
? { ...openAiModelInfoSaneDefaults, contextWindow: Number(this.options.ollamaApiOptionsCtxNum) || 32768 }
|
||||
: openAiModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,10 @@ export class OpenAiHandler implements ApiHandler {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,16 @@ import { convertToOpenAiMessages } from "@api/transform/openai-format"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import { ApiStream } from "@api/transform/stream"
|
||||
|
||||
// Requesty usage includes an extra field for Anthropic use cases.
|
||||
// Safely cast the prompt token details section to the appropriate structure.
|
||||
interface RequestyUsage extends OpenAI.CompletionUsage {
|
||||
prompt_tokens_details?: {
|
||||
caching_tokens?: number
|
||||
cached_tokens?: number
|
||||
}
|
||||
total_cost?: number
|
||||
}
|
||||
|
||||
export class RequestyHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
@@ -41,7 +51,10 @@ export class RequestyHandler implements ApiHandler {
|
||||
thinkingBudget > 0
|
||||
? { thinking: { type: "enabled", budget_tokens: thinkingBudget } }
|
||||
: { thinking: { type: "disabled" } }
|
||||
const thinkingArgs = model.id.includes("claude-3-7-sonnet") ? thinking : {}
|
||||
const thinkingArgs =
|
||||
model.id.includes("claude-3-7-sonnet") || model.id.includes("claude-sonnet-4") || model.id.includes("claude-opus-4")
|
||||
? thinking
|
||||
: {}
|
||||
|
||||
// @ts-ignore-next-line
|
||||
const stream = await this.client.chat.completions.create({
|
||||
@@ -55,6 +68,8 @@ export class RequestyHandler implements ApiHandler {
|
||||
...thinkingArgs,
|
||||
})
|
||||
|
||||
let lastUsage: any = undefined
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
@@ -71,32 +86,26 @@ export class RequestyHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Requesty usage includes an extra field for Anthropic use cases.
|
||||
// Safely cast the prompt token details section to the appropriate structure.
|
||||
interface RequestyUsage extends OpenAI.CompletionUsage {
|
||||
prompt_tokens_details?: {
|
||||
caching_tokens?: number
|
||||
cached_tokens?: number
|
||||
}
|
||||
total_cost?: number
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
const usage = chunk.usage as RequestyUsage
|
||||
const inputTokens = usage.prompt_tokens || 0
|
||||
const outputTokens = usage.completion_tokens || 0
|
||||
const cacheWriteTokens = usage.prompt_tokens_details?.caching_tokens || undefined
|
||||
const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens || undefined
|
||||
const totalCost = calculateApiCostOpenAI(model.info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
lastUsage = chunk.usage
|
||||
}
|
||||
}
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
totalCost: totalCost,
|
||||
}
|
||||
if (lastUsage) {
|
||||
const usage = lastUsage as RequestyUsage
|
||||
const inputTokens = usage.prompt_tokens || 0
|
||||
const outputTokens = usage.completion_tokens || 0
|
||||
const cacheWriteTokens = usage.prompt_tokens_details?.caching_tokens || undefined
|
||||
const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens || undefined
|
||||
const totalCost = calculateApiCostOpenAI(model.info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
totalCost: totalCost,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,15 @@ export class VertexHandler implements ApiHandler {
|
||||
|
||||
// Claude implementation
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
const reasoningOn =
|
||||
(modelId.includes("3-7") || modelId.includes("sonnet-4") || modelId.includes("opus-4")) && budget_tokens !== 0
|
||||
? true
|
||||
: false
|
||||
let stream
|
||||
|
||||
switch (modelId) {
|
||||
case "claude-sonnet-4@20250514":
|
||||
case "claude-opus-4@20250514":
|
||||
case "claude-3-7-sonnet@20250219":
|
||||
case "claude-3-5-sonnet-v2@20241022":
|
||||
case "claude-3-5-sonnet@20240620":
|
||||
@@ -149,7 +154,7 @@ export class VertexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
switch (chunk?.type) {
|
||||
case "message_start":
|
||||
const usage = chunk.message.usage
|
||||
yield {
|
||||
|
||||
@@ -58,10 +58,10 @@ export class XAIHandler implements ApiHandler {
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ export async function createOpenRouterStream(
|
||||
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
|
||||
// handles direct model.id match logic
|
||||
switch (model.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:thinking":
|
||||
@@ -79,6 +81,8 @@ export async function createOpenRouterStream(
|
||||
// (models usually default to max tokens allowed)
|
||||
let maxTokens: number | undefined
|
||||
switch (model.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:thinking":
|
||||
@@ -112,6 +116,8 @@ export async function createOpenRouterStream(
|
||||
|
||||
let reasoning: { max_tokens: number } | undefined = undefined
|
||||
switch (model.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:thinking":
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { JSONParser } from "@streamparser/json"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as os from "os"
|
||||
|
||||
// Fallback type definition based on the error message: "Property 'value' is optional in type 'ParsedElementInfo'"
|
||||
type ParsedElementInfo = {
|
||||
value?: any
|
||||
key?: string | number
|
||||
parent?: any
|
||||
stack?: any[]
|
||||
}
|
||||
|
||||
export interface ReplacementItem {
|
||||
old_string: string
|
||||
new_string: string
|
||||
}
|
||||
|
||||
export interface ChangeLocation {
|
||||
startLine: number
|
||||
endLine: number
|
||||
startChar: number
|
||||
endChar: number
|
||||
}
|
||||
|
||||
export class StreamingJsonReplacer {
|
||||
private currentFileContent: string
|
||||
private parser: JSONParser
|
||||
private onContentUpdated: (newContent: string, isFinalItem: boolean, changeLocation?: ChangeLocation) => void
|
||||
private onErrorCallback: (error: Error) => void
|
||||
private itemsProcessed: number = 0
|
||||
private successfullyParsedItems: ReplacementItem[] = []
|
||||
|
||||
constructor(
|
||||
initialContent: string,
|
||||
onContentUpdatedCallback: (newContent: string, isFinalItem: boolean, changeLocation?: ChangeLocation) => void,
|
||||
onErrorCallback: (error: Error) => void,
|
||||
) {
|
||||
// Initialize log file path
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
||||
|
||||
this.currentFileContent = initialContent
|
||||
this.onContentUpdated = onContentUpdatedCallback
|
||||
this.onErrorCallback = onErrorCallback
|
||||
|
||||
this.parser = new JSONParser({ paths: ["$.*"] })
|
||||
|
||||
this.parser.onValue = (parsedElementInfo: ParsedElementInfo) => {
|
||||
const { value } = parsedElementInfo // Destructure to get value, which might be undefined
|
||||
|
||||
// This callback is triggered for each item matched by '$.replacements.*'
|
||||
if (value && typeof value === "object" && "old_string" in value && "new_string" in value) {
|
||||
const item = value as ReplacementItem // Value here is confirmed to be an object
|
||||
if (typeof item.old_string === "string" && typeof item.new_string === "string") {
|
||||
this.successfullyParsedItems.push(item) // Store the structurally valid item
|
||||
|
||||
if (this.currentFileContent.includes(item.old_string)) {
|
||||
// Calculate the change location before making the replacement
|
||||
const changeLocation = this.calculateChangeLocation(item.old_string, item.new_string)
|
||||
|
||||
const beforeLength = this.currentFileContent.length
|
||||
this.currentFileContent = this.currentFileContent.replace(item.old_string, item.new_string)
|
||||
const afterLength = this.currentFileContent.length
|
||||
|
||||
this.itemsProcessed++
|
||||
|
||||
// Notify that an item has been processed. The `isFinalItem` argument here is tricky
|
||||
// as we don't know from the parser alone if this is the *absolute* last item
|
||||
// until the stream ends. The caller (Task.ts) will manage the final update.
|
||||
// For now, we'll pass `false` and let Task.ts handle the final diff view update.
|
||||
this.onContentUpdated(this.currentFileContent, false, changeLocation)
|
||||
} else {
|
||||
const snippet = item.old_string.length > 50 ? item.old_string.substring(0, 47) + "..." : item.old_string
|
||||
const error = new Error(`Streaming Replacement failed: 'old_string' not found. Snippet: "${snippet}"`)
|
||||
this.onErrorCallback(error) // Call our own error callback
|
||||
}
|
||||
} else {
|
||||
const error = new Error(`Invalid item structure in replacements stream: ${JSON.stringify(item)}`)
|
||||
this.onErrorCallback(error) // Call our own error callback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.parser.onError = (err: Error) => {
|
||||
// Propagate the error to the caller via the callback
|
||||
this.onErrorCallback(err)
|
||||
// Note: The @streamparser/json library might throw synchronously on write if onError is not set,
|
||||
// or if it re-throws. We'll ensure Task.ts wraps write/end in try-catch.
|
||||
}
|
||||
}
|
||||
|
||||
public write(jsonChunk: string): void {
|
||||
try {
|
||||
// Errors during write will be caught by the parser's onError or thrown.
|
||||
this.parser.write(jsonChunk)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentContent(): string {
|
||||
return this.currentFileContent
|
||||
}
|
||||
|
||||
public getSuccessfullyParsedItems(): ReplacementItem[] {
|
||||
return [...this.successfullyParsedItems] // Return a copy
|
||||
}
|
||||
|
||||
private calculateChangeLocation(oldStr: string, newStr: string): ChangeLocation {
|
||||
// Find the index where the old string starts
|
||||
const startIndex = this.currentFileContent.indexOf(oldStr)
|
||||
|
||||
if (startIndex === -1) {
|
||||
// This shouldn't happen since we already checked includes(), but just in case
|
||||
return { startLine: 0, endLine: 0, startChar: 0, endChar: 0 }
|
||||
}
|
||||
|
||||
// Calculate line numbers by counting newlines before the start index
|
||||
const contentBeforeStart = this.currentFileContent.substring(0, startIndex)
|
||||
|
||||
const startLine = (contentBeforeStart.match(/\n/g) || []).length
|
||||
// Calculate the end index after replacement
|
||||
const endIndex = startIndex + oldStr.length
|
||||
|
||||
const contentBeforeEnd = this.currentFileContent.substring(0, endIndex)
|
||||
|
||||
const endLine = (contentBeforeEnd.match(/\n/g) || []).length
|
||||
// Calculate character positions within their respective lines
|
||||
const lastNewlineBeforeStart = contentBeforeStart.lastIndexOf("\n")
|
||||
const startChar = lastNewlineBeforeStart === -1 ? startIndex : startIndex - lastNewlineBeforeStart - 1
|
||||
|
||||
const lastNewlineBeforeEnd = contentBeforeEnd.lastIndexOf("\n")
|
||||
|
||||
const endChar = lastNewlineBeforeEnd === -1 ? endIndex : endIndex - lastNewlineBeforeEnd - 1
|
||||
|
||||
const result = {
|
||||
startLine,
|
||||
endLine,
|
||||
startChar,
|
||||
endChar,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -11,55 +11,55 @@ describe("constructNewFileContent", () => {
|
||||
{
|
||||
name: "empty file",
|
||||
original: "",
|
||||
diff: `<<<<<<< SEARCH
|
||||
diff: `------- SEARCH
|
||||
=======
|
||||
new content
|
||||
>>>>>>> REPLACE`,
|
||||
+++++++ REPLACE`,
|
||||
expected: "new content\n",
|
||||
isFinal: true,
|
||||
},
|
||||
{
|
||||
name: "full file replacement",
|
||||
original: "old content",
|
||||
diff: `<<<<<<< SEARCH
|
||||
diff: `------- SEARCH
|
||||
=======
|
||||
new content
|
||||
>>>>>>> REPLACE`,
|
||||
+++++++ REPLACE`,
|
||||
expected: "new content\n",
|
||||
isFinal: true,
|
||||
},
|
||||
{
|
||||
name: "exact match replacement",
|
||||
original: "line1\nline2\nline3",
|
||||
diff: `<<<<<<< SEARCH
|
||||
diff: `------- SEARCH
|
||||
line2
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE`,
|
||||
+++++++ REPLACE`,
|
||||
expected: "line1\nreplaced\nline3",
|
||||
isFinal: true,
|
||||
},
|
||||
{
|
||||
name: "line-trimmed match replacement",
|
||||
original: "line1\n line2 \nline3",
|
||||
diff: `<<<<<<< SEARCH
|
||||
diff: `------- SEARCH
|
||||
line2
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE`,
|
||||
+++++++ REPLACE`,
|
||||
expected: "line1\nreplaced\nline3",
|
||||
isFinal: true,
|
||||
},
|
||||
{
|
||||
name: "block anchor match replacement",
|
||||
original: "line1\nstart\nmiddle\nend\nline5",
|
||||
diff: `<<<<<<< SEARCH
|
||||
diff: `------- SEARCH
|
||||
start
|
||||
middle
|
||||
end
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE`,
|
||||
+++++++ REPLACE`,
|
||||
expected: "line1\nreplaced\nline5",
|
||||
isFinal: true,
|
||||
},
|
||||
@@ -67,11 +67,11 @@ replaced
|
||||
name: "incremental processing",
|
||||
original: "line1\nline2\nline3",
|
||||
diff: [
|
||||
`<<<<<<< SEARCH
|
||||
`------- SEARCH
|
||||
line2
|
||||
=======`,
|
||||
"replaced\n",
|
||||
">>>>>>> REPLACE",
|
||||
"+++++++ REPLACE",
|
||||
].join("\n"),
|
||||
expected: "line1\nreplaced\n\nline3",
|
||||
isFinal: true,
|
||||
@@ -79,60 +79,60 @@ line2
|
||||
{
|
||||
name: "final chunk with remaining content",
|
||||
original: "line1\nline2\nline3",
|
||||
diff: `<<<<<<< SEARCH
|
||||
diff: `------- SEARCH
|
||||
line2
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE`,
|
||||
+++++++ REPLACE`,
|
||||
expected: "line1\nreplaced\nline3",
|
||||
isFinal: true,
|
||||
},
|
||||
{
|
||||
name: "multiple ordered replacements",
|
||||
original: "First\nSecond\nThird\nFourth",
|
||||
diff: `<<<<<<< SEARCH
|
||||
diff: `------- SEARCH
|
||||
First
|
||||
=======
|
||||
1st
|
||||
>>>>>>> REPLACE
|
||||
+++++++ REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
------- SEARCH
|
||||
Third
|
||||
=======
|
||||
3rd
|
||||
>>>>>>> REPLACE`,
|
||||
+++++++ REPLACE`,
|
||||
expected: "1st\nSecond\n3rd\nFourth",
|
||||
isFinal: true,
|
||||
},
|
||||
{
|
||||
name: "replace then delete",
|
||||
original: "line1\nline2\nline3\nline4",
|
||||
diff: `<<<<<<< SEARCH
|
||||
diff: `------- SEARCH
|
||||
line2
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE
|
||||
+++++++ REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
------- SEARCH
|
||||
line4
|
||||
=======
|
||||
>>>>>>> REPLACE`,
|
||||
+++++++ REPLACE`,
|
||||
expected: "line1\nreplaced\nline3\n",
|
||||
isFinal: true,
|
||||
},
|
||||
{
|
||||
name: "delete then replace",
|
||||
original: "line1\nline2\nline3\nline4",
|
||||
diff: `<<<<<<< SEARCH
|
||||
diff: `------- SEARCH
|
||||
line1
|
||||
=======
|
||||
>>>>>>> REPLACE
|
||||
+++++++ REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
------- SEARCH
|
||||
line3
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE`,
|
||||
+++++++ REPLACE`,
|
||||
expected: "line2\nreplaced\nline4",
|
||||
isFinal: true,
|
||||
},
|
||||
@@ -155,11 +155,11 @@ replaced
|
||||
|
||||
it("should throw error when no match found", async () => {
|
||||
const original = "line1\nline2\nline3"
|
||||
const diff = `<<<<<<< SEARCH
|
||||
const diff = `------- SEARCH
|
||||
non-existent
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
|
||||
try {
|
||||
await cnfc(diff, original, true)
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
const SEARCH_BLOCK_START = "------- SEARCH"
|
||||
const SEARCH_BLOCK_END = "======="
|
||||
const REPLACE_BLOCK_END = "+++++++ REPLACE"
|
||||
|
||||
const SEARCH_BLOCK_CHAR = "-"
|
||||
const REPLACE_BLOCK_CHAR = "+"
|
||||
|
||||
/**
|
||||
* Attempts a line-trimmed fallback match for the given search content in the original content.
|
||||
* It tries to match `searchContent` lines against a block of lines in `originalContent` starting
|
||||
@@ -150,11 +157,11 @@ function blockAnchorFallbackMatch(originalContent: string, searchContent: string
|
||||
*
|
||||
* The diff format is a custom structure that uses three markers to define changes:
|
||||
*
|
||||
* <<<<<<< SEARCH
|
||||
* ------- SEARCH
|
||||
* [Exact content to find in the original file]
|
||||
* =======
|
||||
* [Content to replace with]
|
||||
* >>>>>>> REPLACE
|
||||
* +++++++ REPLACE
|
||||
*
|
||||
* Behavior and Assumptions:
|
||||
* 1. The file is processed chunk-by-chunk. Each chunk of `diffContent` may contain
|
||||
@@ -243,23 +250,23 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
|
||||
const lastLine = lines[lines.length - 1]
|
||||
if (
|
||||
lines.length > 0 &&
|
||||
(lastLine.startsWith("<") || lastLine.startsWith("=") || lastLine.startsWith(">")) &&
|
||||
lastLine !== "<<<<<<< SEARCH" &&
|
||||
lastLine !== "=======" &&
|
||||
lastLine !== ">>>>>>> REPLACE"
|
||||
(lastLine.startsWith(SEARCH_BLOCK_CHAR) || lastLine.startsWith("=") || lastLine.startsWith(REPLACE_BLOCK_CHAR)) &&
|
||||
lastLine !== SEARCH_BLOCK_START &&
|
||||
lastLine !== SEARCH_BLOCK_END &&
|
||||
lastLine !== REPLACE_BLOCK_END
|
||||
) {
|
||||
lines.pop()
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
if (line === "<<<<<<< SEARCH") {
|
||||
if (line === SEARCH_BLOCK_START) {
|
||||
inSearch = true
|
||||
currentSearchContent = ""
|
||||
currentReplaceContent = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if (line === "=======") {
|
||||
if (line === SEARCH_BLOCK_END) {
|
||||
inSearch = false
|
||||
inReplace = true
|
||||
|
||||
@@ -320,7 +327,7 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
|
||||
continue
|
||||
}
|
||||
|
||||
if (line === ">>>>>>> REPLACE") {
|
||||
if (line === REPLACE_BLOCK_END) {
|
||||
// Finished one replace block
|
||||
|
||||
// // Remove the artificially added linebreak in the last line of the REPLACE block
|
||||
@@ -480,7 +487,7 @@ class NewFileContentConstructor {
|
||||
pendingNonStandardLineLimit: number,
|
||||
): number {
|
||||
let removeLineCount = 0
|
||||
if (line === "<<<<<<< SEARCH") {
|
||||
if (line === SEARCH_BLOCK_START) {
|
||||
removeLineCount = this.trimPendingNonStandardTrailingEmptyLines(pendingNonStandardLineLimit)
|
||||
if (removeLineCount > 0) {
|
||||
pendingNonStandardLineLimit = pendingNonStandardLineLimit - removeLineCount
|
||||
@@ -490,7 +497,7 @@ class NewFileContentConstructor {
|
||||
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
|
||||
}
|
||||
this.activateSearchState()
|
||||
} else if (line === "=======") {
|
||||
} else if (line === SEARCH_BLOCK_END) {
|
||||
// 校验非标内容
|
||||
if (!this.isSearchingActive()) {
|
||||
this.tryFixSearchBlock(pendingNonStandardLineLimit)
|
||||
@@ -498,7 +505,7 @@ class NewFileContentConstructor {
|
||||
}
|
||||
this.activateReplaceState()
|
||||
this.beforeReplace()
|
||||
} else if (line === ">>>>>>> REPLACE") {
|
||||
} else if (line === REPLACE_BLOCK_END) {
|
||||
if (!this.isReplacingActive()) {
|
||||
this.tryFixReplaceBlock(pendingNonStandardLineLimit)
|
||||
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
|
||||
@@ -606,11 +613,11 @@ class NewFileContentConstructor {
|
||||
if (!lineLimit) {
|
||||
throw new Error("Invalid SEARCH/REPLACE block structure - no lines available to process")
|
||||
}
|
||||
let searchTagRegexp = /^[<]{3,} SEARCH$/
|
||||
let searchTagRegexp = /^[-]{3,} SEARCH$/
|
||||
const searchTagIndex = this.findLastMatchingLineIndex(searchTagRegexp, lineLimit)
|
||||
if (searchTagIndex !== -1) {
|
||||
let fixLines = this.pendingNonStandardLines.slice(searchTagIndex, lineLimit)
|
||||
fixLines[0] = "<<<<<<< SEARCH"
|
||||
fixLines[0] = SEARCH_BLOCK_START
|
||||
for (const line of fixLines) {
|
||||
removeLineCount += this.internalProcessLine(line, false, searchTagIndex)
|
||||
}
|
||||
@@ -638,7 +645,7 @@ class NewFileContentConstructor {
|
||||
// removeLineCount += this.tryFixSearchBlock(replaceBeginTagIndex)
|
||||
// }
|
||||
let fixLines = this.pendingNonStandardLines.slice(replaceBeginTagIndex - removeLineCount, lineLimit - removeLineCount)
|
||||
fixLines[0] = "======="
|
||||
fixLines[0] = SEARCH_BLOCK_END
|
||||
for (const line of fixLines) {
|
||||
removeLineCount += this.internalProcessLine(line, false, replaceBeginTagIndex - removeLineCount)
|
||||
}
|
||||
@@ -657,7 +664,7 @@ class NewFileContentConstructor {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
let replaceEndTagRegexp = /^[>]{3,} REPLACE$/
|
||||
let replaceEndTagRegexp = /^[+]{3,} REPLACE$/
|
||||
const replaceEndTagIndex = this.findLastMatchingLineIndex(replaceEndTagRegexp, lineLimit)
|
||||
const likeReplaceEndTag = replaceEndTagIndex === lineLimit - 1
|
||||
if (likeReplaceEndTag) {
|
||||
@@ -666,7 +673,7 @@ class NewFileContentConstructor {
|
||||
// removeLineCount += this.tryFixReplaceBlock(replaceEndTagIndex)
|
||||
// }
|
||||
let fixLines = this.pendingNonStandardLines.slice(replaceEndTagIndex - removeLineCount, lineLimit - removeLineCount)
|
||||
fixLines[fixLines.length - 1] = ">>>>>>> REPLACE"
|
||||
fixLines[fixLines.length - 1] = REPLACE_BLOCK_END
|
||||
for (const line of fixLines) {
|
||||
removeLineCount += this.internalProcessLine(line, false, replaceEndTagIndex - removeLineCount)
|
||||
}
|
||||
@@ -706,10 +713,10 @@ export async function constructNewFileContentV2(diffContent: string, originalCon
|
||||
const lastLine = lines[lines.length - 1]
|
||||
if (
|
||||
lines.length > 0 &&
|
||||
(lastLine.startsWith("<") || lastLine.startsWith("=") || lastLine.startsWith(">")) &&
|
||||
lastLine !== "<<<<<<< SEARCH" &&
|
||||
lastLine !== "=======" &&
|
||||
lastLine !== ">>>>>>> REPLACE"
|
||||
(lastLine.startsWith(SEARCH_BLOCK_CHAR) || lastLine.startsWith("=") || lastLine.startsWith(REPLACE_BLOCK_CHAR)) &&
|
||||
lastLine !== SEARCH_BLOCK_START &&
|
||||
lastLine !== SEARCH_BLOCK_END &&
|
||||
lastLine !== REPLACE_BLOCK_END
|
||||
) {
|
||||
lines.pop()
|
||||
}
|
||||
|
||||
@@ -7,122 +7,122 @@ async function cnfc(diffContent: string, originalContent: string, isFinal: boole
|
||||
}
|
||||
|
||||
describe("Diff Format Edge Cases", () => {
|
||||
it("should handle SEARCH prefix symbols < less than 7", async () => {
|
||||
it("should handle SEARCH prefix symbols - less than 7", async () => {
|
||||
const isFinal = true
|
||||
const original = "before\ncontent\nafter"
|
||||
const diff = `<<<<< SEARCH
|
||||
const diff = `----- SEARCH
|
||||
content
|
||||
=======
|
||||
new content
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
expect(result1).to.equal("new content\n")
|
||||
expect(result2).to.equal("before\nnew content\nafter")
|
||||
})
|
||||
|
||||
it("should handle SEARCH prefix symbols < more than 7", async () => {
|
||||
it("should handle SEARCH prefix symbols - more than 7", async () => {
|
||||
const isFinal = true
|
||||
const original = "before\ncontent\nafter"
|
||||
const diff = `<<<<<<<<<<< SEARCH
|
||||
const diff = `----------- SEARCH
|
||||
content
|
||||
=======
|
||||
new content
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
expect(result1).to.equal("new content\n")
|
||||
expect(result2).to.equal("before\nnew content\nafter")
|
||||
})
|
||||
|
||||
it("should handle SEARCH < less than 7 and REPLACE = less than 7", async () => {
|
||||
it("should handle SEARCH - less than 7 and REPLACE = less than 7", async () => {
|
||||
const isFinal = true
|
||||
const original = "before\ncontent\nafter"
|
||||
const diff = `<<<<< SEARCH
|
||||
const diff = `----- SEARCH
|
||||
content
|
||||
=====
|
||||
new content
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
expect(result1).to.equal("r")
|
||||
expect(result2).to.equal("before\nnew content\nafter")
|
||||
})
|
||||
|
||||
it("should handle SEARCH < less than 7 and REPLACE = more than 7", async () => {
|
||||
it("should handle SEARCH - less than 7 and REPLACE = more than 7", async () => {
|
||||
const isFinal = true
|
||||
const original = "before\ncontent\nafter"
|
||||
const diff = `<<<<< SEARCH
|
||||
const diff = `----- SEARCH
|
||||
content
|
||||
========
|
||||
new content
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
expect(result1).to.equal("r")
|
||||
expect(result2).to.equal("before\nnew content\nafter")
|
||||
})
|
||||
|
||||
it("should handle SEARCH < more than 7 and REPLACE = more than 7", async () => {
|
||||
it("should handle SEARCH - more than 7 and REPLACE = more than 7", async () => {
|
||||
const isFinal = true
|
||||
const original = "before\ncontent\nafter"
|
||||
const diff = `<<<<<<<<<<< SEARCH
|
||||
const diff = `----------- SEARCH
|
||||
content
|
||||
==========
|
||||
new content
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
expect(result1).to.equal("r")
|
||||
expect(result2).to.equal("before\nnew content\nafter")
|
||||
})
|
||||
|
||||
it("should handle SEARCH < more than 7 and REPLACE = less than 7", async () => {
|
||||
it("should handle SEARCH - more than 7 and REPLACE = less than 7", async () => {
|
||||
const isFinal = true
|
||||
const original = "before\ncontent\nafter"
|
||||
const diff = `<<<<<<<<<<< SEARCH
|
||||
const diff = `----------- SEARCH
|
||||
content
|
||||
=====
|
||||
new content
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
expect(result1).to.equal("r")
|
||||
expect(result2).to.equal("before\nnew content\nafter")
|
||||
})
|
||||
|
||||
it("should handle consecutive SEARCH-REPLACE with second block SEARCH < less than 7", async () => {
|
||||
it("should handle consecutive SEARCH-REPLACE with second block SEARCH - less than 7", async () => {
|
||||
const isFinal = true
|
||||
const original = "before\nfirst content\nafter\nsecond content\nend"
|
||||
const diff = `<<<<<<< SEARCH
|
||||
const diff = `------- SEARCH
|
||||
first content
|
||||
=======
|
||||
first new content
|
||||
>>>>>>> REPLACE
|
||||
<<<<< SEARCH
|
||||
+++++++ REPLACE
|
||||
----- SEARCH
|
||||
second content
|
||||
=======
|
||||
second new content
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
expect(result1).to.equal("before\nfirst new content\nsecond new content\n")
|
||||
expect(result2).to.equal("before\nfirst new content\nafter\nsecond new content\nend")
|
||||
})
|
||||
|
||||
it("should handle consecutive SEARCH-REPLACE with second block SEARCH < less than 7 and REPLACE = less than 7", async () => {
|
||||
it("should handle consecutive SEARCH-REPLACE with second block SEARCH - less than 7 and REPLACE = less than 7", async () => {
|
||||
const isFinal = true
|
||||
const original = "before\nfirst content\nafter\nsecond content\nend"
|
||||
const diff = `<<<<<<< SEARCH
|
||||
const diff = `------- SEARCH
|
||||
first content
|
||||
=======
|
||||
first new content
|
||||
>>>>>>> REPLACE
|
||||
<<<<< SEARCH
|
||||
+++++++ REPLACE
|
||||
----- SEARCH
|
||||
second content
|
||||
=====
|
||||
second new content
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
expect(result1).to.equal("before\nfirst new content\nd")
|
||||
|
||||
@@ -11,7 +11,7 @@ describe("Diff Format Edge Cases", () => {
|
||||
const original = "line1\nline2"
|
||||
const diff = `=======
|
||||
new content
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, true)
|
||||
expect(result1).to.equal("new content\n")
|
||||
try {
|
||||
@@ -24,14 +24,14 @@ new content
|
||||
|
||||
it("should handle consecutive search blocks", async () => {
|
||||
const original = "text"
|
||||
const diff = `<<<<<<< SEARCH
|
||||
const diff = `------- SEARCH
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE
|
||||
<<<<<<< SEARCH
|
||||
+++++++ REPLACE
|
||||
------- SEARCH
|
||||
=======
|
||||
another
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, true)
|
||||
expect(result1).to.equal("replaced\nanother\n")
|
||||
try {
|
||||
@@ -44,10 +44,10 @@ another
|
||||
|
||||
it("should handle reverse markers order", async () => {
|
||||
const original = "content"
|
||||
const diff = `>>>>>>> SEARCH
|
||||
const diff = `+++++++ SEARCH
|
||||
=======
|
||||
invalid
|
||||
<<<<<<< REPLACE`
|
||||
------- REPLACE`
|
||||
const result1 = await cnfc(diff, original, true)
|
||||
expect(result1).to.equal("invalid\ncontent")
|
||||
try {
|
||||
@@ -60,9 +60,9 @@ invalid
|
||||
|
||||
it("should handle incomplete block structure", async () => {
|
||||
const original = "valid text"
|
||||
const diff = `<<<<<<< SEARCH
|
||||
const diff = `------- SEARCH
|
||||
text
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, true)
|
||||
expect(result1).to.equal("t")
|
||||
try {
|
||||
@@ -75,10 +75,10 @@ text
|
||||
|
||||
it("should handle empty search block", async () => {
|
||||
const original = "any content"
|
||||
const diff = `<<<<<<< SEARCH
|
||||
const diff = `------- SEARCH
|
||||
=======
|
||||
inserted
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, true)
|
||||
const result2 = await cnfc2(diff, original, true)
|
||||
expect(result1).to.equal("inserted\n")
|
||||
@@ -87,11 +87,11 @@ inserted
|
||||
|
||||
it("should handle mixed line endings", async () => {
|
||||
const original = "line1\r\nline2"
|
||||
const diff = `<<<<<<< SEARCH
|
||||
const diff = `------- SEARCH
|
||||
line1\r
|
||||
=======
|
||||
line1
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, true)
|
||||
const result2 = await cnfc2(diff, original, true)
|
||||
expect(result1).to.equal("line1\nline2")
|
||||
@@ -100,11 +100,11 @@ line1
|
||||
|
||||
it("should handle special characters in search", async () => {
|
||||
const original = "text with $^.*\nend"
|
||||
const diff = `<<<<<<< SEARCH
|
||||
const diff = `------- SEARCH
|
||||
$^.*
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, true)
|
||||
const result2 = await cnfc2(diff, original, true)
|
||||
expect(result1).to.equal("text with replaced\nend")
|
||||
@@ -112,18 +112,18 @@ replaced
|
||||
})
|
||||
|
||||
it("should handle special regex chars and nested search markers", async () => {
|
||||
const original = `text with $^.*\n<<< SEARCH\nend`
|
||||
const diff = `<<<<<<< SEARCH
|
||||
const original = `text with $^.*\n--- SEARCH\nend`
|
||||
const diff = `------- SEARCH
|
||||
$^.*
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE
|
||||
+++++++ REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
<<< SEARCH
|
||||
------- SEARCH
|
||||
--- SEARCH
|
||||
=======
|
||||
before
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, true)
|
||||
const result2 = await cnfc2(diff, original, true)
|
||||
expect(result1).to.equal("text with replaced\nbefore\nend")
|
||||
@@ -131,18 +131,18 @@ before
|
||||
})
|
||||
|
||||
it("cnfc2 should handle invalid search marker format", async () => {
|
||||
const original = `text with $^.*\n<<< SEARCH\nend`
|
||||
const diff = `<<< SEARCH
|
||||
const original = `text with $^.*\n--- SEARCH\nend`
|
||||
const diff = `--- SEARCH
|
||||
$^.*
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE
|
||||
+++++++ REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
<<< SEARCH
|
||||
------- SEARCH
|
||||
--- SEARCH
|
||||
=======
|
||||
before
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
try {
|
||||
await cnfc(diff, original, true)
|
||||
expect.fail("Expected an error to be thrown")
|
||||
@@ -154,18 +154,18 @@ before
|
||||
})
|
||||
|
||||
it("cnfc2 should throw error for incomplete search marker", async () => {
|
||||
const original = `text with $^.*\n<<< SEARCH\nend`
|
||||
const diff = `<<< SEARCH
|
||||
const original = `text with $^.*\n--- SEARCH\nend`
|
||||
const diff = `--- SEARCH
|
||||
$^.*
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE
|
||||
+++++++ REPLACE
|
||||
|
||||
<<<<<< SEARCH
|
||||
<<< SEARCH
|
||||
------ SEARCH
|
||||
--- SEARCH
|
||||
=======
|
||||
before
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, true)
|
||||
expect(result1).to.equal("replaced\nbefore\n")
|
||||
try {
|
||||
@@ -177,18 +177,18 @@ before
|
||||
})
|
||||
|
||||
it("cnfc2 should handle custom nested search markers", async () => {
|
||||
const original = `text with $^.*\n<<< SEARCH2\nend`
|
||||
const diff = `<<< SEARCH
|
||||
const original = `text with $^.*\n--- SEARCH2\nend`
|
||||
const diff = `--- SEARCH
|
||||
$^.*
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE
|
||||
+++++++ REPLACE
|
||||
|
||||
<<<<<< SEARCH
|
||||
<<< SEARCH2
|
||||
------ SEARCH
|
||||
--- SEARCH2
|
||||
=======
|
||||
before
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, true)
|
||||
const result2 = await cnfc2(diff, original, true)
|
||||
expect(result1).to.equal("replaced\nbefore\n")
|
||||
@@ -196,18 +196,18 @@ before
|
||||
})
|
||||
|
||||
it("cnfc2 should handle text containing nested search markers", async () => {
|
||||
const original = `text with $^.*\ntext with <<< SEARCH2\nend`
|
||||
const diff = `<<< SEARCH
|
||||
const original = `text with $^.*\ntext with --- SEARCH2\nend`
|
||||
const diff = `--- SEARCH
|
||||
$^.*
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE
|
||||
+++++++ REPLACE
|
||||
|
||||
<<<<<< SEARCH
|
||||
text with <<< SEARCH2
|
||||
------ SEARCH
|
||||
text with --- SEARCH2
|
||||
=======
|
||||
before
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, true)
|
||||
const result2 = await cnfc2(diff, original, true)
|
||||
expect(result1).to.equal("replaced\nbefore\n")
|
||||
@@ -215,15 +215,15 @@ before
|
||||
})
|
||||
|
||||
it("cnfc2 should handle missing replacement marker in lenient mode", async () => {
|
||||
const original = `text with $^.*\ntext with <<< SEARCH2\nend`
|
||||
const diff = `<<< SEARCH
|
||||
const original = `text with $^.*\ntext with --- SEARCH2\nend`
|
||||
const diff = `--- SEARCH
|
||||
$^.*
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE
|
||||
+++++++ REPLACE
|
||||
|
||||
<<<<<< SEARCH
|
||||
text with <<< SEARCH2
|
||||
------ SEARCH
|
||||
text with --- SEARCH2
|
||||
=======
|
||||
before`
|
||||
const result1 = await cnfc(diff, original, false)
|
||||
@@ -233,15 +233,15 @@ before`
|
||||
})
|
||||
|
||||
it("cnfc2 should throw error for missing replacement marker in strict mode", async () => {
|
||||
const original = `text with $^.*\ntext with <<< SEARCH2\nend`
|
||||
const diff = `<<< SEARCH
|
||||
const original = `text with $^.*\ntext with --- SEARCH2\nend`
|
||||
const diff = `--- SEARCH
|
||||
$^.*
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE
|
||||
+++++++ REPLACE
|
||||
|
||||
<<<<<< SEARCH
|
||||
text with <<< SEARCH2
|
||||
------ SEARCH
|
||||
text with --- SEARCH2
|
||||
=======
|
||||
before`
|
||||
const result1 = await cnfc(diff, original, true)
|
||||
@@ -262,23 +262,23 @@ Section 3: sed do eiusmod tempor
|
||||
Section 4: incididunt ut labore
|
||||
Section 5: et dolore magna aliqua`
|
||||
|
||||
const diff = `<<< SEARCH
|
||||
const diff = `--- SEARCH
|
||||
Section 1: Lorem ipsum dolor sit amet
|
||||
=======
|
||||
Section 1: Replaced text
|
||||
>>>>>>> REPLACE
|
||||
+++++++ REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
------- SEARCH
|
||||
Section 3: sed do eiusmod tempor
|
||||
=======
|
||||
Section 3: Modified content
|
||||
>>>>>>> REPLACE
|
||||
+++++++ REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
------- SEARCH
|
||||
Section 5: et dolore magna aliqua
|
||||
=======
|
||||
Section 5: Final replacement
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
|
||||
const expected = `This is a long text with multiple sections.
|
||||
Section 1: Replaced text
|
||||
@@ -293,17 +293,17 @@ Section 5: Final replacement
|
||||
})
|
||||
|
||||
// Test diff containing special regex characters and nested search markers
|
||||
const diff = `<<< SEARCH
|
||||
const diff = `--- SEARCH
|
||||
$^.*
|
||||
=======
|
||||
replaced
|
||||
>>>>>>> REPLACE
|
||||
+++++++ REPLACE
|
||||
|
||||
<<<<<< SEARCH
|
||||
<<< SEARCH
|
||||
------ SEARCH
|
||||
--- SEARCH
|
||||
=======
|
||||
before
|
||||
>>>>>>> REPLACE`
|
||||
+++++++ REPLACE`
|
||||
// expected1 shows the incremental results when processing the diff line by line
|
||||
// Each element represents the result after processing that line number
|
||||
const expected1 = [
|
||||
@@ -335,7 +335,7 @@ before
|
||||
const diffLines = diff.split("\n")
|
||||
for (let i = 1; i < diffLines.length; i++) {
|
||||
it(`cnfc2 should handle partial diff configuration (line ${i})`, async () => {
|
||||
const original = `text with $^.*\n<<< SEARCH\nend`
|
||||
const original = `text with $^.*\n--- SEARCH\nend`
|
||||
const result1 = await cnfc(diffLines.slice(0, i).join("\n"), original, i === diffLines.length - 1)
|
||||
expect(result1).to.equal(expected1[i - 1])
|
||||
})
|
||||
@@ -343,7 +343,7 @@ before
|
||||
|
||||
for (let i = 1; i < diffLines.length; i++) {
|
||||
it(`cnfc2 should handle partial diff configuration (line ${i})`, async () => {
|
||||
const original = `text with $^.*\n<<< SEARCH\nend`
|
||||
const original = `text with $^.*\n--- SEARCH\nend`
|
||||
let expected = expected2[i - 1]
|
||||
if (expected instanceof Error) {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type AssistantMessageContent = TextContent | ToolUse
|
||||
|
||||
export { parseAssistantMessageV1, parseAssistantMessageV2 } from "./parse-assistant-message"
|
||||
export { parseAssistantMessageV1, parseAssistantMessageV2, parseAssistantMessageV3 } from "./parse-assistant-message"
|
||||
|
||||
export interface TextContent {
|
||||
type: "text"
|
||||
@@ -27,6 +27,7 @@ export const toolUseNames = [
|
||||
"condense",
|
||||
"report_bug",
|
||||
"new_rule",
|
||||
"web_fetch",
|
||||
] as const
|
||||
|
||||
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)
|
||||
|
||||
@@ -473,3 +473,621 @@ export function parseAssistantMessageV2(assistantMessage: string): AssistantMess
|
||||
|
||||
return contentBlocks
|
||||
}
|
||||
|
||||
export function parseAssistantMessageV3(assistantMessage: string): AssistantMessageContent[] {
|
||||
const contentBlocks: AssistantMessageContent[] = []
|
||||
let currentTextContentStart = 0 // Index where the current text block started
|
||||
let currentTextContent: TextContent | undefined = undefined
|
||||
let currentToolUseStart = 0 // Index *after* the opening tag of the current tool use
|
||||
let currentToolUse: ToolUse | undefined = undefined
|
||||
let currentParamValueStart = 0 // Index *after* the opening tag of the current param
|
||||
let currentParamName: ToolParamName | undefined = undefined
|
||||
|
||||
// Precompute tags for faster lookups
|
||||
const toolUseOpenTags = new Map<string, ToolUseName>()
|
||||
const toolParamOpenTags = new Map<string, ToolParamName>()
|
||||
for (const name of toolUseNames) {
|
||||
toolUseOpenTags.set(`<${name}>`, name)
|
||||
}
|
||||
for (const name of toolParamNames) {
|
||||
toolParamOpenTags.set(`<${name}>`, name)
|
||||
}
|
||||
|
||||
// Function calls format detection
|
||||
const isFunctionCallsOpen = "<function_calls>"
|
||||
const isFunctionCallsClose = "</function_calls>"
|
||||
const isInvokeStart = '<invoke name="'
|
||||
const isInvokeEnd = '">'
|
||||
const isInvokeClose = "</invoke>"
|
||||
const isParameterStart = '<parameter name="'
|
||||
const isParameterNameEnd = '">'
|
||||
const isParameterClose = "</parameter>"
|
||||
|
||||
// Variables for function calls parsing
|
||||
let inFunctionCalls = false
|
||||
let currentInvokeName = ""
|
||||
let currentParameterName = ""
|
||||
|
||||
const len = assistantMessage.length
|
||||
for (let i = 0; i < len; i++) {
|
||||
const currentCharIndex = i
|
||||
|
||||
// --- State: Parsing Function Calls ---
|
||||
// Check for opening function_calls tag
|
||||
if (
|
||||
!inFunctionCalls &&
|
||||
currentCharIndex >= isFunctionCallsOpen.length - 1 &&
|
||||
assistantMessage.startsWith(isFunctionCallsOpen, currentCharIndex - isFunctionCallsOpen.length + 1)
|
||||
) {
|
||||
// End current text block if one was active
|
||||
if (currentTextContent) {
|
||||
currentTextContent.content = assistantMessage
|
||||
.slice(currentTextContentStart, currentCharIndex - isFunctionCallsOpen.length + 1)
|
||||
.trim()
|
||||
currentTextContent.partial = false
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
currentTextContent = undefined
|
||||
}
|
||||
|
||||
inFunctionCalls = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for invoke start within function_calls
|
||||
if (
|
||||
inFunctionCalls &&
|
||||
currentInvokeName === "" &&
|
||||
!currentToolUse && // Don't create a new tool if we already have one
|
||||
currentCharIndex >= isInvokeStart.length - 1 &&
|
||||
assistantMessage.startsWith(isInvokeStart, currentCharIndex - isInvokeStart.length + 1)
|
||||
) {
|
||||
// Find the end of the invoke name
|
||||
const nameEndPos = assistantMessage.indexOf(isInvokeEnd, currentCharIndex + 1)
|
||||
if (nameEndPos !== -1) {
|
||||
// Extract the invoke name
|
||||
currentInvokeName = assistantMessage.slice(currentCharIndex + 1, nameEndPos)
|
||||
i = nameEndPos + isInvokeEnd.length - 1 // Skip to after the '">
|
||||
|
||||
// If this is an LS invoke, create a list_files tool
|
||||
if (currentInvokeName === "LS") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "list_files",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a Grep invoke, create a search_files tool
|
||||
if (currentInvokeName === "Grep") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "search_files",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInvokeName === "Bash") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "execute_command",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInvokeName === "Read") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "read_file",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInvokeName === "Write") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "write_to_file",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInvokeName === "WebFetch") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "web_fetch",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInvokeName === "AskQuestion") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "ask_followup_question",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInvokeName === "UseMCPTool") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "use_mcp_tool",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInvokeName === "AccessMCPResource") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "access_mcp_resource",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInvokeName === "ListCodeDefinitionNames") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "list_code_definition_names",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInvokeName === "PlanModeRespond") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "plan_mode_respond",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInvokeName === "LoadMcpDocumentation") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "load_mcp_documentation",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInvokeName === "AttemptCompletion") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "attempt_completion",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInvokeName === "BrowserAction") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "browser_action",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentInvokeName === "NewTask") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "new_task",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a MultiEdit invoke, create a replace_in_file tool
|
||||
if (currentInvokeName === "MultiEdit") {
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: "replace_in_file",
|
||||
params: {},
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check for parameter start within invoke
|
||||
if (
|
||||
inFunctionCalls &&
|
||||
currentInvokeName !== "" &&
|
||||
currentParameterName === "" &&
|
||||
currentCharIndex >= isParameterStart.length - 1 &&
|
||||
assistantMessage.startsWith(isParameterStart, currentCharIndex - isParameterStart.length + 1)
|
||||
) {
|
||||
// Find the end of the parameter name
|
||||
const nameEndPos = assistantMessage.indexOf(isParameterNameEnd, currentCharIndex + 1)
|
||||
if (nameEndPos !== -1) {
|
||||
// Extract the parameter name
|
||||
currentParameterName = assistantMessage.slice(currentCharIndex + 1, nameEndPos)
|
||||
currentParamValueStart = nameEndPos + isParameterNameEnd.length
|
||||
i = nameEndPos + isParameterNameEnd.length - 1 // Skip to after the '">'
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check for parameter end
|
||||
if (
|
||||
inFunctionCalls &&
|
||||
currentInvokeName !== "" &&
|
||||
currentParameterName !== "" &&
|
||||
currentCharIndex >= isParameterClose.length - 1 &&
|
||||
assistantMessage.startsWith(isParameterClose, currentCharIndex - isParameterClose.length + 1)
|
||||
) {
|
||||
// Extract parameter value
|
||||
const value = assistantMessage.slice(currentParamValueStart, currentCharIndex - isParameterClose.length + 1).trim()
|
||||
|
||||
// Map parameter to tool params
|
||||
if (currentToolUse && currentInvokeName === "LS" && currentParameterName === "path") {
|
||||
currentToolUse.params["path"] = value
|
||||
// Default recursive to false - only show top level
|
||||
currentToolUse.params["recursive"] = "false"
|
||||
}
|
||||
|
||||
if (currentToolUse && currentInvokeName === "Read" && currentParameterName === "file_path") {
|
||||
currentToolUse.params["path"] = value
|
||||
}
|
||||
|
||||
if (currentToolUse && currentInvokeName === "PlanModeRespond" && currentParameterName === "response") {
|
||||
currentToolUse.params["response"] = value
|
||||
}
|
||||
|
||||
if (currentToolUse && currentInvokeName === "WebFetch" && currentParameterName === "url") {
|
||||
currentToolUse.params["url"] = value
|
||||
}
|
||||
|
||||
if (currentToolUse && currentInvokeName === "ListCodeDefinitionNames" && currentParameterName === "path") {
|
||||
currentToolUse.params["path"] = value
|
||||
}
|
||||
|
||||
if (currentToolUse && currentInvokeName === "NewTask" && currentParameterName === "context") {
|
||||
currentToolUse.params["context"] = value
|
||||
}
|
||||
|
||||
// Map parameter to tool params for Grep
|
||||
if (currentToolUse && currentInvokeName === "Grep") {
|
||||
if (currentParameterName === "pattern") {
|
||||
currentToolUse.params["regex"] = value
|
||||
} else if (currentParameterName === "path") {
|
||||
currentToolUse.params["path"] = value
|
||||
} else if (currentParameterName === "include") {
|
||||
currentToolUse.params["file_pattern"] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (currentToolUse && currentInvokeName === "Bash") {
|
||||
if (currentParameterName === "command") {
|
||||
currentToolUse.params["command"] = value
|
||||
} else if (currentParameterName === "requires_approval") {
|
||||
currentToolUse.params["requires_approval"] = value === "true" ? "true" : "false"
|
||||
}
|
||||
}
|
||||
|
||||
if (currentToolUse && currentInvokeName === "Write") {
|
||||
if (currentParameterName === "file_path") {
|
||||
currentToolUse.params["path"] = value
|
||||
} else if (currentParameterName === "content") {
|
||||
currentToolUse.params["content"] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (currentToolUse && currentInvokeName === "AskQuestion") {
|
||||
if (currentParameterName === "question") {
|
||||
currentToolUse.params["question"] = value
|
||||
} else if (currentParameterName === "options") {
|
||||
currentToolUse.params["options"] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (currentToolUse && currentInvokeName === "UseMCPTool") {
|
||||
if (currentParameterName === "server_name") {
|
||||
currentToolUse.params["server_name"] = value
|
||||
} else if (currentParameterName === "tool_name") {
|
||||
currentToolUse.params["tool_name"] = value
|
||||
} else if (currentParameterName === "arguments") {
|
||||
currentToolUse.params["arguments"] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (currentToolUse && currentInvokeName === "AccessMCPResource") {
|
||||
if (currentParameterName === "server_name") {
|
||||
currentToolUse.params["server_name"] = value
|
||||
} else if (currentParameterName === "uri") {
|
||||
currentToolUse.params["uri"] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (currentToolUse && currentInvokeName === "AttemptCompletion") {
|
||||
if (currentParameterName === "result") {
|
||||
currentToolUse.params["result"] = value
|
||||
}
|
||||
if (currentParameterName === "command") {
|
||||
currentToolUse.params["command"] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (currentToolUse && currentInvokeName === "BrowserAction") {
|
||||
if (currentParameterName === "action") {
|
||||
currentToolUse.params["action"] = value
|
||||
} else if (currentParameterName === "url") {
|
||||
currentToolUse.params["url"] = value
|
||||
} else if (currentParameterName === "coordinate") {
|
||||
currentToolUse.params["coordinate"] = value
|
||||
} else if (currentParameterName === "text") {
|
||||
currentToolUse.params["text"] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Map parameter to tool params for MultiEdit
|
||||
if (currentToolUse && currentInvokeName === "MultiEdit") {
|
||||
if (currentParameterName === "file_path") {
|
||||
currentToolUse.params["path"] = value
|
||||
} else if (currentParameterName === "edits") {
|
||||
// Save the value to the diff parameter for replace_in_file
|
||||
currentToolUse.params["diff"] = value
|
||||
}
|
||||
}
|
||||
|
||||
currentParameterName = ""
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for invoke end
|
||||
if (
|
||||
inFunctionCalls &&
|
||||
currentInvokeName !== "" &&
|
||||
currentCharIndex >= isInvokeClose.length - 1 &&
|
||||
assistantMessage.startsWith(isInvokeClose, currentCharIndex - isInvokeClose.length + 1)
|
||||
) {
|
||||
// If we have a tool use from this invoke, finalize it
|
||||
if (
|
||||
currentToolUse &&
|
||||
(currentInvokeName === "LS" ||
|
||||
currentInvokeName === "Grep" ||
|
||||
currentInvokeName === "Bash" ||
|
||||
currentInvokeName === "Read" ||
|
||||
currentInvokeName === "Write" ||
|
||||
currentInvokeName === "WebFetch" ||
|
||||
currentInvokeName === "AskQuestion" ||
|
||||
currentInvokeName === "UseMCPTool" ||
|
||||
currentInvokeName === "AccessMCPResource" ||
|
||||
currentInvokeName === "ListCodeDefinitionNames" ||
|
||||
currentInvokeName === "PlanModeRespond" ||
|
||||
currentInvokeName === "LoadMcpDocumentation" ||
|
||||
currentInvokeName === "AttemptCompletion" ||
|
||||
currentInvokeName === "BrowserAction" ||
|
||||
currentInvokeName === "NewTask" ||
|
||||
currentInvokeName === "MultiEdit")
|
||||
) {
|
||||
currentToolUse.partial = false
|
||||
contentBlocks.push(currentToolUse)
|
||||
currentToolUse = undefined
|
||||
}
|
||||
currentInvokeName = ""
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for function_calls end
|
||||
if (
|
||||
inFunctionCalls &&
|
||||
currentCharIndex >= isFunctionCallsClose.length - 1 &&
|
||||
assistantMessage.startsWith(isFunctionCallsClose, currentCharIndex - isFunctionCallsClose.length + 1)
|
||||
) {
|
||||
inFunctionCalls = false
|
||||
currentTextContentStart = currentCharIndex + 1
|
||||
// Start a new text content block for any text after function_calls
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: "",
|
||||
partial: true,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip normal parsing when inside function_calls
|
||||
if (inFunctionCalls) {
|
||||
continue
|
||||
}
|
||||
|
||||
// --- State: Parsing a Tool Parameter ---
|
||||
if (currentToolUse && currentParamName) {
|
||||
const closeTag = `</${currentParamName}>`
|
||||
// Check if the string *ending* at index `i` matches the closing tag
|
||||
if (
|
||||
currentCharIndex >= closeTag.length - 1 &&
|
||||
assistantMessage.startsWith(
|
||||
closeTag,
|
||||
currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag
|
||||
)
|
||||
) {
|
||||
// Found the closing tag for the parameter
|
||||
const value = assistantMessage
|
||||
.slice(
|
||||
currentParamValueStart, // Start after the opening tag
|
||||
currentCharIndex - closeTag.length + 1, // End before the closing tag
|
||||
)
|
||||
.trim()
|
||||
currentToolUse.params[currentParamName] = value
|
||||
currentParamName = undefined // Go back to parsing tool content
|
||||
// We don't continue loop here, need to check for tool close or other params at index i
|
||||
} else {
|
||||
continue // Still inside param value, move to next char
|
||||
}
|
||||
}
|
||||
|
||||
// --- State: Parsing a Tool Use (but not a specific parameter) ---
|
||||
if (currentToolUse && !currentParamName) {
|
||||
// Ensure we are not inside a parameter already
|
||||
// Check if starting a new parameter
|
||||
let startedNewParam = false
|
||||
for (const [tag, paramName] of toolParamOpenTags.entries()) {
|
||||
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
|
||||
currentParamName = paramName
|
||||
currentParamValueStart = currentCharIndex + 1 // Value starts after the tag
|
||||
startedNewParam = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (startedNewParam) {
|
||||
continue // Handled start of param, move to next char
|
||||
}
|
||||
|
||||
// Check if closing the current tool use
|
||||
const toolCloseTag = `</${currentToolUse.name}>`
|
||||
if (
|
||||
currentCharIndex >= toolCloseTag.length - 1 &&
|
||||
assistantMessage.startsWith(toolCloseTag, currentCharIndex - toolCloseTag.length + 1)
|
||||
) {
|
||||
// End of the tool use found
|
||||
// Special handling for content params *before* finalizing the tool
|
||||
const toolContentSlice = assistantMessage.slice(
|
||||
currentToolUseStart, // From after the tool opening tag
|
||||
currentCharIndex - toolCloseTag.length + 1, // To before the tool closing tag
|
||||
)
|
||||
|
||||
// Check if content parameter needs special handling (write_to_file/new_rule)
|
||||
// This check is important if the closing </content> tag was missed by the parameter parsing logic
|
||||
// (e.g., if content is empty or parsing logic prioritizes tool close)
|
||||
const contentParamName: ToolParamName = "content"
|
||||
if (
|
||||
currentToolUse.name === "write_to_file" /* || currentToolUse.name === "new_rule" */ &&
|
||||
toolContentSlice.includes(`<${contentParamName}>`)
|
||||
) {
|
||||
const contentStartTag = `<${contentParamName}>`
|
||||
const contentEndTag = `</${contentParamName}>`
|
||||
const contentStart = toolContentSlice.indexOf(contentStartTag)
|
||||
// Use lastIndexOf for robustness against nested tags
|
||||
const contentEnd = toolContentSlice.lastIndexOf(contentEndTag)
|
||||
|
||||
if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) {
|
||||
const contentValue = toolContentSlice.slice(contentStart + contentStartTag.length, contentEnd).trim()
|
||||
currentToolUse.params[contentParamName] = contentValue
|
||||
}
|
||||
}
|
||||
|
||||
currentToolUse.partial = false // Mark as complete
|
||||
contentBlocks.push(currentToolUse)
|
||||
currentToolUse = undefined // Reset state
|
||||
currentTextContentStart = currentCharIndex + 1 // Potential text starts after this tag
|
||||
continue // Move to next char
|
||||
}
|
||||
// If not starting a param and not closing the tool, continue accumulating tool content implicitly
|
||||
continue
|
||||
}
|
||||
|
||||
// --- State: Parsing Text / Looking for Tool Start ---
|
||||
if (!currentToolUse) {
|
||||
// Check if starting a new tool use
|
||||
let startedNewTool = false
|
||||
for (const [tag, toolName] of toolUseOpenTags.entries()) {
|
||||
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
|
||||
// End current text block if one was active
|
||||
if (currentTextContent) {
|
||||
currentTextContent.content = assistantMessage
|
||||
.slice(
|
||||
currentTextContentStart, // From where text started
|
||||
currentCharIndex - tag.length + 1, // To before the tool tag starts
|
||||
)
|
||||
.trim()
|
||||
currentTextContent.partial = false // Ended because tool started
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
currentTextContent = undefined
|
||||
} else {
|
||||
// Check for any text between the last block and this tag
|
||||
const potentialText = assistantMessage
|
||||
.slice(
|
||||
currentTextContentStart, // From where text *might* have started
|
||||
currentCharIndex - tag.length + 1, // To before the tool tag starts
|
||||
)
|
||||
.trim()
|
||||
if (potentialText.length > 0) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
content: potentialText,
|
||||
partial: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Start the new tool use
|
||||
currentToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolName,
|
||||
params: {},
|
||||
partial: true, // Assume partial until closing tag is found
|
||||
}
|
||||
currentToolUseStart = currentCharIndex + 1 // Tool content starts after the opening tag
|
||||
startedNewTool = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (startedNewTool) {
|
||||
continue // Handled start of tool, move to next char
|
||||
}
|
||||
|
||||
// If not starting a tool, it must be text content
|
||||
if (!currentTextContent) {
|
||||
// Start a new text block if we aren't already in one
|
||||
currentTextContentStart = currentCharIndex // Text starts at the current character
|
||||
// Check if the current char is the start of potential text *immediately* after a tag
|
||||
// This needs the previous state - simpler to let slicing handle it later.
|
||||
// Resetting start index accurately is key.
|
||||
// It should be the index *after* the last processed tag.
|
||||
// The logic managing currentTextContentStart after closing tags handles this.
|
||||
|
||||
currentTextContent = {
|
||||
type: "text",
|
||||
content: "", // Will be determined by slicing at the end or when a tool starts
|
||||
partial: true,
|
||||
}
|
||||
}
|
||||
// Continue accumulating text implicitly; content is extracted later.
|
||||
}
|
||||
} // End of loop
|
||||
|
||||
// --- Finalization after loop ---
|
||||
|
||||
// Finalize any open parameter within an open tool use
|
||||
if (currentToolUse && currentParamName) {
|
||||
currentToolUse.params[currentParamName] = assistantMessage
|
||||
.slice(currentParamValueStart) // From param start to end of string
|
||||
.trim()
|
||||
// Tool use remains partial
|
||||
}
|
||||
|
||||
// Finalize any open tool use (which might contain the finalized partial param)
|
||||
if (currentToolUse) {
|
||||
// Tool use is partial because the loop finished before its closing tag
|
||||
contentBlocks.push(currentToolUse)
|
||||
}
|
||||
// Finalize any trailing text content
|
||||
// Only possible if a tool use wasn't open at the very end
|
||||
else if (currentTextContent) {
|
||||
currentTextContent.content = assistantMessage
|
||||
.slice(currentTextContentStart) // From text start to end of string
|
||||
.trim()
|
||||
// Text is partial because the loop finished
|
||||
if (currentTextContent.content.length > 0) {
|
||||
contentBlocks.push(currentTextContent)
|
||||
}
|
||||
}
|
||||
|
||||
return contentBlocks
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fileExistsAtPath, isDirectory, readDirectory } from "@utils/fs"
|
||||
import { ensureRulesDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { ensureRulesDirectoryExists, ensureWorkflowsDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "@core/storage/state"
|
||||
import * as path from "path"
|
||||
import fs from "fs/promises"
|
||||
@@ -172,9 +172,13 @@ export const createRuleFile = async (isGlobal: boolean, filename: string, cwd: s
|
||||
try {
|
||||
let filePath: string
|
||||
if (isGlobal) {
|
||||
// global means its implicitly clinerules
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
filePath = path.join(globalClineRulesFilePath, filename)
|
||||
if (type === "workflow") {
|
||||
const globalClineWorkflowFilePath = await ensureWorkflowsDirectoryExists()
|
||||
filePath = path.join(globalClineWorkflowFilePath, filename)
|
||||
} else {
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
filePath = path.join(globalClineRulesFilePath, filename)
|
||||
}
|
||||
} else {
|
||||
const localClineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
|
||||
@@ -243,9 +247,15 @@ export async function deleteRuleFile(
|
||||
|
||||
// Update the appropriate toggles
|
||||
if (isGlobal) {
|
||||
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateGlobalState(context, "globalClineRulesToggles", toggles)
|
||||
if (type === "workflow") {
|
||||
const toggles = ((await getGlobalState(context, "globalWorkflowToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateGlobalState(context, "globalWorkflowToggles", toggles)
|
||||
} else {
|
||||
const toggles = ((await getGlobalState(context, "globalClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
delete toggles[rulePath]
|
||||
await updateGlobalState(context, "globalClineRulesToggles", toggles)
|
||||
}
|
||||
} else {
|
||||
if (type === "workflow") {
|
||||
const toggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "path"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { GlobalFileNames, ensureWorkflowsDirectoryExists } from "@core/storage/disk"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
|
||||
import { getWorkspaceState, updateWorkspaceState, getGlobalState, updateGlobalState } from "@core/storage/state"
|
||||
import * as vscode from "vscode"
|
||||
import { synchronizeRuleToggles } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
|
||||
@@ -11,10 +11,23 @@ import { synchronizeRuleToggles } from "@core/context/instructions/user-instruct
|
||||
export async function refreshWorkflowToggles(
|
||||
context: vscode.ExtensionContext,
|
||||
workingDirectory: string,
|
||||
): Promise<ClineRulesToggles> {
|
||||
): Promise<{
|
||||
globalWorkflowToggles: ClineRulesToggles
|
||||
localWorkflowToggles: ClineRulesToggles
|
||||
}> {
|
||||
// Global workflows
|
||||
const globalWorkflowToggles = ((await getGlobalState(context, "globalWorkflowToggles")) as ClineRulesToggles) || {}
|
||||
const globalClineWorkflowsFilePath = await ensureWorkflowsDirectoryExists()
|
||||
const updatedGlobalWorkflowToggles = await synchronizeRuleToggles(globalClineWorkflowsFilePath, globalWorkflowToggles)
|
||||
await updateGlobalState(context, "globalWorkflowToggles", updatedGlobalWorkflowToggles)
|
||||
|
||||
const workflowRulesToggles = ((await getWorkspaceState(context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
const workflowsDirPath = path.resolve(workingDirectory, GlobalFileNames.workflows)
|
||||
const updatedWorkflowToggles = await synchronizeRuleToggles(workflowsDirPath, workflowRulesToggles)
|
||||
await updateWorkspaceState(context, "workflowToggles", updatedWorkflowToggles)
|
||||
return updatedWorkflowToggles
|
||||
|
||||
return {
|
||||
globalWorkflowToggles: updatedGlobalWorkflowToggles,
|
||||
localWorkflowToggles: updatedWorkflowToggles,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { EmptyRequest, String } from "../../../shared/proto/common"
|
||||
* @param controller The controller instance.
|
||||
* @returns The login URL as a string.
|
||||
*/
|
||||
export async function accountLoginClicked(controller: Controller, unused: EmptyRequest): Promise<String> {
|
||||
export async function accountLoginClicked(controller: Controller, _: EmptyRequest): Promise<String> {
|
||||
// Generate nonce for state validation
|
||||
const nonce = crypto.randomBytes(32).toString("hex")
|
||||
await storeSecret(controller.context, "authNonce", nonce)
|
||||
@@ -27,7 +27,7 @@ export async function accountLoginClicked(controller: Controller, unused: EmptyR
|
||||
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
|
||||
)
|
||||
await vscode.env.openExternal(authUrl)
|
||||
return {
|
||||
return String.create({
|
||||
value: authUrl.toString(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Empty } from "../../../shared/proto/common"
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import type { EmptyRequest } from "../../../shared/proto/common"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
@@ -10,5 +10,5 @@ import type { Controller } from "../index"
|
||||
*/
|
||||
export async function accountLogoutClicked(controller: Controller, _request: EmptyRequest): Promise<Empty> {
|
||||
await controller.handleSignOut()
|
||||
return {}
|
||||
return Empty.create({})
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
|
||||
import { StreamingResponseHandler } from "../grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create account service registry
|
||||
const accountService = createServiceRegistry("account")
|
||||
|
||||
// Export the method handler types and registration function
|
||||
export type AccountMethodHandler = ServiceMethodHandler
|
||||
export type AccountStreamingMethodHandler = StreamingMethodHandler
|
||||
export const registerMethod = accountService.registerMethod
|
||||
|
||||
// Export the request handlers
|
||||
export const handleAccountServiceRequest = accountService.handleRequest
|
||||
export const handleAccountServiceStreamingRequest = accountService.handleStreamingRequest
|
||||
export const isStreamingMethod = accountService.isStreamingMethod
|
||||
|
||||
// Register all account methods
|
||||
registerAllMethods()
|
||||
@@ -1,14 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { accountLoginClicked } from "./accountLoginClicked"
|
||||
import { accountLogoutClicked } from "./accountLogoutClicked"
|
||||
|
||||
// Register all account service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("accountLoginClicked", accountLoginClicked)
|
||||
registerMethod("accountLogoutClicked", accountLogoutClicked)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Controller } from "../index"
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { String as ProtoString } from "../../../shared/proto/common"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
|
||||
|
||||
// Keep track of active authCallback subscriptions
|
||||
const activeAuthCallbackSubscriptions = new Set<StreamingResponseHandler>()
|
||||
|
||||
/**
|
||||
* Subscribe to authCallback events
|
||||
* @param controller The controller instance
|
||||
* @param request The empty request
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request (passed by the gRPC handler)
|
||||
*/
|
||||
export async function subscribeToAuthCallback(
|
||||
controller: Controller,
|
||||
request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
// Add this subscription to the active subscriptions
|
||||
activeAuthCallbackSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeAuthCallbackSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
if (requestId) {
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "authCallback_subscription" }, responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an authCallback event to all active subscribers
|
||||
* @param customToken The custom token for authentication
|
||||
*/
|
||||
export async function sendAuthCallbackEvent(customToken: string): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(activeAuthCallbackSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event: ProtoString = {
|
||||
value: customToken,
|
||||
}
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending authCallback event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
activeAuthCallbackSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
@@ -24,24 +24,24 @@ export async function discoverBrowser(controller: Controller, request: EmptyRequ
|
||||
const browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
const result = await browserSession.testConnection(discoveredHost)
|
||||
|
||||
return {
|
||||
return BrowserConnection.create({
|
||||
success: true,
|
||||
message: `Successfully discovered and connected to Chrome at ${discoveredHost}`,
|
||||
endpoint: result.endpoint || "",
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return {
|
||||
return BrowserConnection.create({
|
||||
success: false,
|
||||
message:
|
||||
"No Chrome instances found. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
|
||||
endpoint: "",
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
return BrowserConnection.create({
|
||||
success: false,
|
||||
message: `Error discovering browser: ${error instanceof Error ? error.message : String(error)}`,
|
||||
endpoint: "",
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getAllExtensionState } from "@core/storage/state"
|
||||
* @param request The request message
|
||||
* @returns The browser connection info
|
||||
*/
|
||||
export async function getBrowserConnectionInfo(controller: Controller, request: EmptyRequest): Promise<BrowserConnectionInfo> {
|
||||
export async function getBrowserConnectionInfo(controller: Controller, _: EmptyRequest): Promise<BrowserConnectionInfo> {
|
||||
try {
|
||||
// Get browser settings from extension state
|
||||
const { browserSettings } = await getAllExtensionState(controller.context)
|
||||
@@ -23,25 +23,25 @@ export async function getBrowserConnectionInfo(controller: Controller, request:
|
||||
const connectionInfo = browserSession.getConnectionInfo()
|
||||
|
||||
// Convert from BrowserSession.BrowserConnectionInfo to proto.BrowserConnectionInfo
|
||||
return {
|
||||
return BrowserConnectionInfo.create({
|
||||
isConnected: connectionInfo.isConnected,
|
||||
isRemote: connectionInfo.isRemote,
|
||||
host: connectionInfo.host || "", // Ensure host is never undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Fallback to browser settings if no active browser session
|
||||
return {
|
||||
return BrowserConnectionInfo.create({
|
||||
isConnected: false,
|
||||
isRemote: !!browserSettings.remoteBrowserEnabled,
|
||||
host: browserSettings.remoteBrowserHost || "",
|
||||
}
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
console.error("Error getting browser connection info:", error)
|
||||
return {
|
||||
return BrowserConnectionInfo.create({
|
||||
isConnected: false,
|
||||
isRemote: false,
|
||||
host: "",
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,21 +10,21 @@ import { BrowserSession } from "../../../services/browser/BrowserSession"
|
||||
* @param request The empty request message
|
||||
* @returns The detected Chrome path and whether it's bundled
|
||||
*/
|
||||
export async function getDetectedChromePath(controller: Controller, request: EmptyRequest): Promise<ChromePath> {
|
||||
export async function getDetectedChromePath(controller: Controller, _: EmptyRequest): Promise<ChromePath> {
|
||||
try {
|
||||
const { browserSettings } = await getAllExtensionState(controller.context)
|
||||
const browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
const result = await browserSession.getDetectedChromePath()
|
||||
|
||||
return {
|
||||
return ChromePath.create({
|
||||
path: result.path,
|
||||
isBundled: result.isBundled,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error getting detected Chrome path:", error)
|
||||
return {
|
||||
return ChromePath.create({
|
||||
path: "",
|
||||
isBundled: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
|
||||
import { StreamingResponseHandler } from "../grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create browser service registry
|
||||
const browserService = createServiceRegistry("browser")
|
||||
|
||||
// Export the method handler types and registration function
|
||||
export type BrowserMethodHandler = ServiceMethodHandler
|
||||
export type BrowserStreamingMethodHandler = StreamingMethodHandler
|
||||
export const registerMethod = browserService.registerMethod
|
||||
|
||||
// Export the request handlers
|
||||
export const handleBrowserServiceRequest = browserService.handleRequest
|
||||
export const handleBrowserServiceStreamingRequest = browserService.handleStreamingRequest
|
||||
export const isStreamingMethod = browserService.isStreamingMethod
|
||||
|
||||
// Register all browser methods
|
||||
registerAllMethods()
|
||||
@@ -1,22 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { discoverBrowser } from "./discoverBrowser"
|
||||
import { getBrowserConnectionInfo } from "./getBrowserConnectionInfo"
|
||||
import { getDetectedChromePath } from "./getDetectedChromePath"
|
||||
import { relaunchChromeDebugMode } from "./relaunchChromeDebugMode"
|
||||
import { testBrowserConnection } from "./testBrowserConnection"
|
||||
import { updateBrowserSettings } from "./updateBrowserSettings"
|
||||
|
||||
// Register all browser service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("discoverBrowser", discoverBrowser)
|
||||
registerMethod("getBrowserConnectionInfo", getBrowserConnectionInfo)
|
||||
registerMethod("getDetectedChromePath", getDetectedChromePath)
|
||||
registerMethod("relaunchChromeDebugMode", relaunchChromeDebugMode)
|
||||
registerMethod("testBrowserConnection", testBrowserConnection)
|
||||
registerMethod("updateBrowserSettings", updateBrowserSettings)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { BrowserSession } from "../../../services/browser/BrowserSession"
|
||||
* @param request The empty request message
|
||||
* @returns The browser relaunch result as a string message
|
||||
*/
|
||||
export async function relaunchChromeDebugMode(controller: Controller, request: EmptyRequest): Promise<StringMessage> {
|
||||
export async function relaunchChromeDebugMode(controller: Controller, _: EmptyRequest): Promise<StringMessage> {
|
||||
try {
|
||||
const { browserSettings } = await controller.getStateToPostToWebview()
|
||||
const browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
@@ -18,9 +18,9 @@ export async function relaunchChromeDebugMode(controller: Controller, request: E
|
||||
|
||||
// The actual result will be sent via postMessageToWebview in the BrowserSession.relaunchChromeDebugMode method
|
||||
// Here we just return a message as a placeholder
|
||||
return {
|
||||
return StringMessage.create({
|
||||
value: "Chrome relaunch initiated",
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error relaunching Chrome: ${error instanceof Error ? error.message : globalThis.String(error)}`)
|
||||
}
|
||||
|
||||
@@ -24,40 +24,40 @@ export async function testBrowserConnection(controller: Controller, request: Str
|
||||
if (discoveredHost) {
|
||||
// Test the connection to the discovered host
|
||||
const result = await browserSession.testConnection(discoveredHost)
|
||||
return {
|
||||
return BrowserConnection.create({
|
||||
success: result.success,
|
||||
message: `Auto-discovered and tested connection to Chrome at ${discoveredHost}: ${result.message}`,
|
||||
endpoint: result.endpoint || "",
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return {
|
||||
return BrowserConnection.create({
|
||||
success: false,
|
||||
message:
|
||||
"No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
|
||||
endpoint: "",
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
return BrowserConnection.create({
|
||||
success: false,
|
||||
message: `Error during auto-discovery: ${error instanceof Error ? error.message : String(error)}`,
|
||||
endpoint: "",
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Test the provided URL
|
||||
const result = await browserSession.testConnection(text)
|
||||
return {
|
||||
return BrowserConnection.create({
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
endpoint: result.endpoint || "",
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
return BrowserConnection.create({
|
||||
success: false,
|
||||
message: `Error testing connection: ${error instanceof Error ? error.message : String(error)}`,
|
||||
endpoint: "",
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,13 +50,13 @@ export async function updateBrowserSettings(controller: Controller, request: Upd
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return {
|
||||
return Boolean.create({
|
||||
value: true,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error updating browser settings:", error)
|
||||
return {
|
||||
return Boolean.create({
|
||||
value: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +18,5 @@ export async function checkpointRestore(controller: Controller, request: Checkpo
|
||||
// NOTE: cancelTask awaits abortTask, which awaits diffViewProvider.revertChanges, which reverts any edited files, allowing us to reset to a checkpoint rather than running into a state where the revertChanges function is called alongside or after the checkpoint reset
|
||||
await controller.task?.restoreCheckpoint(request.number, request.restoreType as ClineCheckpointRestore, request.offset)
|
||||
}
|
||||
return {}
|
||||
return Empty.create({})
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
|
||||
import { StreamingResponseHandler } from "../grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create checkpoints service registry
|
||||
const checkpointsService = createServiceRegistry("checkpoints")
|
||||
|
||||
// Export the method handler types and registration function
|
||||
export type CheckpointsMethodHandler = ServiceMethodHandler
|
||||
export type CheckpointsStreamingMethodHandler = StreamingMethodHandler
|
||||
export const registerMethod = checkpointsService.registerMethod
|
||||
|
||||
// Export the request handlers
|
||||
export const handleCheckpointsServiceRequest = checkpointsService.handleRequest
|
||||
export const handleCheckpointsServiceStreamingRequest = checkpointsService.handleStreamingRequest
|
||||
export const isStreamingMethod = checkpointsService.isStreamingMethod
|
||||
|
||||
// Register all checkpoints methods
|
||||
registerAllMethods()
|
||||
@@ -1,14 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { checkpointDiff } from "./checkpointDiff"
|
||||
import { checkpointRestore } from "./checkpointRestore"
|
||||
|
||||
// Register all checkpoints service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("checkpointDiff", checkpointDiff)
|
||||
registerMethod("checkpointRestore", checkpointRestore)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
|
||||
import { StreamingResponseHandler } from "../grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create file service registry
|
||||
const fileService = createServiceRegistry("file")
|
||||
|
||||
// Export the method handler types and registration function
|
||||
export type FileMethodHandler = ServiceMethodHandler
|
||||
export type FileStreamingMethodHandler = StreamingMethodHandler
|
||||
export const registerMethod = fileService.registerMethod
|
||||
|
||||
// Export the request handlers
|
||||
export const handleFileServiceRequest = fileService.handleRequest
|
||||
export const handleFileServiceStreamingRequest = fileService.handleStreamingRequest
|
||||
export const isStreamingMethod = fileService.isStreamingMethod
|
||||
|
||||
// Register all file methods
|
||||
registerAllMethods()
|
||||
@@ -1,38 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { copyToClipboard } from "./copyToClipboard"
|
||||
import { createRuleFile } from "./createRuleFile"
|
||||
import { deleteRuleFile } from "./deleteRuleFile"
|
||||
import { getRelativePaths } from "./getRelativePaths"
|
||||
import { openFile } from "./openFile"
|
||||
import { openImage } from "./openImage"
|
||||
import { openMention } from "./openMention"
|
||||
import { refreshRules } from "./refreshRules"
|
||||
import { searchCommits } from "./searchCommits"
|
||||
import { searchFiles } from "./searchFiles"
|
||||
import { selectImages } from "./selectImages"
|
||||
import { toggleClineRule } from "./toggleClineRule"
|
||||
import { toggleCursorRule } from "./toggleCursorRule"
|
||||
import { toggleWindsurfRule } from "./toggleWindsurfRule"
|
||||
|
||||
// Register all file service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("copyToClipboard", copyToClipboard)
|
||||
registerMethod("createRuleFile", createRuleFile)
|
||||
registerMethod("deleteRuleFile", deleteRuleFile)
|
||||
registerMethod("getRelativePaths", getRelativePaths)
|
||||
registerMethod("openFile", openFile)
|
||||
registerMethod("openImage", openImage)
|
||||
registerMethod("openMention", openMention)
|
||||
registerMethod("refreshRules", refreshRules)
|
||||
registerMethod("searchCommits", searchCommits)
|
||||
registerMethod("searchFiles", searchFiles)
|
||||
registerMethod("selectImages", selectImages)
|
||||
registerMethod("toggleClineRule", toggleClineRule)
|
||||
registerMethod("toggleCursorRule", toggleCursorRule)
|
||||
registerMethod("toggleWindsurfRule", toggleWindsurfRule)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "@shared/proto/common"
|
||||
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
import path from "path"
|
||||
/**
|
||||
* Opens a file in the editor
|
||||
* @param controller The controller instance
|
||||
* @param request The request message containing the file path in the 'value' field
|
||||
* @returns Empty response
|
||||
*/
|
||||
export const openTaskHistory: FileMethodHandler = async (controller: Controller, request: StringRequest): Promise<Empty> => {
|
||||
const globalStoragePath = controller.context.globalStorageUri.fsPath
|
||||
const taskHistoryPath = path.join(globalStoragePath, "tasks", request.value, "api_conversation_history.json")
|
||||
if (request.value) {
|
||||
openFileIntegration(taskHistoryPath)
|
||||
}
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -16,15 +16,16 @@ export async function refreshRules(controller: Controller, _request: EmptyReques
|
||||
try {
|
||||
const { globalToggles, localToggles } = await refreshClineRulesToggles(controller.context, cwd)
|
||||
const { cursorLocalToggles, windsurfLocalToggles } = await refreshExternalRulesToggles(controller.context, cwd)
|
||||
const workflowToggles = await refreshWorkflowToggles(controller.context, cwd)
|
||||
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(controller.context, cwd)
|
||||
|
||||
return {
|
||||
return RefreshedRules.create({
|
||||
globalClineRulesToggles: { toggles: globalToggles },
|
||||
localClineRulesToggles: { toggles: localToggles },
|
||||
localCursorRulesToggles: { toggles: cursorLocalToggles },
|
||||
localWindsurfRulesToggles: { toggles: windsurfLocalToggles },
|
||||
workflowToggles: { toggles: workflowToggles },
|
||||
}
|
||||
localWorkflowToggles: { toggles: localWorkflowToggles },
|
||||
globalWorkflowToggles: { toggles: globalWorkflowToggles },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh rules:", error)
|
||||
throw error
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Controller } from ".."
|
||||
import { BooleanRequest, StringArrays } from "@shared/proto/common"
|
||||
import { selectFiles as selectFilesIntegration } from "@integrations/misc/process-files"
|
||||
import { FileMethodHandler } from "./index"
|
||||
|
||||
/**
|
||||
* Prompts the user to select images from the file system and returns them as data URLs
|
||||
* @param controller The controller instance
|
||||
* @param request Boolean request, with the value defining whether this model supports images
|
||||
* @returns Two arrays of image data URLs and other file paths
|
||||
*/
|
||||
export const selectFiles: FileMethodHandler = async (controller: Controller, request: BooleanRequest): Promise<StringArrays> => {
|
||||
try {
|
||||
const { images, files } = await selectFilesIntegration(request.value)
|
||||
return StringArrays.create({ values1: images, values2: files })
|
||||
} catch (error) {
|
||||
console.error("Error selecting images & files:", error)
|
||||
// Return empty array on error
|
||||
return StringArrays.create({ values1: [], values2: [] })
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ToggleClineRuleRequest, ClineRulesToggles, ToggleClineRules } from "../../../shared/proto/file"
|
||||
import { ToggleClineRules } from "../../../shared/proto/file"
|
||||
import type { ToggleClineRuleRequest } from "../../../shared/proto/file"
|
||||
import type { Controller } from "../index"
|
||||
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "../../../core/storage/state"
|
||||
import { ClineRulesToggles as AppClineRulesToggles } from "@shared/cline-rules"
|
||||
@@ -36,8 +37,8 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
|
||||
const globalToggles = ((await getGlobalState(controller.context, "globalClineRulesToggles")) as AppClineRulesToggles) || {}
|
||||
const localToggles = ((await getWorkspaceState(controller.context, "localClineRulesToggles")) as AppClineRulesToggles) || {}
|
||||
|
||||
return {
|
||||
return ToggleClineRules.create({
|
||||
globalClineRulesToggles: { toggles: globalToggles },
|
||||
localClineRulesToggles: { toggles: localToggles },
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ToggleCursorRuleRequest, ClineRulesToggles } from "../../../shared/proto/file"
|
||||
import type { ToggleCursorRuleRequest } from "../../../shared/proto/file"
|
||||
import { ClineRulesToggles } from "../../../shared/proto/file"
|
||||
import type { Controller } from "../index"
|
||||
import { getWorkspaceState, updateWorkspaceState } from "../../../core/storage/state"
|
||||
import { ClineRulesToggles as AppClineRulesToggles } from "@shared/cline-rules"
|
||||
@@ -28,7 +29,7 @@ export async function toggleCursorRule(controller: Controller, request: ToggleCu
|
||||
// Get the current state to return in the response
|
||||
const cursorToggles = ((await getWorkspaceState(controller.context, "localCursorRulesToggles")) as AppClineRulesToggles) || {}
|
||||
|
||||
return {
|
||||
return ClineRulesToggles.create({
|
||||
toggles: cursorToggles,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ToggleWindsurfRuleRequest, ClineRulesToggles } from "../../../shared/proto/file"
|
||||
import type { ToggleWindsurfRuleRequest } from "../../../shared/proto/file"
|
||||
import { ClineRulesToggles } from "../../../shared/proto/file"
|
||||
import type { Controller } from "../index"
|
||||
import { getWorkspaceState, updateWorkspaceState } from "../../../core/storage/state"
|
||||
import { ClineRulesToggles as AppClineRulesToggles } from "@shared/cline-rules"
|
||||
@@ -26,5 +27,5 @@ export async function toggleWindsurfRule(controller: Controller, request: Toggle
|
||||
await updateWorkspaceState(controller.context, "localWindsurfRulesToggles", toggles)
|
||||
|
||||
// Return the toggles directly
|
||||
return { toggles: toggles }
|
||||
return ClineRulesToggles.create({ toggles: toggles })
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { Controller } from "./index"
|
||||
import { StreamingResponseHandler } from "./grpc-handler"
|
||||
import { handleAccountServiceRequest, handleAccountServiceStreamingRequest } from "./account/index"
|
||||
import { handleBrowserServiceRequest, handleBrowserServiceStreamingRequest } from "./browser/index"
|
||||
import { handleCheckpointsServiceRequest, handleCheckpointsServiceStreamingRequest } from "./checkpoints/index"
|
||||
import { handleFileServiceRequest, handleFileServiceStreamingRequest } from "./file/index"
|
||||
import { handleMcpServiceRequest, handleMcpServiceStreamingRequest } from "./mcp/index"
|
||||
import { handleStateServiceRequest, handleStateServiceStreamingRequest } from "./state/index"
|
||||
import { handleTaskServiceRequest, handleTaskServiceStreamingRequest } from "./task/index"
|
||||
import { handleWebServiceRequest, handleWebServiceStreamingRequest } from "./web/index"
|
||||
import { handleModelsServiceRequest, handleModelsServiceStreamingRequest } from "./models/index"
|
||||
import { handleSlashServiceRequest, handleSlashServiceStreamingRequest } from "./slash/index"
|
||||
import { handleUiServiceRequest, handleUiServiceStreamingRequest } from "./ui/index"
|
||||
|
||||
/**
|
||||
* Configuration for a service handler
|
||||
*/
|
||||
export interface ServiceHandlerConfig {
|
||||
requestHandler: (controller: Controller, method: string, message: any) => Promise<any>
|
||||
streamingHandler: (
|
||||
controller: Controller,
|
||||
method: string,
|
||||
message: any,
|
||||
responseStream: StreamingResponseHandler,
|
||||
requestId?: string,
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of service names to their handler configurations
|
||||
*/
|
||||
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {
|
||||
"cline.AccountService": {
|
||||
requestHandler: handleAccountServiceRequest,
|
||||
streamingHandler: handleAccountServiceStreamingRequest,
|
||||
},
|
||||
"cline.BrowserService": {
|
||||
requestHandler: handleBrowserServiceRequest,
|
||||
streamingHandler: handleBrowserServiceStreamingRequest,
|
||||
},
|
||||
"cline.CheckpointsService": {
|
||||
requestHandler: handleCheckpointsServiceRequest,
|
||||
streamingHandler: handleCheckpointsServiceStreamingRequest,
|
||||
},
|
||||
"cline.FileService": {
|
||||
requestHandler: handleFileServiceRequest,
|
||||
streamingHandler: handleFileServiceStreamingRequest,
|
||||
},
|
||||
"cline.McpService": {
|
||||
requestHandler: handleMcpServiceRequest,
|
||||
streamingHandler: handleMcpServiceStreamingRequest,
|
||||
},
|
||||
"cline.StateService": {
|
||||
requestHandler: handleStateServiceRequest,
|
||||
streamingHandler: handleStateServiceStreamingRequest,
|
||||
},
|
||||
"cline.TaskService": {
|
||||
requestHandler: handleTaskServiceRequest,
|
||||
streamingHandler: handleTaskServiceStreamingRequest,
|
||||
},
|
||||
"cline.WebService": {
|
||||
requestHandler: handleWebServiceRequest,
|
||||
streamingHandler: handleWebServiceStreamingRequest,
|
||||
},
|
||||
"cline.ModelsService": {
|
||||
requestHandler: handleModelsServiceRequest,
|
||||
streamingHandler: handleModelsServiceStreamingRequest,
|
||||
},
|
||||
"cline.SlashService": {
|
||||
requestHandler: handleSlashServiceRequest,
|
||||
streamingHandler: handleSlashServiceStreamingRequest,
|
||||
},
|
||||
"cline.UiService": {
|
||||
requestHandler: handleUiServiceRequest,
|
||||
streamingHandler: handleUiServiceStreamingRequest,
|
||||
},
|
||||
}
|
||||
+54
-140
@@ -23,7 +23,7 @@ import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import { ApiProvider, ModelInfo } from "@shared/api"
|
||||
import { ChatContent } from "@shared/ChatContent"
|
||||
import { ChatSettings } from "@shared/ChatSettings"
|
||||
import { ExtensionMessage, ExtensionState, Invoke, Platform } from "@shared/ExtensionMessage"
|
||||
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { McpDownloadResponse, McpMarketplaceCatalog, McpServer } from "@shared/mcp"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
@@ -32,7 +32,12 @@ import { fileExistsAtPath } from "@utils/fs"
|
||||
import { getWorkingState } from "@utils/git"
|
||||
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
|
||||
import { getTotalTasksSize } from "@utils/storage"
|
||||
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
|
||||
import {
|
||||
ensureMcpServersDirectoryExists,
|
||||
ensureSettingsDirectoryExists,
|
||||
GlobalFileNames,
|
||||
ensureWorkflowsDirectoryExists,
|
||||
} from "../storage/disk"
|
||||
import {
|
||||
getAllExtensionState,
|
||||
getGlobalState,
|
||||
@@ -47,6 +52,8 @@ import {
|
||||
import { Task, cwd } from "../task"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { sendStateUpdate } from "./state/subscribeToState"
|
||||
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
|
||||
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
|
||||
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
|
||||
@@ -65,7 +72,7 @@ export class Controller {
|
||||
workspaceTracker: WorkspaceTracker
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
private latestAnnouncementId = "may-16-2025_16:11:00" // update to some unique identifier when we add a new announcement
|
||||
latestAnnouncementId = "may-22-2025_16:11:00" // update to some unique identifier when we add a new announcement
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
@@ -102,9 +109,7 @@ export class Controller {
|
||||
- https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
|
||||
*/
|
||||
async dispose() {
|
||||
this.outputChannel.appendLine("Disposing ClineProvider...")
|
||||
await this.clearTask()
|
||||
this.outputChannel.appendLine("Cleared task")
|
||||
while (this.disposables.length) {
|
||||
const x = this.disposables.pop()
|
||||
if (x) {
|
||||
@@ -113,7 +118,6 @@ export class Controller {
|
||||
}
|
||||
this.workspaceTracker.dispose()
|
||||
this.mcpHub.dispose()
|
||||
this.outputChannel.appendLine("Disposed all disposables")
|
||||
|
||||
console.error("Controller disposed")
|
||||
}
|
||||
@@ -135,7 +139,7 @@ export class Controller {
|
||||
await updateGlobalState(this.context, "userInfo", info)
|
||||
}
|
||||
|
||||
async initTask(task?: string, images?: string[], historyItem?: HistoryItem) {
|
||||
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
|
||||
const {
|
||||
apiConfiguration,
|
||||
@@ -182,6 +186,7 @@ export class Controller {
|
||||
customInstructions,
|
||||
task,
|
||||
images,
|
||||
files,
|
||||
historyItem,
|
||||
)
|
||||
}
|
||||
@@ -189,7 +194,7 @@ export class Controller {
|
||||
async reinitExistingTaskFromId(taskId: string) {
|
||||
const history = await this.getTaskWithId(taskId)
|
||||
if (history) {
|
||||
await this.initTask(undefined, undefined, history.historyItem)
|
||||
await this.initTask(undefined, undefined, undefined, history.historyItem)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,21 +261,7 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// If user already opted in to telemetry, enable telemetry service
|
||||
this.getStateToPostToWebview().then((state) => {
|
||||
const { telemetrySetting } = state
|
||||
const isOptedIn = telemetrySetting !== "disabled"
|
||||
telemetryService.updateTelemetryState(isOptedIn)
|
||||
})
|
||||
break
|
||||
case "showChatView": {
|
||||
this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
break
|
||||
}
|
||||
case "newTask":
|
||||
// Code that should run in response to the hello message command
|
||||
//vscode.window.showInformationMessage(message.text!)
|
||||
@@ -280,7 +271,7 @@ export class Controller {
|
||||
// Could also do this in extension .ts
|
||||
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
|
||||
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
|
||||
await this.initTask(message.text, message.images)
|
||||
await this.initTask(message.text, message.images, message.files)
|
||||
break
|
||||
case "apiConfiguration":
|
||||
if (message.apiConfiguration) {
|
||||
@@ -291,56 +282,10 @@ export class Controller {
|
||||
}
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "autoApprovalSettings":
|
||||
if (message.autoApprovalSettings) {
|
||||
const currentSettings = (await getAllExtensionState(this.context)).autoApprovalSettings
|
||||
const incomingVersion = message.autoApprovalSettings.version ?? 1
|
||||
const currentVersion = currentSettings?.version ?? 1
|
||||
if (incomingVersion > currentVersion) {
|
||||
await updateGlobalState(this.context, "autoApprovalSettings", message.autoApprovalSettings)
|
||||
if (this.task) {
|
||||
this.task.autoApprovalSettings = message.autoApprovalSettings
|
||||
}
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
}
|
||||
break
|
||||
case "togglePlanActMode":
|
||||
if (message.chatSettings) {
|
||||
await this.togglePlanActModeWithChatSettings(message.chatSettings, message.chatContent)
|
||||
}
|
||||
break
|
||||
case "optionsResponse":
|
||||
await this.postMessageToWebview({
|
||||
type: "invoke",
|
||||
invoke: "sendMessage",
|
||||
text: message.text,
|
||||
})
|
||||
break
|
||||
case "didShowAnnouncement":
|
||||
await updateGlobalState(this.context, "lastShownAnnouncementId", this.latestAnnouncementId)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "openInBrowser":
|
||||
if (message.url) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url))
|
||||
}
|
||||
break
|
||||
case "showAccountViewClicked": {
|
||||
await this.postMessageToWebview({ type: "action", action: "accountButtonClicked" })
|
||||
break
|
||||
}
|
||||
case "fetchUserCreditsData": {
|
||||
await this.fetchUserCreditsData()
|
||||
break
|
||||
}
|
||||
case "openMcpSettings": {
|
||||
const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
|
||||
if (mcpSettingsFilePath) {
|
||||
await handleFileServiceRequest(this, "openFile", { value: mcpSettingsFilePath })
|
||||
}
|
||||
break
|
||||
}
|
||||
case "fetchMcpMarketplace": {
|
||||
await this.fetchMcpMarketplace(message.bool)
|
||||
break
|
||||
@@ -381,41 +326,27 @@ export class Controller {
|
||||
// break
|
||||
// }
|
||||
case "toggleWorkflow": {
|
||||
const { workflowPath, enabled } = message
|
||||
if (workflowPath && typeof enabled === "boolean") {
|
||||
const toggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
toggles[workflowPath] = enabled
|
||||
await updateWorkspaceState(this.context, "workflowToggles", toggles)
|
||||
await this.postStateToWebview()
|
||||
const { workflowPath, enabled, isGlobal } = message
|
||||
if (workflowPath && typeof enabled === "boolean" && typeof isGlobal === "boolean") {
|
||||
if (isGlobal) {
|
||||
const globalWorkflowToggles =
|
||||
((await getGlobalState(this.context, "globalWorkflowToggles")) as ClineRulesToggles) || {}
|
||||
globalWorkflowToggles[workflowPath] = enabled
|
||||
await updateGlobalState(this.context, "globalWorkflowToggles", globalWorkflowToggles)
|
||||
await this.postStateToWebview()
|
||||
} else {
|
||||
const toggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
toggles[workflowPath] = enabled
|
||||
await updateWorkspaceState(this.context, "workflowToggles", toggles)
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case "requestTotalTasksSize": {
|
||||
this.refreshTotalTasksSize()
|
||||
break
|
||||
}
|
||||
|
||||
case "fetchLatestMcpServersFromHub": {
|
||||
this.mcpHub?.sendLatestMcpServers()
|
||||
break
|
||||
}
|
||||
case "openExtensionSettings": {
|
||||
const settingsFilter = message.text || ""
|
||||
await vscode.commands.executeCommand(
|
||||
"workbench.action.openSettings",
|
||||
`@ext:saoudrizwan.claude-dev ${settingsFilter}`.trim(), // trim whitespace if no settings filter
|
||||
)
|
||||
break
|
||||
}
|
||||
case "invoke": {
|
||||
if (message.text) {
|
||||
await this.postMessageToWebview({
|
||||
type: "invoke",
|
||||
invoke: message.text as Invoke,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
// telemetry
|
||||
case "telemetrySetting": {
|
||||
if (message.telemetrySetting) {
|
||||
@@ -478,11 +409,9 @@ export class Controller {
|
||||
if (answer === "Delete All Except Favorites") {
|
||||
await this.deleteNonFavoriteTaskHistory()
|
||||
await this.postStateToWebview()
|
||||
this.refreshTotalTasksSize()
|
||||
} else if (answer === "Delete Everything") {
|
||||
await this.deleteAllTaskHistory()
|
||||
await this.postStateToWebview()
|
||||
this.refreshTotalTasksSize()
|
||||
}
|
||||
this.postMessageToWebview({ type: "relinquishControl" })
|
||||
break
|
||||
@@ -499,6 +428,13 @@ export class Controller {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "executeQuickWin":
|
||||
if (message.payload) {
|
||||
const { command, title } = message.payload
|
||||
this.outputChannel.appendLine(`Received executeQuickWin: command='${command}', title='${title}'`)
|
||||
await this.initTask(title)
|
||||
}
|
||||
break
|
||||
|
||||
// Add more switch case statements here as more webview message commands
|
||||
// are created within the webview context (i.e. inside media/main.js)
|
||||
@@ -665,12 +601,12 @@ export class Controller {
|
||||
if (this.task.isAwaitingPlanResponse && didSwitchToActMode) {
|
||||
this.task.didRespondToPlanAskBySwitchingMode = true
|
||||
// Use chatContent if provided, otherwise use default message
|
||||
await this.postMessageToWebview({
|
||||
type: "invoke",
|
||||
invoke: "sendMessage",
|
||||
text: chatContent?.message || "PLAN_MODE_TOGGLE_RESPONSE",
|
||||
images: chatContent?.images,
|
||||
})
|
||||
await this.task.handleWebviewAskResponse(
|
||||
"messageResponse",
|
||||
chatContent?.message || "PLAN_MODE_TOGGLE_RESPONSE",
|
||||
chatContent?.images || [],
|
||||
chatContent?.files || [],
|
||||
)
|
||||
} else {
|
||||
this.cancelTask()
|
||||
}
|
||||
@@ -701,7 +637,7 @@ export class Controller {
|
||||
// 'abandoned' will prevent this cline instance from affecting future cline instance gui. this may happen if its hanging on a streaming request
|
||||
this.task.abandoned = true
|
||||
}
|
||||
await this.initTask(undefined, undefined, historyItem) // clears task again, so we need to abortTask manually above
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -745,10 +681,7 @@ export class Controller {
|
||||
await storeSecret(this.context, "clineApiKey", apiKey)
|
||||
|
||||
// Send custom token to webview for Firebase auth
|
||||
await this.postMessageToWebview({
|
||||
type: "authCallback",
|
||||
customToken,
|
||||
})
|
||||
await sendAuthCallbackEvent(customToken)
|
||||
|
||||
const clineProvider: ApiProvider = "cline"
|
||||
await updateGlobalState(this.context, "apiProvider", clineProvider)
|
||||
@@ -820,6 +753,7 @@ export class Controller {
|
||||
const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "cline-vscode-extension",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -981,10 +915,7 @@ export class Controller {
|
||||
input += `\nProblems:\n${problemsString}`
|
||||
}
|
||||
|
||||
await this.postMessageToWebview({
|
||||
type: "addToInput",
|
||||
text: input,
|
||||
})
|
||||
await sendAddToInputEvent(input)
|
||||
|
||||
console.log("addSelectedCodeToChat", code, filePath, languageId)
|
||||
}
|
||||
@@ -1002,10 +933,7 @@ export class Controller {
|
||||
// terminalName
|
||||
// })
|
||||
|
||||
await this.postMessageToWebview({
|
||||
type: "addToInput",
|
||||
text: `Terminal output:\n\`\`\`\n${output}\n\`\`\``,
|
||||
})
|
||||
await sendAddToInputEvent(`Terminal output:\n\`\`\`\n${output}\n\`\`\``)
|
||||
|
||||
console.log("addSelectedTerminalOutputToChat", output, terminalName)
|
||||
}
|
||||
@@ -1094,7 +1022,7 @@ export class Controller {
|
||||
if (id !== this.task?.taskId) {
|
||||
// non-current task
|
||||
const { historyItem } = await this.getTaskWithId(id)
|
||||
await this.initTask(undefined, undefined, historyItem) // clears existing task
|
||||
await this.initTask(undefined, undefined, undefined, historyItem) // clears existing task
|
||||
}
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
@@ -1166,19 +1094,6 @@ export class Controller {
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
async refreshTotalTasksSize() {
|
||||
getTotalTasksSize(this.context.globalStorageUri.fsPath)
|
||||
.then((newTotalSize) => {
|
||||
this.postMessageToWebview({
|
||||
type: "totalTasksSize",
|
||||
totalTasksSize: newTotalSize,
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error calculating total tasks size:", error)
|
||||
})
|
||||
}
|
||||
|
||||
async deleteTaskWithId(id: string) {
|
||||
console.info("deleteTaskWithId: ", id)
|
||||
|
||||
@@ -1221,7 +1136,7 @@ export class Controller {
|
||||
console.debug(`Error deleting task:`, error)
|
||||
}
|
||||
|
||||
this.refreshTotalTasksSize()
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
async deleteTaskFromState(id: string) {
|
||||
@@ -1238,10 +1153,7 @@ export class Controller {
|
||||
|
||||
async postStateToWebview() {
|
||||
const state = await this.getStateToPostToWebview()
|
||||
// For testing: Bypass gRPC stream and send state directly
|
||||
console.log("[Controller Test Revert] Posting full state via direct 'state' message.")
|
||||
await this.postMessageToWebview({ type: "state", state: state })
|
||||
// await sendStateUpdate(state) // Original line for the GrPC stream
|
||||
await sendStateUpdate(state)
|
||||
}
|
||||
|
||||
async getStateToPostToWebview(): Promise<ExtensionState> {
|
||||
@@ -1259,6 +1171,7 @@ export class Controller {
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting,
|
||||
globalClineRulesToggles,
|
||||
globalWorkflowToggles,
|
||||
shellIntegrationTimeout,
|
||||
isNewUser,
|
||||
} = await getAllExtensionState(this.context)
|
||||
@@ -1272,7 +1185,7 @@ export class Controller {
|
||||
const localCursorRulesToggles =
|
||||
((await getWorkspaceState(this.context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
const workflowToggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
const localWorkflowToggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
return {
|
||||
version: this.context.extension?.packageJSON?.version ?? "",
|
||||
@@ -1296,12 +1209,13 @@ export class Controller {
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
|
||||
vscMachineId: vscode.env.machineId,
|
||||
distinctId: telemetryService.distinctId,
|
||||
globalClineRulesToggles: globalClineRulesToggles || {},
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
|
||||
localCursorRulesToggles: localCursorRulesToggles || {},
|
||||
workflowToggles: workflowToggles || {},
|
||||
localWorkflowToggles: localWorkflowToggles || {},
|
||||
globalWorkflowToggles: globalWorkflowToggles || {},
|
||||
shellIntegrationTimeout,
|
||||
isNewUser,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { convertMcpServersToProtoMcpServers } from "@/shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import type { AddRemoteMcpServerRequest, McpServers } from "../../../shared/proto/mcp"
|
||||
import type { AddRemoteMcpServerRequest } from "../../../shared/proto/mcp"
|
||||
import { McpServers } from "../../../shared/proto/mcp"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
@@ -23,7 +24,7 @@ export async function addRemoteMcpServer(controller: Controller, request: AddRem
|
||||
|
||||
const protoServers = convertMcpServersToProtoMcpServers(servers)
|
||||
|
||||
return { mcpServers: protoServers }
|
||||
return McpServers.create({ mcpServers: protoServers })
|
||||
} catch (error) {
|
||||
console.error(`Failed to add remote MCP server ${request.serverName}:`, error)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Controller } from "../index"
|
||||
import type { McpServers } from "../../../shared/proto/mcp"
|
||||
import { McpServers } from "../../../shared/proto/mcp"
|
||||
import { convertMcpServersToProtoMcpServers } from "../../../shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { StringRequest } from "@/shared/proto/common"
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function deleteMcpServer(controller: Controller, request: StringReq
|
||||
// Convert application types to protobuf types
|
||||
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
|
||||
|
||||
return { mcpServers: protoServers }
|
||||
return McpServers.create({ mcpServers: protoServers })
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete MCP server: ${error}`)
|
||||
throw error
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
|
||||
import { StreamingResponseHandler } from "../grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create mcp service registry
|
||||
const mcpService = createServiceRegistry("mcp")
|
||||
|
||||
// Export the method handler types and registration function
|
||||
export type McpMethodHandler = ServiceMethodHandler
|
||||
export type McpStreamingMethodHandler = StreamingMethodHandler
|
||||
export const registerMethod = mcpService.registerMethod
|
||||
|
||||
// Export the request handlers
|
||||
export const handleMcpServiceRequest = mcpService.handleRequest
|
||||
export const handleMcpServiceStreamingRequest = mcpService.handleStreamingRequest
|
||||
export const isStreamingMethod = mcpService.isStreamingMethod
|
||||
|
||||
// Register all mcp methods
|
||||
registerAllMethods()
|
||||
@@ -1,26 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { addRemoteMcpServer } from "./addRemoteMcpServer"
|
||||
import { deleteMcpServer } from "./deleteMcpServer"
|
||||
import { downloadMcp } from "./downloadMcp"
|
||||
import { refreshMcpMarketplace } from "./refreshMcpMarketplace"
|
||||
import { restartMcpServer } from "./restartMcpServer"
|
||||
import { toggleMcpServer } from "./toggleMcpServer"
|
||||
import { toggleToolAutoApprove } from "./toggleToolAutoApprove"
|
||||
import { updateMcpTimeout } from "./updateMcpTimeout"
|
||||
|
||||
// Register all mcp service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
|
||||
registerMethod("deleteMcpServer", deleteMcpServer)
|
||||
registerMethod("downloadMcp", downloadMcp)
|
||||
registerMethod("refreshMcpMarketplace", refreshMcpMarketplace)
|
||||
registerMethod("restartMcpServer", restartMcpServer)
|
||||
registerMethod("toggleMcpServer", toggleMcpServer)
|
||||
registerMethod("toggleToolAutoApprove", toggleToolAutoApprove)
|
||||
registerMethod("updateMcpTimeout", updateMcpTimeout)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty, EmptyRequest } from "@shared/proto/common"
|
||||
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
|
||||
|
||||
/**
|
||||
* Opens the MCP settings file in the editor
|
||||
* @param controller The controller instance
|
||||
* @param _request Empty request
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function openMcpSettings(controller: Controller, _request: EmptyRequest): Promise<Empty> {
|
||||
const mcpSettingsFilePath = await controller.mcpHub?.getMcpSettingsFilePath()
|
||||
if (mcpSettingsFilePath) {
|
||||
await openFileIntegration(mcpSettingsFilePath)
|
||||
}
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { EmptyRequest } from "../../../shared/proto/common"
|
||||
import type { McpMarketplaceCatalog } from "../../../shared/proto/mcp"
|
||||
import { McpMarketplaceCatalog } from "../../../shared/proto/mcp"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
@@ -19,9 +19,9 @@ export async function refreshMcpMarketplace(controller: Controller, _request: Em
|
||||
}
|
||||
|
||||
// Return empty catalog if nothing was fetched
|
||||
return { items: [] }
|
||||
return McpMarketplaceCatalog.create({ items: [] })
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh MCP marketplace:", error)
|
||||
return { items: [] }
|
||||
return McpMarketplaceCatalog.create({ items: [] })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { McpServers } from "@shared/proto/mcp"
|
||||
import { McpServers } from "@shared/proto/mcp"
|
||||
import type { Controller } from "../index"
|
||||
import { convertMcpServersToProtoMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { StringRequest } from "@/shared/proto/common"
|
||||
@@ -16,7 +16,7 @@ export async function restartMcpServer(controller: Controller, request: StringRe
|
||||
// Convert from McpServer[] to ProtoMcpServer[] ensuring all required fields are set
|
||||
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
|
||||
|
||||
return { mcpServers: protoServers }
|
||||
return McpServers.create({ mcpServers: protoServers })
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart MCP server ${request.value}:`, error)
|
||||
throw error
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ToggleMcpServerRequest, McpServers } from "../../../shared/proto/mcp"
|
||||
import type { ToggleMcpServerRequest } from "../../../shared/proto/mcp"
|
||||
import { McpServers } from "../../../shared/proto/mcp"
|
||||
import type { Controller } from "../index"
|
||||
import { convertMcpServersToProtoMcpServers } from "../../../shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
|
||||
@@ -15,7 +16,7 @@ export async function toggleMcpServer(controller: Controller, request: ToggleMcp
|
||||
// Convert from McpServer[] to ProtoMcpServer[] ensuring all required fields are set
|
||||
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
|
||||
|
||||
return { mcpServers: protoServers }
|
||||
return McpServers.create({ mcpServers: protoServers })
|
||||
} catch (error) {
|
||||
console.error(`Failed to toggle MCP server ${request.serverName}:`, error)
|
||||
throw error
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ToggleToolAutoApproveRequest, McpServers } from "@shared/proto/mcp"
|
||||
import type { ToggleToolAutoApproveRequest } from "@shared/proto/mcp"
|
||||
import { McpServers } from "@shared/proto/mcp"
|
||||
import type { Controller } from "../index"
|
||||
import { convertMcpServersToProtoMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
|
||||
@@ -15,7 +16,7 @@ export async function toggleToolAutoApprove(controller: Controller, request: Tog
|
||||
(await controller.mcpHub?.toggleToolAutoApproveRPC(request.serverName, request.toolNames, request.autoApprove)) || []
|
||||
|
||||
// Convert application types to proto types
|
||||
return { mcpServers: convertMcpServersToProtoMcpServers(mcpServers) }
|
||||
return McpServers.create({ mcpServers: convertMcpServersToProtoMcpServers(mcpServers) })
|
||||
} catch (error) {
|
||||
console.error(`Failed to toggle tool auto-approve for ${request.serverName}:`, error)
|
||||
throw error
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user