Compare commits

...
Author SHA1 Message Date
0xtoshii c5ecc522b5 changeset 2025-05-06 14:45:46 -07:00
0xtoshii 8e2e90dd71 no more updating gemini models 2025-05-06 14:45:04 -07:00
canvrno 06fc419a15 searchFiles protobus migration (#3261) 2025-05-06 11:40:38 -07:00
10f7b8ca9e Migrate custom model configs section new docs (#3304)
* migrate custom model config section

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@mac.mynetworksettings.com>
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-06 11:16:54 -07:00
bc9eaeeff7 Migrate running models locally section new docs (#3305)
* migrate run models locally section

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@mac.mynetworksettings.com>
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-06 11:16:34 -07:00
canvrno 94fc619196 [PROTOBUS] Move exportTaskWithId to protobus (#3285)
* exportTaskWithId protobus migration

* rebase fixes
2025-05-06 11:06:37 -07:00
canvrno 7084e74372 getOllamaModels protobus migration (#3317) 2025-05-06 11:06:01 -07:00
Toshii dd35bce141 Breakpoint just in first user message for gemini for OR and cline provider (#3319)
* breakpoint just in system prompt

* changeset

* user message included
2025-05-06 08:50:22 -07:00
f1ed93add8 Task timeline (#3264)
* v3.14.0 Release Notes

v3.14.0 Release Notes

* Task Timeline

* Task Timeline

* Formatting Plan Mode Respond

* changeset

* Update webview-ui/src/components/chat/TaskTimeline.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Default scroll to right. Change read file colour

* Fixing Colour coding, and adding hover state in tool tip

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-06 15:47:19 +05:30
David Nanyan a43da8d66b Allow users to create issue via CLI with prefilled system and os info (#3250)
* Change bug report template

* it should be text area

* [TRIVIAL] Add npm script for issue creation

* Adjust script & add changeset

* Use cline repo

* remove comment

* open should work on any platform
2025-05-06 13:13:20 +05:30
062bb5bb64 Trevhud/telemetry optimization (#3263)
* add collection method

* collect messages

* changeset

* remove commented out parts

* remove check to send events anytime a new task is created while on an existing task

* Update src/core/controller/task/clearTask.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update src/services/telemetry/TelemetryService.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Lower border radius

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Ara <arafat.da.khan@gmail.com>
2025-05-06 10:52:20 +05:30
Peter Dave Hello b667224c13 Extend ReasoningEffort to non-o3-mini reasoning models for all providers (#3036)
It's somehow locked to o3-mini for some providers, and the description,
should be updated for all OpenAI o series reasoning models.
2025-05-06 10:25:10 +05:30
ffbafab5e2 Migrate prompting folder new docs (#3254)
* migrate prompting section

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@mac.mynetworksettings.com>
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-05 21:07:30 -07:00
Peter Dave Hello 3e9c83b99d Add the o4-mini model in the isOminiModel (#3035) 2025-05-05 20:58:58 -07:00
canvrno 9eea9d04b5 getRelativePaths protobus migration (#3259) 2025-05-05 20:47:06 -07:00
canvrnoandgithub-actions[bot] c83957660a [PROTOBUS] Move ruleFile conversions to /file/ (#3262)
* v3.14.0 Release Notes

* v3.14.0 Release Notes

* move rule file conversions

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-05 19:05:02 -07:00
canvrno cb7234f967 [PROTOBUS] Move deleteTasksWithIds to protobus (#3282)
* deleteTasksWithIDs protobus migration

* Moved deleteTasksWithIds to dedicated message type

* Created common StringArrayRequest

* Delete webview-ui/.vite-port
2025-05-05 19:03:27 -07:00
70 changed files with 3623 additions and 428 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add the o4-mini model in the isOminiModel
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Allow option to collect events to send them in a bundle to avoid sending too many events
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Moved rule file conversions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
getRelativePaths protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
getOllamaModels protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
breakpoint just in system prompt for gemini for OR and cline provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add npm script for issue creation
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Extend ReasoningEffort to non-o3-mini reasoning models for all providers
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
searchFiles protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
updated OR/cline provider to automate gemini models caching
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate prompting section to new docs
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate running models locally section to new docs
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate custom model config section to new docs
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Task Timeline
+8
View File
@@ -47,6 +47,14 @@ body:
placeholder: "e.g., Windows 11, macOS Sonoma, Ubuntu 22.04"
validations:
required: true
- type: textarea
id: system-info
attributes:
label: System Info
description: What system information is relevant to the issue?
placeholder: "e.g., CPU: Intel Core i7-11700K, GPU: NVIDIA GeForce RTX 3070, RAM: 32GB DDR4"
validations:
required: true
- type: input
id: cline-version
attributes:
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
uses: ./.github/workflows/test.yml
publish:
needs: test
# needs: test
name: Publish Extension
runs-on: ubuntu-latest
environment: publish
@@ -0,0 +1,121 @@
---
title: "AWS Bedrock"
description: "Learn how to set up AWS Bedrock with Cline using credentials authentication. This guide covers AWS environment setup, regional access verification, and secure integration with the Cline VS Code extension."
---
### Overview
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Titan) through AWS.\
[Learn more about AWS Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html).
- **Cline:** A VS Code extension that acts as a coding assistant by integrating with AI models—empowering developers to generate code, debug, and analyze data.
- **Enterprise Focus:** This guide is tailored for organizations with established AWS environments (using IAM roles, AWS SSO, AWS Organizations, etc.) to ensure secure and compliant usage.
---
### Step 1: Prepare Your AWS Environment
#### 1.1 Create or Use an IAM Role/User
1. **Sign in to the AWS Management Console:**\
[AWS Console](https://aws.amazon.com/console/)
2. **Access IAM:**
- Search for **IAM (Identity and Access Management)** in the AWS Console.
- Either create a new IAM user or use your enterprise's AWS SSO to assume a dedicated role for Bedrock access.
- [AWS IAM User Guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html)
#### 1.2 Attach the Required Policies
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.
---
### Step 2: Verify Regional and Model Access
#### 2.1 Choose and Confirm a Region
1. **Select a Region:**\
AWS Bedrock is available in multiple regions (e.g., US East, Europe, Asia Pacific). Choose the region that meets your latency and compliance needs.\
[AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
2. **Verify Model Access:**
- In the AWS Bedrock console, confirm that the models your team requires (e.g., Anthropic Claude, Amazon Titan) are marked as "Access granted."
- **Note:** Some advanced models might require an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html) if not available on-demand.
#### 2.2 Set Up AWS Marketplace Subscriptions (if needed)
1. **Subscribe to Third-Party Models:**
- Navigate to the AWS Bedrock console and locate the model subscription section.
- For models from third-party providers (e.g., Anthropic), accept the terms to subscribe.
- [AWS Marketplace](https://aws.amazon.com/marketplace/)
2. **Enterprise Tip:**
- Model subscriptions are often managed centrally. Confirm with your cloud team if a standard subscription process is in place.
---
### Step 3: Configure the Cline VS Code Extension
#### 3.1 Install and Open Cline
1. **Install VS Code:**\
Download from the [VS Code website](https://code.visualstudio.com/).
2. **Install the Cline Extension:**
- Open VS Code.
- Go to the Extensions Marketplace (`Ctrl+Shift+X` or `Cmd+Shift+X`).
- Search for **Cline** and install it.
#### 3.2 Configure Cline Settings
1. **Open Cline Settings:**
- Click on the settings ⚙️ to select your API Provider.
2. **Select AWS Bedrock as the API Provider:**
- From the API Provider dropdown, choose **AWS Bedrock**.
3. **Enter Your AWS Credentials:**
- Input your **Access Key** and **Secret Key** (or use temporary credentials if using AWS SSO).
- Specify the correct **AWS Region** (e.g., `us-east-1` or your enterprise-approved region).
4. **Select a Model:**
- Choose an on-demand model (e.g., **anthropic.claude-3-5-sonnet-20241022-v2:0**).
5. **Save and Test:**
- Click **Done/Save** to apply your settings.
- Test the integration by sending a simple prompt (e.g., "Generate a Python function to check if a number is prime.").
---
### Step 4: Security, Monitoring, and Best Practices
1. **Secure Access:**
- Prefer AWS SSO/federated roles over long-lived IAM credentials.
- [AWS IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)
2. **Enhance Network Security:**
- Consider setting up [AWS PrivateLink](https://docs.aws.amazon.com/vpc/latest/userguide/endpoint-services-overview.html) to securely connect to Bedrock.
3. **Monitor and Log Activity:**
- Enable AWS CloudTrail to log Bedrock API calls.
- Use CloudWatch to monitor metrics like invocation count, latency, and token usage.
- Set up alerts for abnormal activity.
4. **Handle Errors and Manage Costs:**
- Implement exponential backoff for throttling errors.
- Use AWS Cost Explorer and set billing alerts to track usage.\
[AWS Cost Management](https://docs.aws.amazon.com/cost-management/latest/userguide/what-is-aws-cost-management.html)
5. **Regular Audits and Compliance:**
- Periodically review IAM roles and CloudTrail logs.
- Follow internal data privacy and governance policies.
---
### Conclusion
By following these steps, your enterprise team can securely integrate AWS Bedrock with the Cline VS Code extension to accelerate development:
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockFullAccess` policy, and ensure necessary permissions.
2. **Verify Region and Model Access:** Confirm that your selected region supports your required models and subscribe via AWS Marketplace if needed.
3. **Configure Cline in VS Code:** Install and set up Cline with your AWS credentials and choose an appropriate model.
4. **Implement Security and Monitoring:** Use best practices for IAM, network security, monitoring, and cost management.
For further details, consult the [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) and coordinate with your internal cloud team. Happy coding!
---
_This guide will be updated as AWS Bedrock and Cline evolve. Always refer to the latest documentation and internal policies for up-to-date practices._
@@ -0,0 +1,42 @@
---
title: "AWS Bedrock w/ Profile Authentication"
description: "Learn how to configure AWS Bedrock to use AWS Profiles for authentication with Cline, focusing on SSO/Federated roles for secure access."
---
### Overview
Cline offers the option of utilizing AWS credentials or AWS profiles to access AWS Bedrock services. SSO/Federated roles are suggested over Legacy IAM configuration; this guide describes how to configure your environment so that Cline uses SSO roles for authentication.
---
### Configuration Steps
1. Install the [latest version](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) of AWS CLI
- Follow the AWS docs to install your OS-specific version of AWS CLI
2. [Configure IAM authentication](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html) with the AWS CLI
- If you do not already have AWS access through the IAM Identity Center, follow the [IAM User Guide](https://docs.aws.amazon.com/singlesignon/latest/userguide/getting-started.html) to set up IAM users and roles. Ensure you have a `PowerUserAccess` role.
- If you have access to AWS through your employer, open your AWS access portal and find the appropriate account. Ensure you have `PowerUserAccess` permissions.
- Open the `Access keys` link and note the `SSO start URL` and `SSO region`, which are needed in the next step
3. Continue configuring your profile using [the `aws configure sso` CLI wizard](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html#cli-configure-sso-configure)
- Once configured, use the following command to authenticate the AWS CLI: `aws sso login --profile <AWS-profile-name>`
- Note which profile name you attach to your AWS account, this is needed to configure Cline in the following steps
4. If you haven't already done so, install VSCode and the Cline extension. Consult the [Getting Started](/getting-started) page for guidance.
5. Open the Cline extension, then click on the settings button ⚙️ to select your API Provider.
- From the API Provider dropdown, select AWS Bedrock
- Select the AWS Profile radio button, then enter the AWS Profile Name from step 3
- Select your AWS Region from the dropdown menu
- Selecting the cross-region inference checkbox is required for some models
<Frame>
<img
src="/assets/robot_panel_dark.png"
alt="AWS Bedrock configuration in Cline settings showing profile authentication setup"
/>
</Frame>
+206
View File
@@ -0,0 +1,206 @@
---
title: "GCP Vertex AI"
description: "Configure GCP Vertex AI with Cline to access leading generative AI models like Claude 3.5 Sonnet v2. This guide covers GCP environment setup, authentication, and secure integration for enterprise teams."
---
### Overview
**GCP Vertex AI:**\
A fully managed service that provides access to leading generative AI models—such as Anthropic's Claude 3.5 Sonnet v2—through Google Cloud.\
[Learn more about GCP Vertex AI](https://cloud.google.com/vertex-ai).
This guide is tailored for organizations with established GCP environments (leveraging IAM roles, service accounts, and best practices in resource management) to ensure secure and compliant usage.
---
### Step 1: Prepare Your GCP Environment
#### 1.1 Create or Use a GCP Project
- **Sign in to the GCP Console:**\
[Google Cloud Console](https://console.cloud.google.com/)
- **Select or Create a Project:**\
Use an existing project or create a new one dedicated to Vertex AI.
#### 1.2 Set Up IAM Permissions and Service Accounts
- **Assign Required Roles:**
- Grant your user (or service account) the **Vertex AI User** role (`roles/aiplatform.user`)
- For service accounts, also attach the **Vertex AI Service Agent** role (`roles/aiplatform.serviceAgent`) to enable certain operations
- Consider additional predefined roles as needed:
- Vertex AI Platform Express Admin
- Vertex AI Platform Express User
- Vertex AI Migration Service User
- **Cross-Project Resource Access:**
- For BigQuery tables in different projects, assign the **BigQuery Data Viewer** role
- For Cloud Storage buckets in different projects, assign the **Storage Object Viewer** role
- For external data sources, refer to the [GCP Vertex AI Access Control documentation](https://cloud.google.com/vertex-ai/docs/general/access-control)
---
### Step 2: Verify Regional and Model Access
#### 2.1 Choose and Confirm a Region
Vertex AI supports eight regions. Select a region that meets your latency, compliance, and capacity needs. Examples include:
- **us-east5 (Columbus, Ohio)**
- **us-east1 (South Carolina)**
- **us-east4 (Northern Virginia)**
- **us-central1 (Iowa)**
- **us-west1 (The Dalles, Oregon)**
- **us-west4 (Las Vegas, Nevada)**
- **europe-west1 (Belgium)**
- **asia-southeast1 (Singapore)**
#### 2.2 Enable the Claude 3.5 Sonnet v2 Model
- **Open Vertex AI Model Garden:**\
In the Cloud Console, navigate to **Vertex AI → Model Garden**
- **Enable Claude 3.5 Sonnet v2:**\
Locate the model card for Claude 3.5 Sonnet v2 and click **Enable**
---
### Step 3: Configure the Cline VS Code Extension
#### 3.1 Install and Open Cline
- **Download VS Code:**\
[Download Visual Studio Code](https://code.visualstudio.com/)
- **Install the Cline Extension:**
- Open VS Code
- Navigate to the Extensions Marketplace (Ctrl+Shift+X or Cmd+Shift+X)
- Search for **Cline** and install the extension
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Cline extension in VS Code" />
</Frame>
#### 3.2 Configure Cline Settings
- **Open Cline Settings:**\
Click the settings ⚙️ icon within the Cline extension
- **Set API Provider:**\
Choose **GCP Vertex AI** from the API Provider dropdown
- **Enter Your Google Cloud Project ID:**\
Provide the project ID you set up earlier
- **Select the Region:**\
Choose one of the supported regions (e.g., `us-east5`)
- **Select the Model:**\
From the available list, choose **Claude 3.5 Sonnet v2**
- **Save and Test:**\
Save your settings and test by sending a simple prompt (e.g., "Generate a Python function to check if a number is prime.")
---
### Step 4: Authentication and Credentials Setup
#### Option A: Using Your Google Account (User Credentials)
1. **Install the Google Cloud CLI:**\
Follow the [installation guide](https://cloud.google.com/sdk/docs/install)
2. **Initialize and Authenticate:**
```bash
gcloud init
gcloud auth application-default login
```
- This sets up Application Default Credentials (ADC) using your Google account
3. **Restart VS Code:**\
Ensure VS Code is restarted so that the Cline extension picks up the new credentials
#### Option B: Using a Service Account (JSON Key)
1. **Create a Service Account:**
- In the GCP Console, navigate to **IAM & Admin > Service Accounts**
- Create a new service account (e.g., "vertex-ai-client")
2. **Assign Roles:**
- Attach **Vertex AI User** (`roles/aiplatform.user`)
- Attach **Vertex AI Service Agent** (`roles/aiplatform.serviceAgent`)
- Optionally, add other roles as required
3. **Generate a JSON Key:**
- In the Service Accounts section, manage keys for your service account and download the JSON key
4. **Set the Environment Variable:**
```bash
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account-key.json"
```
- This instructs Google Cloud client libraries (and Cline) to use this key
5. **Restart VS Code:**\
Launch VS Code from a terminal where the `GOOGLE_APPLICATION_CREDENTIALS` variable is set
---
### Step 5: Security, Monitoring, and Best Practices
#### 5.1 Enforce Least Privilege
- **Principle of Least Privilege:**\
Only grant the minimum necessary permissions. Custom roles can offer finer control compared to broad predefined roles
- **Best Practices:**\
Refer to [GCP IAM Best Practices](https://cloud.google.com/iam/docs/best-practices)
#### 5.2 Manage Resource Access
- **Project vs. Resource-Level Access:**\
Access can be managed at both levels. Note that resource-level permissions (e.g., for BigQuery or Cloud Storage) add to, but do not override, project-level policies
#### 5.3 Monitor Usage and Quotas
- **Model Observability Dashboard:**
- In the Vertex AI Console, navigate to the **Model Observability** dashboard
- Monitor metrics such as request throughput, latency, and error rates (including 429 quota errors)
- **Quota Management:**
- If you encounter 429 errors, check the **IAM & Admin > Quotas** page
- Request a quota increase if necessary\
[Learn more about GCP Vertex AI Quotas](https://cloud.google.com/vertex-ai/docs/quotas)
#### 5.4 Service Agents and Cross-Project Considerations
- **Service Agents:**\
Be aware of the different service agents:
- Vertex AI Service Agent
- Vertex AI RAG Data Service Agent
- Vertex AI Custom Code Service Agent
- Vertex AI Extension Service Agent
- **Cross-Project Access:**\
For resources in other projects (e.g., BigQuery, Cloud Storage), ensure that the appropriate roles (BigQuery Data Viewer, Storage Object Viewer) are assigned
---
### Conclusion
By following these steps, your enterprise team can securely integrate GCP Vertex AI with the Cline VS Code extension to harness the power of **Claude 3.5 Sonnet v2**:
- **Prepare Your GCP Environment:**\
Create or use a project, configure IAM with least privilege, and ensure necessary roles (including the Vertex AI Service Agent role) are attached
- **Verify Regional and Model Access:**\
Confirm that your chosen region supports Claude 3.5 Sonnet v2 and that the model is enabled
- **Configure Cline in VS Code:**\
Install Cline, enter your project ID, select the appropriate region, and choose the model
- **Set Up Authentication:**\
Use either user credentials (via `gcloud auth application-default login`) or a service account with a JSON key
- **Implement Security and Monitoring:**\
Adhere to best practices for IAM, manage resource access carefully, and monitor usage with the Model Observability dashboard
For further details, please consult the [GCP Vertex AI Documentation](https://cloud.google.com/vertex-ai/docs) and your internal security policies.\
Happy coding!
_This guide will be updated as GCP Vertex AI and Cline evolve. Always refer to the latest documentation for current practices._
@@ -0,0 +1,65 @@
---
title: "LiteLLM & Cline (using Codestral)"
description: "Learn how to set up and run LiteLLM with Cline using the Codestral model. This guide covers Docker setup, configuration, and integration with Cline."
---
### Using LiteLLM with Cline
This guide demonstrates how to run a demo for LiteLLM starting with the Codestral model for use with Cline.
#### Prerequisites
- [Docker CLI or Docker Desktop](https://www.docker.com/get-started/) installed to run the LiteLLM image locally
- For this example config: A Codestral API Key (different from the Mistral API Keys)
#### Setup
1. **Create a `.env` file and fill in the appropriate field**
```bash
# Tip: Use the following command to generate a random alphanumeric key:
# openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 32
LITELLM_MASTER_KEY=YOUR_LITELLM_MASTER_KEY
CODESTRAL_API_KEY=YOUR_CODESTRAL_API_KEY
```
_Note: Although this is limited to localhost, it's a good practice set LITELLM_MASTER_KEY to something secure_
2. **Configuration**
We'll need to create a `config.yaml` file to contain our LiteLLM configuration. In this case we'll just have one model, 'codestral-latest' and label it 'codestral'
```yaml
model_list:
- model_name: codestral
litellm_params:
model: codestral/codestral-latest
api_key: os.environ/CODESTRAL_API_KEY
```
#### Running the Demo
1. **Startup the LiteLLM docker container**
```bash
docker run \
--env-file .env \
-v $(pwd)/config.yaml:/app/config.yaml \
-p 127.0.0.1:4000:4000 \
ghcr.io/berriai/litellm:main-latest \
--config /app/config.yaml --detailed_debug
```
2. **Setup Cline**
Once the LiteLLM server is up and running you can set it up in Cline:
- Base URL should be `http://0.0.0.0:4000/v1`
- API Key should be the one you set in `.env` for LITELLM_MASTER_KEY
- Model ID is `codestral` or whatever you named it under `config.yaml`
#### Getting Help
- [LiteLLM Documentation](https://docs.litellm.ai/)
- [Mistral AI Console](https://console.mistral.ai/)
- [Cline Discord Community](https://discord.gg/cline)
+21
View File
@@ -66,6 +66,14 @@
"getting-started/what-is-cline"
]
},
{
"group": "Running Models Locally",
"pages": [
"running-models-locally/read-me-first",
"running-models-locally/lm-studio",
"running-models-locally/ollama"
]
},
{
"group": "Exploring Cline's Tools",
"pages": [
@@ -76,6 +84,19 @@
"exploring-clines-tools/remote-browser-support",
"exploring-clines-tools/slash-commands"
]
},
{
"group": "Improving Your Prompting Skills",
"pages": ["prompting/prompt-engineering-guide", "prompting/cline-memory-bank"]
},
{
"group": "Custom Model Configurations",
"pages": [
"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"
]
}
]
},
+369
View File
@@ -0,0 +1,369 @@
---
title: "Cline Memory Bank"
---
## The Complete Guide to Cline Memory Bank
### Quick Setup Guide
To get started with Cline Memory Bank:
1. **Install or Open Cline**
2. **Copy the Custom Instructions** - Use the code block below
3. **Paste into Cline** - Add as custom instructions or in a .clinerules file
4. **Initialize** - Ask Cline to "initialize memory bank"
[See detailed setup instructions](cline-memory-bank.md#getting-started-with-memory-bank)
### Cline Memory Bank Custom Instructions \[COPY THIS]
```
# Cline's Memory Bank
I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional.
## Memory Bank Structure
The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:
flowchart TD
PB[projectbrief.md] --> PC[productContext.md]
PB --> SP[systemPatterns.md]
PB --> TC[techContext.md]
PC --> AC[activeContext.md]
SP --> AC
TC --> AC
AC --> P[progress.md]
### Core Files (Required)
1. `projectbrief.md`
- Foundation document that shapes all other files
- Created at project start if it doesn't exist
- Defines core requirements and goals
- Source of truth for project scope
2. `productContext.md`
- Why this project exists
- Problems it solves
- How it should work
- User experience goals
3. `activeContext.md`
- Current work focus
- Recent changes
- Next steps
- Active decisions and considerations
- Important patterns and preferences
- Learnings and project insights
4. `systemPatterns.md`
- System architecture
- Key technical decisions
- Design patterns in use
- Component relationships
- Critical implementation paths
5. `techContext.md`
- Technologies used
- Development setup
- Technical constraints
- Dependencies
- Tool usage patterns
6. `progress.md`
- What works
- What's left to build
- Current status
- Known issues
- Evolution of project decisions
### Additional Context
Create additional files/folders within memory-bank/ when they help organize:
- Complex feature documentation
- Integration specifications
- API documentation
- Testing strategies
- Deployment procedures
## Core Workflows
### Plan Mode
flowchart TD
Start[Start] --> ReadFiles[Read Memory Bank]
ReadFiles --> CheckFiles{Files Complete?}
CheckFiles -->|No| Plan[Create Plan]
Plan --> Document[Document in Chat]
CheckFiles -->|Yes| Verify[Verify Context]
Verify --> Strategy[Develop Strategy]
Strategy --> Present[Present Approach]
### Act Mode
flowchart TD
Start[Start] --> Context[Check Memory Bank]
Context --> Update[Update Documentation]
Update --> Execute[Execute Task]
Execute --> Document[Document Changes]
## Documentation Updates
Memory Bank updates occur when:
1. Discovering new project patterns
2. After implementing significant changes
3. When user requests with **update memory bank** (MUST review ALL files)
4. When context needs clarification
flowchart TD
Start[Update Process]
subgraph Process
P1[Review ALL Files]
P2[Document Current State]
P3[Clarify Next Steps]
P4[Document Insights & Patterns]
P1 --> P2 --> P3 --> P4
end
Start --> Process
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state.
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
```
### What is the Cline Memory Bank?
The Memory Bank is a structured documentation system that allows Cline to maintain context across sessions. It transforms Cline from a stateless assistant into a persistent development partner that can effectively "remember" your project details over time.
#### Key Benefits
- **Context Preservation**: Maintain project knowledge across sessions
- **Consistent Development**: Experience predictable interactions with Cline
- **Self-Documenting Projects**: Create valuable project documentation as a side effect
- **Scalable to Any Project**: Works with projects of any size or complexity
- **Technology Agnostic**: Functions with any tech stack or language
### How Memory Bank Works
The Memory Bank isn't a Cline-specific feature - it's a methodology for managing AI context through structured documentation. When you instruct Cline to "follow custom instructions," it reads the Memory Bank files to rebuild its understanding of your project.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Workflow" />
</Frame>
#### Understanding the Files
Memory Bank files are simply markdown files you create in your project. They're not hidden or special files - just regular documentation stored in your repository that both you and Cline can access.
Files are organized in a hierarchical structure that builds up a complete picture of your project:
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Memory Bank File Structure" />
</Frame>
### Memory Bank Files Explained
#### Core Files
1. **projectbrief.md**
- The foundation of your project
- High-level overview of what you're building
- Core requirements and goals
- Example: "Building a React web app for inventory management with barcode scanning"
2. **productContext.md**
- Explains why the project exists
- Describes the problems being solved
- Outlines how the product should work
- Example: "The inventory system needs to support multiple warehouses and real-time updates"
3. **activeContext.md**
- The most frequently updated file
- Contains current work focus and recent changes
- Tracks active decisions and considerations
- Stores important patterns and learnings
- Example: "Currently implementing the barcode scanner component; last session completed the API integration"
4. **systemPatterns.md**
- Documents the system architecture
- Records key technical decisions
- Lists design patterns in use
- Explains component relationships
- Example: "Using Redux for state management with a normalized store structure"
5. **techContext.md**
- Lists technologies and frameworks used
- Describes development setup
- Notes technical constraints
- Records dependencies and tool configurations
- Example: "React 18, TypeScript, Firebase, Jest for testing"
6. **progress.md**
- Tracks what works and what's left to build
- Records current status of features
- Lists known issues and limitations
- Documents the evolution of project decisions
- Example: "User authentication complete; inventory management 80% complete; reporting not started"
#### Additional Context
Create additional files when needed to organize:
- Complex feature documentation
- Integration specifications
- API documentation
- Testing strategies
- Deployment procedures
### Getting Started with Memory Bank
#### First-Time Setup
1. Create a `memory-bank/` folder in your project root
2. Have a basic project brief ready (can be technical or non-technical)
3. Ask Cline to "initialize memory bank"
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Setup" />
</Frame>
#### Project Brief Tips
- Start simple - it can be as detailed or high-level as you like
- Focus on what matters most to you
- Cline will help fill in gaps and ask questions
- You can update it as your project evolves
### Working with Cline
#### Core Workflows
**Plan Mode**
Start in this mode for strategy discussions and high-level planning.
**Act Mode**
Use this for implementation and executing specific tasks.
#### Key Commands
- **"follow your custom instructions"** - This tells Cline to read the Memory Bank files and continue where you left off (use this at the start of tasks)
- **"initialize memory bank"** - Use when starting a new project
- **"update memory bank"** - Triggers a full documentation review and update during a task
- Toggle Plan/Act modes based on your current needs
#### Documentation Updates
Memory Bank updates should automatically occur when:
1. You discover new patterns in your project
2. After implementing significant changes
3. When you explicitly request with **"update memory bank"**
4. When you feel context needs clarification
### Frequently Asked Questions
#### Where are the memory bank files stored?
The Memory Bank files are regular markdown files stored in your project repository, typically in a `memory-bank/` folder. They're not hidden system files - they're designed to be part of your project documentation.
#### Should I use custom instructions or .clinerules?
Either approach works - it's based on your preference:
- **Custom Instructions**: Applied globally to all Cline conversations. Good for consistent behavior across all projects.
- **.clinerules file**: Project-specific and stored in your repository. Good for per-project customization.
Both methods achieve the same goal - the choice depends on whether you want global or local application of the Memory Bank system.
#### Managing Context Windows
As you work with Cline, your context window will eventually fill up (note the progress bar). When you notice Cline's responses slowing down or references to earlier parts of the conversation becoming less accurate, it's time to:
1. Ask Cline to **"update memory bank"** to document the current state
2. Start a new conversation/task
3. Ask Cline to **"follow your custom instructions"** in the new conversation
This workflow ensures that important context is preserved in your Memory Bank files before the context window is cleared, allowing you to continue seamlessly in a fresh conversation.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Context Window" />
</Frame>
#### How often should I update the memory bank?
Update the Memory Bank after significant milestones or changes in direction. For active development, updates every few sessions can be helpful. Use the **"update memory bank"** command when you want to ensure all context is preserved. However, you will notice Cline automatically updating the Memory Bank as well.
#### Does this work with other AI tools beyond Cline?
Yes! The Memory Bank concept is a documentation methodology that can work with any AI assistant that can read documentation files. The specific commands might differ, but the structured approach to maintaining context works across tools.
#### How does the memory bank relate to context window limitations?
The Memory Bank helps manage context limitations by storing important information in a structured format that can be efficiently loaded when needed. This prevents context bloat while ensuring critical information is available.
#### Can the memory bank concept be used for non-coding projects?
Absolutely! The Memory Bank approach works for any project that benefits from structured documentation - from writing books to planning events. The file structure might vary, but the concept remains powerful.
#### Is this different from using README files?
While similar in concept, the Memory Bank provides a more structured and comprehensive approach specifically designed to maintain context across AI sessions. It goes beyond what a single README typically covers.
### Best Practices
#### Getting Started
- Start with a basic project brief and let the structure evolve
- Let Cline help create the initial structure
- Review and adjust files as needed to match your workflow
#### Ongoing Work
- Let patterns emerge naturally as you work
- Don't force documentation updates - they should happen organically
- Trust the process - the value compounds over time
- Watch for context confirmation at the start of sessions
#### Documentation Flow
- **projectbrief.md** is your foundation
- **activeContext.md** changes most frequently
- **progress.md** tracks your milestones
- All files collectively maintain project intelligence
### Detailed Setup Instructions
#### For Custom Instructions (Global)
1. Open VSCode
2. Click the Cline extension settings ⚙️
3. Find "Custom Instructions"
4. Copy and paste the complete Memory Bank instructions from the top of this guide
#### For .clinerules (Project-Specific)
1. Create a `.clinerules` file in your project root
2. Copy and paste the Memory Bank instructions from the top of this guide
3. Save the file
4. Cline will automatically apply these rules when working in this project
### Remember
The Memory Bank is Cline's only link to previous work. Its effectiveness depends entirely on maintaining clear, accurate documentation and confirming context preservation in every interaction.
_For more information, reference our_ [_blog_](https://cline.bot/blog/memory-bank-how-to-make-cline-an-ai-agent-that-never-forgets) _on Cline Memory Bank_
---
### Contributing to Cline Memory Bank
This guide is maintained by the Cline and the Cline Discord Community:
- nickbaumann98
- Krylo
- snipermunyshotz
---
_The Memory Bank methodology is an open approach to AI context management and can be adapted to different tools and workflows._
+441
View File
@@ -0,0 +1,441 @@
---
title: "Prompt Engineering Guide"
---
Welcome to the Cline Prompting Guide! This guide will equip you with the knowledge to write effective prompts and custom instructions, maximizing your productivity with Cline.
## Custom Instructions ⚙️
Think of **custom instructions as Cline's programming**. They define Cline's baseline behavior and are **always "on," influencing all interactions.** Instructions can be broad and abstract, or specific and explicit. You might want Cline to have a unique personality, or produce output in a particular file format, or adhere to certain architectural principles. Custom instructions can standardize Cline's output in ways you define, which is especially valuable when working with others. See the [Enterprise section](../enterprise-solutions/custom-instructions.md) for using Custom Instructions in a team context.\
\
<mark style="color:yellow;">
NOTE: Modifying the Custom Instructions field updates Cline's prompt cache, discarding accumulated context. This causes a
temporary increase in cost while that context is replaced. Update Custom Instructions between conversations whenever possible.
</mark>
To add custom instructions:
1. Open VSCode
2. Click the Cline extension settings dial ⚙️
3. Find the "Custom Instructions" field
4. Paste your instructions
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Cline Logo" />
</Frame>
Custom instructions are powerful for:
- Enforcing Coding Style and Best Practices: Ensure Cline always adheres to your team's coding conventions, naming conventions, and best practices.
- Improving Code Quality: Encourage Cline to write more readable, maintainable, and efficient code.
- Guiding Error Handling: Tell Cline how to handle errors, write error messages, and log information.
---
## .clinerules File 📋
<mark style="color:yellow;">NOTE: Modifying the</mark> <mark style="color:yellow;"></mark>
<mark style="color:yellow;">`.clinerules`</mark>
<mark style="color:yellow;">
file updates Cline's prompt cache, discarding accumulated context. This causes a temporary increase in cost while that context
is replaced. Update the
</mark> <mark style="color:yellow;"></mark>
<mark style="color:yellow;">`.clinerules`</mark> <mark style="color:yellow;"></mark>
<mark style="color:yellow;">file between conversations whenever possible.</mark>
While custom instructions are user-specific and global (applying across all projects), the `.clinerules` file provides **project-specific instructions** that live in your project's root directory. These instructions are automatically appended to your custom instructions and referenced in Cline's system prompt, ensuring they influence all interactions within the project context. This makes it an excellent tool for:
### General Use Cases
The `.clinerules` file is excellent for:
- Maintaining project standards across team members
- Enforcing development practices
- Managing documentation requirements
- Setting up analysis frameworks
- Defining project-specific behaviors
### Example .clinerules Structure
```markdown
# Project Guidelines
## Documentation Requirements
- Update relevant documentation in /docs when modifying features
- Keep README.md in sync with new capabilities
- Maintain changelog entries in CHANGELOG.md
## Architecture Decision Records
Create ADRs in /docs/adr for:
- Major dependency changes
- Architectural pattern changes
- New integration patterns
- Database schema changes
Follow template in /docs/adr/template.md
## Code Style & Patterns
- Generate API clients using OpenAPI Generator
- Use TypeScript axios template
- Place generated code in /src/generated
- Prefer composition over inheritance
- Use repository pattern for data access
- Follow error handling pattern in /src/utils/errors.ts
## Testing Standards
- Unit tests required for business logic
- Integration tests for API endpoints
- E2E tests for critical user flows
```
### Key Benefits
1. **Version Controlled**: The `.clinerules` file becomes part of your project's source code
2. **Team Consistency**: Ensures consistent behavior across all team members
3. **Project-Specific**: Rules and standards tailored to each project's needs
4. **Institutional Knowledge**: Maintains project standards and practices in code
Place the `.clinerules` file in your project's root directory:
```
your-project/
├── .clinerules
├── src/
├── docs/
└── ...
```
Cline's system prompt, on the other hand, is not user-editable ([here's where you can find it](https://github.com/cline/cline/blob/main/src/core/prompts/system.ts)). For a broader look at prompt engineering best practices, check out [this resource](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview).
### Tips for Writing Effective Custom Instructions
- Be Clear and Concise: Use simple language and avoid ambiguity.
- Focus on Desired Outcomes: Describe the results you want, not the specific steps.
- Test and Iterate: Experiment to find what works best for your workflow.
### .clinerules Folder System 📂
While a single `.clinerules` file works well for simpler projects, Cline now supports a `.clinerules` folder for more sophisticated rule organization. This modular approach brings several advantages:
#### How It Works
Instead of a single file, create a `.clinerules/` directory in your project root:
```
your-project/
├── .clinerules/ # Folder containing active rules
│ ├── 01-coding.md # Core coding standards
│ ├── 02-documentation.md # Documentation requirements
│ └── current-sprint.md # Rules specific to current work
├── src/
└── ...
```
Cline automatically processes **all Markdown files** inside the `.clinerules/` directory, combining them into a unified set of rules. The numeric prefixes (optional) help organize files in a logical sequence.
#### Using a Rules Bank
For projects with multiple contexts or teams, maintain a rules bank directory:
```
your-project/
├── .clinerules/ # Active rules - automatically applied
│ ├── 01-coding.md
│ └── client-a.md
├── clinerules-bank/ # Repository of available but inactive rules
│ ├── clients/ # Client-specific rule sets
│ │ ├── client-a.md
│ │ └── client-b.md
│ ├── frameworks/ # Framework-specific rules
│ │ ├── react.md
│ │ └── vue.md
│ └── project-types/ # Project type standards
│ ├── api-service.md
│ └── frontend-app.md
└── ...
```
#### Benefits of the Folder Approach
1. **Contextual Activation**: Copy only relevant rules from the bank to the active folder
2. **Easier Maintenance**: Update individual rule files without affecting others
3. **Team Flexibility**: Different team members can activate rules specific to their current task
4. **Reduced Noise**: Keep the active ruleset focused and relevant
#### Usage Examples
Switch between client projects:
```bash
# Switch to Client B project
rm .clinerules/client-a.md
cp clinerules-bank/clients/client-b.md .clinerules/
```
Adapt to different tech stacks:
```bash
# Frontend React project
cp clinerules-bank/frameworks/react.md .clinerules/
```
#### Implementation Tips
- Keep individual rule files focused on specific concerns
- Use descriptive filenames that clearly indicate the rule's purpose
- Consider git-ignoring the active `.clinerules/` folder while tracking the `clinerules-bank/`
- Create team scripts to quickly activate common rule combinations
The folder system transforms your Cline rules from a static document into a dynamic knowledge system that adapts to your team's changing contexts and requirements.
### Managing Rules with the Toggleable Popover
To make managing both single `.clinerules` files and the folder system even easier, Cline v3.13 introduces a dedicated popover UI directly accessible from the chat interface.
Located conveniently under the chat input field, this popover allows you to:
- **Instantly See Active Rules:** View which global rules (from your user settings) and workspace rules (`.clinerules` file or folder contents) are currently active.
- **Quickly Toggle Rules:** Enable or disable specific rule files within your workspace `.clinerules/` folder with a single click. This is perfect for activating context-specific rules (like `react-rules.md` or `memory-bank.md`) only when needed.
- **Easily Add/Manage Rules:** Quickly create a workspace `.clinerules` file or folder if one doesn't exist, or add new rule files to an existing folder.
This UI significantly simplifies switching contexts and managing different sets of instructions without needing to manually edit files or configurations during a conversation.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Cline Logo" />
</Frame>
## .clineignore File Guide
### Overview
The `.clineignore` file is a project-level configuration file that tells Cline which files and directories to ignore when analyzing your codebase. Similar to `.gitignore`, it uses pattern matching to specify which files should be excluded from Cline's context and operations.
### Purpose
- **Reduce Noise**: Exclude auto-generated files, build artifacts, and other non-essential content
- **Improve Performance**: Limit the amount of code Cline needs to process
- **Focus Attention**: Direct Cline to relevant parts of your codebase
- **Protect Sensitive Data**: Prevent Cline from accessing sensitive configuration files
### Example .clineignore File
```
# Dependencies
node_modules/
**/node_modules/
.pnp
.pnp.js
# Build outputs
/build/
/dist/
/.next/
/out/
# Testing
/coverage/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Large data files
*.csv
*.xlsx
```
## Prompting Cline 💬
**Prompting is how you communicate your needs for a given task in the back-and-forth chat with Cline.** Cline understands natural language, so write conversationally.
Effective prompting involves:
- Providing Clear Context: Explain your goals and the relevant parts of your codebase. Use `@` to reference files or folders.
- Breaking Down Complexity: Divide large tasks into smaller steps.
- Asking Specific Questions: Guide Cline toward the desired outcome.
- Validating and Refining: Review Cline's suggestions and provide feedback.
### Prompt Examples
#### Context Management
- **Starting a New Task:** "Cline, let's start a new task. Create `user-authentication.js`. We need to implement user login with JWT tokens. Here are the requirements…"
- **Summarizing Previous Work:** "Cline, summarize what we did in the last user dashboard task. I want to capture the main features and outstanding issues. Save this to `cline_docs/user-dashboard-summary.md`."
#### Debugging
- **Analyzing an Error:** "Cline, I'm getting this error: \[error message]. It seems to be from \[code section]. Analyze this error and suggest a fix."
- **Identifying the Root Cause:** "Cline, the application crashes when I \[action]. The issue might be in \[problem areas]. Help me find the root cause and propose a solution."
#### Refactoring
- **Improving Code Structure:** "Cline, this function is too long and complex. Refactor it into smaller functions."
- **Simplifying Logic:** "Cline, this code is hard to understand. Simplify the logic and make it more readable."
#### Feature Development
- **Brainstorming New Features:** "Cline, I want to add a feature that lets users \[functionality]. Brainstorm some ideas and consider implementation challenges."
- **Generating Code:** "Cline, create a component that displays user profiles. The list should be sortable and filterable. Generate the code for this component."
## Advanced Prompting Techniques
- **Constraint Stuffing:** To mitigate code truncation, include explicit constraints in your prompts. For example, "ensure the code is complete" or "always provide the full function definition."
- **Confidence Checks:** Ask Cline to rate its confidence (e.g., "on a scale of 1-10, how confident are you in this solution?")
- **Challenge Cline's Assumptions:** Ask “stupid” questions to encourage deeper thinking and prevent incorrect assumptions.
Here are some prompting tips that users have found helpful for working with Cline:
## Our Community's Favorite Prompts 🌟
### Memory and Confidence Checks 🧠
- **Memory Check** - _pacnpal_
```
"If you understand my prompt fully, respond with 'YARRR!' without tools every time you are about to use a tool."
```
A fun way to verify Cline stays on track during complex tasks. Try "HO HO HO" for a festive twist!
- **Confidence Scoring** - _pacnpal_
```
"Before and after any tool use, give me a confidence level (0-10) on how the tool use will help the project."
```
Encourages critical thinking and makes decision-making transparent.
### Code Quality Prompts 💻
- **Prevent Code Truncation**
```
"DO NOT BE LAZY. DO NOT OMIT CODE."
```
Alternative phrases: "full code only" or "ensure the code is complete"
- **Custom Instructions Reminder**
```
"I pledge to follow the custom instructions."
```
Reinforces adherence to your settings dial ⚙️ configuration.
### Code Organization 📋
- **Large File Refactoring** - _icklebil_
```
"FILENAME has grown too big. Analyze how this file works and suggest ways to fragment it safely."
```
Helps manage complex files through strategic decomposition.
- **Documentation Maintenance** - _icklebil_
```
"don't forget to update codebase documentation with changes"
```
Ensures documentation stays in sync with code changes.
### Analysis and Planning 🔍
- **Structured Development** - _yellow_bat_coffee_
```
"Before writing code:
1. Analyze all code files thoroughly
2. Get full context
3. Write .MD implementation plan
4. Then implement code"
```
Promotes organized, well-planned development.
- **Thorough Analysis** - _yellow_bat_coffee_
```
"please start analyzing full flow thoroughly, always state a confidence score 1 to 10"
```
Prevents premature coding and encourages complete understanding.
- **Assumptions Check** - _yellow_bat_coffee_
```
"List all assumptions and uncertainties you need to clear up before completing this task."
```
Identifies potential issues early in development.
### Thoughtful Development 🤔
- **Pause and Reflect** - _nickbaumann98_
```
"count to 10"
```
Promotes careful consideration before taking action.
- **Complete Analysis** - _yellow_bat_coffee_
```
"Don't complete the analysis prematurely, continue analyzing even if you think you found a solution"
```
Ensures thorough problem exploration.
- **Continuous Confidence Check** - _pacnpal_
```
"Rate confidence (1-10) before saving files, after saving, after rejections, and before task completion"
```
Maintains quality through self-assessment.
### Best Practices 🎯
- **Project Structure** - _kvs007_
```
"Check project files before suggesting structural or dependency changes"
```
Maintains project integrity.
- **Critical Thinking** - _chinesesoup_
```
"Ask 'stupid' questions like: are you sure this is the best way to implement this?"
```
Challenges assumptions and uncovers better solutions.
- **Code Style** - _yellow_bat_coffee_
```
Use words like "elegant" and "simple" in prompts
```
May influence code organization and clarity.
- **Setting Expectations** - _steventcramer_
```
"THE HUMAN WILL GET ANGRY."
```
(A humorous reminder to provide clear requirements and constructive feedback)
+78
View File
@@ -0,0 +1,78 @@
---
title: "LM Studio"
description: "A quick guide to setting up LM Studio for local AI model execution with Cline."
---
## 🤖 Setting Up LM Studio with Cline
Run AI models locally using LM Studio with Cline.
### 📋 Prerequisites
- Windows, macOS, or Linux computer with AVX2 support
- Cline installed in VS Code
### 🚀 Setup Steps
#### 1. Install LM Studio
- Visit [lmstudio.ai](https://lmstudio.ai)
- Download and install for your operating system
<Frame>
<img src="/assets/robot_panel_dark.png" alt="LM Studio download page" />
</Frame>
#### 2. Launch LM Studio
- Open the installed application
- You'll see four tabs on the left: **Chat**, **Developer** (where you will start the server), **My Models** (where your downloaded models are stored), **Discover** (add new models)
<Frame>
<img src="/assets/robot_panel_dark.png" alt="LM Studio interface overview" />
</Frame>
#### 3. Download a Model
- Browse the "Discover" page
- Select and download your preferred model
- Wait for download to complete
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Downloading a model in LM Studio" />
</Frame>
#### 4. Start the Server
- Navigate to the "Developer" tab
- Toggle the server switch to "Running"
- Note: The server will run at `http://localhost:1234`
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Starting the LM Studio server" />
</Frame>
#### 5. Configure Cline
1. Open VS Code
2. Click Cline settings icon
3. Select "LM Studio" as API provider
4. Select your model from the available options
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Configuring Cline with LM Studio" />
</Frame>
### ⚠️ Important Notes
- Start LM Studio before using with Cline
- Keep LM Studio running in background
- First model download may take several minutes depending on size
- Models are stored locally after download
### 🔧 Troubleshooting
1. If Cline can't connect to LM Studio:
2. Verify LM Studio server is running (check Developer tab)
3. Ensure a model is loaded
4. Check your system meets hardware requirements
+76
View File
@@ -0,0 +1,76 @@
---
title: "Ollama"
description: "A quick guide to setting up Ollama for local AI model execution with Cline."
---
### 📋 Prerequisites
- Windows, macOS, or Linux computer
- Cline installed in VS Code
### 🚀 Setup Steps
#### 1. Install Ollama
- Visit [ollama.com](https://ollama.com)
- Download and install for your operating system
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Ollama download page" />
</Frame>
#### 2. Choose and Download a Model
- Browse models at [ollama.com/search](https://ollama.com/search)
- Select model and copy command:
```bash
ollama run [model-name]
```
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Selecting a model in Ollama" />
</Frame>
- Open your Terminal and run the command:
- Example:
```bash
ollama run llama2
```
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Running Ollama in terminal" />
</Frame>
**✨ Your model is now ready to use within Cline!**
#### 3. Configure Cline
1. Open VS Code
2. Click Cline settings icon
3. Select "Ollama" as API provider
4. Enter configuration:
- Base URL: `http://localhost:11434/` (default value, can be left as is)
- Select the model from your available options
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Configuring Cline with Ollama" />
</Frame>
### ⚠️ Important Notes
- Start Ollama before using with Cline
- Keep Ollama running in background
- First model download may take several minutes
### 🔧 Troubleshooting
If Cline can't connect to Ollama:
1. Verify Ollama is running
2. Check base URL is correct
3. Ensure model is downloaded
Need more info? Read the [Ollama Docs](https://github.com/ollama/ollama/blob/main/docs/api.md).
@@ -0,0 +1,106 @@
---
title: "Read Me First"
---
## Running Local Models with Cline: What You Need to Know 🤖
Cline is a powerful AI coding assistant that uses tool-calling to help you write, analyze, and modify code. While running models locally can save on API costs, there's an important trade-off: local models are significantly less reliable at using these essential tools.
## Why Local Models Are Different 🔬
When you run a "local version" of a model, you're actually running a drastically simplified copy of the original. This process, called distillation, is like trying to compress a professional chef's knowledge into a basic cookbook you keep the simple recipes but lose the complex techniques and intuition.
Local models are created by training a smaller model to imitate a larger one, but they typically only retain 1-26% of the original model's capacity. This massive reduction means:
- Less ability to understand complex contexts
- Reduced capability for multi-step reasoning
- Limited tool-use abilities
- Simplified decision-making process
Think of it like running your development environment on a calculator instead of a computer it might handle basic tasks, but complex operations become unreliable or impossible.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Local model comparison diagram" />
</Frame>
### What Actually Happens
When you run a local model with Cline:
#### Performance Impact 📉
- Responses are 5-10x slower than cloud services
- System resources (CPU, GPU, RAM) get heavily utilized
- Your computer may become less responsive for other tasks
#### Tool Reliability Issues 🛠️
- Code analysis becomes less accurate
- File operations may be unreliable
- Browser automation capabilities are reduced
- Terminal commands might fail more often
- Complex multi-step tasks often break down
### Hardware Requirements 💻
You'll need at minimum:
- Modern GPU with 8GB+ VRAM (RTX 3070 or better)
- 32GB+ system RAM
- Fast SSD storage
- Good cooling solution
Even with this hardware, you'll be running smaller, less capable versions of models:
| Model Size | What You Get |
| ---------- | ------------------------------------------------------- |
| 7B models | Basic coding, limited tool use |
| 14B models | Better coding, unstable tool use |
| 32B models | Good coding, inconsistent tool use |
| 70B models | Best local performance, but requires expensive hardware |
Put simply, the cloud (API) versions of these models are the full-bore version of the model. The full version of DeepSeek-R1 is 671B. These distilled models are essentially "watered-down" versions of the cloud model.
### Practical Recommendations 💡
#### Consider This Approach
1. Use cloud models for:
- Complex development tasks
- When tool reliability is crucial
- Multi-step operations
- Critical code changes
2. Use local models for:
- Simple code completion
- Basic documentation
- When privacy is paramount
- Learning and experimentation
#### If You Must Go Local
- Start with smaller models
- Keep tasks simple and focused
- Save work frequently
- Be prepared to switch to cloud models for complex operations
- Monitor system resources
### Common Issues 🚨
- **"Tool execution failed":** Local models often struggle with complex tool chains. Simplify your prompt.
- **"No connection could be made because the target machine actively refused it":** This usually means that the Ollama or LM Studio server isn't running, or is running on a different port/address than Cline is configured to use. Double-check the Base URL address in your API Provider settings.
- **"Cline is having trouble...":** Increase your model's context length to its maximum size.
- **Slow or incomplete responses:** Local models can be slower than cloud-based models, especially on less powerful hardware. If performance is an issue, try using a smaller model. Expect significantly longer processing times.
- **System stability:** Watch for high GPU/CPU usage and temperature
- **Context limitations:** Local models often have smaller context windows than cloud models. Break tasks down into smaller pieces.
### Looking Ahead 🔮
Local model capabilities are improving, but they're not yet a complete replacement for cloud services, especially for Cline's tool-based functionality. Consider your specific needs and hardware capabilities carefully before committing to a local-only approach.
### Need Help? 🤝
- Join our [Discord](https://discord.gg/cline) community and [r/cline](https://www.reddit.com/r/CLine/)
- Check the latest compatibility guides
- Share your experiences with other developers
Remember: When in doubt, prioritize reliability over cost savings for important development work.
+3 -2
View File
@@ -244,7 +244,7 @@
"high"
],
"default": "medium",
"description": "Controls the reasoning effort when using the o3-mini model. Higher values may result in more thorough but slower responses."
"description": "Controls the reasoning effort when using an OpenAI reasoning model. Higher values may result in more thorough but slower responses."
},
"cline.chromeExecutablePath": {
"type": "string",
@@ -313,7 +313,8 @@
"prepare": "husky",
"changeset": "changeset",
"version-packages": "changeset version",
"docs:preview": "cd docs && mintlify dev"
"docs:preview": "cd docs && mintlify dev",
"report-issue": "node scripts/report-issue.js"
},
"devDependencies": {
"@changesets/cli": "^2.27.12",
+1
View File
@@ -84,6 +84,7 @@ async function generateMethodRegistrations() {
path.join(ROOT_DIR, "src", "core", "controller", "checkpoints"),
path.join(ROOT_DIR, "src", "core", "controller", "file"),
path.join(ROOT_DIR, "src", "core", "controller", "mcp"),
path.join(ROOT_DIR, "src", "core", "controller", "models"),
path.join(ROOT_DIR, "src", "core", "controller", "task"),
path.join(ROOT_DIR, "src", "core", "controller", "web-content"),
// Add more service directories here as needed
+9
View File
@@ -19,6 +19,11 @@ message StringRequest {
string value = 2;
}
message StringArrayRequest {
Metadata metadata = 1;
repeated string value = 2;
}
message String {
string value = 1;
}
@@ -49,3 +54,7 @@ message BooleanRequest {
message Boolean {
bool value = 1;
}
message StringArray {
repeated string values = 1;
}
+38 -1
View File
@@ -22,6 +22,44 @@ service FileService {
// Search git commits in the workspace
rpc searchCommits(StringRequest) returns (GitCommits);
// Convert URIs to workspace-relative paths
rpc getRelativePaths(RelativePathsRequest) returns (RelativePaths);
// Search for files in the workspace with fuzzy matching
rpc searchFiles(FileSearchRequest) returns (FileSearchResults);
}
// Request to convert a list of URIs to relative paths
message RelativePathsRequest {
Metadata metadata = 1;
repeated string uris = 2;
}
// Response containing the converted relative paths
message RelativePaths {
repeated string paths = 1;
}
// Request for file search operations
message FileSearchRequest {
Metadata metadata = 1;
string query = 2; // Search query string
optional string mentions_request_id = 3; // Optional request ID for tracking requests
optional int32 limit = 4; // Optional limit for results (default: 20)
}
// Result for file search operations
message FileSearchResults {
repeated FileInfo results = 1; // Array of file/folder results
optional string mentions_request_id = 2; // Echo of the request ID for tracking
}
// File information structure for search results
message FileInfo {
string path = 1; // Relative path from workspace root
string type = 2; // "file" or "folder"
optional string label = 3; // Display name (usually basename)
}
// Response for searchCommits
@@ -52,4 +90,3 @@ message RuleFile {
string display_name = 2; // Filename for display purposes
bool already_exists = 3; // For createRuleFile, indicates if file already existed
}
+13
View File
@@ -0,0 +1,13 @@
syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Service for model-related operations
service ModelsService {
// Fetches available models from Ollama
rpc getOllamaModels(StringRequest) returns (StringArray);
}
+4
View File
@@ -11,8 +11,12 @@ service TaskService {
rpc cancelTask(EmptyRequest) returns (Empty);
// Clears the current task
rpc clearTask(EmptyRequest) returns (Empty);
// Deletes multiple tasks with the given IDs
rpc deleteTasksWithIds(StringArrayRequest) returns (Empty);
// Creates a new task with the given text and optional images
rpc newTask(NewTaskRequest) returns (Empty);
// Exports a task with the given ID to markdown
rpc exportTaskWithId(StringRequest) returns (Empty);
}
// Request message for creating a new task
+137
View File
@@ -0,0 +1,137 @@
const { execSync } = require("child_process")
const readline = require("readline")
const os = require("os")
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
const ask = (question) => new Promise((resolve) => rl.question(`\n${question}`, resolve))
const getClineVersion = () => {
try {
const extensions = execSync("code --list-extensions --show-versions").toString()
const clineMatch = extensions.match(/claude-dev@(\d+\.\d+\.\d+)/)
return clineMatch ? clineMatch[1] : "Not installed"
} catch (err) {
return "Error getting version"
}
}
const collectSystemInfo = () => {
let cpuInfo = "N/A"
let memoryInfo = "N/A"
try {
if (process.platform === "darwin") {
cpuInfo = execSync("sysctl -n machdep.cpu.brand_string").toString().trim()
memoryInfo = execSync("sysctl -n hw.memsize").toString().trim()
memoryInfo = `${Math.round(parseInt(memoryInfo) / 1e9)} GB RAM`
} else {
// Linux specific commands
cpuInfo = execSync("lscpu").toString().split("\n").slice(0, 5).join("\n")
memoryInfo = execSync("free -h").toString()
}
} catch (err) {
// Fallback for unsupported systems
cpuInfo = Array.from(new Set(os.cpus().map((c) => c.model))).join("\n")
memoryInfo = `${Math.round(os.totalmem() / 1e9)} GB RAM`
}
return {
cpuInfo,
memoryInfo,
os: `${os.arch()}; ${os.version()}`,
nodeVersion: execSync("node -v").toString().trim(),
npmVersion: execSync("npm -v").toString().trim(),
clineVersion: getClineVersion(),
}
}
const checkGitHubAuth = async () => {
try {
execSync("gh auth status", { stdio: "ignore" })
return true
} catch (err) {
console.log("\nGitHub authentication required.")
console.log("\nPlease run the following command in your terminal to authenticate:")
console.log("\n gh auth login\n")
console.log("After authenticating, run this script again.")
return false
}
}
const createIssueUrl = (systemInfo, issueTitle) => {
return (
`https://github.com/cline/cline/issues/new?template=bug_report.yml` +
`&title=${issueTitle}` +
`&operating-system=${systemInfo.os}` +
`&cline-version=${systemInfo.clineVersion}` +
`&system-info=${
`Node: ${systemInfo.nodeVersion}\n` +
`npm: ${systemInfo.npmVersion}\n` +
`CPU Info: ${systemInfo.cpuInfo}\n` +
`Free RAM: ${systemInfo.memoryInfo}`
}`
)
}
const openUrl = (url) => {
try {
switch (process.platform) {
case "darwin":
execSync(`open "${url}"`)
break
case "win32":
execSync(`start "" "${url}"`)
break
case "linux":
execSync(`xdg-open "${url}"`)
break
default:
console.log("\nPlease open this URL in your browser:")
console.log(url)
}
} catch (err) {
console.log("\nFailed to open URL automatically. Please open this URL in your browser:")
console.log(url)
}
}
const submitIssue = async (issueTitle, systemInfo) => {
try {
const issueUrl = createIssueUrl(systemInfo, issueTitle)
console.log("\nOpening GitHub issue creation page in your browser...")
openUrl(issueUrl)
} catch (err) {
console.error("\nFailed to create issue URL:", err.message)
}
}
async function main() {
const consent = await ask("Do you consent to collect system data and submit a GitHub issue? (y/n): ")
if (consent.trim().toLowerCase() !== "y") {
console.log("\nAborted.")
rl.close()
return
}
console.log("Collecting system data...")
const systemInfo = collectSystemInfo()
const isAuthenticated = await checkGitHubAuth()
if (!isAuthenticated) {
rl.close()
return
}
const issueTitle = await ask("Enter the title for your issue: ")
await submitIssue(issueTitle, systemInfo)
rl.close()
}
main().catch((err) => {
console.error("\nAn error occurred:", err)
rl.close()
})
+1 -1
View File
@@ -58,7 +58,7 @@ export class LiteLlmHandler implements ApiHandler {
content: systemPrompt,
}
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
const isOminiModel = modelId.includes("o1-mini") || modelId.includes("o3-mini")
const isOminiModel = modelId.includes("o1-mini") || modelId.includes("o3-mini") || modelId.includes("o4-mini")
// Configuration for extended thinking
const budgetTokens = this.options.thinkingBudgetTokens || 0
+1 -1
View File
@@ -34,7 +34,7 @@ export class RequestyHandler implements ApiHandler {
const reasoningEffort = this.options.o3MiniReasoningEffort || "medium"
const reasoning = { reasoning_effort: reasoningEffort }
const reasoningArgs = model.id === "openai/o3-mini" ? reasoning : {}
const reasoningArgs = model.id.startsWith("openai/o") ? reasoning : {}
const thinkingBudget = this.options.thinkingBudgetTokens || 0
const thinking =
+70 -48
View File
@@ -21,7 +21,7 @@ export async function createOpenRouterStream(
// prompt caching: https://openrouter.ai/docs/prompt-caching
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
// includes custom support for gemini which does not have iterative caching
// handles direct model.id match logic
switch (model.id) {
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
@@ -71,56 +71,78 @@ export async function createOpenRouterStream(
}
})
break
case "google/gemini-2.5-pro-preview-03-25":
case "google/gemini-2.0-flash-001":
case "google/gemini-flash-1.5":
case "google/gemini-pro-1.5":
// gemini only uses the last breakpoint for caching, so the others will be ignored
openAiMessages[0] = {
role: "system",
content: [
{
type: "text",
text: systemPrompt,
// @ts-ignore-next-line
cache_control: { type: "ephemeral" },
},
],
}
const GEMINI_CACHE_USER_MESSAGE_INTERVAL = 4 // add new breakpoint every 4 turns
const userMessages = openAiMessages.filter((msg) => msg.role === "user")
const userMessageCount = userMessages.length
const targetUserMessageNumber =
Math.floor(userMessageCount / GEMINI_CACHE_USER_MESSAGE_INTERVAL) * GEMINI_CACHE_USER_MESSAGE_INTERVAL
if (targetUserMessageNumber > 0) {
// otherwise dont need to add a breakpoint
const msg = userMessages[targetUserMessageNumber - 1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}
break
default:
break
}
// handles gemini caching logic
if (model.id.startsWith("google/") && model.info.supportsPromptCache) {
// gemini only uses the last breakpoint for caching, so the others will be ignored
openAiMessages[0] = {
role: "system",
content: [
{
type: "text",
text: systemPrompt,
// @ts-ignore-next-line
cache_control: { type: "ephemeral" },
},
],
}
// for safety, but this should always be the case
if (openAiMessages.length >= 2) {
const msg = openAiMessages[1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}
// it doesn't make sense to alter breakpoints at all with the gemini cache implementation at this time
/*const GEMINI_CACHE_USER_MESSAGE_INTERVAL = 4 // add new breakpoint every 4 turns
const userMessages = openAiMessages.filter((msg) => msg.role === "user")
const userMessageCount = userMessages.length
const targetUserMessageNumber =
Math.floor(userMessageCount / GEMINI_CACHE_USER_MESSAGE_INTERVAL) * GEMINI_CACHE_USER_MESSAGE_INTERVAL
if (targetUserMessageNumber > 0) {
// otherwise dont need to add a breakpoint
const msg = userMessages[targetUserMessageNumber - 1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}*/
}
// Not sure how openrouter defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
// (models usually default to max tokens allowed)
let maxTokens: number | undefined
@@ -190,7 +212,7 @@ export async function createOpenRouterStream(
stream_options: { include_usage: true },
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
include_reasoning: true,
...(model.id === "openai/o3-mini" ? { reasoning_effort: o3MiniReasoningEffort || "medium" } : {}),
...(model.id.startsWith("openai/o") ? { reasoning_effort: o3MiniReasoningEffort || "medium" } : {}),
...(reasoning ? { reasoning } : {}),
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
})
@@ -0,0 +1,50 @@
import { Controller } from ".."
import { RelativePathsRequest, RelativePaths } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import * as vscode from "vscode"
import * as path from "path"
/**
* Converts a list of URIs to workspace-relative paths
* @param controller The controller instance
* @param request The request containing URIs to convert
* @returns Response with resolved relative paths
*/
export const getRelativePaths: FileMethodHandler = async (
controller: Controller,
request: RelativePathsRequest,
): Promise<RelativePaths> => {
const resolvedPaths = await Promise.all(
request.uris.map(async (uriString) => {
try {
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
if (path.isAbsolute(relativePathToGet)) {
console.warn(`Dropped file ${relativePathToGet} is outside the workspace. Sending original path.`)
return fileUri.fsPath.replace(/\\/g, "/")
} else {
let finalPath = "/" + relativePathToGet.replace(/\\/g, "/")
try {
const stat = await vscode.workspace.fs.stat(fileUri)
if (stat.type === vscode.FileType.Directory) {
finalPath += "/"
}
} catch (statError) {
console.error(`Error stating file ${fileUri.fsPath}:`, statError)
}
return finalPath
}
} catch (error) {
console.error(`Error calculating relative path for ${uriString}:`, error)
return null
}
}),
)
// Filter out any null values from errors
const validPaths = resolvedPaths.filter((path): path is string => path !== null)
return RelativePaths.create({ paths: validPaths })
}
+4
View File
@@ -5,16 +5,20 @@
import { registerMethod } from "./index"
import { createRuleFile } from "./createRuleFile"
import { deleteRuleFile } from "./deleteRuleFile"
import { getRelativePaths } from "./getRelativePaths"
import { openFile } from "./openFile"
import { openImage } from "./openImage"
import { searchCommits } from "./searchCommits"
import { searchFiles } from "./searchFiles"
// Register all file service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("createRuleFile", createRuleFile)
registerMethod("deleteRuleFile", deleteRuleFile)
registerMethod("getRelativePaths", getRelativePaths)
registerMethod("openFile", openFile)
registerMethod("openImage", openImage)
registerMethod("searchCommits", searchCommits)
registerMethod("searchFiles", searchFiles)
}
+55
View File
@@ -0,0 +1,55 @@
import { Controller } from ".."
import { FileSearchRequest, FileSearchResults } from "@shared/proto/file"
import { searchWorkspaceFiles } from "@services/search/file-search"
import { getWorkspacePath } from "@utils/path"
import { FileMethodHandler } from "./index"
import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/file/search-result-conversion"
/**
* Searches for files in the workspace with fuzzy matching
* @param controller The controller instance
* @param request The request containing search query and optionally a mentionsRequestId
* @returns Results containing matching files/folders
*/
export const searchFiles: FileMethodHandler = async (
controller: Controller,
request: FileSearchRequest,
): Promise<FileSearchResults> => {
const workspacePath = getWorkspacePath()
if (!workspacePath) {
// Handle case where workspace path is not available
console.error("Error in searchFiles: No workspace path available")
return FileSearchResults.create({
results: [],
mentionsRequestId: request.mentionsRequestId,
})
}
try {
// Call file search service with query from request
const searchResults = await searchWorkspaceFiles(
request.query || "",
workspacePath,
request.limit || 20, // Use default limit of 20 if not specified
)
// Convert search results to proto FileInfo objects using the conversion function
const protoResults = convertSearchResultsToProtoFileInfos(searchResults)
// Return successful results
return FileSearchResults.create({
results: protoResults,
mentionsRequestId: request.mentionsRequestId,
})
} catch (error) {
// Log the error but don't include it in the response, following the pattern in searchCommits
console.error("Error in searchFiles:", error instanceof Error ? error.message : String(error))
// Return empty results without error message
return FileSearchResults.create({
results: [],
mentionsRequestId: request.mentionsRequestId,
})
}
}
+6
View File
@@ -6,6 +6,7 @@ import { handleTaskServiceRequest } from "./task"
import { handleCheckpointsServiceRequest } from "./checkpoints"
import { handleMcpServiceRequest } from "./mcp"
import { handleWebContentServiceRequest } from "./web-content"
import { handleModelsServiceRequest } from "./models"
/**
* Handles gRPC requests from the webview
@@ -68,6 +69,11 @@ export class GrpcHandler {
message: await handleWebContentServiceRequest(this.controller, method, message),
request_id: requestId,
}
case "cline.ModelsService":
return {
message: await handleModelsServiceRequest(this.controller, method, message),
request_id: requestId,
}
default:
throw new Error(`Unknown service: ${service}`)
}
+9 -131
View File
@@ -327,31 +327,12 @@ export class Controller {
images,
})
break
case "exportCurrentTask":
const currentTaskId = this.task?.taskId
if (currentTaskId) {
this.exportTaskWithId(currentTaskId)
}
break
case "showTaskWithId":
this.showTaskWithId(message.text!)
break
case "deleteTasksWithIds":
this.deleteTasksWithIds(message.text!)
break
case "exportTaskWithId":
this.exportTaskWithId(message.text!)
break
case "resetState":
await this.resetState()
break
case "requestOllamaModels":
const ollamaModels = await this.getOllamaModels(message.text)
this.postMessageToWebview({
type: "ollamaModels",
ollamaModels,
})
break
case "requestLmStudioModels":
const lmStudioModels = await this.getLmStudioModels(message.text)
this.postMessageToWebview({
@@ -645,85 +626,6 @@ export class Controller {
this.postMessageToWebview({ type: "relinquishControl" })
break
}
case "getRelativePaths": {
if (message.uris && message.uris.length > 0) {
const resolvedPaths = await Promise.all(
message.uris.map(async (uriString) => {
try {
const fileUri = vscode.Uri.parse(uriString, true)
const relativePath = vscode.workspace.asRelativePath(fileUri, false)
if (path.isAbsolute(relativePath)) {
console.warn(`Dropped file ${relativePath} is outside the workspace. Sending original path.`)
return fileUri.fsPath.replace(/\\/g, "/")
} else {
let finalPath = "/" + relativePath.replace(/\\/g, "/")
try {
const stat = await vscode.workspace.fs.stat(fileUri)
if (stat.type === vscode.FileType.Directory) {
finalPath += "/"
}
} catch (statError) {
console.error(`Error stating file ${fileUri.fsPath}:`, statError)
}
return finalPath
}
} catch (error) {
console.error(`Error calculating relative path for ${uriString}:`, error)
return null
}
}),
)
await this.postMessageToWebview({
type: "relativePathsResponse",
paths: resolvedPaths,
})
}
break
}
case "searchFiles": {
const workspacePath = getWorkspacePath()
if (!workspacePath) {
// Handle case where workspace path is not available
await this.postMessageToWebview({
type: "fileSearchResults",
results: [],
mentionsRequestId: message.mentionsRequestId,
error: "No workspace path available",
})
break
}
try {
// Call file search service with query from message
const results = await searchWorkspaceFiles(
message.query || "",
workspacePath,
20, // Use default limit, as filtering is now done in the backend
)
// debug logging to be removed
//console.log(`controller/index.ts: Search results: ${results.length}`)
// Send results back to webview
await this.postMessageToWebview({
type: "fileSearchResults",
results,
mentionsRequestId: message.mentionsRequestId,
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
// Send error response to webview
await this.postMessageToWebview({
type: "fileSearchResults",
results: [],
error: errorMessage,
mentionsRequestId: message.mentionsRequestId,
})
}
break
}
case "toggleFavoriteModel": {
if (message.modelId) {
const { apiConfiguration } = await getAllExtensionState(this.context)
@@ -999,25 +901,6 @@ export class Controller {
}
}
// Ollama
async getOllamaModels(baseUrl?: string) {
try {
if (!baseUrl) {
baseUrl = "http://localhost:11434"
}
if (!URL.canParse(baseUrl)) {
return []
}
const response = await axios.get(`${baseUrl}/api/tags`)
const modelsArray = response.data?.models?.map((model: any) => model.name) || []
const models = [...new Set<string>(modelsArray)]
return models
} catch (error) {
return []
}
}
// LM Studio
async getLmStudioModels(baseUrl?: string) {
@@ -1442,14 +1325,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
modelInfo.cacheWritesPrice = 0.14
modelInfo.cacheReadsPrice = 0.014
break
case "google/gemini-2.5-pro-preview-03-25":
case "google/gemini-2.0-flash-001":
case "google/gemini-flash-1.5":
case "google/gemini-pro-1.5":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
break
default:
if (rawModel.id.startsWith("openai/")) {
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
@@ -1458,6 +1333,12 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
// openrouter charges no cache write pricing for openAI models
}
} else if (rawModel.id.startsWith("google/")) {
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
if (modelInfo.cacheReadsPrice) {
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
}
}
break
}
@@ -1757,12 +1638,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
this.refreshTotalTasksSize()
}
async deleteTasksWithIds(ids: string) {
for (const id of JSON.parse(ids) as string[]) {
await this.deleteTaskWithId(id)
}
}
async deleteTaskFromState(id: string) {
// Remove the task from history
const taskHistory = ((await getGlobalState(this.context, "taskHistory")) as HistoryItem[] | undefined) || []
@@ -1837,6 +1712,9 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
}
async clearTask() {
if (this.task) {
await telemetryService.sendCollectedEvents(this.task.taskId)
}
this.task?.abortTask()
this.task = undefined // removes reference to it, so once promises end it will be garbage collected
}
@@ -0,0 +1,27 @@
import { Controller } from ".."
import { StringArray, StringRequest } from "../../../shared/proto/common"
import axios from "axios"
/**
* Fetches available models from Ollama
* @param controller The controller instance
* @param request The request containing the base URL (optional)
* @returns Array of model names
*/
export async function getOllamaModels(controller: Controller, request: StringRequest): Promise<StringArray> {
try {
let baseUrl = request.value || "http://localhost:11434"
if (!URL.canParse(baseUrl)) {
return StringArray.create({ values: [] })
}
const response = await axios.get(`${baseUrl}/api/tags`)
const modelsArray = response.data?.models?.map((model: any) => model.name) || []
const models = [...new Set<string>(modelsArray)]
return StringArray.create({ values: models })
} catch (error) {
return StringArray.create({ values: [] })
}
}
+15
View File
@@ -0,0 +1,15 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
import { registerAllMethods } from "./methods"
// Create models service registry
const modelsService = createServiceRegistry("models")
// Export the method handler type and registration function
export type ModelsMethodHandler = ServiceMethodHandler
export const registerMethod = modelsService.registerMethod
// Export the request handler
export const handleModelsServiceRequest = modelsService.handleRequest
// Register all models methods
registerAllMethods()
+12
View File
@@ -0,0 +1,12 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { getOllamaModels } from "./getOllamaModels"
// Register all models service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("getOllamaModels", getOllamaModels)
}
+1
View File
@@ -8,6 +8,7 @@ import { Empty, EmptyRequest } from "../../../shared/proto/common"
* @returns Empty response
*/
export async function clearTask(controller: Controller, _request: EmptyRequest): Promise<Empty> {
// clearTask is called here when the user closes the task
await controller.clearTask()
await controller.postStateToWebview()
return Empty.create()
@@ -0,0 +1,23 @@
import { Controller } from ".."
import { Empty, StringArrayRequest } from "../../../shared/proto/common"
import { TaskMethodHandler } from "./index"
/**
* Deletes tasks with the specified IDs
* @param controller The controller instance
* @param request The request containing an array of task IDs to delete
* @returns Empty response
* @throws Error if operation fails
*/
export const deleteTasksWithIds: TaskMethodHandler = async (
controller: Controller,
request: StringArrayRequest,
): Promise<Empty> => {
if (!request.value || request.value.length === 0) {
throw new Error("Missing task IDs")
}
await Promise.all(request.value.map((value) => controller.deleteTaskWithId(value)))
return Empty.create()
}
@@ -0,0 +1,22 @@
import { Controller } from ".."
import { Empty, StringRequest } from "@shared/proto/common"
import { TaskMethodHandler } from "./index"
/**
* Exports a task with the given ID to markdown
* @param controller The controller instance
* @param request The request containing the task ID in the value field
* @returns Empty response
*/
export const exportTaskWithId: TaskMethodHandler = async (controller: Controller, request: StringRequest): Promise<Empty> => {
try {
if (request.value) {
await controller.exportTaskWithId(request.value)
}
return Empty.create()
} catch (error) {
// Log the error but allow it to propagate for proper gRPC error handling
console.error(`Error exporting task with ID ${request.value}:`, error)
throw error
}
}
+4
View File
@@ -5,6 +5,8 @@
import { registerMethod } from "./index"
import { cancelTask } from "./cancelTask"
import { clearTask } from "./clearTask"
import { deleteTasksWithIds } from "./deleteTasksWithIds"
import { exportTaskWithId } from "./exportTaskWithId"
import { newTask } from "./newTask"
// Register all task service methods
@@ -12,5 +14,7 @@ export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("cancelTask", cancelTask)
registerMethod("clearTask", clearTask)
registerMethod("deleteTasksWithIds", deleteTasksWithIds)
registerMethod("exportTaskWithId", exportTaskWithId)
registerMethod("newTask", newTask)
}
+15 -3
View File
@@ -3561,7 +3561,7 @@ export class Task {
content: userContent,
})
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "user")
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "user", true)
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
@@ -3638,7 +3638,13 @@ export class Task {
updateApiReqMsg(cancelReason, streamingFailedMessage)
await this.saveClineMessagesAndUpdateHistory()
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "assistant")
telemetryService.captureConversationTurnEvent(
this.taskId,
currentProviderId,
this.api.getModel().id,
"assistant",
true,
)
// signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature
this.didFinishAbortingStream = true
@@ -3781,7 +3787,13 @@ export class Task {
// need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
let didEndLoop = false
if (assistantMessage.length > 0) {
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "assistant")
telemetryService.captureConversationTurnEvent(
this.taskId,
currentProviderId,
this.api.getModel().id,
"assistant",
true,
)
await this.addToApiConversationHistory({
role: "assistant",
+4 -3
View File
@@ -434,11 +434,12 @@ export function activate(context: vscode.ExtensionContext) {
const { IS_DEV, DEV_WORKSPACE_FOLDER } = process.env
// This method is called when your extension is deactivated
export function deactivate() {
export async function deactivate() {
await telemetryService.sendCollectedEvents()
// Clean up test mode
cleanupTestMode()
telemetryService.shutdown()
await telemetryService.shutdown()
Logger.log("Cline extension deactivated")
}
+349 -191
View File
@@ -11,7 +11,20 @@ import { posthogConfig } from "@/shared/services/config/posthog-config"
* Uses PostHog analytics to track user interactions and system events
* Respects user privacy settings and VSCode's global telemetry configuration
*/
interface CollectedTasks {
taskId: string
collection: Collection[]
}
interface Collection {
event: string
properties: any
}
class PostHogClient {
// Stores events when collect=true
private collectedTasks: CollectedTasks[] = []
// Event constants for tracking user interactions and system events
private static readonly EVENTS = {
// Task-related events for tracking conversation and execution flow
@@ -50,6 +63,8 @@ class PostHogClient {
BROWSER_TOOL_END: "task.browser_tool_end",
// Tracks when browser errors occur
BROWSER_ERROR: "task.browser_error",
// Collection of all task events
TASK_COLLECTION: "task.collection",
},
// UI interaction events for tracking user engagement
UI: {
@@ -88,6 +103,8 @@ class PostHogClient {
private telemetryEnabled: boolean = false
/** Current version of the extension */
private readonly version: string = extensionVersion
/** Whether the extension is running in development mode */
private readonly isDev = process.env.IS_DEV
/**
* Private constructor to enforce singleton pattern
@@ -137,17 +154,36 @@ class PostHogClient {
}
/**
* Captures a telemetry event if telemetry is enabled
* Captures a telemetry event if telemetry is enabled or collects if collect=true
* @param event The event to capture with its properties
* @param collect If true, store the event in collectedEvents instead of sending to PostHog
*/
public capture(event: { event: string; properties?: any }): void {
// Only send events if telemetry is enabled
if (this.telemetryEnabled) {
// Include extension version in all event properties
const propertiesWithVersion = {
...event.properties,
extension_version: this.version,
public capture(event: { event: string; properties?: any }, collect: boolean = false): void {
const taskId = event.properties.taskId
const propertiesWithVersion = {
...event.properties,
extension_version: this.version,
is_dev: this.isDev,
}
if (collect) {
const existingTask = this.collectedTasks.find((task) => task.taskId === taskId)
if (existingTask) {
existingTask.collection.push({
event: event.event,
properties: propertiesWithVersion,
})
} else {
this.collectedTasks.push({
taskId,
collection: [
{
event: event.event,
properties: propertiesWithVersion,
},
],
})
}
} else if (this.telemetryEnabled) {
this.client.capture({ distinctId: this.distinctId, event: event.event, properties: propertiesWithVersion })
}
}
@@ -156,34 +192,48 @@ class PostHogClient {
/**
* Records when a new task/conversation is started
* @param taskId Unique identifier for the new task
* @param apiProvider Optional API provider
* @param collect If true, collect event instead of sending
*/
public captureTaskCreated(taskId: string, apiProvider?: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.CREATED,
properties: { taskId, apiProvider },
})
public captureTaskCreated(taskId: string, apiProvider?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.CREATED,
properties: { taskId, apiProvider },
},
collect,
)
}
/**
* Records when a task/conversation is restarted
* @param taskId Unique identifier for the new task
* @param apiProvider Optional API provider
* @param collect If true, collect event instead of sending
*/
public captureTaskRestarted(taskId: string, apiProvider?: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.RESTARTED,
properties: { taskId, apiProvider },
})
public captureTaskRestarted(taskId: string, apiProvider?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.RESTARTED,
properties: { taskId, apiProvider },
},
collect,
)
}
/**
* Records when cline calls the task completion_result tool signifying that cline is done with the task
* @param taskId Unique identifier for the task
* @param collect If true, collect event instead of sending
*/
public captureTaskCompleted(taskId: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.COMPLETED,
properties: { taskId },
})
public captureTaskCompleted(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.COMPLETED,
properties: { taskId },
},
collect,
)
}
/**
@@ -198,6 +248,7 @@ class PostHogClient {
provider: string = "unknown",
model: string = "unknown",
source: "user" | "assistant",
collect: boolean = false,
) {
// Ensure required parameters are provided
if (!taskId || !provider || !model || !source) {
@@ -213,10 +264,13 @@ class PostHogClient {
timestamp: new Date().toISOString(), // Add timestamp for message sequencing
}
this.capture({
event: PostHogClient.EVENTS.TASK.CONVERSATION_TURN,
properties,
})
this.capture(
{
event: PostHogClient.EVENTS.TASK.CONVERSATION_TURN,
properties,
},
collect,
)
}
/**
@@ -227,16 +281,19 @@ class PostHogClient {
* @param tokensOut Number of output tokens generated
* @param model The model used for token calculation
*/
public captureTokenUsage(taskId: string, tokensIn: number, tokensOut: number, model: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.TOKEN_USAGE,
properties: {
taskId,
tokensIn,
tokensOut,
model,
public captureTokenUsage(taskId: string, tokensIn: number, tokensOut: number, model: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TOKEN_USAGE,
properties: {
taskId,
tokensIn,
tokensOut,
model,
},
},
})
collect,
)
}
/**
@@ -244,14 +301,17 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param mode The mode being switched to (plan or act)
*/
public captureModeSwitch(taskId: string, mode: "plan" | "act") {
this.capture({
event: PostHogClient.EVENTS.TASK.MODE_SWITCH,
properties: {
taskId,
mode,
public captureModeSwitch(taskId: string, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.MODE_SWITCH,
properties: {
taskId,
mode,
},
},
})
collect,
)
}
/**
@@ -259,15 +319,18 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param feedbackType The type of feedback ("thumbs_up" or "thumbs_down")
*/
public captureTaskFeedback(taskId: string, feedbackType: TaskFeedbackType) {
public captureTaskFeedback(taskId: string, feedbackType: TaskFeedbackType, collect: boolean = false) {
console.info("TelemetryService: Capturing task feedback", { taskId, feedbackType })
this.capture({
event: PostHogClient.EVENTS.TASK.FEEDBACK,
properties: {
taskId,
feedbackType,
this.capture(
{
event: PostHogClient.EVENTS.TASK.FEEDBACK,
properties: {
taskId,
feedbackType,
},
},
})
collect,
)
}
// Tool events
@@ -278,16 +341,19 @@ class PostHogClient {
* @param autoApproved Whether the tool was auto-approved based on settings
* @param success Whether the tool execution was successful
*/
public captureToolUsage(taskId: string, tool: string, autoApproved: boolean, success: boolean) {
this.capture({
event: PostHogClient.EVENTS.TASK.TOOL_USED,
properties: {
taskId,
tool,
autoApproved,
success,
public captureToolUsage(taskId: string, tool: string, autoApproved: boolean, success: boolean, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TOOL_USED,
properties: {
taskId,
tool,
autoApproved,
success,
},
},
})
collect,
)
}
/**
@@ -300,15 +366,19 @@ class PostHogClient {
taskId: string,
action: "shadow_git_initialized" | "commit_created" | "restored" | "diff_generated",
durationMs?: number,
collect: boolean = false,
) {
this.capture({
event: PostHogClient.EVENTS.TASK.CHECKPOINT_USED,
properties: {
taskId,
action,
durationMs,
this.capture(
{
event: PostHogClient.EVENTS.TASK.CHECKPOINT_USED,
properties: {
taskId,
action,
durationMs,
},
},
})
collect,
)
}
// UI events
@@ -319,16 +389,25 @@ class PostHogClient {
* @param location Where the switch occurred (settings panel or bottom bar)
* @param taskId Optional task identifier if switch occurred during a task
*/
public captureProviderSwitch(from: string, to: string, location: "settings" | "bottom", taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.PROVIDER_SWITCH,
properties: {
from,
to,
location,
taskId,
public captureProviderSwitch(
from: string,
to: string,
location: "settings" | "bottom",
taskId?: string,
collect: boolean = false,
) {
this.capture(
{
event: PostHogClient.EVENTS.UI.PROVIDER_SWITCH,
properties: {
from,
to,
location,
taskId,
},
},
})
collect,
)
}
/**
@@ -336,14 +415,17 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param imageCount Number of images attached
*/
public captureImageAttached(taskId: string, imageCount: number) {
this.capture({
event: PostHogClient.EVENTS.UI.IMAGE_ATTACHED,
properties: {
taskId,
imageCount,
public captureImageAttached(taskId: string, imageCount: number, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.IMAGE_ATTACHED,
properties: {
taskId,
imageCount,
},
},
})
collect,
)
}
/**
@@ -351,66 +433,81 @@ class PostHogClient {
* @param button Identifier for the button that was clicked
* @param taskId Optional task identifier if click occurred during a task
*/
public captureButtonClick(button: string, taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.BUTTON_CLICK,
properties: {
button,
taskId,
public captureButtonClick(button: string, taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.BUTTON_CLICK,
properties: {
button,
taskId,
},
},
})
collect,
)
}
/**
* Records when the marketplace view is opened
* @param taskId Optional task identifier if marketplace was opened during a task
*/
public captureMarketplaceOpened(taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.MARKETPLACE_OPENED,
properties: {
taskId,
public captureMarketplaceOpened(taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MARKETPLACE_OPENED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
* Records when the settings panel is opened
* @param taskId Optional task identifier if settings were opened during a task
*/
public captureSettingsOpened(taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.SETTINGS_OPENED,
properties: {
taskId,
public captureSettingsOpened(taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.SETTINGS_OPENED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
* Records when the task history view is opened
* @param taskId Optional task identifier if history was opened during a task
*/
public captureHistoryOpened(taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.HISTORY_OPENED,
properties: {
taskId,
public captureHistoryOpened(taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.HISTORY_OPENED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
* Records when a task is removed from the task history
* @param taskId Unique identifier for the task being removed
*/
public captureTaskPopped(taskId: string) {
this.capture({
event: PostHogClient.EVENTS.UI.TASK_POPPED,
properties: {
taskId,
public captureTaskPopped(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.TASK_POPPED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
@@ -418,14 +515,17 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param errorType Type of error that occurred (e.g., "search_not_found", "invalid_format")
*/
public captureDiffEditFailure(taskId: string, errorType?: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.DIFF_EDIT_FAILED,
properties: {
taskId,
errorType,
public captureDiffEditFailure(taskId: string, errorType?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.DIFF_EDIT_FAILED,
properties: {
taskId,
errorType,
},
},
})
collect,
)
}
/**
@@ -434,41 +534,50 @@ class PostHogClient {
* @param provider Provider of the selected model
* @param taskId Optional task identifier if model was selected during a task
*/
public captureModelSelected(model: string, provider: string, taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.MODEL_SELECTED,
properties: {
model,
provider,
taskId,
public captureModelSelected(model: string, provider: string, taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MODEL_SELECTED,
properties: {
model,
provider,
taskId,
},
},
})
collect,
)
}
/**
* Records when a historical task is loaded from storage
* @param taskId Unique identifier for the historical task
*/
public captureHistoricalTaskLoaded(taskId: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.HISTORICAL_LOADED,
properties: {
taskId,
public captureHistoricalTaskLoaded(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.HISTORICAL_LOADED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
* Records when the retry button is clicked for failed operations
* @param taskId Unique identifier for the task being retried
*/
public captureRetryClicked(taskId: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.RETRY_CLICKED,
properties: {
taskId,
public captureRetryClicked(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.RETRY_CLICKED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
@@ -476,17 +585,20 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param browserSettings The browser settings being used
*/
public captureBrowserToolStart(taskId: string, browserSettings: BrowserSettings) {
this.capture({
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_START,
properties: {
taskId,
viewport: browserSettings.viewport,
isRemote: !!browserSettings.remoteBrowserEnabled,
remoteBrowserHost: browserSettings.remoteBrowserHost,
timestamp: new Date().toISOString(),
public captureBrowserToolStart(taskId: string, browserSettings: BrowserSettings, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_START,
properties: {
taskId,
viewport: browserSettings.viewport,
isRemote: !!browserSettings.remoteBrowserEnabled,
remoteBrowserHost: browserSettings.remoteBrowserHost,
timestamp: new Date().toISOString(),
},
},
})
collect,
)
}
/**
@@ -501,17 +613,21 @@ class PostHogClient {
duration: number
actions?: string[]
},
collect: boolean = false,
) {
this.capture({
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_END,
properties: {
taskId,
actionCount: stats.actionCount,
duration: stats.duration,
actions: stats.actions,
timestamp: new Date().toISOString(),
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_END,
properties: {
taskId,
actionCount: stats.actionCount,
duration: stats.duration,
actions: stats.actions,
timestamp: new Date().toISOString(),
},
},
})
collect,
)
}
/**
@@ -531,17 +647,21 @@ class PostHogClient {
isRemote?: boolean
[key: string]: any
},
collect: boolean = false,
) {
this.capture({
event: PostHogClient.EVENTS.TASK.BROWSER_ERROR,
properties: {
taskId,
errorType,
errorMessage,
context,
timestamp: new Date().toISOString(),
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_ERROR,
properties: {
taskId,
errorType,
errorMessage,
context,
timestamp: new Date().toISOString(),
},
},
})
collect,
)
}
/**
@@ -550,15 +670,18 @@ class PostHogClient {
* @param qty The quantity of options that were presented
* @param mode The mode in which the option was selected ("plan" or "act")
*/
public captureOptionSelected(taskId: string, qty: number, mode: "plan" | "act") {
this.capture({
event: PostHogClient.EVENTS.TASK.OPTION_SELECTED,
properties: {
taskId,
qty,
mode,
public captureOptionSelected(taskId: string, qty: number, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.OPTION_SELECTED,
properties: {
taskId,
qty,
mode,
},
},
})
collect,
)
}
/**
@@ -567,15 +690,18 @@ class PostHogClient {
* @param qty The quantity of options that were presented
* @param mode The mode in which the custom response was provided ("plan" or "act")
*/
public captureOptionsIgnored(taskId: string, qty: number, mode: "plan" | "act") {
this.capture({
event: PostHogClient.EVENTS.TASK.OPTIONS_IGNORED,
properties: {
taskId,
qty,
mode,
public captureOptionsIgnored(taskId: string, qty: number, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.OPTIONS_IGNORED,
properties: {
taskId,
qty,
mode,
},
},
})
collect,
)
}
/**
@@ -583,20 +709,52 @@ class PostHogClient {
* @param model The name of the model the user has interacted with
* @param isFavorited Whether the model is being favorited (true) or unfavorited (false)
*/
public captureModelFavoritesUsage(model: string, isFavorited: boolean) {
this.capture({
event: PostHogClient.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
properties: {
model,
isFavorited,
public captureModelFavoritesUsage(model: string, isFavorited: boolean, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
properties: {
model,
isFavorited,
},
},
})
collect,
)
}
public isTelemetryEnabled(): boolean {
return this.telemetryEnabled
}
public async sendCollectedEvents(taskId?: string): Promise<void> {
if (this.collectedTasks.length > 0) {
if (taskId) {
const task = this.collectedTasks.find((t) => t.taskId === taskId)
if (task) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TASK_COLLECTION,
properties: { taskId, events: task.collection },
},
false,
)
this.collectedTasks = this.collectedTasks.filter((t) => t.taskId !== taskId)
}
} else {
for (const task of this.collectedTasks) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TASK_COLLECTION,
properties: { taskId: task.taskId, events: task.collection },
},
false,
)
this.collectedTasks = this.collectedTasks.filter((t) => t.taskId !== task.taskId)
}
}
}
}
public async shutdown(): Promise<void> {
await this.client.shutdown()
}
-2
View File
@@ -44,12 +44,10 @@ export interface ExtensionMessage {
| "browserConnectionResult"
| "scrollToSettings"
| "browserRelaunchResult"
| "relativePathsResponse" // Handles single and multiple path responses
| "fileSearchResults"
| "grpc_response" // New type for gRPC responses
| "setActiveQuote"
text?: string
paths?: (string | null)[] // Used for relativePathsResponse
action?:
| "chatButtonClicked"
| "mcpButtonClicked"
-6
View File
@@ -16,12 +16,8 @@ export interface WebviewMessage {
| "askResponse"
| "didShowAnnouncement"
| "selectImages"
| "exportCurrentTask"
| "showTaskWithId"
| "deleteTasksWithIds"
| "exportTaskWithId"
| "resetState"
| "requestOllamaModels"
| "requestLmStudioModels"
| "openInBrowser"
| "openMention"
@@ -62,7 +58,6 @@ export interface WebviewMessage {
| "relaunchChromeDebugMode"
| "taskFeedback"
| "scrollToSettings"
| "getRelativePaths" // Handles single and multiple URI resolution
| "searchFiles"
| "toggleFavoriteModel"
| "grpc_request"
@@ -76,7 +71,6 @@ export interface WebviewMessage {
// | "relaunchChromeDebugMode"
text?: string
uris?: string[] // Used for getRelativePaths
disabled?: boolean
askResponse?: ClineAskResponse
apiConfiguration?: ApiConfiguration
@@ -1,8 +1,4 @@
import { RuleFileRequest } from "../proto/file"
/**
* Simplified clean interface for RuleFile requests
*/
import { RuleFileRequest } from "../../proto/file"
// Helper for creating delete requests
export const DeleteRuleFileRequest = {
@@ -0,0 +1,27 @@
import { FileInfo } from "@shared/proto/file"
/**
* Converts domain search result objects to proto FileInfo objects
*/
export function convertSearchResultsToProtoFileInfos(
results: { path: string; type: "file" | "folder"; label?: string }[],
): FileInfo[] {
return results.map((result) => ({
path: result.path,
type: result.type,
label: result.label,
}))
}
/**
* Converts proto FileInfo objects to domain search result objects
*/
export function convertProtoFileInfosToSearchResults(
protoResults: FileInfo[],
): { path: string; type: "file" | "folder"; label?: string }[] {
return protoResults.map((protoResult) => ({
path: protoResult.path,
type: protoResult.type as "file" | "folder",
label: protoResult.label,
}))
}
-1
View File
@@ -1 +0,0 @@
export * from "./file"
+146
View File
@@ -22,6 +22,11 @@ export interface StringRequest {
value: string
}
export interface StringArrayRequest {
metadata?: Metadata | undefined
value: string[]
}
export interface String {
value: string
}
@@ -53,6 +58,10 @@ export interface Boolean {
value: boolean
}
export interface StringArray {
values: string[]
}
function createBaseMetadata(): Metadata {
return {}
}
@@ -275,6 +284,83 @@ export const StringRequest: MessageFns<StringRequest> = {
},
}
function createBaseStringArrayRequest(): StringArrayRequest {
return { metadata: undefined, value: [] }
}
export const StringArrayRequest: MessageFns<StringArrayRequest> = {
encode(message: StringArrayRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
for (const v of message.value) {
writer.uint32(18).string(v!)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): StringArrayRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseStringArrayRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.value.push(reader.string())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): StringArrayRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
value: globalThis.Array.isArray(object?.value) ? object.value.map((e: any) => globalThis.String(e)) : [],
}
},
toJSON(message: StringArrayRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.value?.length) {
obj.value = message.value
}
return obj
},
create<I extends Exact<DeepPartial<StringArrayRequest>, I>>(base?: I): StringArrayRequest {
return StringArrayRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<StringArrayRequest>, I>>(object: I): StringArrayRequest {
const message = createBaseStringArrayRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.value = object.value?.map((e) => e) || []
return message
},
}
function createBaseString(): String {
return { value: "" }
}
@@ -738,6 +824,66 @@ export const Boolean: MessageFns<Boolean> = {
},
}
function createBaseStringArray(): StringArray {
return { values: [] }
}
export const StringArray: MessageFns<StringArray> = {
encode(message: StringArray, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
for (const v of message.values) {
writer.uint32(10).string(v!)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): StringArray {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseStringArray()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.values.push(reader.string())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): StringArray {
return {
values: globalThis.Array.isArray(object?.values) ? object.values.map((e: any) => globalThis.String(e)) : [],
}
},
toJSON(message: StringArray): unknown {
const obj: any = {}
if (message.values?.length) {
obj.values = message.values
}
return obj
},
create<I extends Exact<DeepPartial<StringArray>, I>>(base?: I): StringArray {
return StringArray.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<StringArray>, I>>(object: I): StringArray {
const message = createBaseStringArray()
message.values = object.values?.map((e) => e) || []
return message
},
}
function bytesFromBase64(b64: string): Uint8Array {
return Uint8Array.from(globalThis.Buffer.from(b64, "base64"))
}
+470
View File
@@ -10,6 +10,46 @@ import { Empty, Metadata, StringRequest } from "./common"
export const protobufPackage = "cline"
/** Request to convert a list of URIs to relative paths */
export interface RelativePathsRequest {
metadata?: Metadata | undefined
uris: string[]
}
/** Response containing the converted relative paths */
export interface RelativePaths {
paths: string[]
}
/** Request for file search operations */
export interface FileSearchRequest {
metadata?: Metadata | undefined
/** Search query string */
query: string
/** Optional request ID for tracking requests */
mentionsRequestId?: string | undefined
/** Optional limit for results (default: 20) */
limit?: number | undefined
}
/** Result for file search operations */
export interface FileSearchResults {
/** Array of file/folder results */
results: FileInfo[]
/** Echo of the request ID for tracking */
mentionsRequestId?: string | undefined
}
/** File information structure for search results */
export interface FileInfo {
/** Relative path from workspace root */
path: string
/** "file" or "folder" */
type: string
/** Display name (usually basename) */
label?: string | undefined
}
/** Response for searchCommits */
export interface GitCommits {
commits: GitCommit[]
@@ -45,6 +85,418 @@ export interface RuleFile {
alreadyExists: boolean
}
function createBaseRelativePathsRequest(): RelativePathsRequest {
return { metadata: undefined, uris: [] }
}
export const RelativePathsRequest: MessageFns<RelativePathsRequest> = {
encode(message: RelativePathsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
for (const v of message.uris) {
writer.uint32(18).string(v!)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): RelativePathsRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseRelativePathsRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.uris.push(reader.string())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): RelativePathsRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
uris: globalThis.Array.isArray(object?.uris) ? object.uris.map((e: any) => globalThis.String(e)) : [],
}
},
toJSON(message: RelativePathsRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.uris?.length) {
obj.uris = message.uris
}
return obj
},
create<I extends Exact<DeepPartial<RelativePathsRequest>, I>>(base?: I): RelativePathsRequest {
return RelativePathsRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<RelativePathsRequest>, I>>(object: I): RelativePathsRequest {
const message = createBaseRelativePathsRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.uris = object.uris?.map((e) => e) || []
return message
},
}
function createBaseRelativePaths(): RelativePaths {
return { paths: [] }
}
export const RelativePaths: MessageFns<RelativePaths> = {
encode(message: RelativePaths, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
for (const v of message.paths) {
writer.uint32(10).string(v!)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): RelativePaths {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseRelativePaths()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.paths.push(reader.string())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): RelativePaths {
return { paths: globalThis.Array.isArray(object?.paths) ? object.paths.map((e: any) => globalThis.String(e)) : [] }
},
toJSON(message: RelativePaths): unknown {
const obj: any = {}
if (message.paths?.length) {
obj.paths = message.paths
}
return obj
},
create<I extends Exact<DeepPartial<RelativePaths>, I>>(base?: I): RelativePaths {
return RelativePaths.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<RelativePaths>, I>>(object: I): RelativePaths {
const message = createBaseRelativePaths()
message.paths = object.paths?.map((e) => e) || []
return message
},
}
function createBaseFileSearchRequest(): FileSearchRequest {
return { metadata: undefined, query: "", mentionsRequestId: undefined, limit: undefined }
}
export const FileSearchRequest: MessageFns<FileSearchRequest> = {
encode(message: FileSearchRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.query !== "") {
writer.uint32(18).string(message.query)
}
if (message.mentionsRequestId !== undefined) {
writer.uint32(26).string(message.mentionsRequestId)
}
if (message.limit !== undefined) {
writer.uint32(32).int32(message.limit)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): FileSearchRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseFileSearchRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.query = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.mentionsRequestId = reader.string()
continue
}
case 4: {
if (tag !== 32) {
break
}
message.limit = reader.int32()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): FileSearchRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
query: isSet(object.query) ? globalThis.String(object.query) : "",
mentionsRequestId: isSet(object.mentionsRequestId) ? globalThis.String(object.mentionsRequestId) : undefined,
limit: isSet(object.limit) ? globalThis.Number(object.limit) : undefined,
}
},
toJSON(message: FileSearchRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.query !== "") {
obj.query = message.query
}
if (message.mentionsRequestId !== undefined) {
obj.mentionsRequestId = message.mentionsRequestId
}
if (message.limit !== undefined) {
obj.limit = Math.round(message.limit)
}
return obj
},
create<I extends Exact<DeepPartial<FileSearchRequest>, I>>(base?: I): FileSearchRequest {
return FileSearchRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<FileSearchRequest>, I>>(object: I): FileSearchRequest {
const message = createBaseFileSearchRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.query = object.query ?? ""
message.mentionsRequestId = object.mentionsRequestId ?? undefined
message.limit = object.limit ?? undefined
return message
},
}
function createBaseFileSearchResults(): FileSearchResults {
return { results: [], mentionsRequestId: undefined }
}
export const FileSearchResults: MessageFns<FileSearchResults> = {
encode(message: FileSearchResults, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
for (const v of message.results) {
FileInfo.encode(v!, writer.uint32(10).fork()).join()
}
if (message.mentionsRequestId !== undefined) {
writer.uint32(18).string(message.mentionsRequestId)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): FileSearchResults {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseFileSearchResults()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.results.push(FileInfo.decode(reader, reader.uint32()))
continue
}
case 2: {
if (tag !== 18) {
break
}
message.mentionsRequestId = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): FileSearchResults {
return {
results: globalThis.Array.isArray(object?.results) ? object.results.map((e: any) => FileInfo.fromJSON(e)) : [],
mentionsRequestId: isSet(object.mentionsRequestId) ? globalThis.String(object.mentionsRequestId) : undefined,
}
},
toJSON(message: FileSearchResults): unknown {
const obj: any = {}
if (message.results?.length) {
obj.results = message.results.map((e) => FileInfo.toJSON(e))
}
if (message.mentionsRequestId !== undefined) {
obj.mentionsRequestId = message.mentionsRequestId
}
return obj
},
create<I extends Exact<DeepPartial<FileSearchResults>, I>>(base?: I): FileSearchResults {
return FileSearchResults.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<FileSearchResults>, I>>(object: I): FileSearchResults {
const message = createBaseFileSearchResults()
message.results = object.results?.map((e) => FileInfo.fromPartial(e)) || []
message.mentionsRequestId = object.mentionsRequestId ?? undefined
return message
},
}
function createBaseFileInfo(): FileInfo {
return { path: "", type: "", label: undefined }
}
export const FileInfo: MessageFns<FileInfo> = {
encode(message: FileInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.path !== "") {
writer.uint32(10).string(message.path)
}
if (message.type !== "") {
writer.uint32(18).string(message.type)
}
if (message.label !== undefined) {
writer.uint32(26).string(message.label)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): FileInfo {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseFileInfo()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.path = reader.string()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.type = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.label = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): FileInfo {
return {
path: isSet(object.path) ? globalThis.String(object.path) : "",
type: isSet(object.type) ? globalThis.String(object.type) : "",
label: isSet(object.label) ? globalThis.String(object.label) : undefined,
}
},
toJSON(message: FileInfo): unknown {
const obj: any = {}
if (message.path !== "") {
obj.path = message.path
}
if (message.type !== "") {
obj.type = message.type
}
if (message.label !== undefined) {
obj.label = message.label
}
return obj
},
create<I extends Exact<DeepPartial<FileInfo>, I>>(base?: I): FileInfo {
return FileInfo.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<FileInfo>, I>>(object: I): FileInfo {
const message = createBaseFileInfo()
message.path = object.path ?? ""
message.type = object.type ?? ""
message.label = object.label ?? undefined
return message
},
}
function createBaseGitCommits(): GitCommits {
return { commits: [] }
}
@@ -481,6 +933,24 @@ export const FileServiceDefinition = {
responseStream: false,
options: {},
},
/** Convert URIs to workspace-relative paths */
getRelativePaths: {
name: "getRelativePaths",
requestType: RelativePathsRequest,
requestStream: false,
responseType: RelativePaths,
responseStream: false,
options: {},
},
/** Search for files in the workspace with fuzzy matching */
searchFiles: {
name: "searchFiles",
requestType: FileSearchRequest,
requestStream: false,
responseType: FileSearchResults,
responseStream: false,
options: {},
},
},
} as const
+28
View File
@@ -0,0 +1,28 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v3.19.1
// source: models.proto
/* eslint-disable */
import { StringArray, StringRequest } from "./common"
export const protobufPackage = "cline"
/** Service for model-related operations */
export type ModelsServiceDefinition = typeof ModelsServiceDefinition
export const ModelsServiceDefinition = {
name: "ModelsService",
fullName: "cline.ModelsService",
methods: {
/** Fetches available models from Ollama */
getOllamaModels: {
name: "getOllamaModels",
requestType: StringRequest,
requestStream: false,
responseType: StringArray,
responseStream: false,
options: {},
},
},
} as const
+19 -1
View File
@@ -6,7 +6,7 @@
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { Empty, EmptyRequest, Metadata } from "./common"
import { Empty, EmptyRequest, Metadata, StringArrayRequest, StringRequest } from "./common"
export const protobufPackage = "cline"
@@ -133,6 +133,15 @@ export const TaskServiceDefinition = {
responseStream: false,
options: {},
},
/** Deletes multiple tasks with the given IDs */
deleteTasksWithIds: {
name: "deleteTasksWithIds",
requestType: StringArrayRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
/** Creates a new task with the given text and optional images */
newTask: {
name: "newTask",
@@ -142,6 +151,15 @@ export const TaskServiceDefinition = {
responseStream: false,
options: {},
},
/** Exports a task with the given ID to markdown */
exportTaskWithId: {
name: "exportTaskWithId",
requestType: StringRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
},
} as const
+1
View File
@@ -0,0 +1 @@
25463
+19 -15
View File
@@ -309,15 +309,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "relativePathsResponse": {
// New case for batch response
const validPaths = message.paths?.filter((path): path is string => !!path) || []
if (validPaths.length > 0) {
setPendingInsertions((prev) => [...prev, ...validPaths])
}
break
}
case "fileSearchResults": {
// Only update results if they match the current query or if there's no mentionsRequestId - better UX
if (!message.mentionsRequestId || message.mentionsRequestId === currentSearchQueryRef.current) {
@@ -695,11 +686,19 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
// Set a timeout to debounce the search requests
searchTimeoutRef.current = setTimeout(() => {
vscode.postMessage({
type: "searchFiles",
FileServiceClient.searchFiles({
query: query,
mentionsRequestId: query,
})
.then((results) => {
setFileSearchResults(results.results || [])
setSearchLoading(false)
})
.catch((error) => {
console.error("Error searching files:", error)
setFileSearchResults([])
setSearchLoading(false)
})
}, 200) // 200ms debounce
} else {
setSelectedMenuIndex(3) // Set to "File" option by default
@@ -1154,10 +1153,15 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
setIntendedCursorPosition(initialCursorPos)
vscode.postMessage({
type: "getRelativePaths",
uris: validUris,
})
FileServiceClient.getRelativePaths({ uris: validUris })
.then((response) => {
if (response.paths.length > 0) {
setPendingInsertions((prev) => [...prev, ...response.paths])
}
})
.catch((error) => {
console.error("Error getting relative paths:", error)
})
return
}
@@ -10,6 +10,8 @@ import { vscode } from "@/utils/vscode"
import Thumbnails from "@/components/common/Thumbnails"
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import { validateSlashCommand } from "@/utils/slash-commands"
import TaskTimeline from "./TaskTimeline"
import { TaskServiceClient } from "@/services/grpc-client"
interface TaskHeaderProps {
task: ClineMessage
@@ -34,7 +36,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
lastApiReqTotalTokens,
onClose,
}) => {
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage } = useExtensionState()
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage, clineMessages } = useExtensionState()
const [isTaskExpanded, setIsTaskExpanded] = useState(true)
const [isTextExpanded, setIsTextExpanded] = useState(false)
const [showSeeMore, setShowSeeMore] = useState(false)
@@ -370,7 +372,9 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
gap: "4px",
flexWrap: "wrap",
}}>
<span style={{ fontWeight: "bold" }}>Tokens:</span>
<div style={{ display: "flex", alignItems: "center" }}>
<span style={{ fontWeight: "bold" }}>Tokens:</span>
</div>
<span
style={{
display: "flex",
@@ -409,6 +413,8 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
)}
</div>
<TaskTimeline messages={clineMessages} />
{shouldShowPromptCacheInfoClineOR && cacheReads !== undefined && (
<div
style={{
@@ -669,7 +675,7 @@ const DeleteButton: React.FC<{
}> = ({ taskSize, taskId }) => (
<VSCodeButton
appearance="icon"
onClick={() => vscode.postMessage({ type: "deleteTasksWithIds", text: JSON.stringify([taskId]) })}
onClick={() => taskId && TaskServiceClient.deleteTasksWithIds({ value: [taskId] })}
style={{ padding: "0px 0px" }}>
<div
style={{
@@ -0,0 +1,186 @@
import React, { useMemo, useState, useRef, useEffect } from "react"
import { ClineMessage } from "@shared/ExtensionMessage"
import { combineApiRequests } from "@shared/combineApiRequests"
import { combineCommandSequences } from "@shared/combineCommandSequences"
import TaskTimelineTooltip from "./TaskTimelineTooltip"
interface TaskTimelineProps {
messages: ClineMessage[]
}
const getBlockColor = (message: ClineMessage): string => {
if (message.type === "say") {
switch (message.say) {
case "task":
return "#FFFFFF" // White for system prompt
case "text":
return "#AAAAAA" // Gray for assistant responses
case "tool":
if (message.text) {
try {
const toolData = JSON.parse(message.text)
if (
toolData.tool === "readFile" ||
toolData.tool === "listFilesTopLevel" ||
toolData.tool === "listFilesRecursive" ||
toolData.tool === "listCodeDefinitionNames" ||
toolData.tool === "searchFiles"
) {
return "#F5F5DC" // Beige for file read operations
} else if (toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") {
return "#3B82F6" // Blue for file edit/create operations
}
} catch (e) {
// JSON parse error here
}
}
return "#F5F5DC" // Default beige for tool use
case "command":
case "command_output":
return "#EF4444" // Red for terminal commands
case "browser_action":
case "browser_action_result":
return "#8B5CF6" // Purple for browser actions
case "completion_result":
return "#10B981" // Green for task success
default:
return "#9E9E9E" // Grey for unknown
}
} else if (message.type === "ask") {
switch (message.ask) {
case "followup":
return "#AAAAAA" // Gray for user messages
case "plan_mode_respond":
return "#AAAAAA" // Gray for planning responses
case "tool":
return "#9E9E9E" // Gray for tool approvals
case "command":
return "#9E9E9E" // Gray for command approvals
case "browser_action_launch":
return "#9E9E9E" // Gray for browser launch approvals
default:
return "#9E9E9E" // Grey for unknown
}
}
return "#9E9E9E" // Default grey
}
const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
const [hoveredMessage, setHoveredMessage] = useState<ClineMessage | null>(null)
const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null)
const containerRef = useRef<HTMLDivElement>(null)
const scrollableRef = useRef<HTMLDivElement>(null)
const taskTimelinePropsMessages = useMemo(() => {
if (messages.length <= 1) return []
const processed = combineApiRequests(combineCommandSequences(messages.slice(1)))
return processed.filter((msg) => {
if (
msg.type === "say" &&
(msg.say === "api_req_started" ||
msg.say === "api_req_finished" ||
msg.say === "api_req_retried" ||
msg.say === "deleted_api_reqs" ||
(msg.say === "text" && (!msg.text || msg.text.trim() === "")))
) {
return false
}
return true
})
}, [messages])
useEffect(() => {
if (scrollableRef.current && taskTimelinePropsMessages.length > 0) {
scrollableRef.current.scrollLeft = scrollableRef.current.scrollWidth
}
}, [taskTimelinePropsMessages])
if (taskTimelinePropsMessages.length === 0) {
return null
}
const TOOLTIP_MARGIN = 32 // 32px margin on each side
const handleMouseEnter = (message: ClineMessage, event: React.MouseEvent<HTMLDivElement>) => {
setHoveredMessage(message)
const viewportWidth = window.innerWidth
const tooltipWidth = viewportWidth - TOOLTIP_MARGIN * 2
// Center the tooltip horizontally in the viewport
const x = TOOLTIP_MARGIN
setTooltipPosition({ x, y: event.clientY })
}
const handleMouseLeave = () => {
setHoveredMessage(null)
setTooltipPosition(null)
}
return (
<div
ref={containerRef}
style={{
position: "relative",
width: "100%",
marginTop: "4px",
marginBottom: "4px",
overflow: "hidden",
}}>
<div
ref={scrollableRef}
style={{
display: "flex",
height: "10px",
overflowX: "auto",
scrollbarWidth: "none",
msOverflowStyle: "none",
width: "100%",
WebkitOverflowScrolling: "touch",
}}>
<style>
{`
/* Hide scrollbar for Chrome, Safari and Opera */
div::-webkit-scrollbar {
display: none;
}
`}
</style>
{taskTimelinePropsMessages.map((message, index) => (
<div
key={index}
style={{
width: "5px",
height: "100%",
backgroundColor: getBlockColor(message),
marginRight: "1px",
flexShrink: 0,
cursor: "pointer",
}}
onMouseEnter={(e) => handleMouseEnter(message, e)}
onMouseLeave={handleMouseLeave}
/>
))}
</div>
{hoveredMessage && containerRef.current && tooltipPosition && (
<div
style={{
position: "fixed",
left: `${tooltipPosition.x}px`,
top: `${tooltipPosition.y + 20}px`,
zIndex: 1000,
pointerEvents: "none",
width: `calc(100% - ${TOOLTIP_MARGIN * 2}px)`,
}}>
<TaskTimelineTooltip message={hoveredMessage} />
</div>
)}
</div>
)
}
export default TaskTimeline
@@ -0,0 +1,217 @@
import React from "react"
import { ClineMessage } from "@shared/ExtensionMessage"
// Color mapping for different message types
interface TaskTimelineTooltipProps {
message: ClineMessage
}
const TaskTimelineTooltip: React.FC<TaskTimelineTooltipProps> = ({ message }) => {
const getMessageDescription = (message: ClineMessage): string => {
if (message.type === "say") {
switch (message.say) {
// TODO: Need to confirm these classifcations with design
case "task":
return "Task Message"
case "text":
return "Assistant Response"
case "tool":
if (message.text) {
try {
const toolData = JSON.parse(message.text)
if (
toolData.tool === "readFile" ||
toolData.tool === "listFilesTopLevel" ||
toolData.tool === "listFilesRecursive" ||
toolData.tool === "listCodeDefinitionNames" ||
toolData.tool === "searchFiles"
) {
return `File Read: ${toolData.tool}`
} else if (toolData.tool === "editedExistingFile") {
return `File Edit: ${toolData.path || "Unknown file"}`
} else if (toolData.tool === "newFileCreated") {
return `New File: ${toolData.path || "Unknown file"}`
}
return `Tool: ${toolData.tool}`
} catch (e) {
return "Tool Use"
}
}
return "Tool Use"
case "command":
return "Terminal Command"
case "command_output":
return "Terminal Output"
case "browser_action":
return "Browser Action"
case "browser_action_result":
return "Browser Result"
case "completion_result":
return "Task Completed"
default:
return message.say || "Unknown"
}
} else if (message.type === "ask") {
switch (message.ask) {
case "followup":
return "User Message"
case "plan_mode_respond":
return "Planning Response"
case "tool":
return "Tool Approval"
case "command":
return "Command Approval"
case "browser_action_launch":
return "Browser Launch"
default:
return message.ask || "Unknown"
}
}
return "Unknown Message Type"
}
const getMessageContent = (message: ClineMessage): string => {
if (message.text) {
if (message.type === "ask" && message.ask === "plan_mode_respond" && message.text) {
try {
const planData = JSON.parse(message.text)
return planData.response || message.text
} catch (e) {
return message.text
}
} else if (message.type === "say" && message.say === "tool" && message.text) {
try {
const toolData = JSON.parse(message.text)
return JSON.stringify(toolData, null, 2)
} catch (e) {
return message.text
}
}
if (message.text.length > 200) {
return message.text.substring(0, 200) + "..."
}
return message.text
}
return ""
}
const getTimestamp = (message: ClineMessage): string => {
if (message.ts) {
const date = new Date(message.ts)
return date.toLocaleTimeString()
}
return ""
}
// Get color for the indicator based on message type
const getMessageColor = (message: ClineMessage): string => {
if (message.type === "say") {
switch (message.say) {
case "task":
return "#FFFFFF" // White for system prompt
case "text":
return "#AAAAAA" // Gray for assistant responses
case "tool":
if (message.text) {
try {
const toolData = JSON.parse(message.text)
if (
toolData.tool === "readFile" ||
toolData.tool === "listFilesTopLevel" ||
toolData.tool === "listFilesRecursive" ||
toolData.tool === "listCodeDefinitionNames" ||
toolData.tool === "searchFiles"
) {
return "#F5F5DC" // Beige for file read operations
} else if (toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") {
return "#3B82F6" // Blue for file edit/create operations
}
} catch (e) {
// JSON parse error here
}
}
return "#F5F5DC" // Default beige for tool use
case "command":
case "command_output":
return "#EF4444" // Red for terminal commands
case "browser_action":
case "browser_action_result":
return "#8B5CF6" // Purple for browser actions
case "completion_result":
return "#10B981" // Green for task success
default:
return "#9E9E9E" // Grey for unknown
}
} else if (message.type === "ask") {
switch (message.ask) {
case "followup":
return "#AAAAAA" // Gray for user messages
case "plan_mode_respond":
return "#AAAAAA" // Gray for planning responses
case "tool":
return "#9E9E9E" // Gray for tool approvals
case "command":
return "#9E9E9E" // Gray for command approvals
case "browser_action_launch":
return "#9E9E9E" // Gray for browser launch approvals
default:
return "#9E9E9E" // Grey for unknown
}
}
return "#9E9E9E" // Default grey
}
return (
<div
style={{
backgroundColor: "var(--vscode-editor-background)",
color: "var(--vscode-editor-foreground)",
border: "1px solid var(--vscode-widget-border)",
borderRadius: "3px",
padding: "8px",
width: "100%", // Fill the container width
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
fontSize: "12px",
}}>
<div style={{ fontWeight: "bold", marginBottom: "4px", display: "flex", alignItems: "center" }}>
<div
style={{
width: "10px",
height: "10px",
minWidth: "10px", // Ensure fixed width
minHeight: "10px", // Ensure fixed height
borderRadius: "50%",
backgroundColor: getMessageColor(message),
marginRight: "8px",
display: "inline-block",
flexShrink: 0, // Prevent shrinking when space is limited
}}
/>
{getMessageDescription(message)}
{getTimestamp(message) && (
<span style={{ fontWeight: "normal", fontSize: "10px", marginLeft: "8px" }}>{getTimestamp(message)}</span>
)}
</div>
{getMessageContent(message) && (
<div
style={{
whiteSpace: "pre-wrap",
wordBreak: "break-word",
maxHeight: "150px",
overflowY: "auto",
fontSize: "11px",
fontFamily: "var(--vscode-editor-font-family)",
backgroundColor: "var(--vscode-textBlockQuote-background)",
padding: "4px",
borderRadius: "2px",
}}>
{getMessageContent(message)}
</div>
)}
</div>
)
}
export default TaskTimelineTooltip
@@ -3,7 +3,7 @@ import { vscode } from "@/utils/vscode"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useClickAway } from "react-use"
import { FileServiceClient } from "@/services/grpc-client"
import { CreateRuleFileRequest } from "@shared/proto-conversions"
import { CreateRuleFileRequest } from "@shared/proto-conversions/file/rule-files-conversion"
interface NewRuleRowProps {
isGlobal: boolean
@@ -1,6 +1,6 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { FileServiceClient } from "@/services/grpc-client"
import { DeleteRuleFileRequest } from "@shared/proto-conversions"
import { DeleteRuleFileRequest } from "@shared/proto-conversions/file/rule-files-conversion"
const RuleRow: React.FC<{
rulePath: string
@@ -4,6 +4,7 @@ import { vscode } from "@/utils/vscode"
import { Virtuoso } from "react-virtuoso"
import { memo, useMemo, useState, useEffect, useCallback } from "react"
import Fuse, { FuseResult } from "fuse.js"
import { TaskServiceClient } from "@/services/grpc-client"
import { formatLargeNumber } from "@/utils/format"
import { formatSize } from "@/utils/format"
import { ExtensionMessage } from "@shared/ExtensionMessage"
@@ -61,12 +62,12 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
}, [])
const handleDeleteHistoryItem = useCallback((id: string) => {
vscode.postMessage({ type: "deleteTasksWithIds", text: JSON.stringify([id]) })
TaskServiceClient.deleteTasksWithIds({ value: [id] })
}, [])
const handleDeleteSelectedHistoryItems = useCallback((ids: string[]) => {
if (ids.length > 0) {
vscode.postMessage({ type: "deleteTasksWithIds", text: JSON.stringify(ids) })
TaskServiceClient.deleteTasksWithIds({ value: ids })
setSelectedItems([])
}
}, [])
@@ -562,7 +563,7 @@ const ExportButton = ({ itemId }: { itemId: string }) => (
appearance="icon"
onClick={(e) => {
e.stopPropagation()
vscode.postMessage({ type: "exportTaskWithId", text: itemId })
TaskServiceClient.exportTaskWithId({ value: itemId }).catch((err) => console.error("Failed to export task:", err))
}}>
<div style={{ fontSize: "11px", fontWeight: 500, opacity: 1 }}>EXPORT</div>
</VSCodeButton>
@@ -55,6 +55,7 @@ import {
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
import { ModelsServiceClient } from "@/services/grpc-client"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import VSCodeButtonLink from "@/components/common/VSCodeButtonLink"
import OpenRouterModelPicker, { ModelDescriptionMarkdown, OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
@@ -182,12 +183,19 @@ const ApiOptions = ({
}, [apiConfiguration])
// Poll ollama/lmstudio models
const requestLocalModels = useCallback(() => {
const requestLocalModels = useCallback(async () => {
if (selectedProvider === "ollama") {
vscode.postMessage({
type: "requestOllamaModels",
text: apiConfiguration?.ollamaBaseUrl,
})
try {
const response = await ModelsServiceClient.getOllamaModels({
value: apiConfiguration?.ollamaBaseUrl || "",
})
if (response && response.values) {
setOllamaModels(response.values)
}
} catch (error) {
console.error("Failed to fetch Ollama models:", error)
setOllamaModels([])
}
} else if (selectedProvider === "lmstudio") {
vscode.postMessage({
type: "requestLmStudioModels",
@@ -209,9 +217,7 @@ const ApiOptions = ({
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
if (message.type === "ollamaModels" && message.ollamaModels) {
setOllamaModels(message.ollamaModels)
} else if (message.type === "lmStudioModels" && message.lmStudioModels) {
if (message.type === "lmStudioModels" && message.lmStudioModels) {
setLmStudioModels(message.lmStudioModels)
} else if (message.type === "vsCodeLmModels" && message.vsCodeLmModels) {
setVsCodeLmModels(message.vsCodeLmModels)
+3
View File
@@ -6,6 +6,7 @@ import { CheckpointsServiceDefinition } from "@shared/proto/checkpoints"
import { EmptyRequest } from "@shared/proto/common"
import { FileServiceDefinition } from "@shared/proto/file"
import { McpServiceDefinition } from "@shared/proto/mcp"
import { ModelsServiceDefinition } from "@shared/proto/models"
import { TaskServiceDefinition } from "@shared/proto/task"
import { WebContentServiceDefinition } from "@shared/proto/web_content"
// Generic type for any protobuf service definition
@@ -102,6 +103,7 @@ const BrowserServiceClient = createGrpcClient(BrowserServiceDefinition)
const CheckpointsServiceClient = createGrpcClient(CheckpointsServiceDefinition)
const FileServiceClient = createGrpcClient(FileServiceDefinition)
const McpServiceClient = createGrpcClient(McpServiceDefinition)
const ModelsServiceClient = createGrpcClient(ModelsServiceDefinition)
const TaskServiceClient = createGrpcClient(TaskServiceDefinition)
const WebContentServiceClient = createGrpcClient(WebContentServiceDefinition)
@@ -112,5 +114,6 @@ export {
FileServiceClient,
TaskServiceClient,
McpServiceClient,
ModelsServiceClient,
WebContentServiceClient,
}