mirror of
https://github.com/cline/cline.git
synced 2026-09-05 05:02:27 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35a97fd2a1 | |||
| 7250f6e592 | |||
| c8b2e88649 | |||
| 4c9027cf5e | |||
| f2dcd9f55e |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate settingsButtonClicked to protobus
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
update chat box ui
|
||||
+2
-4
@@ -5,7 +5,7 @@
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "eslint-rules"],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"warn",
|
||||
@@ -19,9 +19,7 @@
|
||||
"eqeqeq": "warn",
|
||||
"no-throw-literal": "warn",
|
||||
"semi": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"eslint-rules/no-protobuf-object-literals": "error",
|
||||
"eslint-rules/no-grpc-client-object-literals": "error"
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
},
|
||||
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# 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 }}
|
||||
@@ -1,32 +0,0 @@
|
||||
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
|
||||
@@ -86,9 +86,8 @@ jobs:
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
|
||||
# Unit Tests disabled due to module system conflicts between backend and webview-ui
|
||||
# - name: Unit Tests
|
||||
# run: npm run test:unit
|
||||
- name: Unit Tests
|
||||
run: npm run test:unit
|
||||
|
||||
# Run extension tests with coverage
|
||||
- name: Extension Tests with Coverage
|
||||
|
||||
+1
-10
@@ -22,18 +22,9 @@ 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
|
||||
# Shared
|
||||
src/shared/proto/*.ts
|
||||
src/shared/proto/host/*.ts
|
||||
# Webview
|
||||
webview-ui/src/services/grpc-client.ts
|
||||
# Standalone
|
||||
src/standalone/server-setup.ts
|
||||
src/standalone/services/host-grpc-client.ts
|
||||
# Host bridge
|
||||
hosts/vscode/*/methods.ts
|
||||
hosts/vscode/*/index.ts
|
||||
hosts/vscode/host-grpc-service-config.ts
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extension": ["ts"],
|
||||
"spec": ["src/**/__tests__/*.ts", "eslint-rules/__tests__/**/*.test.ts"],
|
||||
"spec": "src/**/__tests__/*.ts",
|
||||
"require": ["ts-node/register", "source-map-support/register", "./src/test/requires.ts"],
|
||||
"recursive": true
|
||||
}
|
||||
|
||||
@@ -3,5 +3,3 @@ node_modules
|
||||
webview-ui/build/
|
||||
*.md
|
||||
package-lock.json
|
||||
src/core/prompts/system.ts
|
||||
src/core/prompts/model_prompts/claude4.ts
|
||||
|
||||
@@ -1,41 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [3.17.10]
|
||||
|
||||
- Add support for Qwen 3 series models with thinking mode options (Thanks @Jonny-china!)
|
||||
- Add new AskSage models: Claude 4 Sonnet, Claude 4 Opus, GPT 4.1, Gemini 2.5 Pro (Thanks @swhite24!)
|
||||
- Add VSCode walkthrough to help new users get started with Cline
|
||||
- Add support for streamable MCP servers
|
||||
- Improve Ollama model selection with filterable dropdown instead of radio buttons (Thanks @paulgear!)
|
||||
- Add setting to disable aggressive terminal reuse to help users experiencing task lockout issues
|
||||
- Fix settings dialog applying changes even when cancel button is clicked
|
||||
|
||||
## [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!)
|
||||
|
||||
+9
-23
@@ -34,20 +34,18 @@ If you're planning to work on a bigger feature, please create a [feature request
|
||||
3. **Linux-specific Setup**
|
||||
VS Code extension tests on Linux require the following system libraries:
|
||||
|
||||
- `dbus`
|
||||
- `libasound2`
|
||||
- `libatk-bridge2.0-0`
|
||||
- `libatk1.0-0`
|
||||
- `libdrm2`
|
||||
- `libgbm1`
|
||||
- `libgtk-3-0`
|
||||
- `libnss3`
|
||||
- `libatk-bridge2.0-0`
|
||||
- `libxkbfile1`
|
||||
- `libx11-xcb1`
|
||||
- `libxcomposite1`
|
||||
- `libxdamage1`
|
||||
- `libxfixes3`
|
||||
- `libxkbfile1`
|
||||
- `libxrandr2`
|
||||
- `libgbm1`
|
||||
- `libdrm2`
|
||||
- `libgtk-3-0`
|
||||
- `dbus`
|
||||
- `xvfb`
|
||||
|
||||
These libraries provide necessary GUI components and system services for the test environment.
|
||||
@@ -56,21 +54,9 @@ If you're planning to work on a bigger feature, please create a [feature request
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
dbus \
|
||||
libasound2 \
|
||||
libatk-bridge2.0-0 \
|
||||
libatk1.0-0 \
|
||||
libdrm2 \
|
||||
libgbm1 \
|
||||
libgtk-3-0 \
|
||||
libnss3 \
|
||||
libx11-xcb1 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxfixes3 \
|
||||
libxkbfile1 \
|
||||
libxrandr2 \
|
||||
xvfb
|
||||
libatk1.0-0 libatk-bridge2.0-0 libxkbfile1 libx11-xcb1 \
|
||||
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 \
|
||||
libdrm2 libgtk-3-0 dbus xvfb
|
||||
```
|
||||
|
||||
- Run `npm run test:ci` to run tests locally
|
||||
|
||||
+6
-35
@@ -25,41 +25,12 @@ description: "Learn how to set up AWS Bedrock with Cline using credentials authe
|
||||
|
||||
#### 1.2 Attach the Required Policies
|
||||
|
||||
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockFullAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
|
||||
|
||||
- `bedrock:InvokeModel`
|
||||
- `bedrock:InvokeModelWithResponseStream`
|
||||
|
||||
You can create a custom IAM policy with these permissions and attach it to your IAM user or role.
|
||||
|
||||
**Option 1: Minimal Permissions (Recommended for Production & Least Privilege)**
|
||||
|
||||
1. In the AWS IAM console, create a new policy.
|
||||
2. Use the JSON editor to add the following policy document:
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
|
||||
"Resource": "*" // For enhanced security, scope this to specific model ARNs if possible.
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
3. Name the policy (e.g., `ClineBedrockInvokeAccess`) and attach it to your IAM user or role.
|
||||
|
||||
**Option 2: Using a Managed Policy (Simpler Initial Setup)**
|
||||
|
||||
- Alternatively, you can attach the AWS managed policy **`AmazonBedrockFullAccess`**. This grants broader permissions, including the ability to list models, manage provisioning, and other Bedrock features. This might be simpler for initial setup or if you require these wider capabilities.
|
||||
[View AmazonBedrockFullAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
|
||||
|
||||
**Important Considerations:**
|
||||
|
||||
- **Model Listing in Cline:** The minimal permissions (`bedrock:InvokeModel`, `bedrock:InvokeModelWithResponseStream`) are sufficient for Cline to _use_ a model if you specify the model ID directly in Cline's settings. If you rely on Cline to dynamically list available Bedrock models, you might need additional permissions like `bedrock:ListFoundationModels`.
|
||||
- **AWS Marketplace Subscriptions:** For third-party models (e.g., Anthropic Claude), ensure you have active AWS Marketplace subscriptions. This is typically managed in the AWS Bedrock console under "Model access" and might require `aws-marketplace:Subscribe` permissions if not already handled.
|
||||
- _Enterprise Tip:_ Always apply least-privilege practices. Where possible, scope resource ARNs in your IAM policies to specific models or regions. Utilize [Service Control Policies (SCPs)](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html) for overarching governance in AWS Organizations.
|
||||
1. **Attach the Managed Policy:**
|
||||
- Attach the **`AmazonBedrockFullAccess`** managed policy to your user/role.\
|
||||
[View AmazonBedrockFullAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
|
||||
2. **Confirm Additional Permissions:**
|
||||
- Ensure your policy includes permissions for model invocation (e.g., `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream`), model listing, and AWS Marketplace actions (like `aws-marketplace:Subscribe`).
|
||||
- _Enterprise Tip:_ Apply least-privilege practices by scoping resource ARNs and using [Service Control Policies (SCPs)](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html) to restrict access where necessary.
|
||||
|
||||
---
|
||||
|
||||
+8
-15
@@ -15,24 +15,17 @@ Cline offers _experimental_ support for the [VS Code Language Model API](https:/
|
||||
- **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
|
||||
- **Alternative Extensions:** Explore the VS Code Marketplace for extensions mentioning "Language Model API" or "lm". Other experimental options may be available.
|
||||
|
||||
### Configuration Steps
|
||||
|
||||
1. **Ensure Copilot Account is Active and Extensions are installed:** User logged into either the Copilot or Copilot Chat extension should be able to gain access via Cline.
|
||||
2. **Access Cline Settings:** Click the gear icon (⚙️) located in the Cline panel.
|
||||
3. **Choose Provider:** Select "VS Code LM API" from the "API Provider" dropdown menu.
|
||||
4. **Select Model:** If the Copilot extension(s) are installed and the user is logged into their Copilot account, the "Language Model" dropdown will populate with available models after a short time. The naming convention is `vendor/family`. For instance, if Copilot is active, you might encounter options such as:
|
||||
- `copilot - gpt-3.5-turbo`
|
||||
- `copilot - gpt-4o-mini`
|
||||
- `copilot - gpt-4`
|
||||
- `copilot - gpt-4-turbo`
|
||||
- `copilot - gpt-4o`
|
||||
- `copilot - claude-3.5-sonnet` **NOTE:** this model does not work.
|
||||
- `copilot - gemini-2.0-flash`
|
||||
- `copilot - gpt-4.1`
|
||||
|
||||
For best results with the VSCode LM API Provider, we suggest using the OpenAI Models (GPT 3, 4, 4.1, 4o etc.)
|
||||
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
|
||||
|
||||
+6
-15
@@ -143,22 +143,13 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Provider Configuration",
|
||||
"group": "Custom Model Configurations",
|
||||
"pages": [
|
||||
"provider-config/anthropic",
|
||||
"provider-config/aws-bedrock-with-credentials-authentication",
|
||||
"provider-config/aws-bedrock-with-profile-authentication",
|
||||
"provider-config/gcp-vertex-ai",
|
||||
"provider-config/litellm-and-cline-using-codestral",
|
||||
"provider-config/vscode-language-model-api",
|
||||
"provider-config/xai-grok",
|
||||
"provider-config/mistral-ai",
|
||||
"provider-config/deepseek",
|
||||
"provider-config/ollama",
|
||||
"provider-config/openai",
|
||||
"provider-config/openai-compatible",
|
||||
"provider-config/openrouter",
|
||||
"provider-config/requesty"
|
||||
"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/vscode-language-model-api"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -14,9 +14,9 @@ Certain scenarios may warrant using local models, including handling highly sens
|
||||
|
||||
#### [IAM Security Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) (For administrators)
|
||||
|
||||
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/provider-config/aws-bedrock-with-credentials-authentication)
|
||||
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/custom-model-configs/aws-bedrock-with-credentials-authentication.mdx)
|
||||
|
||||
#### [AWS Bedrock setup for SSO token (AWS Profile)](/provider-config/aws-bedrock-with-profile-authentication)
|
||||
#### [AWS Bedrock setup for SSO token (AWS Profile)](/custom-model-configs/aws-bedrock-with-profile-authentication.mdx)
|
||||
|
||||
#### VPC Endpoint Setup
|
||||
|
||||
|
||||
@@ -6,13 +6,10 @@ sidebarTitle: "Plan & Act"
|
||||
Plan & Act modes represent Cline's approach to structured AI development, emphasizing thoughtful planning before implementation. This dual-mode system helps developers create more maintainable, accurate code while reducing iteration time.
|
||||
|
||||
<Frame>
|
||||
<iframe
|
||||
style={{ width: "100%", aspectRatio: "16/9" }}
|
||||
src="https://www.youtube.com/embed/b7o6URFPp64"
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen></iframe>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/planningThenActing%20(1).gif"
|
||||
alt="Use Plan to gather context before using Act to implement the plan"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
#### Plan Mode: Think First
|
||||
|
||||
@@ -60,6 +60,7 @@ Now that you have Cline installed, let's get you set up with your account:
|
||||
- DeepSeek Chat (cost-effective alternative)
|
||||
- Google Gemini 2.0 Flash
|
||||
- And more — all through your Cline account.
|
||||
4. -
|
||||
|
||||
### 💻 Your First Interaction with Cline
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
title: "Anthropic"
|
||||
description: "Learn how to configure and use Anthropic Claude models with Cline. Covers API key setup, model selection, and advanced features like prompt caching."
|
||||
---
|
||||
|
||||
**Website:** [https://www.anthropic.com/](https://www.anthropic.com/)
|
||||
|
||||
### Getting an API Key
|
||||
|
||||
1. **Sign Up/Sign In:** Go to the [Anthropic Console](https://console.anthropic.com/). Create an account or sign in.
|
||||
2. **Navigate to API Keys:** Go to the [API keys](https://console.anthropic.com/settings/keys) section.
|
||||
3. **Create a Key:** Click "Create Key". Give your key a descriptive name (e.g., "Cline").
|
||||
4. **Copy the Key:** **Important:** Copy the API key _immediately_. You will not be able to see it again. Store it securely.
|
||||
|
||||
### Supported Models
|
||||
|
||||
Cline supports the following Anthropic Claude models:
|
||||
|
||||
- `claude-opus-4-20250514`
|
||||
- `claude-opus-4-20250514:thinking` (Extended Thinking variant)
|
||||
- `claude-sonnet-4-20250514` (Recommended)
|
||||
- `claude-sonnet-4-20250514:thinking` (Extended Thinking variant)
|
||||
- `claude-3-7-sonnet-20250219`
|
||||
- `claude-3-7-sonnet-20250219:thinking` (Extended Thinking variant)
|
||||
- `claude-3-5-sonnet-20241022`
|
||||
- `claude-3-5-haiku-20241022`
|
||||
- `claude-3-opus-20240229`
|
||||
- `claude-3-haiku-20240307`
|
||||
|
||||
See [Anthropic's Model Documentation](https://docs.anthropic.com/en/docs/about-claude/models) for more details on each model's capabilities.
|
||||
|
||||
### Configuration in Cline
|
||||
|
||||
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
|
||||
2. **Select Provider:** Choose "Anthropic" from the "API Provider" dropdown.
|
||||
3. **Enter API Key:** Paste your Anthropic API key into the "Anthropic API Key" field.
|
||||
4. **Select Model:** Choose your desired Claude model from the "Model" dropdown.
|
||||
5. **(Optional) Custom Base URL:** If you need to use a custom base URL for the Anthropic API, check "Use custom base URL" and enter the URL. Most users won't need to adjust this setting.
|
||||
|
||||
### Extended Thinking
|
||||
|
||||
Anthropic models offer an "Extended Thinking" feature, designed to give them enhanced reasoning capabilities for complex tasks. This feature allows the model to output its step-by-step thought process before delivering a final answer, providing transparency and enabling more thorough analysis for challenging prompts.
|
||||
|
||||
When extended thinking is in Cline, the model generates `thinking` content blocks that detail its internal reasoning. These insights are then incorporated into its final response.
|
||||
Cline users can leverage this by checking the `Enable Extended Thinking` box below the model selection menu after selecting a Claude Model from any provider.
|
||||
|
||||
**Key Aspects of Extended Thinking:**
|
||||
|
||||
- **Supported Models:** This feature is available for select models, including variants of Claude Opus 4, Claude Sonnet 4, and Claude Sonnet 3.7. The specific models listed in the "Supported Models" section above with the `:thinking` suffix are pre-configured in Cline to utilize this.
|
||||
- **Summarized Thinking (Claude 4):** For Claude 4 models, the API returns a summary of the full thinking process to balance insight with efficiency and prevent misuse. You are billed for the full thinking tokens, not just the summary.
|
||||
- **Streaming:** Extended thinking responses, including the `thinking` blocks, can be streamed.
|
||||
- **Tool Use & Prompt Caching:** Extended thinking interacts with tool use (requiring thinking blocks to be passed back) and prompt caching (with specific behaviors around cache invalidation and context).
|
||||
|
||||
For comprehensive details on how extended thinking works, including API examples, interaction with tool use, prompt caching, and pricing, please refer to the [official Anthropic documentation on Extended Thinking](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking).
|
||||
|
||||
### Tips and Notes
|
||||
|
||||
- **Prompt Caching:** Claude 3 models support [prompt caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching), which can significantly reduce costs and latency for repeated prompts.
|
||||
- **Context Window:** Claude models have large context windows (200,000 tokens), allowing you to include a significant amount of code and context in your prompts.
|
||||
- **Pricing:** Refer to the [Anthropic Pricing](https://www.anthropic.com/pricing) page for the latest pricing information.
|
||||
- **Rate Limits:** Anthropic has strict rate limits based on [usage tiers](https://docs.anthropic.com/en/api/rate-limits#requirements-to-advance-tier). If you're repeatedly hitting rate limits, consider contacting Anthropic sales or accessing Claude through a different provider like [OpenRouter](/provider-config/openrouter) or [Requesty](/provider-config/requesty).
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
title: "DeepSeek"
|
||||
description: "Learn how to configure and use DeepSeek models like deepseek-chat and deepseek-reasoner with Cline."
|
||||
---
|
||||
|
||||
Cline supports accessing models through the DeepSeek API, including `deepseek-chat` and `deepseek-reasoner`.
|
||||
|
||||
**Website:** [https://platform.deepseek.com/](https://platform.deepseek.com/)
|
||||
|
||||
### Getting an API Key
|
||||
|
||||
1. **Sign Up/Sign In:** Go to the [DeepSeek Platform](https://platform.deepseek.com/). Create an account or sign in.
|
||||
2. **Navigate to API Keys:** Find your API keys in the [API keys](https://platform.deepseek.com/api_keys) section of the platform.
|
||||
3. **Create a Key:** Click "Create new API key". Give your key a descriptive name (e.g., "Cline").
|
||||
4. **Copy the Key:** **Important:** Copy the API key _immediately_. You will not be able to see it again. Store it securely.
|
||||
|
||||
### Supported Models
|
||||
|
||||
Cline supports the following DeepSeek models:
|
||||
|
||||
- `deepseek-v3-0324` (Recommended for coding tasks)
|
||||
- `deepseek-r1` (Recommended for reasoning tasks)
|
||||
|
||||
### Configuration in Cline
|
||||
|
||||
1. **Open Cline Settings:** Click the ⚙️ icon in the Cline panel.
|
||||
2. **Select Provider:** Choose "DeepSeek" from the "API Provider" dropdown.
|
||||
3. **Enter API Key:** Paste your DeepSeek API key into the "DeepSeek API Key" field.
|
||||
4. **Select Model:** Choose your desired model from the "Model" dropdown.
|
||||
|
||||
### Tips and Notes
|
||||
|
||||
- **Pricing:** Refer to the [DeepSeek Pricing](https://api-docs.deepseek.com/quick_start/pricing/) page for details on model costs.
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
title: "Mistral"
|
||||
description: "Learn how to configure and use Mistral AI models, including Codestral, with Cline. Covers API key setup and model selection."
|
||||
---
|
||||
|
||||
Cline supports accessing models through the Mistral AI API, including both standard Mistral models and the code-specialized Codestral model.
|
||||
|
||||
**Website:** [https://mistral.ai/](https://mistral.ai/)
|
||||
|
||||
### Getting an API Key
|
||||
|
||||
1. **Sign Up/Sign In:** Go to the [Mistral Platform](https://console.mistral.ai/). Create an account or sign in. You may need to go through a verification process.
|
||||
2. **Create an API Key:**
|
||||
- [La Plateforme API Key](https://console.mistral.ai/api-keys/) and/or
|
||||
- [Codestral API Key](https://console.mistral.ai/codestral)
|
||||
|
||||
### Supported Models
|
||||
|
||||
Cline supports the following Mistral models:
|
||||
|
||||
- pixtral-large-2411
|
||||
- ministral-3b-2410
|
||||
- ministral-8b-2410
|
||||
- mistral-small-latest
|
||||
- mistral-medium-latest
|
||||
- mistral-small-2501
|
||||
- pixtral-12b-2409
|
||||
- open-mistral-nemo-2407
|
||||
- open-codestral-mamba
|
||||
- codestral-2501
|
||||
- devstral-small-2505
|
||||
|
||||
**Note:** Model availability and specifications may change.
|
||||
Refer to the [Mistral AI documentation](https://docs.mistral.ai/api/) and [Mistral Model Overview](https://docs.mistral.ai/getting-started/models/models_overview/) for the most current information.
|
||||
|
||||
### Configuration in Cline
|
||||
|
||||
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
|
||||
2. **Select Provider:** Choose "Mistral" from the "API Provider" dropdown.
|
||||
3. **Enter API Key:** Paste your Mistral API key into the "Mistral API Key" field if you're using a standard `mistral` model. If you intend to use `codestral-latest`, see the "Using Codestral" section below.
|
||||
4. **Select Model:** Choose your desired model from the "Model" dropdown.
|
||||
|
||||
### Using Codestral
|
||||
|
||||
[Codestral](https://docs.mistral.ai/capabilities/code_generation/) is a model specifically designed for code generation and interaction.
|
||||
For Codestral, you can use different endpoints (Default: codestral.mistral.ai).
|
||||
If using the La Plateforme API Key for Codestral, change the **Codestral Base Url** to: `https://api.mistral.ai`
|
||||
|
||||
To use Codestral with Cline:
|
||||
|
||||
1. **Select "Mistral" as the API Provider in Cline Settings.**
|
||||
2. **Select a Codestral Model** (e.g., `codestral-latest`) from the "Model" dropdown.
|
||||
3. **Enter your Codestral API Key** (from `codestral.mistral.ai`) or your La Plateforme API Key (from `api.mistral.ai`) into the appropriate API key field in Cline.
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
title: "Ollama"
|
||||
---
|
||||
|
||||
Cline supports running models locally using Ollama. This approach offers privacy, offline access, and potentially reduced costs. It requires some initial setup and a sufficiently powerful computer. Because of the present state of consumer hardware, it's not recommended to use Ollama with Cline as performance will likely be poor for average hardware configurations.
|
||||
|
||||
**Website:** [https://ollama.com/](https://ollama.com/)
|
||||
|
||||
### Setting up Ollama
|
||||
|
||||
1. **Download and Install Ollama:**
|
||||
Obtain the Ollama installer for your operating system from the [Ollama website](https://ollama.com/) and follow their installation guide. Ensure Ollama is running. You can typically start it with:
|
||||
|
||||
```bash
|
||||
ollama serve
|
||||
```
|
||||
|
||||
2. **Download a Model:**
|
||||
Ollama supports a wide variety of models. A list of available models can be found on the [Ollama model library](https://ollama.com/library). Some models recommended for coding tasks include:
|
||||
|
||||
- `codellama:7b-code` (a good, smaller starting point)
|
||||
- `codellama:13b-code` (offers better quality, larger size)
|
||||
- `codellama:34b-code` (provides even higher quality, very large)
|
||||
- `qwen2.5-coder:32b`
|
||||
- `mistralai/Mistral-7B-Instruct-v0.1` (a solid general-purpose model)
|
||||
- `deepseek-coder:6.7b-base` (effective for coding)
|
||||
- `llama3:8b-instruct-q5_1` (suitable for general tasks)
|
||||
|
||||
To download a model, open your terminal and execute:
|
||||
|
||||
```bash
|
||||
ollama pull <model_name>
|
||||
```
|
||||
|
||||
For instance:
|
||||
|
||||
```bash
|
||||
ollama pull qwen2.5-coder:32b
|
||||
```
|
||||
|
||||
3. **Configure the Model's Context Window:**
|
||||
By default, Ollama models often use a context window of 2048 tokens, which can be insufficient for many Cline requests. A minimum of 12,000 tokens is advisable for decent results, with 32,000 tokens being ideal. To adjust this, you'll modify the model's parameters and save it as a new version.
|
||||
|
||||
First, load the model (using `qwen2.5-coder:32b` as an example):
|
||||
|
||||
```bash
|
||||
ollama run qwen2.5-coder:32b
|
||||
```
|
||||
|
||||
Once the model is loaded within the Ollama interactive session, set the context size parameter:
|
||||
|
||||
```
|
||||
/set parameter num_ctx 32768
|
||||
```
|
||||
|
||||
Then, save this configured model with a new name:
|
||||
|
||||
```
|
||||
/save your_custom_model_name
|
||||
```
|
||||
|
||||
(Replace `your_custom_model_name` with a name of your choice.)
|
||||
|
||||
4. **Configure Cline:**
|
||||
- Open the Cline sidebar (usually indicated by the Cline icon).
|
||||
- Click the settings gear icon (⚙️).
|
||||
- Select "ollama" as the API Provider.
|
||||
- Enter the Model name you saved in the previous step (e.g., `your_custom_model_name`).
|
||||
- (Optional) Adjust the base URL if Ollama is running on a different machine or port. The default is `http://localhost:11434`.
|
||||
- (Optional) Configure the Model context size in Cline's Advanced settings. This helps Cline manage its context window effectively with your customized Ollama model.
|
||||
|
||||
### Tips and Notes
|
||||
|
||||
- **Resource Demands:** Running large language models locally can be demanding on system resources. Ensure your computer meets the requirements for your chosen model.
|
||||
- **Model Choice:** Experiment with various models to discover which best fits your specific tasks and preferences.
|
||||
- **Offline Capability:** After downloading a model, you can use Cline with that model even without an internet connection.
|
||||
- **Token Usage Tracking:** Cline tracks token usage for models accessed via Ollama, allowing you to monitor consumption.
|
||||
- **Ollama's Own Documentation:** For more detailed information, consult the official [Ollama documentation](https://ollama.com/docs).
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
title: "OpenAI Compatible"
|
||||
description: "Learn how to configure Cline with various AI model providers that offer OpenAI-compatible APIs."
|
||||
---
|
||||
|
||||
Cline supports a wide range of AI model providers that offer APIs compatible with the OpenAI API standard. This allows you to use models from providers _other than_ OpenAI, while still utilizing a familiar API interface. This includes providers such as:
|
||||
|
||||
- **Local models** running through tools like Ollama and LM Studio (which are covered in their respective sections).
|
||||
- **Cloud providers** like Perplexity, Together AI, Anyscale, and many others.
|
||||
- **Any other provider** that offers an OpenAI-compatible API endpoint.
|
||||
|
||||
This document focuses on setting up providers _other than_ the official OpenAI API (which has its own [dedicated configuration page](/provider-config/openai)).
|
||||
|
||||
### General Configuration
|
||||
|
||||
The key to using an OpenAI-compatible provider with Cline is to configure these main settings:
|
||||
|
||||
1. **Base URL:** This is the API endpoint specific to the provider. It will _not_ be `https://api.openai.com/v1` (that URL is for the official OpenAI API).
|
||||
2. **API Key:** This is the secret key you obtain from your chosen provider.
|
||||
3. **Model ID:** This is the specific name or identifier for the model you wish to use.
|
||||
|
||||
You'll find these settings in the Cline settings panel (click the ⚙️ icon):
|
||||
|
||||
- **API Provider:** Select "OpenAI Compatible".
|
||||
- **Base URL:** Enter the base URL provided by your chosen provider. **This is a crucial step.**
|
||||
- **API Key:** Enter your API key from the provider.
|
||||
- **Model:** Choose or enter the model ID.
|
||||
- **Model Configuration:** This section allows you to customize advanced parameters for the model, such as:
|
||||
- Max Output Tokens
|
||||
- Context Window size
|
||||
- Image Support capabilities
|
||||
- Computer Use (e.g., for models with tool/function calling)
|
||||
- Input Price (per token/million tokens)
|
||||
- Output Price (per token/million tokens)
|
||||
|
||||
### Supported Models (for OpenAI Native Endpoint)
|
||||
|
||||
While the "OpenAI Compatible" provider type allows connecting to various endpoints, if you are connecting directly to the official OpenAI API (or an endpoint that mirrors it exactly), Cline recognizes the following model IDs based on the `openAiNativeModels` definition in its source code:
|
||||
|
||||
- `o3-mini`
|
||||
- `o3-mini-high`
|
||||
- `o3-mini-low`
|
||||
- `o1`
|
||||
- `o1-preview`
|
||||
- `o1-mini`
|
||||
- `gpt-4.5-preview`
|
||||
- `gpt-4o`
|
||||
- `gpt-4o-mini`
|
||||
|
||||
**Note:** If you are using a different OpenAI-compatible provider (such as Together AI, Anyscale, etc.), the available model IDs will differ. Always refer to your specific provider's documentation for their supported model names and any unique configuration details.
|
||||
|
||||
### v0 (Vercel SDK) in Cline:
|
||||
|
||||
- For developers working with v0, their [AI SDK documentation](https://vercel.com/docs/v0/cline) provides valuable insights and examples for integrating various models, many of which are OpenAI-compatible. This can be a helpful resource for understanding how to structure calls and manage configurations when using Cline with services deployed on or integrated with Vercel.
|
||||
|
||||
- v0 can be used in Cline with the OpenAI Compatible provider.
|
||||
|
||||
- ### Quickstart
|
||||
|
||||
- 1. With the OpenAI Compatible provider selected, set the Base URL to https://api.v0.dev/v1.
|
||||
- 2. Paste in your v0 API Key
|
||||
- 3. Set the Model ID: v0-1.0-md
|
||||
- 4. Click Verify to confirm the connection.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- **"Invalid API Key":** Double-check that you've entered the API key correctly and that it's for the correct provider.
|
||||
- **"Model Not Found":** Ensure you're using a valid model ID for your chosen provider and that it's available at the specified Base URL.
|
||||
- **Connection Errors:** Verify the Base URL is correct, that your provider's API is accessible from your machine, and that there are no firewall or network issues.
|
||||
- **Unexpected Results:** If you're getting unexpected outputs, try a different model or double-check all configuration parameters.
|
||||
|
||||
By using an OpenAI-compatible provider, you can leverage the flexibility of Cline with a wider array of AI models. Remember to always consult your provider's documentation for the most accurate and up-to-date information.
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
title: "OpenAI"
|
||||
description: "Learn how to configure and use official OpenAI models with Cline."
|
||||
---
|
||||
|
||||
Cline supports accessing models directly through the official OpenAI API.
|
||||
|
||||
**Website:** [https://openai.com/](https://openai.com/)
|
||||
|
||||
### Getting an API Key
|
||||
|
||||
1. **Sign Up/Sign In:** Visit the [OpenAI Platform](https://platform.openai.com/). You'll need to create an account or sign in if you already have one.
|
||||
2. **Navigate to API Keys:** Once logged in, go to the [API keys section](https://platform.openai.com/api-keys) of your account.
|
||||
3. **Create a Key:** Click on "Create new secret key". It's good practice to give your key a descriptive name (e.g., "Cline API Key").
|
||||
4. **Copy the Key:** **Crucial:** Copy the generated API key immediately. For security reasons, OpenAI will not show it to you again. Store this key in a safe and secure location.
|
||||
|
||||
### Supported Models
|
||||
|
||||
Cline is compatible with a variety of OpenAI models, including but not limited to:
|
||||
|
||||
- 'o3'
|
||||
- `o3-mini` (medium reasoning effort)
|
||||
- 'o4-mini'
|
||||
- `o3-mini-high` (high reasoning effort)
|
||||
- `o3-mini-low` (low reasoning effort)
|
||||
- `o1`
|
||||
- `o1-preview`
|
||||
- `o1-mini`
|
||||
- `gpt-4.5-preview`
|
||||
- `gpt-4o`
|
||||
- `gpt-4o-mini`
|
||||
- 'gpt-4.1'
|
||||
- 'gpt-4.1-mini'
|
||||
|
||||
For the most current list of available models and their capabilities, please refer to the official [OpenAI Models documentation](https://platform.openai.com/docs/models).
|
||||
|
||||
### Configuration in Cline
|
||||
|
||||
1. **Open Cline Settings:** Click the settings gear icon (⚙️) in the Cline panel.
|
||||
2. **Select Provider:** Choose "OpenAI" from the "API Provider" dropdown menu.
|
||||
3. **Enter API Key:** Paste your OpenAI API key into the "OpenAI API Key" field.
|
||||
4. **Select Model:** Choose your desired model from the "Model" dropdown list.
|
||||
5. **(Optional) Base URL:** If you need to use a proxy or a custom base URL for the OpenAI API, you can enter it here. Most users will not need to change this from the default.
|
||||
|
||||
### Tips and Notes
|
||||
|
||||
- **Pricing:** Be sure to review the [OpenAI Pricing page](https://openai.com/pricing) for detailed information on the costs associated with different models.
|
||||
- **Azure OpenAI Service:** If you are looking to use the Azure OpenAI service, please note that specific documentation for Azure OpenAI with Cline may be found separately, or you might need to configure it as an OpenAI-compatible endpoint if such functionality is supported by Cline for custom configurations.
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
title: "OpenRouter"
|
||||
description: "Learn how to use OpenRouter with Cline to access a wide variety of language models through a single API."
|
||||
---
|
||||
|
||||
OpenRouter is an AI platform that provides access to a wide variety of language models from different providers, all through a single API. This can simplify setup and allow you to easily experiment with different models.
|
||||
|
||||
**Website:** [https://openrouter.ai/](https://openrouter.ai/)
|
||||
|
||||
### Getting an API Key
|
||||
|
||||
1. **Sign Up/Sign In:** Go to the [OpenRouter website](https://openrouter.ai/). Sign in with your Google or GitHub account.
|
||||
2. **Get an API Key:** Go to the [keys page](https://openrouter.ai/keys). You should see an API key listed. If not, create a new key.
|
||||
3. **Copy the Key:** Copy the API key.
|
||||
|
||||
### Supported Models
|
||||
|
||||
OpenRouter supports a large and growing number of models. Cline automatically fetches the list of available models. Refer to the [OpenRouter Models page](https://openrouter.ai/models) for the complete and up-to-date list.
|
||||
|
||||
### Configuration in Cline
|
||||
|
||||
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
|
||||
2. **Select Provider:** Choose "OpenRouter" from the "API Provider" dropdown.
|
||||
3. **Enter API Key:** Paste your OpenRouter API key into the "OpenRouter API Key" field.
|
||||
4. **Select Model:** Choose your desired model from the "Model" dropdown.
|
||||
5. **(Optional) Custom Base URL:** If you need to use a custom base URL for the OpenRouter API, check "Use custom base URL" and enter the URL. Leave this blank for most users.
|
||||
|
||||
### Supported Transforms
|
||||
|
||||
OpenRouter provides an [optional "middle-out" message transform](https://openrouter.ai/docs/features/message-transforms) to help with prompts that exceed the maximum context size of a model. You can enable it by checking the "Compress prompts and message chains to the context size" box.
|
||||
|
||||
### Tips and Notes
|
||||
|
||||
- **Model Selection:** OpenRouter offers a wide range of models. Experiment to find the best one for your needs.
|
||||
- **Pricing:** OpenRouter charges based on the underlying model's pricing. See the [OpenRouter Models page](https://openrouter.ai/models) for details.
|
||||
- **Prompt Caching:**
|
||||
- OpenRouter passes caching requests to underlying models that support it. Check the [OpenRouter Models page](https://openrouter.ai/models) to see which models offer caching.
|
||||
- For most models, caching should activate automatically if supported by the model itself (similar to how Requesty works).
|
||||
- **Exception for Gemini Models via OpenRouter:** Due to potential response delays sometimes observed with Google's caching mechanism when accessed via OpenRouter, a manual activation step is required _specifically for Gemini models_.
|
||||
- If using a **Gemini model** via OpenRouter, you **must manually check** the "Enable Prompt Caching" box in the provider settings to activate caching for that model. This checkbox serves as a temporary workaround. For non-Gemini models on OpenRouter, this checkbox is not necessary for caching.
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
title: "Requesty"
|
||||
description: "Learn how to use Requesty with Cline to access and optimize over 150 large language models."
|
||||
---
|
||||
|
||||
Cline supports accessing models through the [Requesty](https://www.requesty.ai/) AI platform. Requesty provides an easy and optimized API for interacting with 150+ large language models (LLMs).
|
||||
|
||||
**Website:** [https://www.requesty.ai/](https://www.requesty.ai/)
|
||||
|
||||
### Getting an API Key
|
||||
|
||||
1. **Sign Up/Sign In:** Go to the [Requesty website](https://www.requesty.ai/) and create an account or sign in.
|
||||
2. **Get API Key:** You can get an API key from the [API Management](https://app.requesty.ai/manage-api) section of your Requesty dashboard.
|
||||
|
||||
### Supported Models
|
||||
|
||||
Requesty provides access to a wide range of models. Cline will automatically fetch the latest list of available models. You can see the full list of available models on the [Model List](https://app.requesty.ai/router/list) page.
|
||||
|
||||
### Configuration in Cline
|
||||
|
||||
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
|
||||
2. **Select Provider:** Choose "Requesty" from the "API Provider" dropdown.
|
||||
3. **Enter API Key:** Paste your Requesty API key into the "Requesty API Key" field.
|
||||
4. **Select Model:** Choose your desired model from the "Model" dropdown.
|
||||
|
||||
### Tips and Notes
|
||||
|
||||
- **Optimizations**: Requesty offers a range of in-flight cost optimizations to lower your costs.
|
||||
- **Unified and simplified billing**: Unrestricted access to all providers and models, automatic balance top ups and more via a single [API key](https://app.requesty.ai/manage-api).
|
||||
- **Cost tracking**: Track cost per model, coding language, changed file, and more via the [Cost dashboard](https://app.requesty.ai/cost-management) or the [Requesty VS Code extension](https://marketplace.visualstudio.com/items?itemName=Requesty.requesty).
|
||||
- **Stats and logs**: See your [coding stats dashboard](https://app.requesty.ai/usage-stats) or go through your [LLM interaction logs](https://app.requesty.ai/logs).
|
||||
- **Fallback policies**: Keep your LLM working for you with fallback policies when providers are down.
|
||||
- **Prompt Caching:** Some providers support prompt caching. [Search models with caching](https://app.requesty.ai/router/list).
|
||||
|
||||
### Relevant resources
|
||||
|
||||
- [Requesty Youtube channel](https://www.youtube.com/@requestyAI)
|
||||
- [Requesty Discord](https://requesty.ai/discord)
|
||||
@@ -1,85 +0,0 @@
|
||||
---
|
||||
title: "xAI (Grok)"
|
||||
description: "Learn how to configure and use xAI's Grok models with Cline, including API key setup, supported models, and reasoning capabilities."
|
||||
---
|
||||
|
||||
xAI is the company behind Grok, a large language model known for its conversational abilities and large context window. Grok models are designed to provide helpful, informative, and contextually relevant responses.
|
||||
|
||||
**Website:** [https://x.ai/](https://x.ai/)
|
||||
|
||||
### Getting an API Key
|
||||
|
||||
1. **Sign Up/Sign In:** Go to the [xAI Console](https://console.x.ai/). Create an account or sign in.
|
||||
2. **Navigate to API Keys:** Go to the API keys section in your dashboard.
|
||||
3. **Create a Key:** Click to create a new API key. Give your key a descriptive name (e.g., "Cline").
|
||||
4. **Copy the Key:** **Important:** Copy the API key _immediately_. You will not be able to see it again. Store it securely.
|
||||
|
||||
### Supported Models
|
||||
|
||||
Cline supports the following xAI Grok models:
|
||||
|
||||
#### Grok-3 Models
|
||||
|
||||
- `grok-3-beta` (Default) - xAI's Grok-3 beta model with 131K context window
|
||||
- `grok-3-fast-beta` - xAI's Grok-3 fast beta model with 131K context window
|
||||
- `grok-3-mini-beta` - xAI's Grok-3 mini beta model with 131K context window
|
||||
- `grok-3-mini-fast-beta` - xAI's Grok-3 mini fast beta model with 131K context window
|
||||
|
||||
#### Grok-2 Models
|
||||
|
||||
- `grok-2-latest` - xAI's Grok-2 model - latest version with 131K context window
|
||||
- `grok-2` - xAI's Grok-2 model with 131K context window
|
||||
- `grok-2-1212` - xAI's Grok-2 model (version 1212) with 131K context window
|
||||
|
||||
#### Grok Vision Models
|
||||
|
||||
- `grok-2-vision-latest` - xAI's Grok-2 Vision model - latest version with image support and 32K context window
|
||||
- `grok-2-vision` - xAI's Grok-2 Vision model with image support and 32K context window
|
||||
- `grok-2-vision-1212` - xAI's Grok-2 Vision model (version 1212) with image support and 32K context window
|
||||
- `grok-vision-beta` - xAI's Grok Vision Beta model with image support and 8K context window
|
||||
|
||||
#### Legacy Models
|
||||
|
||||
- `grok-beta` - xAI's Grok Beta model (legacy) with 131K context window
|
||||
|
||||
### Configuration in Cline
|
||||
|
||||
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
|
||||
2. **Select Provider:** Choose "xAI" from the "API Provider" dropdown.
|
||||
3. **Enter API Key:** Paste your xAI API key into the "xAI API Key" field.
|
||||
4. **Select Model:** Choose your desired Grok model from the "Model" dropdown.
|
||||
|
||||
### Reasoning Capabilities
|
||||
|
||||
Grok 3 Mini models feature specialized reasoning capabilities, allowing them to "think before responding" - particularly useful for complex problem-solving tasks.
|
||||
|
||||
#### Reasoning-Enabled Models
|
||||
|
||||
Reasoning is only supported by:
|
||||
|
||||
- `grok-3-mini-beta`
|
||||
- `grok-3-mini-fast-beta`
|
||||
|
||||
The Grok 3 models `grok-3-beta` and `grok-3-fast-beta` do not support reasoning.
|
||||
|
||||
#### Controlling Reasoning Effort
|
||||
|
||||
When using reasoning-enabled models, you can control how hard the model thinks with the `reasoning_effort` parameter:
|
||||
|
||||
- `low`: Minimal thinking time, using fewer tokens for quick responses
|
||||
- `high`: Maximum thinking time, leveraging more tokens for complex problems
|
||||
|
||||
Choose `low` for simple queries that should complete quickly, and `high` for harder problems where response latency is less important.
|
||||
|
||||
#### Key Features
|
||||
|
||||
- **Step-by-Step Problem Solving**: The model thinks through problems methodically before delivering an answer
|
||||
- **Math & Quantitative Strength**: Excels at numerical challenges and logic puzzles
|
||||
- **Reasoning Trace Access**: The model's thinking process is available via the `reasoning_content` field in the response completion object
|
||||
|
||||
### Tips and Notes
|
||||
|
||||
- **Context Window:** Most Grok models feature large context windows (up to 131K tokens), allowing you to include substantial amounts of code and context in your prompts.
|
||||
- **Vision Capabilities:** Select vision-enabled models (`grok-2-vision-latest`, `grok-2-vision`, etc.) when you need to process or analyze images.
|
||||
- **Pricing:** Pricing varies by model, with input costs ranging from $0.3 to $5.0 per million tokens and output costs from $0.5 to $25.0 per million tokens. Refer to the xAI documentation for the most current pricing information.
|
||||
- **Performance Tradeoffs:** "Fast" variants typically offer quicker response times but may have higher costs, while "mini" variants are more economical but may have reduced capabilities.
|
||||
@@ -1,174 +0,0 @@
|
||||
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" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -1,214 +0,0 @@
|
||||
const { RuleTester } = require("eslint")
|
||||
const rule = require("../no-protobuf-object-literals")
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: require.resolve("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ruleTester.run("no-protobuf-object-literals", rule, {
|
||||
valid: [
|
||||
// Valid case: Using .create() method
|
||||
{
|
||||
code: `
|
||||
import { State } from '@shared/proto/state';
|
||||
|
||||
const state = State.create({
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
});
|
||||
`,
|
||||
},
|
||||
// Valid case: Using .fromPartial() method
|
||||
{
|
||||
code: `
|
||||
import { ChatSettings } from '@shared/proto/state';
|
||||
|
||||
const settings = ChatSettings.fromPartial({
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
openAiReasoningEffort: 'thorough'
|
||||
});
|
||||
`,
|
||||
},
|
||||
// Valid case: Object literal not used with protobuf type
|
||||
{
|
||||
code: `
|
||||
interface MyInterface {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const obj: MyInterface = {
|
||||
id: 123,
|
||||
name: 'test'
|
||||
};
|
||||
`,
|
||||
},
|
||||
// Valid case: Using object literal for non-protobuf import
|
||||
{
|
||||
code: `
|
||||
import { SomeType } from '@some/other/package';
|
||||
|
||||
const obj: SomeType = {
|
||||
id: 123,
|
||||
name: 'test'
|
||||
};
|
||||
`,
|
||||
},
|
||||
// Valid case: Regular function call with object literal (should not be flagged)
|
||||
{
|
||||
code: `
|
||||
import { State } from '@shared/proto/state';
|
||||
|
||||
// This should not be flagged because it's a regular function call
|
||||
// not directly tied to a protobuf type
|
||||
process({
|
||||
id: 123,
|
||||
name: 'test',
|
||||
data: { nested: true }
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Invalid case: Using object literal with imported protobuf type
|
||||
{
|
||||
code: `
|
||||
import { State } from '@shared/proto/state';
|
||||
|
||||
const state: State = {
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
};
|
||||
`,
|
||||
output: `
|
||||
import { State } from '@shared/proto/state';
|
||||
|
||||
const state: State = State.create({
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Invalid case: Using object literal with namespaced protobuf type
|
||||
{
|
||||
code: `
|
||||
import * as stateProto from '@shared/proto/state';
|
||||
|
||||
const state: stateProto.State = {
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
};
|
||||
`,
|
||||
output: `
|
||||
import * as stateProto from '@shared/proto/state';
|
||||
|
||||
const state: stateProto.State = stateProto.State.create({
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethodGeneric" }],
|
||||
},
|
||||
// Invalid case: Using object literal in a return statement (with protobuf return type)
|
||||
{
|
||||
code: `
|
||||
import { ChatSettings } from '@shared/proto/state';
|
||||
|
||||
function createSettings(): ChatSettings {
|
||||
return {
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
openAiReasoningEffort: 'thorough'
|
||||
};
|
||||
}
|
||||
`,
|
||||
output: `
|
||||
import { ChatSettings } from '@shared/proto/state';
|
||||
|
||||
function createSettings(): ChatSettings {
|
||||
return ChatSettings.create({
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
openAiReasoningEffort: 'thorough'
|
||||
});
|
||||
}
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Invalid case: Using object literal in a function parameter (with protobuf types imported)
|
||||
{
|
||||
code: `
|
||||
import { ChatContent } from '@shared/proto/state';
|
||||
|
||||
function processContent(content: ChatContent) {
|
||||
// process the content
|
||||
}
|
||||
|
||||
processContent({
|
||||
message: 'Hello, this is a test message',
|
||||
images: ['image1.png', 'image2.jpg'],
|
||||
files: ['file1.txt', 'file2.pdf']
|
||||
});
|
||||
`,
|
||||
output: `
|
||||
import { ChatContent } from '@shared/proto/state';
|
||||
|
||||
function processContent(content: ChatContent) {
|
||||
// process the content
|
||||
}
|
||||
|
||||
processContent(ChatContent.create({
|
||||
message: 'Hello, this is a test message',
|
||||
images: ['image1.png', 'image2.jpg'],
|
||||
files: ['file1.txt', 'file2.pdf']
|
||||
}));
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethodGeneric" }],
|
||||
},
|
||||
// Invalid case: Using object literal in assignment expression
|
||||
{
|
||||
code: `
|
||||
import { State } from '@shared/proto/state';
|
||||
|
||||
let state: State;
|
||||
state = {
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
};
|
||||
`,
|
||||
output: `
|
||||
import { State } from '@shared/proto/state';
|
||||
|
||||
let state: State;
|
||||
state = State.create({
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Test with custom protobufPackages option
|
||||
{
|
||||
code: `
|
||||
import { CustomProto } from 'custom/proto/package';
|
||||
|
||||
const obj: CustomProto = {
|
||||
field1: 'value',
|
||||
field2: 123
|
||||
};
|
||||
`,
|
||||
output: `
|
||||
import { CustomProto } from 'custom/proto/package';
|
||||
|
||||
const obj: CustomProto = CustomProto.create({
|
||||
field1: 'value',
|
||||
field2: 123
|
||||
});
|
||||
`,
|
||||
options: [{ protobufPackages: ["custom/proto"] }],
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -1,19 +0,0 @@
|
||||
// eslint-rules/index.js
|
||||
const noProtobufObjectLiterals = require("./no-protobuf-object-literals")
|
||||
const noGrpcClientObjectLiterals = require("./no-grpc-client-object-literals")
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
"no-protobuf-object-literals": noProtobufObjectLiterals,
|
||||
"no-grpc-client-object-literals": noGrpcClientObjectLiterals,
|
||||
},
|
||||
configs: {
|
||||
recommended: {
|
||||
plugins: ["local"],
|
||||
rules: {
|
||||
"local/no-protobuf-object-literals": "error",
|
||||
"local/no-grpc-client-object-literals": "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,556 +0,0 @@
|
||||
const { ESLintUtils } = require("@typescript-eslint/utils")
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
|
||||
|
||||
module.exports = createRule({
|
||||
name: "no-protobuf-object-literals",
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Enforce using .create() or .fromPartial() for protobuf objects instead of object literals",
|
||||
recommended: "error",
|
||||
},
|
||||
fixable: "code",
|
||||
messages: {
|
||||
useProtobufMethod:
|
||||
"Use {{typeName}}.create() or {{typeName}}.fromPartial() instead of " +
|
||||
"object literal for protobuf type from @shared/proto\n" +
|
||||
"Found: {{code}}\n Suggestion: " +
|
||||
"{{typeName}}.create({{objectContent}})",
|
||||
useProtobufMethodGeneric:
|
||||
"Use .create() or .fromPartial() instead of object literal for protobuf " +
|
||||
"type from @shared/proto\n Found: {{code}}",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
protobufPackages: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
default: ["shared/proto/"],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultOptions: [{ protobufPackages: ["shared/proto/"] }],
|
||||
|
||||
create(context, [options]) {
|
||||
const protobufPackages = options.protobufPackages
|
||||
const protobufImports = new Set() // Set of imported protobuf types
|
||||
const protobufNamespaceImports = new Set() // For namespace imports like "import * as proto"
|
||||
const safeObjectExpressions = new Set() // 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.add(node.arguments[0])
|
||||
}
|
||||
},
|
||||
|
||||
// Track imports from protobuf packages
|
||||
ImportDeclaration(node) {
|
||||
const packageName = node.source.value
|
||||
|
||||
if (matchesProtobufPackage(packageName, protobufPackages)) {
|
||||
// This is a protobuf package.
|
||||
node.specifiers.forEach((spec) => {
|
||||
if (spec.type === "ImportSpecifier") {
|
||||
// import { MyRequest } from '@shared/proto'
|
||||
protobufImports.add(spec.imported.name)
|
||||
} else if (spec.type === "ImportNamespaceSpecifier") {
|
||||
// import * as proto from '@shared/proto'
|
||||
protobufNamespaceImports.add(spec.local.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Check variable declarations with type annotations
|
||||
"VariableDeclarator > ObjectExpression"(node) {
|
||||
// Skip if this is inside a create/fromPartial call
|
||||
if (safeObjectExpressions.has(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Found object literal in variable declaration
|
||||
const declarator = node.parent
|
||||
|
||||
if (declarator.id && declarator.id.typeAnnotation) {
|
||||
const typeName = getTypeName(declarator.id.typeAnnotation.typeAnnotation)
|
||||
if (typeName) {
|
||||
// Check if it's a direct protobuf import
|
||||
if (protobufImports.has(typeName)) {
|
||||
//console.log('🚨 VIOLATION: Using object literal for protobuf type:', typeName);
|
||||
const sourceCode = context.getSourceCode()
|
||||
const declaratorText = sourceCode.getText(declarator)
|
||||
const objectText = sourceCode.getText(node)
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
typeName,
|
||||
code: declaratorText,
|
||||
objectContent: objectText,
|
||||
},
|
||||
fix(fixer) {
|
||||
// Replace the object literal with Type.create() call
|
||||
return fixer.replaceText(node, `${typeName}.create(${objectText})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a namespaced protobuf type (e.g., proto.MyRequest)
|
||||
if (isNamespacedProtobufType(protobufNamespaceImports, typeName)) {
|
||||
//console.log('🚨 VIOLATION: Using object literal for namespaced protobuf type:', typeName);
|
||||
const sourceCode = context.getSourceCode()
|
||||
const declaratorText = sourceCode.getText(declarator)
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethodGeneric",
|
||||
data: { code: declaratorText },
|
||||
fix(fixer) {
|
||||
// For namespaced types, use the full type name to call create()
|
||||
return fixer.replaceText(node, `${typeName}.create(${sourceCode.getText(node)})`)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Check assignment expressions
|
||||
"AssignmentExpression > ObjectExpression"(node) {
|
||||
// Skip if this is inside a create/fromPartial call
|
||||
if (safeObjectExpressions.has(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
const assignment = node.parent
|
||||
|
||||
// For assignment to variables without inline type annotation
|
||||
if (assignment.left && assignment.right === node) {
|
||||
let typeName = null
|
||||
|
||||
// Check if there's a typeAnnotation directly on the left
|
||||
if (assignment.left.typeAnnotation) {
|
||||
typeName = getTypeName(assignment.left.typeAnnotation.typeAnnotation)
|
||||
}
|
||||
// Otherwise try to infer from the variable name if it's a simple identifier
|
||||
else if (assignment.left.type === "Identifier") {
|
||||
const varName = assignment.left.name
|
||||
// Check variable declarations in the current scope
|
||||
const sourceCode = context.getSourceCode()
|
||||
const scope = sourceCode.getScope(node)
|
||||
const variable = scope.variables.find((v) => v.name === varName)
|
||||
if (variable && variable.defs.length > 0) {
|
||||
const def = variable.defs[0]
|
||||
if (def.node.id && def.node.id.typeAnnotation) {
|
||||
typeName = getTypeName(def.node.id.typeAnnotation.typeAnnotation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeName && protobufImports.has(typeName)) {
|
||||
//console.log('🚨 VIOLATION: Using object literal in assignment for protobuf type:', typeName);
|
||||
const sourceCode = context.getSourceCode()
|
||||
const assignmentText = sourceCode.getText(assignment.left) + " = "
|
||||
const objectText = sourceCode.getText(node)
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
typeName,
|
||||
code: assignmentText + "{",
|
||||
objectContent: objectText,
|
||||
},
|
||||
fix(fixer) {
|
||||
// Replace the object literal with Type.create() call in assignments
|
||||
return fixer.replaceText(node, `${typeName}.create(${objectText})`)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Check return statements
|
||||
"ReturnStatement > ObjectExpression"(node) {
|
||||
// Skip if this is inside a create/fromPartial call
|
||||
if (safeObjectExpressions.has(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find the parent function to get its return type
|
||||
const functionNode = findParentFunction(node)
|
||||
if (!functionNode) {
|
||||
return
|
||||
}
|
||||
|
||||
// Try to get the return type using our enhanced helper
|
||||
const sourceCode = context.getSourceCode()
|
||||
let returnTypeName = getFunctionReturnType(functionNode, sourceCode)
|
||||
|
||||
// For async functions with Promise<Type> return type, extract the inner type
|
||||
if (returnTypeName && returnTypeName.startsWith("Promise<") && returnTypeName.endsWith(">")) {
|
||||
returnTypeName = returnTypeName.slice(8, -1)
|
||||
}
|
||||
|
||||
// Check if the return type is a protobuf type
|
||||
if (returnTypeName) {
|
||||
if (protobufImports.has(returnTypeName)) {
|
||||
//console.log('🚨 VIOLATION: Return type is a protobuf type:', returnTypeName);
|
||||
const sourceCode = context.getSourceCode()
|
||||
const returnText = sourceCode.getText(node.parent)
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
typeName: returnTypeName,
|
||||
code: returnText,
|
||||
objectContent: sourceCode.getText(node),
|
||||
},
|
||||
fix(fixer) {
|
||||
// Replace the object literal with Type.create() call in return statements
|
||||
return fixer.replaceText(node, `${returnTypeName}.create(${sourceCode.getText(node)})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a namespaced protobuf type
|
||||
if (isNamespacedProtobufType(protobufNamespaceImports, returnTypeName)) {
|
||||
const sourceCode = context.getSourceCode()
|
||||
const returnText = sourceCode.getText(node.parent)
|
||||
//console.log('🚨 VIOLATION: Return type is a namespaced protobuf type:', returnTypeName);
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethodGeneric",
|
||||
data: { code: returnText },
|
||||
fix(fixer) {
|
||||
// For namespaced types in return statements, we need to extract the full type name
|
||||
const objectCode = sourceCode.getText(node)
|
||||
// Since we may not know the exact type, we'll use the more generic namespaced type
|
||||
return fixer.replaceText(node, `${returnTypeName}.create(${objectCode})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback - if there are any protobuf imports and the function signature
|
||||
// mentions a return type that matches one of the imported types
|
||||
const functionText = functionNode ? sourceCode.getText(functionNode) : ""
|
||||
|
||||
for (const protoType of protobufImports) {
|
||||
// Use more precise regex to match return type patterns specifically
|
||||
// Rather than just checking if the type name appears anywhere in the signature
|
||||
const returnTypeRegex = new RegExp(
|
||||
// Match arrow function return type
|
||||
`=>\\s*:?\\s*${protoType}\\b|` +
|
||||
// Match function declaration return type
|
||||
`\\)\\s*:?\\s*${protoType}\\b|` +
|
||||
// Match Promise return type
|
||||
`\\)\\s*:?\\s*Promise<\\s*${protoType}\\s*>|` +
|
||||
// Match function type in variable declaration
|
||||
`:\\s*\\(.*\\)\\s*=>\\s*${protoType}\\b`,
|
||||
)
|
||||
|
||||
if (returnTypeRegex.test(functionText)) {
|
||||
const returnText = sourceCode.getText(node.parent)
|
||||
//console.log('🚨 VIOLATION: regex matched protobuf type:', functionText);
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
typeName: protoType,
|
||||
code: returnText,
|
||||
objectContent: sourceCode.getText(node),
|
||||
},
|
||||
fix(fixer) {
|
||||
// Replace the object literal with Type.create() call
|
||||
return fixer.replaceText(node, `${protoType}.create(${sourceCode.getText(node)})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
// Check for namespace imports too
|
||||
for (const namespace of protobufNamespaceImports) {
|
||||
// Similar to above, but for namespaced types
|
||||
const namespaceReturnTypeRegex = new RegExp(
|
||||
// Match arrow function return type
|
||||
`=>\\s*:?\\s*${namespace}\\.\\w+\\b|` +
|
||||
// Match function declaration return type
|
||||
`\\)\\s*:?\\s*${namespace}\\.\\w+\\b|` +
|
||||
// Match Promise return type
|
||||
`\\)\\s*:?\\s*Promise<\\s*${namespace}\\.\\w+\\s*>|` +
|
||||
// Match function type in variable declaration
|
||||
`:\\s*\\(.*\\)\\s*=>\\s*${namespace}\\.\\w+\\b`,
|
||||
)
|
||||
|
||||
if (namespaceReturnTypeRegex.test(functionText)) {
|
||||
const returnText = sourceCode.getText(node.parent)
|
||||
//console.log('🚨 VIOLATION: regex matched namespaced protobuf type:', functionText, "namespace:", namespace);
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethodGeneric",
|
||||
data: { code: returnText },
|
||||
fix(fixer) {
|
||||
// For namespaced types based on function signature patterns
|
||||
// Extract the namespace and type from the function text using more precise patterns
|
||||
const match = functionText.match(
|
||||
new RegExp(
|
||||
// Match return type patterns more precisely
|
||||
`\\)\\s*:?\\s*(${namespace}\\.[\\w]+)\\b|` + // Function declaration
|
||||
`=>\\s*:?\\s*(${namespace}\\.[\\w]+)\\b|` + // Arrow function
|
||||
`Promise<\\s*(${namespace}\\.[\\w]+)\\s*>`, // Promise wrapped
|
||||
),
|
||||
)
|
||||
if (match) {
|
||||
const fullType = match[1] || match[2]
|
||||
return fixer.replaceText(node, `${fullType}.create(${sourceCode.getText(node)})`)
|
||||
}
|
||||
// Fallback - we can't determine the exact type, but we know it's from the namespace
|
||||
// Use a namespace-based approach
|
||||
return fixer.replaceText(node, `${namespace}.create(${sourceCode.getText(node)})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Check function call arguments (more selective approach)
|
||||
"CallExpression > ObjectExpression"(node) {
|
||||
// Skip if this is inside a create/fromPartial call
|
||||
if (safeObjectExpressions.has(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
// We need to be more selective to avoid false positives
|
||||
// Only warn if:
|
||||
// 1. The function is called on a protobuf namespace
|
||||
// 2. The call argument has a type annotation that matches a protobuf type
|
||||
// 3. The call is to a function that we know takes a protobuf type
|
||||
|
||||
// Check if it's a call on a protobuf namespace
|
||||
if (
|
||||
node.parent.callee &&
|
||||
node.parent.callee.type === "MemberExpression" &&
|
||||
node.parent.callee.object.type === "Identifier"
|
||||
) {
|
||||
const namespace = node.parent.callee.object.name
|
||||
if (protobufNamespaceImports.has(namespace)) {
|
||||
const sourceCode = context.getSourceCode()
|
||||
const callText = sourceCode.getText(node.parent)
|
||||
//console.log('🚨 VIOLATION: Check function call arguments object literal:', callText);
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethodGeneric",
|
||||
data: { code: callText },
|
||||
fix(fixer) {
|
||||
// For calls on a protobuf namespace
|
||||
const memberExpr = node.parent.callee
|
||||
// Try to determine if this is calling a method that expects a specific type
|
||||
const methodName = memberExpr.property.name
|
||||
|
||||
// If method name looks like 'create' + Type, we can infer the type
|
||||
const possibleTypeName = methodName.replace(/^create/, "")
|
||||
|
||||
// Check if namespace has a type with this name
|
||||
// Since we can't directly check at lint time, we'll use the namespace + inferred type
|
||||
if (possibleTypeName && possibleTypeName !== methodName) {
|
||||
return fixer.replaceText(
|
||||
node,
|
||||
`${namespace}.${possibleTypeName}.create(${sourceCode.getText(node)})`,
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback - use a more generic approach with namespace
|
||||
return fixer.replaceText(node, `${namespace}.create(${sourceCode.getText(node)})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// For regular function calls with object literals, check if there are protobuf imports
|
||||
// and if the function might expect a protobuf type
|
||||
if (node.parent.callee) {
|
||||
// This is a more permissive check to catch cases like processContent({ ... })
|
||||
// which might be passing a protobuf type
|
||||
const sourceCode = context.getSourceCode()
|
||||
const scope = sourceCode.getScope(node)
|
||||
|
||||
// Try to find the function definition
|
||||
if (node.parent.callee.type === "Identifier") {
|
||||
const functionName = node.parent.callee.name
|
||||
const variable = scope.variables.find((v) => v.name === functionName)
|
||||
|
||||
// If we found the function and it has parameter type annotations
|
||||
// that match protobuf types, flag it
|
||||
if (variable && variable.defs.length > 0) {
|
||||
const def = variable.defs[0]
|
||||
if (def.node.params && node.parent.arguments.indexOf(node) < def.node.params.length) {
|
||||
const param = def.node.params[node.parent.arguments.indexOf(node)]
|
||||
if (param.typeAnnotation) {
|
||||
const typeName = getTypeName(param.typeAnnotation.typeAnnotation)
|
||||
if (
|
||||
typeName &&
|
||||
(protobufImports.has(typeName) ||
|
||||
isNamespacedProtobufType(protobufNamespaceImports, typeName))
|
||||
) {
|
||||
const callText = sourceCode.getText(node.parent)
|
||||
//console.log('🚨 VIOLATION: Function call arguments object literal:', callText);
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethodGeneric",
|
||||
data: { code: callText },
|
||||
fix(fixer) {
|
||||
// For function calls with protobuf type parameters
|
||||
return fixer.replaceText(node, `${typeName}.create(${sourceCode.getText(node)})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Helper functions
|
||||
function getTypeName(typeAnnotation) {
|
||||
if (!typeAnnotation) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeAnnotation.type === "TSTypeReference") {
|
||||
if (typeAnnotation.typeName.type === "Identifier") {
|
||||
return typeAnnotation.typeName.name
|
||||
} else if (typeAnnotation.typeName.type === "TSQualifiedName") {
|
||||
// Handle namespaced types like proto.MyRequest
|
||||
return `${typeAnnotation.typeName.left.name}.${typeAnnotation.typeName.right.name}`
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function matchesProtobufPackage(packageName, protobufPackages) {
|
||||
return protobufPackages.some((protobufPackage) => {
|
||||
// Remove leading and trailing @ and / from protobufPackage
|
||||
const cleanedPackage = protobufPackage.replace(/^[@\/]/, "").replace(/[\/]$/, "")
|
||||
const pattern = new RegExp(`(.*[@/]|)${escapeRegex(cleanedPackage)}[/].*`)
|
||||
return pattern.test(packageName)
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to escape special regex characters
|
||||
function escapeRegex(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
}
|
||||
|
||||
// Helper to extract function return type more reliably
|
||||
function getFunctionReturnType(functionNode, sourceCode) {
|
||||
// 1. Check explicit return type annotation
|
||||
if (functionNode.returnType) {
|
||||
return getTypeName(functionNode.returnType.typeAnnotation)
|
||||
}
|
||||
|
||||
// 2. For variable declarations like const foo: (arg: Type) => ReturnType = ...
|
||||
if (functionNode.parent && functionNode.parent.type === "VariableDeclarator") {
|
||||
const declarator = functionNode.parent
|
||||
if (declarator.id && declarator.id.typeAnnotation) {
|
||||
const typeAnnotation = declarator.id.typeAnnotation.typeAnnotation
|
||||
|
||||
// Handle function type annotations
|
||||
if (typeAnnotation.type === "TSFunctionType" && typeAnnotation.typeAnnotation) {
|
||||
return getTypeName(typeAnnotation.typeAnnotation)
|
||||
}
|
||||
|
||||
// Handle type references to function types
|
||||
if (typeAnnotation.type === "TSTypeReference") {
|
||||
// This might be a type like Promise<ReturnType>
|
||||
if (
|
||||
typeAnnotation.typeName.name === "Promise" &&
|
||||
typeAnnotation.typeParameters &&
|
||||
typeAnnotation.typeParameters.params.length > 0
|
||||
) {
|
||||
return getTypeName(typeAnnotation.typeParameters.params[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. For class methods, check if it's part of an interface implementation
|
||||
if (
|
||||
functionNode.parent &&
|
||||
functionNode.parent.type === "MethodDefinition" &&
|
||||
functionNode.parent.parent &&
|
||||
functionNode.parent.parent.type === "ClassBody"
|
||||
) {
|
||||
const className = getEnclosingClassName(functionNode)
|
||||
const methodName = functionNode.parent.key.name
|
||||
|
||||
if (className && methodName) {
|
||||
// Look for interface declarations in the scope
|
||||
const scope = sourceCode.getScope(functionNode)
|
||||
// This would require more complex scope analysis which is limited in ESLint
|
||||
// For now, we'll return null and rely on other methods
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Helper to get the class name for a method
|
||||
function getEnclosingClassName(node) {
|
||||
let current = node.parent
|
||||
while (current) {
|
||||
if (current.type === "ClassDeclaration" && current.id) {
|
||||
return current.id.name
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
function isNamespacedProtobufType(protobufNamespaceImports, typeName) {
|
||||
if (!typeName.includes(".")) {
|
||||
return false
|
||||
}
|
||||
|
||||
const namespace = typeName.split(".")[0]
|
||||
return protobufNamespaceImports.has(namespace)
|
||||
}
|
||||
|
||||
function findParentFunction(node) {
|
||||
let current = node.parent
|
||||
while (current) {
|
||||
if (
|
||||
current.type === "FunctionDeclaration" ||
|
||||
current.type === "FunctionExpression" ||
|
||||
current.type === "ArrowFunctionExpression"
|
||||
) {
|
||||
return current
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
Generated
-2479
File diff suppressed because it is too large
Load Diff
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { hostServiceHandlers } from "./host-grpc-service-config"
|
||||
import { GrpcRequestRegistry } from "../../src/core/controller/grpc-request-registry"
|
||||
|
||||
/**
|
||||
* Type definition for a streaming response handler
|
||||
*/
|
||||
export type StreamingResponseHandler = (response: any, isLast?: boolean, sequenceNumber?: number) => Promise<void>
|
||||
|
||||
// Registry to track active gRPC requests and their cleanup functions
|
||||
const requestRegistry = new GrpcRequestRegistry()
|
||||
|
||||
/**
|
||||
* Callback interface for streaming requests
|
||||
*/
|
||||
export interface StreamingCallbacks<T = any> {
|
||||
onResponse: (response: T) => void
|
||||
onError?: (error: Error) => void
|
||||
onComplete?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles gRPC requests from the webview
|
||||
*/
|
||||
export class GrpcHandler {
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Handle a gRPC request from the webview
|
||||
* @param service The service name
|
||||
* @param method The method name
|
||||
* @param message The request message
|
||||
* @param requestId The request ID for response correlation
|
||||
* @param streamingCallbacks Optional callbacks for streaming responses
|
||||
* @returns For unary requests: the response message or error. For streaming requests: a cancel function.
|
||||
*/
|
||||
async handleRequest<T = any>(
|
||||
service: string,
|
||||
method: string,
|
||||
message: any,
|
||||
requestId: string,
|
||||
streamingCallbacks?: StreamingCallbacks<T>,
|
||||
): Promise<
|
||||
| {
|
||||
message?: any
|
||||
error?: string
|
||||
request_id: string
|
||||
}
|
||||
| (() => void)
|
||||
> {
|
||||
// If streaming callbacks are provided, handle as a streaming request
|
||||
if (streamingCallbacks) {
|
||||
let completionCalled = false
|
||||
|
||||
// Create a response handler that will call the client's callbacks
|
||||
const responseHandler: StreamingResponseHandler = async (response, isLast = false, sequenceNumber) => {
|
||||
try {
|
||||
// Call the client's onResponse callback with the response
|
||||
streamingCallbacks.onResponse(response)
|
||||
|
||||
// If this is the last response, call the onComplete callback
|
||||
if (isLast && streamingCallbacks.onComplete && !completionCalled) {
|
||||
completionCalled = true
|
||||
streamingCallbacks.onComplete()
|
||||
}
|
||||
} catch (error) {
|
||||
// If there's an error in the callback, call the onError callback
|
||||
if (streamingCallbacks.onError) {
|
||||
streamingCallbacks.onError(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the response handler with the registry
|
||||
requestRegistry.registerRequest(
|
||||
requestId,
|
||||
() => {
|
||||
console.log(`[DEBUG] Cleaning up streaming request: ${requestId}`)
|
||||
if (streamingCallbacks.onComplete && !completionCalled) {
|
||||
completionCalled = true
|
||||
streamingCallbacks.onComplete()
|
||||
}
|
||||
},
|
||||
{ type: "streaming_request", service, method },
|
||||
responseHandler,
|
||||
)
|
||||
|
||||
// Call the streaming handler directly
|
||||
console.log(`[DEBUG] Streaming gRPC host call to ${service}.${method} req:${requestId}`)
|
||||
try {
|
||||
await this.handleStreamingRequest(service, method, message, requestId)
|
||||
} catch (error) {
|
||||
if (streamingCallbacks.onError) {
|
||||
streamingCallbacks.onError(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
}
|
||||
|
||||
// Return a function to cancel the stream
|
||||
return () => {
|
||||
console.log(`[DEBUG] Cancelling streaming request: ${requestId}`)
|
||||
this.cancelRequest(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle as a unary request
|
||||
try {
|
||||
// Get the service handler from the config
|
||||
const serviceConfig = hostServiceHandlers[service]
|
||||
if (!serviceConfig) {
|
||||
throw new Error(`Unknown service: ${service}`)
|
||||
}
|
||||
|
||||
// Handle unary request
|
||||
return {
|
||||
message: await serviceConfig.requestHandler(method, message),
|
||||
request_id: requestId,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
request_id: requestId,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a gRPC request
|
||||
* @param requestId The request ID to cancel
|
||||
* @returns True if the request was found and cancelled, false otherwise
|
||||
*/
|
||||
public async cancelRequest(requestId: string): Promise<boolean> {
|
||||
const cancelled = requestRegistry.cancelRequest(requestId)
|
||||
|
||||
if (cancelled) {
|
||||
// Get the registered response handler from the registry
|
||||
const requestInfo = requestRegistry.getRequestInfo(requestId)
|
||||
if (requestInfo && requestInfo.responseStream) {
|
||||
try {
|
||||
// Send cancellation confirmation using the registered response handler
|
||||
await requestInfo.responseStream(
|
||||
{ cancelled: true },
|
||||
true, // Mark as last message
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(`Error sending cancellation response for ${requestId}:`, e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`[DEBUG] Request not found for cancellation: ${requestId}`)
|
||||
}
|
||||
|
||||
return cancelled
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a streaming gRPC request
|
||||
* @param service The service name
|
||||
* @param method The method name
|
||||
* @param message The request message
|
||||
* @param requestId The request ID for response correlation
|
||||
*/
|
||||
private async handleStreamingRequest(service: string, method: string, message: any, requestId: string): Promise<void> {
|
||||
// Get the service handler from the config
|
||||
const serviceConfig = hostServiceHandlers[service]
|
||||
if (!serviceConfig) {
|
||||
throw new Error(`Unknown service: ${service}`)
|
||||
}
|
||||
|
||||
// Check if the service supports streaming
|
||||
if (!serviceConfig.streamingHandler) {
|
||||
throw new Error(`Service ${service} does not support streaming`)
|
||||
}
|
||||
|
||||
// Get the registered response handler from the registry
|
||||
const requestInfo = requestRegistry.getRequestInfo(requestId)
|
||||
if (!requestInfo || !requestInfo.responseStream) {
|
||||
throw new Error(`No response handler registered for request: ${requestId}`)
|
||||
}
|
||||
|
||||
// Use the registered response handler
|
||||
const responseStream = requestInfo.responseStream
|
||||
|
||||
// Handle streaming request and pass the requestId to all streaming handlers
|
||||
await serviceConfig.streamingHandler(method, message, responseStream, requestId)
|
||||
|
||||
// Don't send a final message here - the stream should stay open for future updates
|
||||
// The stream will be closed when the client disconnects or when the service explicitly ends it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request registry instance
|
||||
* This allows other parts of the code to access the registry
|
||||
*/
|
||||
export function getRequestRegistry(): GrpcRequestRegistry {
|
||||
return requestRegistry
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { StreamingResponseHandler } from "./host-grpc-handler"
|
||||
import { handleUriServiceRequest, handleUriServiceStreamingRequest } from "./uri/index"
|
||||
import { handleWatchServiceRequest, handleWatchServiceStreamingRequest } from "./watch/index"
|
||||
|
||||
/**
|
||||
* Configuration for a host service handler
|
||||
*/
|
||||
export interface HostServiceHandlerConfig {
|
||||
requestHandler: (method: string, message: any) => Promise<any>
|
||||
streamingHandler: (
|
||||
method: string,
|
||||
message: any,
|
||||
responseStream: StreamingResponseHandler,
|
||||
requestId?: string,
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of host service names to their handler configurations
|
||||
*/
|
||||
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {
|
||||
"host.UriService": {
|
||||
requestHandler: handleUriServiceRequest,
|
||||
streamingHandler: handleUriServiceStreamingRequest,
|
||||
},
|
||||
"host.WatchService": {
|
||||
requestHandler: handleWatchServiceRequest,
|
||||
streamingHandler: handleWatchServiceStreamingRequest,
|
||||
},
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import { StreamingResponseHandler } from "./host-grpc-handler"
|
||||
|
||||
/**
|
||||
* Generic type for service method handlers
|
||||
*/
|
||||
export type ServiceMethodHandler = (message: any) => Promise<any>
|
||||
|
||||
/**
|
||||
* Type for streaming method handlers
|
||||
*/
|
||||
export type StreamingMethodHandler = (message: any, responseStream: StreamingResponseHandler, requestId?: string) => Promise<void>
|
||||
|
||||
/**
|
||||
* Method metadata including streaming information
|
||||
*/
|
||||
export interface MethodMetadata {
|
||||
isStreaming: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic service registry for gRPC services
|
||||
*/
|
||||
export class ServiceRegistry {
|
||||
private serviceName: string
|
||||
private methodRegistry: Record<string, ServiceMethodHandler> = {}
|
||||
private streamingMethodRegistry: Record<string, StreamingMethodHandler> = {}
|
||||
private methodMetadata: Record<string, MethodMetadata> = {}
|
||||
|
||||
/**
|
||||
* Create a new service registry
|
||||
* @param serviceName The name of the service (used for logging)
|
||||
*/
|
||||
constructor(serviceName: string) {
|
||||
this.serviceName = serviceName
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a method handler
|
||||
* @param methodName The name of the method to register
|
||||
* @param handler The handler function for the method
|
||||
* @param metadata Optional metadata about the method
|
||||
*/
|
||||
registerMethod(methodName: string, handler: ServiceMethodHandler | StreamingMethodHandler, metadata?: MethodMetadata): void {
|
||||
const isStreaming = metadata?.isStreaming || false
|
||||
|
||||
if (isStreaming) {
|
||||
this.streamingMethodRegistry[methodName] = handler as StreamingMethodHandler
|
||||
} else {
|
||||
this.methodRegistry[methodName] = handler as ServiceMethodHandler
|
||||
}
|
||||
|
||||
this.methodMetadata[methodName] = { isStreaming, ...metadata }
|
||||
console.log(`Registered ${this.serviceName} method: ${methodName}${isStreaming ? " (streaming)" : ""}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a method is a streaming method
|
||||
* @param method The method name
|
||||
* @returns True if the method is a streaming method
|
||||
*/
|
||||
isStreamingMethod(method: string): boolean {
|
||||
return this.methodMetadata[method]?.isStreaming || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a streaming method handler
|
||||
* @param method The method name
|
||||
* @returns The streaming method handler or undefined if not found
|
||||
*/
|
||||
getStreamingHandler(method: string): StreamingMethodHandler | undefined {
|
||||
return this.streamingMethodRegistry[method]
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a service request
|
||||
* @param method The method name
|
||||
* @param message The request message
|
||||
* @returns The response message
|
||||
*/
|
||||
async handleRequest(method: string, message: any): Promise<any> {
|
||||
const handler = this.methodRegistry[method]
|
||||
|
||||
if (!handler) {
|
||||
if (this.isStreamingMethod(method)) {
|
||||
throw new Error(`Method ${method} is a streaming method and should be handled with handleStreamingRequest`)
|
||||
}
|
||||
throw new Error(`Unknown ${this.serviceName} method: ${method}`)
|
||||
}
|
||||
|
||||
return handler(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a streaming service request
|
||||
* @param method The method name
|
||||
* @param message The request message
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The request ID for correlation and cleanup
|
||||
*/
|
||||
async handleStreamingRequest(
|
||||
method: string,
|
||||
message: any,
|
||||
responseStream: StreamingResponseHandler,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const handler = this.streamingMethodRegistry[method]
|
||||
|
||||
if (!handler) {
|
||||
if (this.methodRegistry[method]) {
|
||||
throw new Error(`Method ${method} is not a streaming method and should be handled with handleRequest`)
|
||||
}
|
||||
throw new Error(`Unknown ${this.serviceName} streaming method: ${method}`)
|
||||
}
|
||||
|
||||
await handler(message, responseStream, requestId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a service registry factory function
|
||||
* @param serviceName The name of the service
|
||||
* @returns An object with register and handle functions
|
||||
*/
|
||||
export function createServiceRegistry(serviceName: string) {
|
||||
const registry = new ServiceRegistry(serviceName)
|
||||
|
||||
return {
|
||||
registerMethod: (methodName: string, handler: ServiceMethodHandler | StreamingMethodHandler, metadata?: MethodMetadata) =>
|
||||
registry.registerMethod(methodName, handler, metadata),
|
||||
|
||||
handleRequest: (method: string, message: any) => registry.handleRequest(method, message),
|
||||
|
||||
handleStreamingRequest: (method: string, message: any, responseStream: StreamingResponseHandler, requestId?: string) =>
|
||||
registry.handleStreamingRequest(method, message, responseStream, requestId),
|
||||
|
||||
isStreamingMethod: (method: string) => registry.isStreamingMethod(method),
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Uri } from "../../../src/shared/proto/host/uri"
|
||||
import { StringRequest } from "../../../src/shared/proto/common"
|
||||
|
||||
/**
|
||||
* Creates a file URI from a file path
|
||||
* @param request The request containing the file path
|
||||
* @returns A URI object representing the file
|
||||
*/
|
||||
export async function file(request: StringRequest): Promise<Uri> {
|
||||
const uri = vscode.Uri.file(request.value)
|
||||
return Uri.create({
|
||||
scheme: uri.scheme,
|
||||
authority: uri.authority,
|
||||
path: uri.path,
|
||||
query: uri.query,
|
||||
fragment: uri.fragment,
|
||||
fsPath: uri.fsPath,
|
||||
})
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
|
||||
import { StreamingResponseHandler } from "../host-grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create uri service registry
|
||||
const uriService = createServiceRegistry("uri")
|
||||
|
||||
// Export the method handler types and registration function
|
||||
export type UriMethodHandler = ServiceMethodHandler
|
||||
export type UriStreamingMethodHandler = StreamingMethodHandler
|
||||
export const registerMethod = uriService.registerMethod
|
||||
|
||||
// Export the request handlers
|
||||
export const handleUriServiceRequest = uriService.handleRequest
|
||||
export const handleUriServiceStreamingRequest = uriService.handleStreamingRequest
|
||||
export const isStreamingMethod = uriService.isStreamingMethod
|
||||
|
||||
// Register all uri methods
|
||||
registerAllMethods()
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
import { JoinPathRequest, Uri } from "../../../src/shared/proto/host/uri"
|
||||
|
||||
/**
|
||||
* Joins a URI with additional path segments
|
||||
* @param request The request containing the base URI and path segments
|
||||
* @returns A new URI with the path segments joined
|
||||
*/
|
||||
export async function joinPath(request: JoinPathRequest): Promise<Uri> {
|
||||
// Convert proto Uri to vscode.Uri
|
||||
if (!request.base) {
|
||||
throw new Error("Base URI is required")
|
||||
}
|
||||
const baseUri = vscode.Uri.parse(`${request.base.scheme}://${request.base.authority}${request.base.path}`)
|
||||
|
||||
// Join paths
|
||||
const result = vscode.Uri.joinPath(baseUri, ...request.pathSegments)
|
||||
|
||||
// Convert back to proto Uri
|
||||
return Uri.create({
|
||||
scheme: result.scheme,
|
||||
authority: result.authority,
|
||||
path: result.path,
|
||||
query: result.query,
|
||||
fragment: result.fragment,
|
||||
fsPath: result.fsPath,
|
||||
})
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { file } from "./file"
|
||||
import { joinPath } from "./joinPath"
|
||||
import { parse } from "./parse"
|
||||
|
||||
// Register all uri service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("file", file)
|
||||
registerMethod("joinPath", joinPath)
|
||||
registerMethod("parse", parse)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Uri } from "../../../src/shared/proto/host/uri"
|
||||
import { StringRequest } from "../../../src/shared/proto/common"
|
||||
|
||||
/**
|
||||
* Parses a string URI into a Uri object
|
||||
* @param request The request containing the URI string
|
||||
* @returns A URI object representing the parsed URI
|
||||
*/
|
||||
export async function parse(request: StringRequest): Promise<Uri> {
|
||||
const uri = vscode.Uri.parse(request.value)
|
||||
return Uri.create({
|
||||
scheme: uri.scheme,
|
||||
authority: uri.authority,
|
||||
path: uri.path,
|
||||
query: uri.query,
|
||||
fragment: uri.fragment,
|
||||
fsPath: uri.fsPath,
|
||||
})
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
|
||||
import { StreamingResponseHandler } from "../host-grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create watch service registry
|
||||
const watchService = createServiceRegistry("watch")
|
||||
|
||||
// Export the method handler types and registration function
|
||||
export type WatchMethodHandler = ServiceMethodHandler
|
||||
export type WatchStreamingMethodHandler = StreamingMethodHandler
|
||||
export const registerMethod = watchService.registerMethod
|
||||
|
||||
// Export the request handlers
|
||||
export const handleWatchServiceRequest = watchService.handleRequest
|
||||
export const handleWatchServiceStreamingRequest = watchService.handleStreamingRequest
|
||||
export const isStreamingMethod = watchService.isStreamingMethod
|
||||
|
||||
// Register all watch methods
|
||||
registerAllMethods()
|
||||
@@ -1,15 +0,0 @@
|
||||
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"
|
||||
import { subscribeToFile } from "./subscribeToFile"
|
||||
|
||||
// Streaming methods for this service
|
||||
export const streamingMethods = ["subscribeToFile"]
|
||||
|
||||
// Register all watch service methods
|
||||
export function registerAllMethods(): void {
|
||||
// Register each method with the registry
|
||||
registerMethod("subscribeToFile", subscribeToFile, { isStreaming: true })
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as fsSync from "fs"
|
||||
import { SubscribeToFileRequest, FileChangeEvent, FileChangeEvent_ChangeType } from "../../../src/shared/proto/host/watch"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "../host-grpc-handler"
|
||||
|
||||
// Debounce configuration
|
||||
const DEBOUNCE_DELAY = 100 // ms
|
||||
|
||||
// Keep track of active file watchers
|
||||
const fileWatchers = new Map<
|
||||
string,
|
||||
{
|
||||
watcher: fsSync.FSWatcher
|
||||
subscribers: Set<StreamingResponseHandler>
|
||||
lastEventTime: Map<FileChangeEvent_ChangeType, number> // Track last event time by event type
|
||||
}
|
||||
>()
|
||||
|
||||
/**
|
||||
* Subscribe to file changes
|
||||
* @param request The request containing the file path
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request (passed by the gRPC handler)
|
||||
*/
|
||||
export async function subscribeToFile(
|
||||
request: SubscribeToFileRequest,
|
||||
responseStream: StreamingResponseHandler,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const filePath = request.path
|
||||
console.log(`[DEBUG] Setting up file subscription for ${filePath}`)
|
||||
|
||||
try {
|
||||
// We don't send an initial event to avoid triggering handlers immediately
|
||||
console.log(`[DEBUG] Now watching file: ${filePath}`)
|
||||
|
||||
// Set up or reuse file watcher
|
||||
if (!fileWatchers.has(filePath)) {
|
||||
// Create a new watcher for this file using Node.js fs.watch API
|
||||
// This is more reliable than the VSCode FileSystemWatcher for detecting file saves
|
||||
const watcher = fsSync.watch(filePath, { persistent: true }, async (eventType, filename) => {
|
||||
if (eventType === "change") {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf8")
|
||||
console.log(`[DEBUG] File changed: ${filePath}`)
|
||||
|
||||
// Get the watcher info
|
||||
const watcherInfo = fileWatchers.get(filePath)
|
||||
if (watcherInfo) {
|
||||
// Check if this event should be debounced
|
||||
const eventType = FileChangeEvent_ChangeType.CHANGED
|
||||
const now = Date.now()
|
||||
const lastTime = watcherInfo.lastEventTime.get(eventType) || 0
|
||||
|
||||
if (now - lastTime < DEBOUNCE_DELAY) {
|
||||
console.log(
|
||||
`[DEBUG] Debouncing change event for ${filePath} (${now - lastTime}ms since last event)`,
|
||||
)
|
||||
return // Skip this event due to debounce
|
||||
}
|
||||
|
||||
// Update the last event time
|
||||
watcherInfo.lastEventTime.set(eventType, now)
|
||||
|
||||
// Notify all subscribers
|
||||
for (const subscriber of watcherInfo.subscribers) {
|
||||
try {
|
||||
await subscriber({
|
||||
path: filePath,
|
||||
type: eventType,
|
||||
content,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error sending file change event: ${error}`)
|
||||
watcherInfo.subscribers.delete(subscriber)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading changed file: ${error}`)
|
||||
}
|
||||
} else if (eventType === "rename") {
|
||||
// In Node.js fs.watch, 'rename' can mean either creation or deletion
|
||||
// We need to check if the file exists to determine which it is
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
// File exists, so it was created or renamed
|
||||
const content = await fs.readFile(filePath, "utf8")
|
||||
console.log(`[DEBUG] File created/renamed: ${filePath}`)
|
||||
|
||||
// Get the watcher info
|
||||
const watcherInfo = fileWatchers.get(filePath)
|
||||
if (watcherInfo) {
|
||||
// Check if this event should be debounced
|
||||
const eventType = FileChangeEvent_ChangeType.CREATED
|
||||
const now = Date.now()
|
||||
const lastTime = watcherInfo.lastEventTime.get(eventType) || 0
|
||||
|
||||
if (now - lastTime < DEBOUNCE_DELAY) {
|
||||
console.log(
|
||||
`[DEBUG] Debouncing creation event for ${filePath} (${now - lastTime}ms since last event)`,
|
||||
)
|
||||
return // Skip this event due to debounce
|
||||
}
|
||||
|
||||
// Update the last event time
|
||||
watcherInfo.lastEventTime.set(eventType, now)
|
||||
|
||||
// Notify all subscribers
|
||||
for (const subscriber of watcherInfo.subscribers) {
|
||||
try {
|
||||
await subscriber({
|
||||
path: filePath,
|
||||
type: eventType,
|
||||
content,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error sending file creation event: ${error}`)
|
||||
watcherInfo.subscribers.delete(subscriber)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// File doesn't exist, so it was deleted
|
||||
console.log(`[DEBUG] File deleted: ${filePath}`)
|
||||
|
||||
// Get the watcher info
|
||||
const watcherInfo = fileWatchers.get(filePath)
|
||||
if (watcherInfo) {
|
||||
// Check if this event should be debounced
|
||||
const eventType = FileChangeEvent_ChangeType.DELETED
|
||||
const now = Date.now()
|
||||
const lastTime = watcherInfo.lastEventTime.get(eventType) || 0
|
||||
|
||||
if (now - lastTime < DEBOUNCE_DELAY) {
|
||||
console.log(
|
||||
`[DEBUG] Debouncing deletion event for ${filePath} (${now - lastTime}ms since last event)`,
|
||||
)
|
||||
return // Skip this event due to debounce
|
||||
}
|
||||
|
||||
// Update the last event time
|
||||
watcherInfo.lastEventTime.set(eventType, now)
|
||||
|
||||
// Notify all subscribers
|
||||
for (const subscriber of watcherInfo.subscribers) {
|
||||
try {
|
||||
await subscriber({
|
||||
path: filePath,
|
||||
type: eventType,
|
||||
content: "",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Error sending file deletion event: ${error}`)
|
||||
watcherInfo.subscribers.delete(subscriber)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the watcher
|
||||
cleanupWatcher(filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Set up the watcher info
|
||||
const watcherInfo = {
|
||||
watcher,
|
||||
subscribers: new Set<StreamingResponseHandler>(),
|
||||
lastEventTime: new Map<FileChangeEvent_ChangeType, number>(),
|
||||
}
|
||||
|
||||
fileWatchers.set(filePath, watcherInfo)
|
||||
}
|
||||
|
||||
// Add this subscriber to the watcher
|
||||
const watcherInfo = fileWatchers.get(filePath)!
|
||||
watcherInfo.subscribers.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
console.log(`[DEBUG] Cleaning up file subscription for ${filePath}`)
|
||||
const watcherInfo = fileWatchers.get(filePath)
|
||||
if (watcherInfo) {
|
||||
watcherInfo.subscribers.delete(responseStream)
|
||||
|
||||
// If no subscribers left, clean up the watcher
|
||||
if (watcherInfo.subscribers.size === 0) {
|
||||
cleanupWatcher(filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry
|
||||
if (requestId) {
|
||||
getRequestRegistry().registerRequest(
|
||||
requestId,
|
||||
cleanup,
|
||||
{ type: "file_subscription", path: filePath },
|
||||
responseStream,
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error setting up file subscription: ${error}`)
|
||||
// Send an error response
|
||||
await responseStream({
|
||||
path: filePath,
|
||||
type: FileChangeEvent_ChangeType.DELETED,
|
||||
content: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up a file watcher
|
||||
* @param filePath The path of the file to clean up
|
||||
*/
|
||||
function cleanupWatcher(filePath: string): void {
|
||||
const watcherInfo = fileWatchers.get(filePath)
|
||||
if (watcherInfo) {
|
||||
watcherInfo.watcher.close()
|
||||
fileWatchers.delete(filePath)
|
||||
console.log(`[DEBUG] Removed file watcher for ${filePath}`)
|
||||
}
|
||||
}
|
||||
Generated
+1617
-3450
File diff suppressed because it is too large
Load Diff
+12
-68
@@ -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.17.10",
|
||||
"version": "3.17.6",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -46,58 +46,6 @@
|
||||
],
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"walkthroughs": [
|
||||
{
|
||||
"id": "ClineWalkthrough",
|
||||
"title": "Meet Cline, your new coding partner",
|
||||
"description": "Cline codes like a developer because it thinks like one. Here are 5 ways to put it to work:",
|
||||
"steps": [
|
||||
{
|
||||
"id": "welcome",
|
||||
"title": "Start with a Goal, Not Just a Prompt",
|
||||
"description": "Tell Cline what you want to achieve. It plans, asks, and then codes, like a true partner.",
|
||||
"media": {
|
||||
"markdown": "walkthrough/step1.md"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "learn",
|
||||
"title": "Let Cline Learn Your Codebase",
|
||||
"description": "Point Cline to your project. It builds understanding to make smart, context-aware changes.",
|
||||
"media": {
|
||||
"markdown": "walkthrough/step2.md"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "advanced-features",
|
||||
"title": "Always Use the Best AI Models",
|
||||
"description": "Cline empowers you with State-of-the-Art AI, connecting to top models (Anthropic, Gemini, OpenAI & more).",
|
||||
"media": {
|
||||
"markdown": "walkthrough/step3.md"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mcp",
|
||||
"title": "Extend with Powerful Tools (MCP)",
|
||||
"description": "Connect to databases, APIs, or discover new capabilities in the MCP Marketplace.",
|
||||
"media": {
|
||||
"markdown": "walkthrough/step4.md"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "getting-started",
|
||||
"title": "You're Always in Control",
|
||||
"description": "Review Cline's plans and diffs. Approve changes before they happen. No surprises.",
|
||||
"media": {
|
||||
"markdown": "walkthrough/step5.md"
|
||||
},
|
||||
"content": {
|
||||
"path": "walkthrough/step5.md"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
"activitybar": [
|
||||
{
|
||||
@@ -110,7 +58,13 @@
|
||||
"id": "claude-dev-ActivityBar",
|
||||
"title": "Cline (Ctrl+')",
|
||||
"icon": "assets/icons/icon.svg",
|
||||
"when": "!isMac"
|
||||
"when": "isWindows"
|
||||
},
|
||||
{
|
||||
"id": "claude-dev-ActivityBar",
|
||||
"title": "Cline (Ctrl+')",
|
||||
"icon": "assets/icons/icon.svg",
|
||||
"when": "isLinux || !isMac && !isWindows"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -195,11 +149,6 @@
|
||||
"command": "cline.improveCode",
|
||||
"title": "Improve with Cline",
|
||||
"category": "Cline"
|
||||
},
|
||||
{
|
||||
"command": "cline.openWalkthrough",
|
||||
"title": "Open Walkthrough",
|
||||
"category": "Cline"
|
||||
}
|
||||
],
|
||||
"keybindings": [
|
||||
@@ -334,9 +283,9 @@
|
||||
"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 compile-standalone && npm run lint",
|
||||
"pretest": "npm run compile-tests && npm run compile && 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",
|
||||
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts",
|
||||
"format": "prettier . --check",
|
||||
"format:fix": "prettier . --write",
|
||||
"test": "npm-run-all test:unit test:integration",
|
||||
@@ -373,15 +322,13 @@
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@types/vscode": "^1.84.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@typescript-eslint/utils": "^8.33.0",
|
||||
"@typescript-eslint/parser": "^7.11.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",
|
||||
@@ -400,7 +347,7 @@
|
||||
"@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.821.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
@@ -426,7 +373,6 @@
|
||||
"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",
|
||||
@@ -452,14 +398,12 @@
|
||||
"posthog-node": "^4.8.1",
|
||||
"puppeteer-chromium-resolver": "^23.0.0",
|
||||
"puppeteer-core": "^23.4.0",
|
||||
"reconnecting-eventsource": "^1.6.4",
|
||||
"serialize-error": "^11.0.3",
|
||||
"simple-git": "^3.27.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"ts-morph": "^25.0.1",
|
||||
"turndown": "^7.2.0",
|
||||
"vscode-uri": "^3.1.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
|
||||
+4
-257
@@ -6,7 +6,6 @@ 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)
|
||||
@@ -40,25 +39,13 @@ const serviceNameMap = {
|
||||
}
|
||||
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
|
||||
|
||||
// List of host gRPC services (IDE API bridge)
|
||||
// These services are implemented in the IDE extension and called by the standalone Cline Core
|
||||
const hostServiceNameMap = {
|
||||
uri: "host.UriService",
|
||||
watch: "host.WatchService",
|
||||
// Add new host services here
|
||||
}
|
||||
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) => path.join(ROOT_DIR, "hosts", "vscode", serviceKey))
|
||||
|
||||
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")
|
||||
|
||||
// Create output directories if they don't exist
|
||||
// Create output directory if it doesn't exist
|
||||
await fs.mkdir(TS_OUT_DIR, { recursive: true })
|
||||
|
||||
// Clean up existing generated files
|
||||
@@ -73,7 +60,7 @@ async function main() {
|
||||
|
||||
// Process all proto files
|
||||
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
|
||||
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR, realpath: true })
|
||||
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR, realpath: true })
|
||||
|
||||
// Build the protoc command with proper path handling for cross-platform
|
||||
const tsProtocCommand = [
|
||||
@@ -81,8 +68,6 @@ async function main() {
|
||||
`--proto_path="${SCRIPT_DIR}"`,
|
||||
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
|
||||
`--ts_proto_out="${TS_OUT_DIR}"`,
|
||||
"--ts_proto_opt=exportCommonSymbols=false",
|
||||
"--ts_proto_opt=outputIndex=true",
|
||||
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
|
||||
...protoFiles,
|
||||
].join(" ")
|
||||
@@ -96,6 +81,7 @@ async function main() {
|
||||
|
||||
const descriptorOutDir = path.join(ROOT_DIR, "dist-standalone", "proto")
|
||||
await fs.mkdir(descriptorOutDir, { recursive: true })
|
||||
|
||||
const descriptorFile = path.join(descriptorOutDir, "descriptor_set.pb")
|
||||
const descriptorProtocCommand = [
|
||||
protoc,
|
||||
@@ -116,11 +102,8 @@ async function main() {
|
||||
console.log(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
|
||||
|
||||
await generateMethodRegistrations()
|
||||
await generateHostMethodRegistrations()
|
||||
await generateServiceConfig()
|
||||
await generateHostServiceConfig()
|
||||
await generateGrpcClientConfig()
|
||||
await generateHostGrpcClientConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,7 +246,7 @@ async function generateMethodRegistrations() {
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"\n`
|
||||
|
||||
// Import implementations directly
|
||||
// Add imports for all implementation files
|
||||
for (const file of implementationFiles) {
|
||||
const baseName = path.basename(file, ".ts")
|
||||
methodsContent += `import { ${baseName} } from "./${baseName}"\n`
|
||||
@@ -437,242 +420,6 @@ service ${serviceClassName} {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate method registration files for host services
|
||||
*/
|
||||
async function generateHostMethodRegistrations() {
|
||||
console.log(chalk.cyan("Generating host method registration files..."))
|
||||
|
||||
// Parse proto files for streaming methods
|
||||
const hostProtoFiles = await globby("*.proto", { cwd: path.join(SCRIPT_DIR, "host") })
|
||||
const streamingMethodsMap = await parseProtoForStreamingMethods(hostProtoFiles, path.join(SCRIPT_DIR, "host"))
|
||||
|
||||
for (const serviceDir of hostServiceDirs) {
|
||||
try {
|
||||
await fs.access(serviceDir)
|
||||
} catch (error) {
|
||||
console.log(chalk.cyan(`Creating directory ${serviceDir} for new host service`))
|
||||
await fs.mkdir(serviceDir, { recursive: true })
|
||||
}
|
||||
|
||||
const serviceName = path.basename(serviceDir)
|
||||
const registryFile = path.join(serviceDir, "methods.ts")
|
||||
const indexFile = path.join(serviceDir, "index.ts")
|
||||
|
||||
const fullServiceName = hostServiceNameMap[serviceName]
|
||||
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
|
||||
|
||||
console.log(chalk.cyan(`Generating method registrations for host ${serviceName}...`))
|
||||
|
||||
// Get all TypeScript files in the service directory
|
||||
const files = await globby("*.ts", { cwd: serviceDir })
|
||||
|
||||
// Filter out index.ts and methods.ts
|
||||
const implementationFiles = files.filter((file) => file !== "index.ts" && file !== "methods.ts")
|
||||
|
||||
// Create the methods.ts file with header
|
||||
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"\n`
|
||||
|
||||
// Import implementations directly
|
||||
for (const file of implementationFiles) {
|
||||
const baseName = path.basename(file, ".ts")
|
||||
methodsContent += `import { ${baseName} } from "./${baseName}"\n`
|
||||
}
|
||||
|
||||
// Add streaming methods information
|
||||
if (streamingMethods.length > 0) {
|
||||
methodsContent += `\n// Streaming methods for this service
|
||||
export const streamingMethods = ${JSON.stringify(
|
||||
streamingMethods.map((m) => m.name),
|
||||
null,
|
||||
2,
|
||||
)}\n`
|
||||
}
|
||||
|
||||
// Add registration function
|
||||
methodsContent += `\n// Register all ${serviceName} service methods
|
||||
export function registerAllMethods(): void {
|
||||
\t// Register each method with the registry\n`
|
||||
|
||||
// Add registration statements
|
||||
for (const file of implementationFiles) {
|
||||
const baseName = path.basename(file, ".ts")
|
||||
const isStreaming = streamingMethods.some((m) => m.name === baseName)
|
||||
|
||||
if (isStreaming) {
|
||||
methodsContent += `\tregisterMethod("${baseName}", ${baseName}, { isStreaming: true })\n`
|
||||
} else {
|
||||
methodsContent += `\tregisterMethod("${baseName}", ${baseName})\n`
|
||||
}
|
||||
}
|
||||
|
||||
// Close the function
|
||||
methodsContent += `}`
|
||||
|
||||
// Write the methods.ts file
|
||||
await fs.writeFile(registryFile, methodsContent)
|
||||
console.log(chalk.green(`Generated ${registryFile}`))
|
||||
|
||||
// Generate index.ts file
|
||||
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
|
||||
const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
|
||||
import { StreamingResponseHandler } from "../host-grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create ${serviceName} service registry
|
||||
const ${serviceName}Service = createServiceRegistry("${serviceName}")
|
||||
|
||||
// Export the method handler types and registration function
|
||||
export type ${capitalizedServiceName}MethodHandler = ServiceMethodHandler
|
||||
export type ${capitalizedServiceName}StreamingMethodHandler = StreamingMethodHandler
|
||||
export const registerMethod = ${serviceName}Service.registerMethod
|
||||
|
||||
// Export the request handlers
|
||||
export const handle${capitalizedServiceName}ServiceRequest = ${serviceName}Service.handleRequest
|
||||
export const handle${capitalizedServiceName}ServiceStreamingRequest = ${serviceName}Service.handleStreamingRequest
|
||||
export const isStreamingMethod = ${serviceName}Service.isStreamingMethod
|
||||
|
||||
// Register all ${serviceName} methods
|
||||
registerAllMethods()`
|
||||
|
||||
// Write the index.ts file
|
||||
await fs.writeFile(indexFile, indexContent)
|
||||
console.log(chalk.green(`Generated ${indexFile}`))
|
||||
}
|
||||
|
||||
console.log(chalk.green("Host method registration files generated successfully."))
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a service configuration file for host services
|
||||
*/
|
||||
async function generateHostServiceConfig() {
|
||||
console.log(chalk.cyan("Generating host service configuration file..."))
|
||||
|
||||
const serviceImports = []
|
||||
const serviceConfigs = []
|
||||
|
||||
// Add all services from the hostServiceNameMap
|
||||
for (const [dirName, fullServiceName] of Object.entries(hostServiceNameMap)) {
|
||||
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
|
||||
serviceImports.push(
|
||||
`import { handle${capitalizedName}ServiceRequest, handle${capitalizedName}ServiceStreamingRequest } from "./${dirName}/index"`,
|
||||
)
|
||||
serviceConfigs.push(`
|
||||
"${fullServiceName}": {
|
||||
requestHandler: handle${capitalizedName}ServiceRequest,
|
||||
streamingHandler: handle${capitalizedName}ServiceStreamingRequest
|
||||
}`)
|
||||
}
|
||||
|
||||
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { StreamingResponseHandler } from "./host-grpc-handler"
|
||||
${serviceImports.join("\n")}
|
||||
|
||||
/**
|
||||
* Configuration for a host service handler
|
||||
*/
|
||||
export interface HostServiceHandlerConfig {
|
||||
requestHandler: (method: string, message: any) => Promise<any>;
|
||||
streamingHandler: (method: string, message: any, responseStream: StreamingResponseHandler, requestId?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of host service names to their handler configurations
|
||||
*/
|
||||
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {${serviceConfigs.join(",")}
|
||||
};`
|
||||
|
||||
const configPath = path.join(ROOT_DIR, "hosts", "vscode", "host-grpc-service-config.ts")
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true })
|
||||
await fs.writeFile(configPath, content)
|
||||
console.log(chalk.green(`Generated host service configuration at ${configPath}`))
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a gRPC client configuration file for host services
|
||||
*/
|
||||
async function generateHostGrpcClientConfig() {
|
||||
console.log(chalk.cyan("Generating host gRPC client configuration..."))
|
||||
|
||||
const serviceImports = []
|
||||
const serviceClientCreations = []
|
||||
const serviceExports = []
|
||||
|
||||
// Process each service in the hostServiceNameMap
|
||||
for (const [dirName, fullServiceName] of Object.entries(hostServiceNameMap)) {
|
||||
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
|
||||
|
||||
// Add import statement
|
||||
serviceImports.push(`import { ${capitalizedName}ServiceDefinition } from "@shared/proto/host/${dirName}"`)
|
||||
|
||||
// Add client creation
|
||||
serviceClientCreations.push(
|
||||
`const ${capitalizedName}ServiceClient = createGrpcClient(${capitalizedName}ServiceDefinition)`,
|
||||
)
|
||||
|
||||
// Add to exports
|
||||
serviceExports.push(`${capitalizedName}ServiceClient`)
|
||||
}
|
||||
|
||||
// Generate the file content
|
||||
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by proto/build-proto.js
|
||||
|
||||
import { createGrpcClient } from "./host-grpc-client-base"
|
||||
${serviceImports.join("\n")}
|
||||
|
||||
${serviceClientCreations.join("\n")}
|
||||
|
||||
export {
|
||||
${serviceExports.join(",\n\t")}
|
||||
}`
|
||||
|
||||
const configPath = path.join(ROOT_DIR, "src", "standalone", "services", "host-grpc-client.ts")
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true })
|
||||
await fs.writeFile(configPath, content)
|
||||
console.log(chalk.green(`Generated host gRPC client at ${configPath}`))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(chalk.yellow("Could not determine Rosetta installation status. Proceeding anyway."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red("Error:"), error)
|
||||
|
||||
+1
-4
@@ -33,7 +33,7 @@ service FileService {
|
||||
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);
|
||||
rpc selectFiles(EmptyRequest) returns (StringArrays);
|
||||
|
||||
// Convert URIs to workspace-relative paths
|
||||
rpc getRelativePaths(RelativePathsRequest) returns (RelativePaths);
|
||||
@@ -52,9 +52,6 @@ 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
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
// UriService provides methods for working with URIs in the IDE
|
||||
service UriService {
|
||||
// Create a new file URI from a file path
|
||||
rpc file(cline.StringRequest) returns (Uri);
|
||||
|
||||
// Join a URI with additional path segments
|
||||
rpc joinPath(JoinPathRequest) returns (Uri);
|
||||
|
||||
// Parse a string URI into a Uri object
|
||||
rpc parse(cline.StringRequest) returns (Uri);
|
||||
}
|
||||
|
||||
// Uri represents a URI in the IDE
|
||||
message Uri {
|
||||
string scheme = 1;
|
||||
string authority = 2;
|
||||
string path = 3;
|
||||
string query = 4;
|
||||
string fragment = 5;
|
||||
string fsPath = 6;
|
||||
}
|
||||
|
||||
// Request for joining path segments to a URI
|
||||
message JoinPathRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
Uri base = 2;
|
||||
repeated string pathSegments = 3;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
// WatchService provides methods for watching files in the IDE
|
||||
service WatchService {
|
||||
// Subscribe to file changes
|
||||
rpc subscribeToFile(SubscribeToFileRequest) returns (stream FileChangeEvent);
|
||||
}
|
||||
|
||||
// Request to subscribe to file changes
|
||||
message SubscribeToFileRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
string path = 2;
|
||||
}
|
||||
|
||||
// Event representing a file change
|
||||
message FileChangeEvent {
|
||||
enum ChangeType {
|
||||
CREATED = 0;
|
||||
CHANGED = 1;
|
||||
DELETED = 2;
|
||||
}
|
||||
|
||||
string path = 1;
|
||||
ChangeType type = 2;
|
||||
string content = 3; // Optional content of the file after change
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
syntax = "proto3";
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
|
||||
-233
@@ -6,218 +6,6 @@ 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;
|
||||
}
|
||||
|
||||
// Enum for ClineMessage type
|
||||
enum ClineMessageType {
|
||||
ASK = 0;
|
||||
SAY = 1;
|
||||
}
|
||||
|
||||
// Enum for ClineAsk types
|
||||
enum ClineAsk {
|
||||
FOLLOWUP = 0;
|
||||
PLAN_MODE_RESPOND = 1;
|
||||
COMMAND = 2;
|
||||
COMMAND_OUTPUT = 3;
|
||||
COMPLETION_RESULT = 4;
|
||||
TOOL = 5;
|
||||
API_REQ_FAILED = 6;
|
||||
RESUME_TASK = 7;
|
||||
RESUME_COMPLETED_TASK = 8;
|
||||
MISTAKE_LIMIT_REACHED = 9;
|
||||
AUTO_APPROVAL_MAX_REQ_REACHED = 10;
|
||||
BROWSER_ACTION_LAUNCH = 11;
|
||||
USE_MCP_SERVER = 12;
|
||||
NEW_TASK = 13;
|
||||
CONDENSE = 14;
|
||||
REPORT_BUG = 15;
|
||||
}
|
||||
|
||||
// Enum for ClineSay types
|
||||
enum ClineSay {
|
||||
TASK = 0;
|
||||
ERROR = 1;
|
||||
API_REQ_STARTED = 2;
|
||||
API_REQ_FINISHED = 3;
|
||||
TEXT = 4;
|
||||
REASONING = 5;
|
||||
COMPLETION_RESULT_SAY = 6;
|
||||
USER_FEEDBACK = 7;
|
||||
USER_FEEDBACK_DIFF = 8;
|
||||
API_REQ_RETRIED = 9;
|
||||
COMMAND_SAY = 10;
|
||||
COMMAND_OUTPUT_SAY = 11;
|
||||
TOOL_SAY = 12;
|
||||
SHELL_INTEGRATION_WARNING = 13;
|
||||
BROWSER_ACTION_LAUNCH_SAY = 14;
|
||||
BROWSER_ACTION = 15;
|
||||
BROWSER_ACTION_RESULT = 16;
|
||||
MCP_SERVER_REQUEST_STARTED = 17;
|
||||
MCP_SERVER_RESPONSE = 18;
|
||||
USE_MCP_SERVER_SAY = 19;
|
||||
DIFF_ERROR = 20;
|
||||
DELETED_API_REQS = 21;
|
||||
CLINEIGNORE_ERROR = 22;
|
||||
CHECKPOINT_CREATED = 23;
|
||||
LOAD_MCP_DOCUMENTATION = 24;
|
||||
INFO = 25;
|
||||
}
|
||||
|
||||
// Enum for ClineSayTool tool types
|
||||
enum ClineSayToolType {
|
||||
EDITED_EXISTING_FILE = 0;
|
||||
NEW_FILE_CREATED = 1;
|
||||
READ_FILE = 2;
|
||||
LIST_FILES_TOP_LEVEL = 3;
|
||||
LIST_FILES_RECURSIVE = 4;
|
||||
LIST_CODE_DEFINITION_NAMES = 5;
|
||||
SEARCH_FILES = 6;
|
||||
WEB_FETCH = 7;
|
||||
}
|
||||
|
||||
// Enum for browser actions
|
||||
enum BrowserAction {
|
||||
LAUNCH = 0;
|
||||
CLICK = 1;
|
||||
TYPE = 2;
|
||||
SCROLL_DOWN = 3;
|
||||
SCROLL_UP = 4;
|
||||
CLOSE = 5;
|
||||
}
|
||||
|
||||
// Enum for MCP server request types
|
||||
enum McpServerRequestType {
|
||||
USE_MCP_TOOL = 0;
|
||||
ACCESS_MCP_RESOURCE = 1;
|
||||
}
|
||||
|
||||
// Enum for API request cancel reasons
|
||||
enum ClineApiReqCancelReason {
|
||||
STREAMING_FAILED = 0;
|
||||
USER_CANCELLED = 1;
|
||||
RETRIES_EXHAUSTED = 2;
|
||||
}
|
||||
|
||||
// Message for conversation history deleted range
|
||||
message ConversationHistoryDeletedRange {
|
||||
int32 start_index = 1;
|
||||
int32 end_index = 2;
|
||||
}
|
||||
|
||||
// Message for ClineSayTool
|
||||
message ClineSayTool {
|
||||
ClineSayToolType tool = 1;
|
||||
string path = 2;
|
||||
string diff = 3;
|
||||
string content = 4;
|
||||
string regex = 5;
|
||||
string file_pattern = 6;
|
||||
bool operation_is_located_in_workspace = 7;
|
||||
}
|
||||
|
||||
// Message for ClineSayBrowserAction
|
||||
message ClineSayBrowserAction {
|
||||
BrowserAction action = 1;
|
||||
string coordinate = 2;
|
||||
string text = 3;
|
||||
}
|
||||
|
||||
// Message for BrowserActionResult
|
||||
message BrowserActionResult {
|
||||
string screenshot = 1;
|
||||
string logs = 2;
|
||||
string current_url = 3;
|
||||
string current_mouse_position = 4;
|
||||
}
|
||||
|
||||
// Message for ClineAskUseMcpServer
|
||||
message ClineAskUseMcpServer {
|
||||
string server_name = 1;
|
||||
McpServerRequestType type = 2;
|
||||
string tool_name = 3;
|
||||
string arguments = 4;
|
||||
string uri = 5;
|
||||
}
|
||||
|
||||
// Message for ClinePlanModeResponse
|
||||
message ClinePlanModeResponse {
|
||||
string response = 1;
|
||||
repeated string options = 2;
|
||||
string selected = 3;
|
||||
}
|
||||
|
||||
// Message for ClineAskQuestion
|
||||
message ClineAskQuestion {
|
||||
string question = 1;
|
||||
repeated string options = 2;
|
||||
string selected = 3;
|
||||
}
|
||||
|
||||
// Message for ClineAskNewTask
|
||||
message ClineAskNewTask {
|
||||
string context = 1;
|
||||
}
|
||||
|
||||
// Message for API request retry status
|
||||
message ApiReqRetryStatus {
|
||||
int32 attempt = 1;
|
||||
int32 max_attempts = 2;
|
||||
int32 delay_sec = 3;
|
||||
string error_snippet = 4;
|
||||
}
|
||||
|
||||
// Message for ClineApiReqInfo
|
||||
message ClineApiReqInfo {
|
||||
string request = 1;
|
||||
int32 tokens_in = 2;
|
||||
int32 tokens_out = 3;
|
||||
int32 cache_writes = 4;
|
||||
int32 cache_reads = 5;
|
||||
double cost = 6;
|
||||
ClineApiReqCancelReason cancel_reason = 7;
|
||||
string streaming_failed_message = 8;
|
||||
ApiReqRetryStatus retry_status = 9;
|
||||
}
|
||||
|
||||
// Main ClineMessage type
|
||||
message ClineMessage {
|
||||
int64 ts = 1;
|
||||
ClineMessageType type = 2;
|
||||
ClineAsk ask = 3;
|
||||
ClineSay say = 4;
|
||||
string text = 5;
|
||||
string reasoning = 6;
|
||||
repeated string images = 7;
|
||||
repeated string files = 8;
|
||||
bool partial = 9;
|
||||
string last_checkpoint_hash = 10;
|
||||
bool is_checkpoint_checked_out = 11;
|
||||
bool is_operation_outside_workspace = 12;
|
||||
int32 conversation_history_index = 13;
|
||||
ConversationHistoryDeletedRange conversation_history_deleted_range = 14;
|
||||
|
||||
// Additional fields for specific ask/say types
|
||||
ClineSayTool say_tool = 15;
|
||||
ClineSayBrowserAction say_browser_action = 16;
|
||||
BrowserActionResult browser_action_result = 17;
|
||||
ClineAskUseMcpServer ask_use_mcp_server = 18;
|
||||
ClinePlanModeResponse plan_mode_response = 19;
|
||||
ClineAskQuestion ask_question = 20;
|
||||
ClineAskNewTask ask_new_task = 21;
|
||||
ClineApiReqInfo api_req_info = 22;
|
||||
}
|
||||
|
||||
// UiService provides methods for managing UI interactions
|
||||
service UiService {
|
||||
// Scrolls to a specific settings section in the settings view
|
||||
@@ -225,25 +13,4 @@ service UiService {
|
||||
|
||||
// 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);
|
||||
|
||||
// Subscribe to chat button clicked events (when the chat button is clicked in VSCode)
|
||||
rpc subscribeToChatButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to account button click events
|
||||
rpc subscribeToAccountButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to settings button clicked events
|
||||
rpc subscribeToSettingsButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to partial message updates (streaming Cline messages as they're built)
|
||||
rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage);
|
||||
}
|
||||
|
||||
@@ -32,15 +32,13 @@ function generateHandlersAndExports() {
|
||||
handlerSetup.push(` server.addService(proto.cline.${name}.service, {`)
|
||||
for (const [rpcName, rpc] of Object.entries(def.service)) {
|
||||
imports.push(`import { ${rpcName} } from "../core/controller/${dir}/${rpcName}"`)
|
||||
const requestType = "proto.cline." + rpc.requestType.type.name
|
||||
if (rpc.requestStream) {
|
||||
throw new Error("Request streaming is not supported")
|
||||
}
|
||||
if (rpc.responseStream) {
|
||||
handlerSetup.push(` ${rpcName}: wrapStreamingResponse<${requestType},void>(${rpcName}, controller),`)
|
||||
handlerSetup.push(` ${rpcName}: wrapStreamingResponse(${rpcName}, controller),`)
|
||||
} else {
|
||||
const responseType = "proto.cline." + rpc.responseType.type.name
|
||||
handlerSetup.push(` ${rpcName}: wrapper<${requestType},${responseType}>(${rpcName}, controller),`)
|
||||
handlerSetup.push(` ${rpcName}: wrapper(${rpcName}, controller),`)
|
||||
}
|
||||
}
|
||||
handlerSetup.push(` });`)
|
||||
@@ -60,11 +58,11 @@ const scriptName = path.basename(fileURLToPath(import.meta.url))
|
||||
let output = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by ${scriptName}
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as proto from "@/shared/proto"
|
||||
import { Controller } from "../core/controller"
|
||||
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
|
||||
|
||||
${imports}
|
||||
|
||||
export function addServices(
|
||||
server: grpc.Server,
|
||||
proto: any,
|
||||
|
||||
@@ -39,7 +39,7 @@ const output = fs.createWriteStream(zipPath)
|
||||
const archive = archiver("zip", { zlib: { level: 9 } })
|
||||
|
||||
output.on("close", () => {
|
||||
console.log(`Created ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(1)} MB)`)
|
||||
console.log(`Created ${zipPath} (${archive.pointer()} bytes)`)
|
||||
})
|
||||
|
||||
archive.on("error", (err) => {
|
||||
|
||||
@@ -133,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
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
askSageDefaultURL,
|
||||
} from "@shared/api"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
type AskSageRequest = {
|
||||
system_prompt: string
|
||||
@@ -46,7 +45,6 @@ export class AskSageHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
try {
|
||||
const model = this.getModel()
|
||||
|
||||
@@ -120,7 +120,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk?.type) {
|
||||
switch (chunk.type) {
|
||||
case "message_start":
|
||||
const usage = chunk.message.usage
|
||||
yield {
|
||||
@@ -223,19 +223,8 @@ 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(providerOptions)
|
||||
const providerChain = fromNodeProviderChain()
|
||||
return await AwsBedrockHandler.withTempEnv(
|
||||
() => {
|
||||
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
|
||||
|
||||
@@ -6,7 +6,6 @@ import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import axios from "axios"
|
||||
import { OpenRouterErrorResponse } from "./types"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
export class ClineHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -26,7 +25,6 @@ export class ClineHandler implements ApiHandler {
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
this.lastGenerationId = undefined
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
export class DoubaoHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -29,7 +28,6 @@ export class DoubaoHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
|
||||
@@ -171,22 +171,8 @@ export class GeminiHandler implements ApiHandler {
|
||||
|
||||
// Gemini doesn't include status codes in their errors
|
||||
// https://github.com/googleapis/js-genai/blob/61f7f27b866c74333ca6331883882489bcb708b9/src/_api_client.ts#L569
|
||||
const rateLimitPatterns = [
|
||||
/got status: 429/i,
|
||||
/429 Too Many Requests/i,
|
||||
/rate limit exceeded/i,
|
||||
/too many requests/i,
|
||||
]
|
||||
|
||||
const isRateLimit =
|
||||
error.name === "ClientError" && rateLimitPatterns.some((pattern) => pattern.test(error.message))
|
||||
|
||||
if (isRateLimit) {
|
||||
const rateLimitError = Object.assign(new Error(error.message), {
|
||||
...error,
|
||||
status: 429,
|
||||
})
|
||||
throw rateLimitError
|
||||
if (error.name === "ClientError" && error.message.includes("got status: 429 Too Many Requests.")) {
|
||||
;(error as any).status = 429
|
||||
}
|
||||
} else {
|
||||
apiError = String(error)
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ApiHandlerOptions, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults
|
||||
import { ApiHandler } from ".."
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
export class LiteLlmHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -52,7 +51,6 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const formattedMessages = convertToOpenAiMessages(messages)
|
||||
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
export class LmStudioHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -18,7 +17,6 @@ export class LmStudioHandler implements ApiHandler {
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry({ retryAllErrors: true })
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
@@ -29,6 +27,7 @@ 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) {
|
||||
@@ -39,12 +38,6 @@ 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
|
||||
|
||||
@@ -80,9 +80,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.ollamaModelId || "",
|
||||
info: this.options.ollamaApiOptionsCtxNum
|
||||
? { ...openAiModelInfoSaneDefaults, contextWindow: Number(this.options.ollamaApiOptionsCtxNum) || 32768 }
|
||||
: openAiModelInfoSaneDefaults,
|
||||
info: openAiModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,10 +98,6 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
export class QwenHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -49,41 +48,23 @@ export class QwenHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
const isDeepseekReasoner = model.id.includes("deepseek-r1")
|
||||
const isReasoningModelFamily = model.id.includes("qwen3") || ["qwen-plus-latest", "qwen-turbo-latest"].includes(model.id)
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
let temperature: number | undefined = 0
|
||||
// Configuration for extended thinking
|
||||
const budgetTokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = budgetTokens !== 0 ? true : false
|
||||
const thinkingArgs = isReasoningModelFamily
|
||||
? {
|
||||
enable_thinking: reasoningOn,
|
||||
thinking_budget: reasoningOn ? budgetTokens : undefined,
|
||||
}
|
||||
: undefined
|
||||
|
||||
if (isDeepseekReasoner || (reasoningOn && isReasoningModelFamily)) {
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
temperature = undefined
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature,
|
||||
...thinkingArgs,
|
||||
...(model.id === "deepseek-r1" ? {} : { temperature: 0 }),
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
|
||||
@@ -154,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 {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { convertToVsCodeLmMessages } from "@api/transform/vscode-lm-format"
|
||||
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
// Cline does not update VSCode type definitions or engine requirements to maintain compatibility.
|
||||
// This declaration (as seen in src/integrations/TerminalManager.ts) provides types for the Language Model API in newer versions of VSCode.
|
||||
@@ -407,7 +406,6 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
return content
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
// Ensure clean state before starting a new request
|
||||
this.ensureCleanState()
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ApiHandlerOptions, XAIModelId, ModelInfo, xaiDefaultModelId, xaiModels
|
||||
import { convertToOpenAiMessages } from "@api/transform/openai-format"
|
||||
import { ApiStream } from "@api/transform/stream"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
export class XAIHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -19,7 +18,6 @@ export class XAIHandler implements ApiHandler {
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const modelId = this.getModel().id
|
||||
// ensure reasoning effort is either "low" or "high" for grok-3-mini
|
||||
@@ -60,10 +58,10 @@ export class XAIHandler implements ApiHandler {
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
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 = {
|
||||
@@ -12,8 +9,8 @@ type ParsedElementInfo = {
|
||||
}
|
||||
|
||||
export interface ReplacementItem {
|
||||
old_string: string
|
||||
new_string: string
|
||||
old_str: string
|
||||
new_str: string
|
||||
}
|
||||
|
||||
export interface ChangeLocation {
|
||||
@@ -36,48 +33,50 @@ export class StreamingJsonReplacer {
|
||||
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 = new JSONParser({ paths: ["$.replacements.*"] })
|
||||
|
||||
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) {
|
||||
if (value && typeof value === "object" && "old_str" in value && "new_str" 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") {
|
||||
|
||||
if (typeof item.old_str === "string" && typeof item.new_str === "string") {
|
||||
this.successfullyParsedItems.push(item) // Store the structurally valid item
|
||||
|
||||
if (this.currentFileContent.includes(item.old_string)) {
|
||||
if (this.currentFileContent.includes(item.old_str)) {
|
||||
// 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
|
||||
const changeLocation = this.calculateChangeLocation(item.old_str, item.new_str)
|
||||
|
||||
this.currentFileContent = this.currentFileContent.replace(item.old_str, item.new_str)
|
||||
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}"`)
|
||||
const snippet = item.old_str.length > 50 ? item.old_str.substring(0, 47) + "..." : item.old_str
|
||||
const error = new Error(`Streaming Replacement failed: 'old_str' 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
|
||||
}
|
||||
} else if (value && (Array.isArray(value) || (typeof value === "object" && "replacements" in value))) {
|
||||
// This might be the 'replacements' array itself or the root object.
|
||||
// The `paths: ['$.replacements.*']` should mean we only get items.
|
||||
// If we get here, it's likely the root object if paths wasn't specific enough or if it's an empty replacements array.
|
||||
console.log("Streaming parser emitted container:", value)
|
||||
} else {
|
||||
// Value is not a ReplacementItem or a known container, could be an issue with the JSON structure or path.
|
||||
// If `paths` is correct, this path should ideally not be hit often for valid streams.
|
||||
console.warn("Streaming parser emitted unexpected value:", value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,12 +89,8 @@ export class StreamingJsonReplacer {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// Errors during write will be caught by the parser's onError or thrown.
|
||||
this.parser.write(jsonChunk)
|
||||
}
|
||||
|
||||
public getCurrentContent(): string {
|
||||
@@ -109,7 +104,6 @@ export class StreamingJsonReplacer {
|
||||
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 }
|
||||
@@ -117,29 +111,25 @@ export class StreamingJsonReplacer {
|
||||
|
||||
// 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 = {
|
||||
return {
|
||||
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,10 +1,3 @@
|
||||
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
|
||||
@@ -157,11 +150,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
|
||||
@@ -250,23 +243,23 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
|
||||
const lastLine = lines[lines.length - 1]
|
||||
if (
|
||||
lines.length > 0 &&
|
||||
(lastLine.startsWith(SEARCH_BLOCK_CHAR) || lastLine.startsWith("=") || lastLine.startsWith(REPLACE_BLOCK_CHAR)) &&
|
||||
lastLine !== SEARCH_BLOCK_START &&
|
||||
lastLine !== SEARCH_BLOCK_END &&
|
||||
lastLine !== REPLACE_BLOCK_END
|
||||
(lastLine.startsWith("<") || lastLine.startsWith("=") || lastLine.startsWith(">")) &&
|
||||
lastLine !== "<<<<<<< SEARCH" &&
|
||||
lastLine !== "=======" &&
|
||||
lastLine !== ">>>>>>> REPLACE"
|
||||
) {
|
||||
lines.pop()
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
if (line === SEARCH_BLOCK_START) {
|
||||
if (line === "<<<<<<< SEARCH") {
|
||||
inSearch = true
|
||||
currentSearchContent = ""
|
||||
currentReplaceContent = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if (line === SEARCH_BLOCK_END) {
|
||||
if (line === "=======") {
|
||||
inSearch = false
|
||||
inReplace = true
|
||||
|
||||
@@ -327,7 +320,7 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
|
||||
continue
|
||||
}
|
||||
|
||||
if (line === REPLACE_BLOCK_END) {
|
||||
if (line === ">>>>>>> REPLACE") {
|
||||
// Finished one replace block
|
||||
|
||||
// // Remove the artificially added linebreak in the last line of the REPLACE block
|
||||
@@ -487,7 +480,7 @@ class NewFileContentConstructor {
|
||||
pendingNonStandardLineLimit: number,
|
||||
): number {
|
||||
let removeLineCount = 0
|
||||
if (line === SEARCH_BLOCK_START) {
|
||||
if (line === "<<<<<<< SEARCH") {
|
||||
removeLineCount = this.trimPendingNonStandardTrailingEmptyLines(pendingNonStandardLineLimit)
|
||||
if (removeLineCount > 0) {
|
||||
pendingNonStandardLineLimit = pendingNonStandardLineLimit - removeLineCount
|
||||
@@ -497,7 +490,7 @@ class NewFileContentConstructor {
|
||||
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
|
||||
}
|
||||
this.activateSearchState()
|
||||
} else if (line === SEARCH_BLOCK_END) {
|
||||
} else if (line === "=======") {
|
||||
// 校验非标内容
|
||||
if (!this.isSearchingActive()) {
|
||||
this.tryFixSearchBlock(pendingNonStandardLineLimit)
|
||||
@@ -505,7 +498,7 @@ class NewFileContentConstructor {
|
||||
}
|
||||
this.activateReplaceState()
|
||||
this.beforeReplace()
|
||||
} else if (line === REPLACE_BLOCK_END) {
|
||||
} else if (line === ">>>>>>> REPLACE") {
|
||||
if (!this.isReplacingActive()) {
|
||||
this.tryFixReplaceBlock(pendingNonStandardLineLimit)
|
||||
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
|
||||
@@ -528,6 +521,7 @@ class NewFileContentConstructor {
|
||||
} else {
|
||||
let appendToPendingNonStandardLines = canWritependingNonStandardLines
|
||||
if (appendToPendingNonStandardLines) {
|
||||
console.log("unstandard line:" + line)
|
||||
// 处理非标内容
|
||||
this.pendingNonStandardLines.push(line)
|
||||
}
|
||||
@@ -612,11 +606,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_BLOCK_START
|
||||
fixLines[0] = "<<<<<<< SEARCH"
|
||||
for (const line of fixLines) {
|
||||
removeLineCount += this.internalProcessLine(line, false, searchTagIndex)
|
||||
}
|
||||
@@ -644,7 +638,7 @@ class NewFileContentConstructor {
|
||||
// removeLineCount += this.tryFixSearchBlock(replaceBeginTagIndex)
|
||||
// }
|
||||
let fixLines = this.pendingNonStandardLines.slice(replaceBeginTagIndex - removeLineCount, lineLimit - removeLineCount)
|
||||
fixLines[0] = SEARCH_BLOCK_END
|
||||
fixLines[0] = "======="
|
||||
for (const line of fixLines) {
|
||||
removeLineCount += this.internalProcessLine(line, false, replaceBeginTagIndex - removeLineCount)
|
||||
}
|
||||
@@ -663,7 +657,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) {
|
||||
@@ -672,7 +666,7 @@ class NewFileContentConstructor {
|
||||
// removeLineCount += this.tryFixReplaceBlock(replaceEndTagIndex)
|
||||
// }
|
||||
let fixLines = this.pendingNonStandardLines.slice(replaceEndTagIndex - removeLineCount, lineLimit - removeLineCount)
|
||||
fixLines[fixLines.length - 1] = REPLACE_BLOCK_END
|
||||
fixLines[fixLines.length - 1] = ">>>>>>> REPLACE"
|
||||
for (const line of fixLines) {
|
||||
removeLineCount += this.internalProcessLine(line, false, replaceEndTagIndex - removeLineCount)
|
||||
}
|
||||
@@ -712,10 +706,10 @@ export async function constructNewFileContentV2(diffContent: string, originalCon
|
||||
const lastLine = lines[lines.length - 1]
|
||||
if (
|
||||
lines.length > 0 &&
|
||||
(lastLine.startsWith(SEARCH_BLOCK_CHAR) || lastLine.startsWith("=") || lastLine.startsWith(REPLACE_BLOCK_CHAR)) &&
|
||||
lastLine !== SEARCH_BLOCK_START &&
|
||||
lastLine !== SEARCH_BLOCK_END &&
|
||||
lastLine !== REPLACE_BLOCK_END
|
||||
(lastLine.startsWith("<") || lastLine.startsWith("=") || lastLine.startsWith(">")) &&
|
||||
lastLine !== "<<<<<<< SEARCH" &&
|
||||
lastLine !== "=======" &&
|
||||
lastLine !== ">>>>>>> REPLACE"
|
||||
) {
|
||||
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, parseAssistantMessageV3 } from "./parse-assistant-message"
|
||||
export { parseAssistantMessageV1, parseAssistantMessageV2 } from "./parse-assistant-message"
|
||||
|
||||
export interface TextContent {
|
||||
type: "text"
|
||||
@@ -27,7 +27,6 @@ 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,621 +473,3 @@ 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
|
||||
}
|
||||
|
||||
@@ -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, _: EmptyRequest): Promise<String> {
|
||||
export async function accountLoginClicked(controller: Controller, unused: EmptyRequest): Promise<String> {
|
||||
// Generate nonce for state validation
|
||||
const nonce = crypto.randomBytes(32).toString("hex")
|
||||
await storeSecret(controller.context, "authNonce", nonce)
|
||||
@@ -27,7 +27,7 @@ export async function accountLoginClicked(controller: Controller, _: EmptyReques
|
||||
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
|
||||
)
|
||||
await vscode.env.openExternal(authUrl)
|
||||
return String.create({
|
||||
return {
|
||||
value: authUrl.toString(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import type { 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 Empty.create({})
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -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 BrowserConnection.create({
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully discovered and connected to Chrome at ${discoveredHost}`,
|
||||
endpoint: result.endpoint || "",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return BrowserConnection.create({
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"No Chrome instances found. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
|
||||
endpoint: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return BrowserConnection.create({
|
||||
return {
|
||||
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, _: EmptyRequest): Promise<BrowserConnectionInfo> {
|
||||
export async function getBrowserConnectionInfo(controller: Controller, request: 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, _: EmptyR
|
||||
const connectionInfo = browserSession.getConnectionInfo()
|
||||
|
||||
// Convert from BrowserSession.BrowserConnectionInfo to proto.BrowserConnectionInfo
|
||||
return BrowserConnectionInfo.create({
|
||||
return {
|
||||
isConnected: connectionInfo.isConnected,
|
||||
isRemote: connectionInfo.isRemote,
|
||||
host: connectionInfo.host || "", // Ensure host is never undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to browser settings if no active browser session
|
||||
return BrowserConnectionInfo.create({
|
||||
return {
|
||||
isConnected: false,
|
||||
isRemote: !!browserSettings.remoteBrowserEnabled,
|
||||
host: browserSettings.remoteBrowserHost || "",
|
||||
})
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error("Error getting browser connection info:", error)
|
||||
return BrowserConnectionInfo.create({
|
||||
return {
|
||||
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, _: EmptyRequest): Promise<ChromePath> {
|
||||
export async function getDetectedChromePath(controller: Controller, request: EmptyRequest): Promise<ChromePath> {
|
||||
try {
|
||||
const { browserSettings } = await getAllExtensionState(controller.context)
|
||||
const browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
const result = await browserSession.getDetectedChromePath()
|
||||
|
||||
return ChromePath.create({
|
||||
return {
|
||||
path: result.path,
|
||||
isBundled: result.isBundled,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting detected Chrome path:", error)
|
||||
return ChromePath.create({
|
||||
return {
|
||||
path: "",
|
||||
isBundled: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, _: EmptyRequest): Promise<StringMessage> {
|
||||
export async function relaunchChromeDebugMode(controller: Controller, request: 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, _: EmptyRe
|
||||
|
||||
// The actual result will be sent via postMessageToWebview in the BrowserSession.relaunchChromeDebugMode method
|
||||
// Here we just return a message as a placeholder
|
||||
return StringMessage.create({
|
||||
return {
|
||||
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 BrowserConnection.create({
|
||||
return {
|
||||
success: result.success,
|
||||
message: `Auto-discovered and tested connection to Chrome at ${discoveredHost}: ${result.message}`,
|
||||
endpoint: result.endpoint || "",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return BrowserConnection.create({
|
||||
return {
|
||||
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 BrowserConnection.create({
|
||||
return {
|
||||
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 BrowserConnection.create({
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
endpoint: result.endpoint || "",
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return BrowserConnection.create({
|
||||
return {
|
||||
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 Boolean.create({
|
||||
return {
|
||||
value: true,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating browser settings:", error)
|
||||
return Boolean.create({
|
||||
return {
|
||||
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 Empty.create({})
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ import { RelativePathsRequest, RelativePaths } from "@shared/proto/file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { UriServiceClient } from "../../../standalone/services/host-grpc-client"
|
||||
import { Metadata, StringRequest } from "@shared/proto/common"
|
||||
|
||||
/**
|
||||
* Converts a list of URIs to workspace-relative paths
|
||||
@@ -19,15 +17,7 @@ export const getRelativePaths: FileMethodHandler = async (
|
||||
const resolvedPaths = await Promise.all(
|
||||
request.uris.map(async (uriString) => {
|
||||
try {
|
||||
// Use the host URI service client instead of directly using vscode.Uri.parse
|
||||
const parseResponse = await UriServiceClient.parse(
|
||||
StringRequest.create({
|
||||
metadata: Metadata.create({}),
|
||||
value: uriString,
|
||||
}),
|
||||
)
|
||||
const fileUri = vscode.Uri.parse(`${parseResponse.scheme}://${parseResponse.authority}${parseResponse.path}`)
|
||||
console.log("[DEBUG] UriServiceClient.parse:", fileUri)
|
||||
const fileUri = vscode.Uri.parse(uriString, true)
|
||||
const relativePathToGet = vscode.workspace.asRelativePath(fileUri, false)
|
||||
|
||||
// If the path is still absolute, it's outside the workspace
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -18,14 +18,14 @@ export async function refreshRules(controller: Controller, _request: EmptyReques
|
||||
const { cursorLocalToggles, windsurfLocalToggles } = await refreshExternalRulesToggles(controller.context, cwd)
|
||||
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(controller.context, cwd)
|
||||
|
||||
return RefreshedRules.create({
|
||||
return {
|
||||
globalClineRulesToggles: { toggles: globalToggles },
|
||||
localClineRulesToggles: { toggles: localToggles },
|
||||
localCursorRulesToggles: { toggles: cursorLocalToggles },
|
||||
localWindsurfRulesToggles: { toggles: windsurfLocalToggles },
|
||||
localWorkflowToggles: { toggles: localWorkflowToggles },
|
||||
globalWorkflowToggles: { toggles: globalWorkflowToggles },
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh rules:", error)
|
||||
throw error
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ToggleClineRules } from "../../../shared/proto/file"
|
||||
import type { ToggleClineRuleRequest } from "../../../shared/proto/file"
|
||||
import type { ToggleClineRuleRequest, ClineRulesToggles, ToggleClineRules } 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"
|
||||
@@ -37,8 +36,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 ToggleClineRules.create({
|
||||
return {
|
||||
globalClineRulesToggles: { toggles: globalToggles },
|
||||
localClineRulesToggles: { toggles: localToggles },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ToggleCursorRuleRequest } from "../../../shared/proto/file"
|
||||
import { ClineRulesToggles } from "../../../shared/proto/file"
|
||||
import type { ToggleCursorRuleRequest, 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"
|
||||
@@ -29,7 +28,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 ClineRulesToggles.create({
|
||||
return {
|
||||
toggles: cursorToggles,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ToggleWindsurfRuleRequest } from "../../../shared/proto/file"
|
||||
import { ClineRulesToggles } from "../../../shared/proto/file"
|
||||
import type { ToggleWindsurfRuleRequest, 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"
|
||||
@@ -27,5 +26,5 @@ export async function toggleWindsurfRule(controller: Controller, request: Toggle
|
||||
await updateWorkspaceState(controller.context, "localWindsurfRulesToggles", toggles)
|
||||
|
||||
// Return the toggles directly
|
||||
return ClineRulesToggles.create({ toggles: toggles })
|
||||
return { toggles: toggles }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import axios from "axios"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
import fs from "fs/promises"
|
||||
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
@@ -53,9 +52,7 @@ 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 { sendChatButtonClickedEvent } from "./ui/subscribeToChatButtonClicked"
|
||||
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"
|
||||
@@ -67,7 +64,6 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
|
||||
*/
|
||||
|
||||
export class Controller {
|
||||
readonly id: string = uuidv4()
|
||||
private postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined
|
||||
|
||||
private disposables: vscode.Disposable[] = []
|
||||
@@ -112,7 +108,9 @@ 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) {
|
||||
@@ -121,6 +119,7 @@ export class Controller {
|
||||
}
|
||||
this.workspaceTracker.dispose()
|
||||
this.mcpHub.dispose()
|
||||
this.outputChannel.appendLine("Disposed all disposables")
|
||||
|
||||
console.error("Controller disposed")
|
||||
}
|
||||
@@ -151,7 +150,6 @@ export class Controller {
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
enableCheckpointsSetting,
|
||||
isNewUser,
|
||||
taskHistory,
|
||||
@@ -186,7 +184,6 @@ export class Controller {
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled ?? true,
|
||||
enableCheckpointsSetting ?? true,
|
||||
customInstructions,
|
||||
task,
|
||||
@@ -266,14 +263,14 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Initialize telemetry service with user's current setting
|
||||
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!)
|
||||
@@ -294,6 +291,11 @@ export class Controller {
|
||||
}
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "optionsResponse":
|
||||
if (this.task) {
|
||||
await this.task.handleWebviewAskResponse("messageResponse", message.text || "", [])
|
||||
}
|
||||
break
|
||||
case "fetchUserCreditsData": {
|
||||
await this.fetchUserCreditsData()
|
||||
break
|
||||
@@ -403,15 +405,6 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
// terminal settings
|
||||
if (typeof message.shellIntegrationTimeout === "number") {
|
||||
await updateGlobalState(this.context, "shellIntegrationTimeout", message.shellIntegrationTimeout)
|
||||
}
|
||||
|
||||
if (typeof message.terminalReuseEnabled === "boolean") {
|
||||
await updateGlobalState(this.context, "terminalReuseEnabled", message.terminalReuseEnabled)
|
||||
}
|
||||
|
||||
// after settings are updated, post state to webview
|
||||
await this.postStateToWebview()
|
||||
|
||||
@@ -936,7 +929,10 @@ export class Controller {
|
||||
input += `\nProblems:\n${problemsString}`
|
||||
}
|
||||
|
||||
await sendAddToInputEvent(input)
|
||||
await this.postMessageToWebview({
|
||||
type: "addToInput",
|
||||
text: input,
|
||||
})
|
||||
|
||||
console.log("addSelectedCodeToChat", code, filePath, languageId)
|
||||
}
|
||||
@@ -954,7 +950,10 @@ export class Controller {
|
||||
// terminalName
|
||||
// })
|
||||
|
||||
await sendAddToInputEvent(`Terminal output:\n\`\`\`\n${output}\n\`\`\``)
|
||||
await this.postMessageToWebview({
|
||||
type: "addToInput",
|
||||
text: `Terminal output:\n\`\`\`\n${output}\n\`\`\``,
|
||||
})
|
||||
|
||||
console.log("addSelectedTerminalOutputToChat", output, terminalName)
|
||||
}
|
||||
@@ -1039,6 +1038,18 @@ export class Controller {
|
||||
throw new Error("Task not found")
|
||||
}
|
||||
|
||||
async showTaskWithId(id: string) {
|
||||
if (id !== this.task?.taskId) {
|
||||
// non-current task
|
||||
const { historyItem } = await this.getTaskWithId(id)
|
||||
await this.initTask(undefined, undefined, undefined, historyItem) // clears existing task
|
||||
}
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}
|
||||
|
||||
async exportTaskWithId(id: string) {
|
||||
const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
|
||||
await downloadTask(historyItem.ts, apiConversationHistory)
|
||||
@@ -1182,7 +1193,6 @@ export class Controller {
|
||||
globalClineRulesToggles,
|
||||
globalWorkflowToggles,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
isNewUser,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
@@ -1227,7 +1237,6 @@ export class Controller {
|
||||
localWorkflowToggles: localWorkflowToggles || {},
|
||||
globalWorkflowToggles: globalWorkflowToggles || {},
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { convertMcpServersToProtoMcpServers } from "@/shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import type { AddRemoteMcpServerRequest } from "../../../shared/proto/mcp"
|
||||
import { McpServers } from "../../../shared/proto/mcp"
|
||||
import type { AddRemoteMcpServerRequest, McpServers } from "../../../shared/proto/mcp"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
@@ -24,7 +23,7 @@ export async function addRemoteMcpServer(controller: Controller, request: AddRem
|
||||
|
||||
const protoServers = convertMcpServersToProtoMcpServers(servers)
|
||||
|
||||
return McpServers.create({ mcpServers: protoServers })
|
||||
return { 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 { McpServers } from "../../../shared/proto/mcp"
|
||||
import type { 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.create({ mcpServers: protoServers })
|
||||
return { mcpServers: protoServers }
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete MCP server: ${error}`)
|
||||
throw error
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
import { McpServer, McpDownloadResponse } from "@shared/mcp"
|
||||
import axios from "axios"
|
||||
import * as vscode from "vscode"
|
||||
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
|
||||
|
||||
/**
|
||||
* Download an MCP server from the marketplace
|
||||
@@ -78,7 +77,10 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
|
||||
// Initialize task and show chat view
|
||||
await controller.initTask(task)
|
||||
await sendChatButtonClickedEvent(controller.id)
|
||||
await controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
|
||||
// Return an empty response - the client only cares if the call succeeded
|
||||
return Empty.create()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { McpMarketplaceCatalog } from "../../../shared/proto/mcp"
|
||||
import type { 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 McpMarketplaceCatalog.create({ items: [] })
|
||||
return { items: [] }
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh MCP marketplace:", error)
|
||||
return McpMarketplaceCatalog.create({ items: [] })
|
||||
return { items: [] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { McpServers } from "@shared/proto/mcp"
|
||||
import type { 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.create({ mcpServers: protoServers })
|
||||
return { mcpServers: protoServers }
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart MCP server ${request.value}:`, 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