Compare commits

..
Author SHA1 Message Date
Cline Evaluation 58c67c5a1e removing sparkle from command name 2025-05-08 11:43:09 -07:00
watany bddc1b5e96 fix(bedrock); update bedrock api (#3157)
* fix nova

* haiku

* changeset

* changeset

* clean up duplicate changeset

* commented caching write
2025-05-08 10:52:08 -07:00
pashpashpashandCline Evaluation cb0de8f17e tracking models in diff edit failures (#3297)
* tracking models in diff edit failures

* prettier

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-08 12:33:29 -05:00
EvanandElephant Lumps e1a0b244de Conditionally initialize posthog webview (#3381)
* conditionally initialize posthog client webview

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-08 12:08:40 -05:00
Dennis Bartlett a9d5411bf0 Revert "Update deployer team name (#3377)" 2025-05-08 06:57:18 -05:00
Dennis Bartlett 16af9125ec Update variable name (#3384) 2025-05-08 06:51:55 -05:00
Ara 2792e7698f Raise Errors when users try to upload images larger than 7500x7500 pixels (#3336)
* Adding iamge dimension check

* Adding iamge dimension check

* Adding iamge dimension check

* Adding iamge dimension check

* Adding iamge dimension check

* Adding iamge dimension check
2025-05-07 23:48:04 -07:00
d02e5a89e5 fix excessive markdown format character escaping (#3355)
* fix excessive markdown format character escaping

* add changeset

* made it a little more robust

---------

Co-authored-by: Wesley Smith <wes@neofactory.ai>
Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-07 22:31:31 -07:00
Dennis Bartlett 20f19917d3 Add org to team affiliation check (#3380) 2025-05-08 00:01:42 -05:00
EvanandElephant Lumps 7e5cd52864 Always allow textarea typing (#3356)
* enable text area while cline is doing stuff

* changeset

* add sendingDisabled to dependency array

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-07 22:01:07 -07:00
pashpashpashandCline Evaluation 4622ad767b Copy buttons (#3373)
* copy button in task header

* changeset

* added copy buttons to assistant messages that show up on hover

* added aria

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-07 21:59:42 -07:00
Dennis Bartlett 96048d5ac5 Update deployer team name (#3377)
* Update deployer team name

* Create clever-balloons-wave.md
2025-05-07 23:37:33 -05:00
Ara facec93082 Adding Mistral 3 medium model (#3366)
* Fixing Gemini and vertex cache pricing

* Fixing Gemini and vertex cache pricing
2025-05-08 05:46:09 +05:30
Saoud Rizwan c040be9eb1 Disables autocaptures when initializing feature flags 2025-05-07 15:55:33 -05:00
EvanandElephant Lumps 8d3cf53289 Docs: image links (#3350)
* add cdn image links

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-07 13:41:42 -05:00
Trevor Hudson 83c4a82e6d Diable auto track (#3364)
* disable autocatpure

* changeset
2025-05-07 11:32:33 -07:00
Toshii 6ad11badf3 Systematic selection of gemini models w/ caching (#3343)
* no more updating gemini models

* changeset
2025-05-07 10:07:45 -07:00
Tomás Barreiro 39c7da301c fix path tests on windows (#3276) 2025-05-07 22:22:58 +05:30
DrobConsulting 5275f2eabc Updated OpenAiHandler to support Azure GCC region (#3235)
- Added a check for azureApiVersion to determine if the endpoint is an Azure endpoint.
    - Included conditions to check for 'azure.com' and 'azure.us' in the openAiBaseUrl.
    - Ensured that the openAiModelId does not include 'deepseek' when determining the Azure endpoint.
2025-05-07 01:06:12 -07:00
Caleb EomandCline Evaluation 7cf68ff279 Improve time display and filter out resume_task in Task Timeline (#3333)
* Improve time display and filter out resume_task in Task Timeline

* changeset

* polishing it up a little

* a little bigger

* more tooltips + task header

* further refinement

* spacing

* moving delete button up one row conditionally

* removed log

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-05-07 02:02:41 -05:00
EvanandElephant Lumps b8af02ebaa Stop doomscrolling (#3354)
* disable auto scroll on user scroll up

* changeset

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-05-07 01:29:57 -05:00
56 changed files with 1032 additions and 472 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
disable autocapture
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Update deployer team name
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
added copy buttons to task header and assistant messages
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add org to team affiliation check
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix-bedrock-api
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix for markdown copy excessive escape characters
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Only initialize posthog in the webview if the user has opted into telemetry
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Adding mistral medium-3 model
+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": patch
---
Raise Errors when users try to upload images larger than 7500x7500 pixels
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
timestamp visualization
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Allow the user to scroll when Cline is editing a file by disabling auto-scroll when the user scrolls up
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add image links to docs
@@ -33,6 +33,7 @@ jobs:
uses: morfien101/actions-authorized-user@4a3cfbf0bcb3cafe4a71710a278920c5d94bb38b
with:
username: ${{ github.actor }}
org: ${{ github.repository_owner }}
team: "deployer"
github_token: ${{ secrets.GITHUB_TOKEN }}
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## [3.14.1]
- Disables autocaptures when initializing feature flags
## [3.14.0]
- Add support for custom model ID in AWS Bedrock provider, enabling use of Application Inference Profile (Thanks @clicube!)
@@ -36,7 +36,7 @@ Cline offers the option of utilizing AWS credentials or AWS profiles to access A
<Frame>
<img
src="/assets/robot_panel_dark.png"
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-aws-setup-markup%20(1).png"
alt="AWS Bedrock configuration in Cline settings showing profile authentication setup"
/>
</Frame>
+4 -1
View File
@@ -76,7 +76,10 @@ Vertex AI supports eight regions. Select a region that meets your latency, compl
- Search for **Cline** and install the extension
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Cline extension in VS Code" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-extension-arrow.png"
alt="Cline extension in VS Code"
/>
</Frame>
#### 3.2 Configure Cline Settings
@@ -6,7 +6,7 @@ Cline supports major cloud providers like AWS Bedrock and Google's Cloud Vertex;
For the purpose of this document, we assume your organization will use cloud-based frontier models. Cloud inference providers offer cutting-edge capabilities and the flexibility to select models which best suit your needs.
Certain scenarios may warrant using local models, including handling highly sensitive data, applications requiring consistent low-latency responses, or compliance with strict data sovereignty requirements. If your team needs to utilize local models, see [Running Local Models ](/running-local-models/read-me-first.mdx)with Cline.
Certain scenarios may warrant using local models, including handling highly sensitive data, applications requiring consistent low-latency responses, or compliance with strict data sovereignty requirements. If your team needs to utilize local models, see [Running Local Models ](/running-models-locally/read-me-first.mdx)with Cline.
---
@@ -14,9 +14,9 @@ Certain scenarios may warrant using local models, including handling highly sens
#### [IAM Security Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) (For administrators)
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/custom-model-configs/aws-bedrock.mdx)
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/custom-model-configs/aws-bedrock-with-credentials-authentication.mdx)
#### [AWS Bedrock setup for SSO token (AWS Profile)](/custom-model-configs/aws-bedrock-w-profile-authentication.mdx)
#### [AWS Bedrock setup for SSO token (AWS Profile)](/custom-model-configs/aws-bedrock-with-profile-authentication.mdx)
#### VPC Endpoint Setup
@@ -28,12 +28,12 @@ To protect your team's data, Cline supports VPC (Virtual Private Cloud) endpoint
2. Follow the directions for [creating a VPC endpoint](https://docs.aws.amazon.com/vpc/latest/privatelink/create-interface-endpoint.html#create-interface-endpoint-aws) in the AWS console. The image below pertains to steps 4 and 5 of the AWS guide linked above.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="VPC Console" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/vpc-console.png" alt="VPC Console" />
</Frame>
3. Note the IP address of your VPC endpoint, open Cline's settings menu, and select `AWS Bedrock`from the API Provider dropdown.
4. Click the `Use Custom VPC endpoint`checkbox and enter the IP address of your VPC endpoint
<Frame>
<img src="/assets/robot_panel_dark.png" alt="VPC Settings Menu" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/vpc-settings-menu.png" alt="VPC Settings Menu" />
</Frame>
@@ -13,7 +13,10 @@ title: "Security Concerns"
Cline operates exclusively as a client-side VSCode extension with zero server-side components. This fundamental design choice ensures that your code and data remain within your secure environment at all times. Unlike traditional AI assistants that send data to external servers for processing, Cline connects directly to your chosen cloud provider's AI endpoints, keeping all sensitive information within your infrastructure boundaries.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Cline's relationship to local and remote assets" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-arch.png"
alt="Cline's relationship to local and remote assets"
/>
</Frame>
### Data Privacy Commitment
+9 -3
View File
@@ -28,7 +28,10 @@ After each tool use, you can:
2. Click the "Restore" button to open restore options
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Checkpoint comparison and restore options" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(13).png"
alt="Checkpoint comparison and restore options"
/>
</Frame>
#### Rolling Back
@@ -60,7 +63,7 @@ Checkpoints let you be more experimental with Cline. While human coding is often
- Ideal for exploring different design patterns or architectural approaches
<Frame caption="In this case, I didn't like the changes Cline made to my robot dog-walking website (still working on the robots) and I wanted to revert both the codebase and the task to before any changes were made so I could start fresh.">
<img src="/assets/robot_panel_dark.png" alt="Checkpoint restore demo" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/checkpointsDemo.gif" alt="Checkpoint restore demo" />
</Frame>
### ✨ Best Practices
@@ -104,5 +107,8 @@ Perhaps you didn't get the results you wanted, thought of a better way to phrase
- Shift + Enter: Insert new line / line break
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Message editing interface" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/message-editing.png"
alt="Message editing interface"
/>
</Frame>
@@ -7,7 +7,10 @@ title: "Plan & Act Modes: A Guide to Effective AI Development"
Plan & Act modes represent Cline's approach to structured AI development, emphasizing thoughtful planning before implementation. This dual-mode system helps developers create more maintainable, accurate code while reducing iteration time.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Use Plan to gather context before using Act to implement the plan" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/planningThenActing%20(1).gif"
alt="Use Plan to gather context before using Act to implement the plan"
/>
</Frame>
### Understanding the Modes
@@ -27,7 +30,7 @@ Plan & Act modes represent Cline's approach to structured AI development, emphas
- Can execute changes to your codebase
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Act mode capabilities" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(5).png" alt="Act mode capabilities" />
</Frame>
### Workflow Guide
@@ -39,7 +42,7 @@ Begin every significant development task in Plan mode:
In this mode:
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Plan mode workflow" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(5)%20(1).png" alt="Plan mode workflow" />
</Frame>
- Share your requirements
@@ -48,7 +51,10 @@ In this mode:
- Develop implementation strategy
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Planning phase" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(2)%20(1)%20(1)%20(1).png"
alt="Planning phase"
/>
</Frame>
#### 2. Switch to Act Mode
@@ -56,7 +62,7 @@ In this mode:
Once you have a clear plan, switch to Act mode:
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Switching to Act mode" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/switching-to-act.gif" alt="Switching to Act mode" />
</Frame>
Act mode allows Cline to:
@@ -90,7 +96,10 @@ Complex projects often require multiple plan-act cycles:
4. Document significant decisions
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Implementation best practices" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(3)%20(1).png"
alt="Implementation best practices"
/>
</Frame>
### Power User Tips
@@ -119,7 +128,7 @@ Complex projects often require multiple plan-act cycles:
- Executing test cases
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Mode usage patterns" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(6).png" alt="Mode usage patterns" />
</Frame>
### Contributing
+4 -1
View File
@@ -22,7 +22,10 @@ Follow these steps to get Cline up and running:
4. **Search for 'Cline':** In the Extensions search bar, type `Cline`.
<Frame caption="VS Code marketplace with Cline extension ready to install">
<img src="/assets/robot_panel_dark.png" alt="VS Code marketplace showing Cline extension" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(20).png"
alt="VS Code marketplace showing Cline extension"
/>
</Frame>
1. **Install the Extension:** Click the "Install" button next to the Cline extension.
@@ -28,7 +28,10 @@ Cline helps you manage this limitation with its Context Window Progress Bar, whi
- The total capacity for your chosen model
<Frame caption="Visual representation of the context window usage in Cline">
<img src="/assets/robot_panel_light.png" alt="Context window progress bar example" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(11).png"
alt="Context window progress bar example"
/>
</Frame>
This visibility helps you work more effectively with Cline by letting you know when you might need to start fresh or break tasks into smaller chunks.
@@ -14,7 +14,7 @@ description: "Context is key to getting the most out of Cline"
<Frame caption="In a world of infinite context, the context window is what Cline currently has available">
<img
src="/assets/robot_panel_dark.png"
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(2).png"
alt="In a world of infinite context, the context window is what Cline currently has available"
/>
</Frame>
@@ -65,7 +65,10 @@ Think of context like a whiteboard you and Cline share:
Cline provides a visual way to monitor your context window usage through a progress bar:
<Frame caption="Visual representation of the context window usage">
<img src="/assets/robot_panel_light.png" alt="Context window progress bar" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1)%20(1).png"
alt="Context window progress bar"
/>
</Frame>
### Reading the Bar
+8 -6
View File
@@ -11,7 +11,10 @@ Utilizing MCP servers will increase your token usage. Cline offers the ability t
3. Cline will open a new settings window. find `Cline>Mcp:Mode` and make your selection from the dropdown menu.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="MCP settings edit" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/MCP-settings-edit%20(1).png"
alt="MCP settings edit"
/>
</Frame>
## Managing Individual MCP Servers
@@ -22,7 +25,10 @@ Each MCP server has its own configuration panel where you can modify settings, m
2. Locate the MCP server you want to manage in the list, and open it by clicking on its name.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="MCP settings individual" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/MCP-settings-individual.png"
alt="MCP settings individual"
/>
</Frame>
### Deleting a Server
@@ -45,10 +51,6 @@ To set the maximum time to wait for a response after a tool call to the MCP serv
1. Click the `Network Timeout` dropdown at the bottom of the individual MCP server's config box and change the time. Default is 1 minute but it can be set between 30 seconds and 1 hour.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Network Timeout pulldown" />
</Frame>
## Editing MCP Settings Files
Settings for all installed MCP servers are located in the `cline_mcp_settings.json` file:
+1 -1
View File
@@ -14,7 +14,7 @@ Model Context Protocol is an open protocol that standardizes how applications pr
<Frame>
<img
src="/assets/robot_panel_dark.png"
src="https://storage.googleapis.com/cline_public_images/docs/assets/mcp-diagram.png"
alt="MCP diagram showing how MCP servers connect LLMs to external tools and data sources"
/>
</Frame>
+8 -2
View File
@@ -242,7 +242,10 @@ Let's walk through the development process of our AlphaAdvantage MCP server, whi
### Planning Phase
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Planning phase demonstration" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/planning-phase.gif"
alt="Planning phase demonstration"
/>
</Frame>
During the planning phase, we:
@@ -267,7 +270,10 @@ During the planning phase, we:
### Implementation
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Building MCP plugin demonstration" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/building-mcp-plugin.gif"
alt="Building MCP plugin demonstration"
/>
</Frame>
We began by bootstrapping the project:
+5 -5
View File
@@ -13,7 +13,7 @@ To get started with Cline Memory Bank:
3. **Paste into Cline** - Add as custom instructions or in a .clinerules file
4. **Initialize** - Ask Cline to "initialize memory bank"
[See detailed setup instructions](cline-memory-bank.md#getting-started-with-memory-bank)
[See detailed setup instructions](#getting-started-with-memory-bank)
### Cline Memory Bank Custom Instructions \[COPY THIS]
@@ -152,7 +152,7 @@ The Memory Bank is a structured documentation system that allows Cline to mainta
The Memory Bank isn't a Cline-specific feature - it's a methodology for managing AI context through structured documentation. When you instruct Cline to "follow custom instructions," it reads the Memory Bank files to rebuild its understanding of your project.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Workflow" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(15).png" alt="Memory Bank Workflow" />
</Frame>
#### Understanding the Files
@@ -162,7 +162,7 @@ Memory Bank files are simply markdown files you create in your project. They're
Files are organized in a hierarchical structure that builds up a complete picture of your project:
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Memory Bank File Structure" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(16).png" alt="Memory Bank File Structure" />
</Frame>
### Memory Bank Files Explained
@@ -223,7 +223,7 @@ Create additional files when needed to organize:
3. Ask Cline to "initialize memory bank"
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Setup" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(17).png" alt="Memory Bank Setup" />
</Frame>
#### Project Brief Tips
@@ -287,7 +287,7 @@ As you work with Cline, your context window will eventually fill up (note the pr
This workflow ensures that important context is preserved in your Memory Bank files before the context window is cleared, allowing you to continue seamlessly in a fresh conversation.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Memory Bank Context Window" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(18).png" alt="Memory Bank Context Window" />
</Frame>
#### How often should I update the memory bank?
+2 -2
View File
@@ -22,7 +22,7 @@ To add custom instructions:
4. Paste your instructions
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Cline Logo" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
</Frame>
Custom instructions are powerful for:
@@ -207,7 +207,7 @@ Located conveniently under the chat input field, this popover allows you to:
This UI significantly simplifies switching contexts and managing different sets of instructions without needing to manually edit files or configurations during a conversation.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Cline Logo" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
</Frame>
## .clineignore File Guide
+17 -5
View File
@@ -20,7 +20,7 @@ Run AI models locally using LM Studio with Cline.
- Download and install for your operating system
<Frame>
<img src="/assets/robot_panel_dark.png" alt="LM Studio download page" />
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(7).png" alt="LM Studio download page" />
</Frame>
#### 2. Launch LM Studio
@@ -29,7 +29,10 @@ Run AI models locally using LM Studio with Cline.
- You'll see four tabs on the left: **Chat**, **Developer** (where you will start the server), **My Models** (where your downloaded models are stored), **Discover** (add new models)
<Frame>
<img src="/assets/robot_panel_dark.png" alt="LM Studio interface overview" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(10).png"
alt="LM Studio interface overview"
/>
</Frame>
#### 3. Download a Model
@@ -39,7 +42,10 @@ Run AI models locally using LM Studio with Cline.
- Wait for download to complete
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Downloading a model in LM Studio" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/lm-studio-download-model.gif"
alt="Downloading a model in LM Studio"
/>
</Frame>
#### 4. Start the Server
@@ -49,7 +55,10 @@ Run AI models locally using LM Studio with Cline.
- Note: The server will run at `http://localhost:1234`
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Starting the LM Studio server" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/lm-studio-starting-server.gif"
alt="Starting the LM Studio server"
/>
</Frame>
#### 5. Configure Cline
@@ -60,7 +69,10 @@ Run AI models locally using LM Studio with Cline.
4. Select your model from the available options
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Configuring Cline with LM Studio" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/lm-studio-select-model-cline.gif"
alt="Configuring Cline with LM Studio"
/>
</Frame>
### ⚠️ Important Notes
+16 -4
View File
@@ -16,7 +16,10 @@ description: "A quick guide to setting up Ollama for local AI model execution wi
- Download and install for your operating system
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Ollama download page" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(2)%20(1)%20(1).png"
alt="Ollama download page"
/>
</Frame>
#### 2. Choose and Download a Model
@@ -29,7 +32,10 @@ description: "A quick guide to setting up Ollama for local AI model execution wi
```
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Selecting a model in Ollama" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/ollama-model-grab%20(2).gif"
alt="Selecting a model in Ollama"
/>
</Frame>
- Open your Terminal and run the command:
@@ -41,7 +47,10 @@ description: "A quick guide to setting up Ollama for local AI model execution wi
```
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Running Ollama in terminal" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/starting-ollama-terminal%20(2).gif"
alt="Running Ollama in terminal"
/>
</Frame>
**✨ Your model is now ready to use within Cline!**
@@ -56,7 +65,10 @@ description: "A quick guide to setting up Ollama for local AI model execution wi
- Select the model from your available options
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Configuring Cline with Ollama" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/selecting-ollama-model-cline%20(3).gif"
alt="Configuring Cline with Ollama"
/>
</Frame>
### ⚠️ Important Notes
@@ -20,7 +20,10 @@ Local models are created by training a smaller model to imitate a larger one, bu
Think of it like running your development environment on a calculator instead of a computer it might handle basic tasks, but complex operations become unreliable or impossible.
<Frame>
<img src="/assets/robot_panel_dark.png" alt="Local model comparison diagram" />
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(4).png"
alt="Local model comparison diagram"
/>
</Frame>
### What Actually Happens
+18
View File
@@ -42,6 +42,7 @@
"globby": "^14.0.2",
"iconv-lite": "^0.6.3",
"ignore": "^7.0.3",
"image-size": "^2.0.2",
"isbinaryfile": "^5.0.2",
"jschardet": "^3.1.4",
"mammoth": "^1.8.0",
@@ -16579,6 +16580,18 @@
"node": ">= 4"
}
},
"node_modules/image-size": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz",
"integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==",
"license": "MIT",
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=16.x"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@@ -37429,6 +37442,11 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz",
"integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="
},
"image-size": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz",
"integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="
},
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+6 -3
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.14.0",
"version": "3.14.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -121,7 +121,7 @@
},
{
"command": "cline.generateGitCommitMessage",
"title": "Generate Commit Message with Cline",
"title": "Generate Commit Message with Cline",
"category": "Cline",
"icon": "$(robot)"
}
@@ -336,7 +336,9 @@
"prepare": "husky",
"changeset": "changeset",
"version-packages": "changeset version",
"docs:preview": "cd docs && mintlify dev",
"docs": "cd docs && mintlify dev",
"docs:check-links": "cd docs && mintlify broken-links",
"docs:rename-file": "cd docs && mintlify rename",
"report-issue": "node scripts/report-issue.js"
},
"devDependencies": {
@@ -409,6 +411,7 @@
"globby": "^14.0.2",
"iconv-lite": "^0.6.3",
"ignore": "^7.0.3",
"image-size": "^2.0.2",
"isbinaryfile": "^5.0.2",
"jschardet": "^3.1.4",
"mammoth": "^1.8.0",
+2 -1
View File
@@ -18,7 +18,8 @@ export class OpenAiHandler implements ApiHandler {
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
if (
this.options.azureApiVersion ||
(this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") &&
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
) {
this.client = new AzureOpenAI({
+69 -70
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,79 +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" },
},
],
}
// for safety, but this should always be the case
if (openAiMessages.length >= 2) {
const msg = openAiMessages[1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}
// it doesn't make sense to alter breakpoints at all with the gemini cache implementation at this time
/*const GEMINI_CACHE_USER_MESSAGE_INTERVAL = 4 // add new breakpoint every 4 turns
const userMessages = openAiMessages.filter((msg) => msg.role === "user")
const userMessageCount = userMessages.length
const targetUserMessageNumber =
Math.floor(userMessageCount / GEMINI_CACHE_USER_MESSAGE_INTERVAL) * GEMINI_CACHE_USER_MESSAGE_INTERVAL
if (targetUserMessageNumber > 0) {
// otherwise dont need to add a breakpoint
const msg = userMessages[targetUserMessageNumber - 1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}*/
break
default:
break
}
// 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
+6 -8
View File
@@ -1301,14 +1301,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)
@@ -1317,6 +1309,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
}
+1 -1
View File
@@ -1916,7 +1916,7 @@ export class Task {
: "other_diff_error"
// Add telemetry for diff edit failure
telemetryService.captureDiffEditFailure(this.taskId, errorType)
telemetryService.captureDiffEditFailure(this.taskId, this.api.getModel().id, errorType)
pushToolResult(
formatResponse.toolError(
+52 -17
View File
@@ -26,6 +26,9 @@ export class DiffViewProvider {
private streamedLines: string[] = []
private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
private fileEncoding: string = "utf8"
private lastFirstVisibleLine: number = 0
private shouldAutoScroll: boolean = true
private scrollListener?: vscode.Disposable
constructor(private cwd: string) {}
@@ -34,6 +37,8 @@ export class DiffViewProvider {
const fileExists = this.editType === "modify"
const absolutePath = path.resolve(this.cwd, relPath)
this.isEditing = true
this.shouldAutoScroll = true
this.lastFirstVisibleLine = 0
// if the file is already open, ensure it's not dirty before getting its contents
if (fileExists) {
const existingDocument = vscode.workspace.textDocuments.find((doc) => arePathsEqual(doc.uri.fsPath, absolutePath))
@@ -79,6 +84,21 @@ export class DiffViewProvider {
this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
this.scrollEditorToLine(0) // will this crash for new files?
this.streamedLines = []
// Add scroll detection to disable auto-scrolling when user scrolls up
this.scrollListener = vscode.window.onDidChangeTextEditorVisibleRanges((e: vscode.TextEditorVisibleRangesChangeEvent) => {
if (e.textEditor === this.activeDiffEditor) {
const currentFirstVisibleLine = e.visibleRanges[0]?.start.line || 0
// If the first visible line moved upward, user scrolled up
if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
this.shouldAutoScroll = false
}
// Always update our tracking variable
this.lastFirstVisibleLine = currentFirstVisibleLine
}
})
}
async update(accumulatedContent: string, isFinal: boolean) {
@@ -128,25 +148,30 @@ export class DiffViewProvider {
this.activeLineController.setActiveLine(currentLine)
this.fadedOverlayController.updateOverlayAfterLine(currentLine, document.lineCount)
// Scroll to the last changed line
if (diffLines.length <= 5) {
// For small changes, just jump directly to the line
this.scrollEditorToLine(currentLine)
} else {
// For larger changes, create a quick scrolling animation
const startLine = this.streamedLines.length
const endLine = currentLine
const totalLines = endLine - startLine
const numSteps = 10 // Adjust this number to control animation speed
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
// Scroll to the last changed line only if the user hasn't scrolled up
if (this.shouldAutoScroll) {
if (diffLines.length <= 5) {
// For small changes, just jump directly to the line
this.scrollEditorToLine(currentLine)
} else {
// For larger changes, create a quick scrolling animation
const startLine = this.streamedLines.length
const endLine = currentLine
const totalLines = endLine - startLine
const numSteps = 10 // Adjust this number to control animation speed
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
// Create and await the smooth scrolling animation
for (let line = startLine; line <= endLine; line += stepSize) {
this.activeDiffEditor?.revealRange(new vscode.Range(line, 0, line, 0), vscode.TextEditorRevealType.InCenter)
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
// Create and await the smooth scrolling animation
for (let line = startLine; line <= endLine; line += stepSize) {
this.activeDiffEditor?.revealRange(
new vscode.Range(line, 0, line, 0),
vscode.TextEditorRevealType.InCenter,
)
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
}
// Ensure we end at the final line
this.scrollEditorToLine(currentLine)
}
// Ensure we end at the final line
this.scrollEditorToLine(currentLine)
}
}
@@ -418,5 +443,15 @@ export class DiffViewProvider {
this.activeLineController = undefined
this.streamedLines = []
this.preDiagnostics = []
// Clean up the scroll listener
if (this.scrollListener) {
this.scrollListener.dispose()
this.scrollListener = undefined
}
// Reset auto-scroll state
this.shouldAutoScroll = true
this.lastFirstVisibleLine = 0
}
}
+31 -10
View File
@@ -1,6 +1,7 @@
import * as vscode from "vscode"
import fs from "fs/promises"
import * as path from "path"
import sizeOf from "image-size"
export async function selectImages(): Promise<string[]> {
const options: vscode.OpenDialogOptions = {
@@ -17,16 +18,36 @@ export async function selectImages(): Promise<string[]> {
return []
}
return await Promise.all(
fileUris.map(async (uri) => {
const imagePath = uri.fsPath
const buffer = await fs.readFile(imagePath)
const base64 = buffer.toString("base64")
const mimeType = getMimeType(imagePath)
const dataUrl = `data:${mimeType};base64,${base64}`
return dataUrl
}),
)
const processedImagePromises = fileUris.map(async (uri) => {
const imagePath = uri.fsPath
let buffer: Buffer
try {
// Read the file into a buffer first
buffer = await fs.readFile(imagePath)
// Convert Node.js Buffer to Uint8Array
const uint8Array = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
const dimensions = sizeOf(uint8Array) // Get dimensions from Uint8Array
if (dimensions.width! > 7500 || dimensions.height! > 7500) {
console.warn(`Image dimensions exceed 7500px, skipping: ${imagePath}`)
vscode.window.showErrorMessage(
`Image too large: ${path.basename(imagePath)} was skipped (dimensions exceed 7500px).`,
)
return null
}
} catch (error) {
console.error(`Error reading file or getting dimensions for ${imagePath}:`, error)
vscode.window.showErrorMessage(`Could not read dimensions for ${path.basename(imagePath)}, skipping.`)
return null
}
// If dimensions are valid, proceed to convert the existing buffer to base64
const base64 = buffer.toString("base64")
const mimeType = getMimeType(imagePath)
return `data:${mimeType};base64,${base64}`
})
const dataUrlsWithNulls = await Promise.all(processedImagePromises)
return dataUrlsWithNulls.filter((url) => url !== null) as string[] // Filter out skipped images
}
function getMimeType(filePath: string): string {
@@ -512,13 +512,14 @@ 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, collect: boolean = false) {
public captureDiffEditFailure(taskId: string, modelId: string, errorType?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.DIFF_EDIT_FAILED,
properties: {
taskId,
errorType,
modelId,
},
},
collect,
+20 -6
View File
@@ -204,27 +204,33 @@ export const bedrockModels = {
contextWindow: 300_000,
supportsImages: true,
supportsPromptCache: false,
supportsPromptCache: true,
inputPrice: 0.8,
outputPrice: 3.2,
// cacheWritesPrice: 3.2, // not written
cacheReadsPrice: 0.2,
},
"amazon.nova-lite-v1:0": {
maxTokens: 5000,
contextWindow: 300_000,
supportsImages: true,
supportsPromptCache: false,
supportsPromptCache: true,
inputPrice: 0.06,
outputPrice: 0.24,
// cacheWritesPrice: 0.24, // not written
cacheReadsPrice: 0.015,
},
"amazon.nova-micro-v1:0": {
maxTokens: 5000,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
supportsPromptCache: true,
inputPrice: 0.035,
outputPrice: 0.14,
// cacheWritesPrice: 0.14, // not written
cacheReadsPrice: 0.00875,
},
"anthropic.claude-3-7-sonnet-20250219-v1:0": {
maxTokens: 8192,
@@ -251,10 +257,10 @@ export const bedrockModels = {
"anthropic.claude-3-5-haiku-20241022-v1:0": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: false,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 1.0,
outputPrice: 5.0,
inputPrice: 0.8,
outputPrice: 4.0,
cacheWritesPrice: 1.0,
cacheReadsPrice: 0.08,
},
@@ -1387,6 +1393,14 @@ export const mistralModels = {
inputPrice: 0.1,
outputPrice: 0.3,
},
"mistral-medium-latest": {
maxTokens: 128_000,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.4,
outputPrice: 2.0,
},
"mistral-small-2501": {
maxTokens: 32_000,
contextWindow: 32_000,
+6 -6
View File
@@ -34,20 +34,20 @@ describe("Path Utilities", () => {
})
it("should show relative paths within cwd", () => {
const cwd = "/home/user/project"
const filePath = "/home/user/project/src/file.txt"
const cwd = path.resolve("/home/user/project")
const filePath = path.resolve("/home/user/project/src/file.txt")
getReadablePath(cwd, filePath).should.equal("src/file.txt")
})
it("should show basename when path equals cwd", () => {
const cwd = "/home/user/project"
const cwd = path.resolve("/home/user/project")
getReadablePath(cwd, cwd).should.equal("project")
})
it("should show absolute path when outside cwd", () => {
const cwd = "/home/user/project"
const filePath = "/home/user/other/file.txt"
getReadablePath(cwd, filePath).should.equal(filePath)
const cwd = path.resolve("/home/user/project")
const filePath = path.resolve("/home/user/other/file.txt")
getReadablePath(cwd, filePath).should.equal(filePath.toPosix())
})
})
})
-9
View File
@@ -74,15 +74,6 @@ const AppContent = () => {
useEvent("message", handleMessage)
// useEffect(() => {
// if (telemetrySetting === "enabled") {
// posthog.identify(vscMachineId)
// posthog.opt_in_capturing()
// } else {
// posthog.opt_out_capturing()
// }
// }, [telemetrySetting, vscMachineId])
useEffect(() => {
if (shouldShowAnnouncement) {
setShowAnnouncement(true)
+24
View File
@@ -0,0 +1,24 @@
import { useEffect, type ReactNode } from "react"
import { PostHogProvider } from "posthog-js/react"
import posthog from "posthog-js"
import { posthogConfig } from "@shared/services/config/posthog-config"
import { useExtensionState } from "./context/ExtensionStateContext"
export function CustomPostHogProvider({ children }: { children: ReactNode }) {
const { telemetrySetting } = useExtensionState()
const isTelemetryEnabled = telemetrySetting === "enabled"
useEffect(() => {
if (isTelemetryEnabled) {
posthog.init(posthogConfig.apiKey, {
api_host: posthogConfig.host,
autocapture: false,
disable_session_recording: true,
})
} else {
posthog.opt_out_capturing()
}
}, [isTelemetryEnabled])
return <PostHogProvider client={posthog}>{children}</PostHogProvider>
}
+4 -10
View File
@@ -1,24 +1,18 @@
import type { ReactNode } from "react"
import { type ReactNode } from "react"
import { ExtensionStateContextProvider } from "./context/ExtensionStateContext"
import { FirebaseAuthProvider } from "./context/FirebaseAuthContext"
import { HeroUIProvider } from "@heroui/react"
import { PostHogProvider } from "posthog-js/react"
import { posthogConfig } from "@shared/services/config/posthog-config"
import posthog from "posthog-js"
posthog.init(posthogConfig.apiKey, {
api_host: posthogConfig.host,
})
import { CustomPostHogProvider } from "./CustomPostHogProvider"
export function Providers({ children }: { children: ReactNode }) {
return (
<ExtensionStateContextProvider>
<PostHogProvider client={posthog}>
<CustomPostHogProvider>
<FirebaseAuthProvider>
<HeroUIProvider>{children}</HeroUIProvider>
</FirebaseAuthProvider>
</PostHogProvider>
</CustomPostHogProvider>
</ExtensionStateContextProvider>
)
}
+101 -40
View File
@@ -1,4 +1,4 @@
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import { VSCodeBadge, VSCodeProgressRing, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import deepEqual from "fast-deep-equal"
import React, { memo, useCallback, useEffect, useMemo, useRef, useState, MouseEvent } from "react"
@@ -20,6 +20,62 @@ import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils
import { vscode } from "@/utils/vscode"
import { FileServiceClient } from "@/services/grpc-client"
import { CheckmarkControl } from "@/components/common/CheckmarkControl"
interface CopyButtonProps {
textToCopy: string | undefined
}
const CopyButtonStyled = styled(VSCodeButton)`
position: absolute;
bottom: 2px;
right: 2px;
z-index: 1;
opacity: 0;
`
interface WithCopyButtonProps {
children: React.ReactNode
textToCopy?: string
style?: React.CSSProperties
ref?: React.Ref<HTMLDivElement>
onMouseUp?: (event: MouseEvent<HTMLDivElement>) => void
}
const StyledContainer = styled.div`
position: relative;
&:hover ${CopyButtonStyled} {
opacity: 1;
}
`
const WithCopyButton = React.forwardRef<HTMLDivElement, WithCopyButtonProps>(
({ children, textToCopy, style, onMouseUp, ...props }, ref) => {
const [copied, setCopied] = useState(false)
const handleCopy = () => {
if (!textToCopy) return
navigator.clipboard.writeText(textToCopy).then(() => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 1500)
})
}
return (
<StyledContainer ref={ref} onMouseUp={onMouseUp} style={style} {...props}>
{children}
{textToCopy && (
<CopyButtonStyled appearance="icon" onClick={handleCopy} aria-label={copied ? "Copied" : "Copy"}>
<span className={`codicon codicon-${copied ? "check" : "copy"}`}></span>
</CopyButtonStyled>
)}
</StyledContainer>
)
},
)
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
@@ -90,6 +146,7 @@ const Markdown = memo(({ markdown }: { markdown?: string }) => {
overflowWrap: "anywhere",
marginBottom: -15,
marginTop: -15,
overflow: "hidden", // contain child margins so that parent diff matches height of children
}}>
<MarkdownBlock markdown={markdown} />
</div>
@@ -901,7 +958,7 @@ export const ChatRowContent = ({
return <McpResponseDisplay responseText={message.text || ""} />
case "text":
return (
<div ref={contentRef} onMouseUp={handleMouseUp} style={{ position: "relative" }}>
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={message.text}>
<Markdown markdown={message.text} />
{quoteButtonState.visible && (
<QuoteButton
@@ -912,7 +969,7 @@ export const ChatRowContent = ({
}}
/>
)}
</div>
</WithCopyButton>
)
case "reasoning":
return (
@@ -1132,13 +1189,13 @@ export const ChatRowContent = ({
}}
/>
</div>
<div
ref={contentRef} // Added ref
onMouseUp={handleMouseUp} // Added handler
<WithCopyButton
ref={contentRef}
onMouseUp={handleMouseUp}
textToCopy={text}
style={{
color: "var(--vscode-charts-green)",
paddingTop: 10,
position: "relative", // Added position
}}>
<Markdown markdown={text} />
{quoteButtonState.visible && (
@@ -1148,7 +1205,7 @@ export const ChatRowContent = ({
onClick={handleQuoteClick}
/>
)}
</div>
</WithCopyButton>
{message.partial !== true && hasChanges && (
<div style={{ paddingTop: 17 }}>
<SuccessButton
@@ -1295,13 +1352,13 @@ export const ChatRowContent = ({
}}
/>
</div>
<div
ref={contentRef} // Added ref
onMouseUp={handleMouseUp} // Added handler
<WithCopyButton
ref={contentRef}
onMouseUp={handleMouseUp}
textToCopy={text}
style={{
color: "var(--vscode-charts-green)",
paddingTop: 10,
position: "relative", // Added position
}}>
<Markdown markdown={text} />
{quoteButtonState.visible && (
@@ -1311,30 +1368,30 @@ export const ChatRowContent = ({
onClick={handleQuoteClick}
/>
)}
{message.partial !== true && hasChanges && (
<div style={{ marginTop: 15 }}>
<SuccessButton
appearance="secondary"
disabled={seeNewChangesDisabled}
onClick={() => {
setSeeNewChangesDisabled(true)
vscode.postMessage({
type: "taskCompletionViewChanges",
number: message.ts,
})
}}>
<i
className="codicon codicon-new-file"
style={{
marginRight: 6,
cursor: seeNewChangesDisabled ? "wait" : "pointer",
}}
/>
See new changes
</SuccessButton>
</div>
)}
</div>
</WithCopyButton>
{message.partial !== true && hasChanges && (
<div style={{ marginTop: 15 }}>
<SuccessButton
appearance="secondary"
disabled={seeNewChangesDisabled}
onClick={() => {
setSeeNewChangesDisabled(true)
vscode.postMessage({
type: "taskCompletionViewChanges",
number: message.ts,
})
}}>
<i
className="codicon codicon-new-file"
style={{
marginRight: 6,
cursor: seeNewChangesDisabled ? "wait" : "pointer",
}}
/>
See new changes
</SuccessButton>
</div>
)}
</div>
)
} else {
@@ -1362,7 +1419,11 @@ export const ChatRowContent = ({
{title}
</div>
)}
<div ref={contentRef} onMouseUp={handleMouseUp} style={{ position: "relative", paddingTop: 10 }}>
<WithCopyButton
ref={contentRef}
onMouseUp={handleMouseUp}
textToCopy={question}
style={{ paddingTop: 10 }}>
<Markdown markdown={question} />
<OptionsButtons
options={options}
@@ -1379,7 +1440,7 @@ export const ChatRowContent = ({
}}
/>
)}
</div>
</WithCopyButton>
</>
)
case "new_task":
@@ -1428,7 +1489,7 @@ export const ChatRowContent = ({
response = message.text
}
return (
<div ref={contentRef} onMouseUp={handleMouseUp} style={{ position: "relative" }}>
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={response}>
<Markdown markdown={response} />
<OptionsButtons
options={options}
@@ -1445,7 +1506,7 @@ export const ChatRowContent = ({
}}
/>
)}
</div>
</WithCopyButton>
)
}
default:
@@ -40,6 +40,24 @@ import { ChatSettings } from "@shared/ChatSettings"
import ServersToggleModal from "./ServersToggleModal"
import ClineRulesToggleModal from "../cline-rules/ClineRulesToggleModal"
const getImageDimensions = (dataUrl: string): Promise<{ width: number; height: number }> => {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
if (img.naturalWidth > 7500 || img.naturalHeight > 7500) {
reject(new Error("Image dimensions exceed maximum allowed size of 7500px."))
} else {
resolve({ width: img.naturalWidth, height: img.naturalHeight })
}
}
img.onerror = (err) => {
console.error("Failed to load image for dimension check:", err)
reject(new Error("Failed to load image to check dimensions."))
}
img.src = dataUrl
})
}
interface ChatTextAreaProps {
inputValue: string
activeQuote: string | null
@@ -276,6 +294,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const shiftHoldTimerRef = useRef<NodeJS.Timeout | null>(null)
const [showUnsupportedFileError, setShowUnsupportedFileError] = useState(false)
const unsupportedFileTimerRef = useRef<NodeJS.Timeout | null>(null)
const [showDimensionError, setShowDimensionError] = useState(false)
const dimensionErrorTimerRef = useRef<NodeJS.Timeout | null>(null)
const [fileSearchResults, setFileSearchResults] = useState<SearchResult[]>([])
const [searchLoading, setSearchLoading] = useState(false)
@@ -733,6 +753,17 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
onFocusChange?.(false) // Call prop on blur
}, [isMouseDownOnMenu, onFocusChange])
const showDimensionErrorMessage = useCallback(() => {
setShowDimensionError(true)
if (dimensionErrorTimerRef.current) {
clearTimeout(dimensionErrorTimerRef.current)
}
dimensionErrorTimerRef.current = setTimeout(() => {
setShowDimensionError(false)
dimensionErrorTimerRef.current = null
}, 3000)
}, [])
const handlePaste = useCallback(
async (e: React.ClipboardEvent) => {
const items = e.clipboardData.items
@@ -778,13 +809,24 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
return
}
const reader = new FileReader()
reader.onloadend = () => {
reader.onloadend = async () => {
if (reader.error) {
console.error("Error reading file:", reader.error)
resolve(null)
} else {
const result = reader.result
resolve(typeof result === "string" ? result : null)
if (typeof result === "string") {
try {
await getImageDimensions(result)
resolve(result)
} catch (error) {
console.warn((error as Error).message)
showDimensionErrorMessage()
resolve(null)
}
} else {
resolve(null)
}
}
}
reader.readAsDataURL(blob)
@@ -800,7 +842,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
}
},
[shouldDisableImages, setSelectedImages, cursorPosition, setInputValue, inputValue],
[shouldDisableImages, setSelectedImages, cursorPosition, setInputValue, inputValue, showDimensionErrorMessage],
)
const handleThumbnailsHeightChange = useCallback((height: number) => {
@@ -1230,13 +1272,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
(file) =>
new Promise<string | null>((resolve) => {
const reader = new FileReader()
reader.onloadend = () => {
reader.onloadend = async () => {
// Make async
if (reader.error) {
console.error("Error reading file:", reader.error)
resolve(null)
} else {
const result = reader.result
resolve(typeof result === "string" ? result : null)
if (typeof result === "string") {
try {
await getImageDimensions(result) // Check dimensions
resolve(result)
} catch (error) {
console.warn((error as Error).message)
showDimensionErrorMessage() // Show error to user
resolve(null) // Don't add this image
}
} else {
resolve(null)
}
}
}
reader.readAsDataURL(file)
@@ -1260,6 +1314,31 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
onDragOver={onDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}>
{showDimensionError && (
<div
style={{
position: "absolute",
inset: "10px 15px",
backgroundColor: "rgba(var(--vscode-errorForeground-rgb), 0.1)",
border: "2px solid var(--vscode-errorForeground)",
borderRadius: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 10, // Ensure it's above other elements
pointerEvents: "none",
}}>
<span
style={{
color: "var(--vscode-errorForeground)",
fontWeight: "bold",
fontSize: "12px",
textAlign: "center",
}}>
Image dimensions exceed 7500px
</span>
</div>
)}
{showUnsupportedFileError && (
<div
style={{
@@ -1380,7 +1459,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
onHeightChange?.(height)
}}
placeholder={showUnsupportedFileError ? "" : placeholderText}
placeholder={showUnsupportedFileError || showDimensionError ? "" : placeholderText}
maxRows={10}
autoFocus={true}
style={{
+26 -2
View File
@@ -41,6 +41,27 @@ interface ChatViewProps {
showHistoryView: () => void
}
// Function to clean up markdown escape characters
function cleanupMarkdownEscapes(markdown: string): string {
return (
markdown
// Handle underscores and asterisks (single or multiple)
.replace(/\\([_*]+)/g, "$1")
// Handle angle brackets (for generics and XML)
.replace(/\\([<>])/g, "$1")
// Handle backticks (for code)
.replace(/\\(`)/g, "$1")
// Handle other common markdown special characters
.replace(/\\([[\]()#.!])/g, "$1")
// Fix multiple consecutive backslashes
.replace(/\\{2,}([_*`<>[\]()#.!])/g, "$1")
)
}
async function convertHtmlToMarkdown(html: string) {
// Process the HTML to Markdown
const result = await unified()
@@ -55,10 +76,14 @@ async function convertHtmlToMarkdown(html: string) {
rule: "-", // Use - for horizontal rules
ruleSpaces: false, // No spaces in horizontal rules
fences: true,
escape: false,
entities: false,
})
.process(html)
return String(result)
const md = String(result)
// Apply comprehensive cleanup of escape characters
return cleanupMarkdownEscapes(md)
}
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
@@ -120,7 +145,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
// Convert HTML to Markdown
const markdown = await convertHtmlToMarkdown(selectedHtml)
vscode.postMessage({ type: "copyToClipboard", text: markdown })
e.preventDefault()
}
+156 -168
View File
@@ -12,6 +12,7 @@ import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import { validateSlashCommand } from "@/utils/slash-commands"
import TaskTimeline from "./TaskTimeline"
import { TaskServiceClient } from "@/services/grpc-client"
import HeroTooltip from "@/components/common/HeroTooltip"
interface TaskHeaderProps {
task: ClineMessage
@@ -136,12 +137,12 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
)
}, [apiConfiguration?.apiProvider, apiConfiguration?.openAiModelInfo])
const shouldShowPromptCacheInfo =
doesModelSupportPromptCache && apiConfiguration?.apiProvider !== "openrouter" && apiConfiguration?.apiProvider !== "cline"
const shouldShowPromptCacheInfoClineOR =
doesModelSupportPromptCache &&
(apiConfiguration?.apiProvider === "openrouter" || apiConfiguration?.apiProvider === "cline")
const shouldShowPromptCacheInfo = () => {
return (
doesModelSupportPromptCache &&
((cacheReads !== undefined && cacheReads > 0) || (cacheWrites !== undefined && cacheWrites > 0))
)
}
const ContextWindowComponent = (
<>
@@ -152,18 +153,6 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
flexDirection: windowWidth < 270 ? "column" : "row",
gap: "4px",
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
flexShrink: 0, // Prevents shrinking
}}>
<span style={{ fontWeight: "bold" }}>
{/* {windowWidth > 280 && windowWidth < 310 ? "Context:" : "Context Window:"} */}
Context Window:
</span>
</div>
<div
style={{
display: "flex",
@@ -172,7 +161,9 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
flex: 1,
whiteSpace: "nowrap",
}}>
<span>{formatLargeNumber(lastApiReqTotalTokens || 0)}</span>
<HeroTooltip content="Current tokens used in this request">
<span className="cursor-pointer">{formatLargeNumber(lastApiReqTotalTokens || 0)}</span>
</HeroTooltip>
<div
style={{
display: "flex",
@@ -180,24 +171,29 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
gap: "3px",
flex: 1,
}}>
<div
style={{
flex: 1,
height: "4px",
backgroundColor: "color-mix(in srgb, var(--vscode-badge-foreground) 20%, transparent)",
borderRadius: "2px",
overflow: "hidden",
}}>
<HeroTooltip content="Context window usage">
<div
style={{
width: `${((lastApiReqTotalTokens || 0) / contextWindow) * 100}%`,
height: "100%",
backgroundColor: "var(--vscode-badge-foreground)",
flex: 1,
height: "4px",
backgroundColor: "color-mix(in srgb, var(--vscode-badge-foreground) 20%, transparent)",
borderRadius: "2px",
overflow: "hidden",
}}
/>
</div>
<span>{formatLargeNumber(contextWindow)}</span>
className="cursor-pointer">
<div
style={{
width: `${((lastApiReqTotalTokens || 0) / contextWindow) * 100}%`,
height: "100%",
backgroundColor: "var(--vscode-badge-foreground)",
borderRadius: "2px",
}}
/>
</div>
</HeroTooltip>
<HeroTooltip content="Maximum context window size for this model">
<span className="cursor-pointer">{formatLargeNumber(contextWindow)}</span>
</HeroTooltip>
</div>
</div>
</div>
@@ -263,7 +259,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
{!isTaskExpanded && <span style={{ marginLeft: 4 }}>{highlightText(task.text, false)}</span>}
</div>
</div>
{!isTaskExpanded && isCostAvailable && (
{isCostAvailable && (
<div
style={{
marginLeft: 10,
@@ -299,7 +295,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
ref={textRef}
style={{
display: "-webkit-box",
WebkitLineClamp: isTextExpanded ? "unset" : 3,
WebkitLineClamp: isTextExpanded ? "unset" : 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
whiteSpace: "pre-wrap",
@@ -352,18 +348,19 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
</div>
)}
{task.images && task.images.length > 0 && <Thumbnails images={task.images} />}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "4px",
gap: "2px",
}}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
height: 17,
flexWrap: "wrap",
}}>
<div
style={{
@@ -375,77 +372,48 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
<div style={{ display: "flex", alignItems: "center" }}>
<span style={{ fontWeight: "bold" }}>Tokens:</span>
</div>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-arrow-up"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-2px",
}}
/>
{formatLargeNumber(tokensIn || 0)}
</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-arrow-down"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-2px",
}}
/>
{formatLargeNumber(tokensOut || 0)}
</span>
<HeroTooltip content="Prompt Tokens">
<span className="flex items-center gap-[3px] cursor-pointer">
<i
className="codicon codicon-arrow-up"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-2px",
}}
/>
{formatLargeNumber(tokensIn || 0)}
</span>
</HeroTooltip>
<HeroTooltip content="Completion Tokens">
<span className="flex items-center gap-[3px] cursor-pointer">
<i
className="codicon codicon-arrow-down"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-2px",
}}
/>
{formatLargeNumber(tokensOut || 0)}
</span>
</HeroTooltip>
</div>
{!isCostAvailable && (
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
{!shouldShowPromptCacheInfo() && (
<div className="flex items-center flex-wrap">
<CopyButton taskText={task.text} />
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
</div>
)}
</div>
<TaskTimeline messages={clineMessages} />
{shouldShowPromptCacheInfoClineOR && cacheReads !== undefined && (
{shouldShowPromptCacheInfo() && (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: "4px",
flexWrap: "wrap",
}}>
<span style={{ fontWeight: "bold" }}>Cache:</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-arrow-right"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: 0,
}}
/>
{formatLargeNumber(cacheReads || 0)}
</span>
</div>
)}
{shouldShowPromptCacheInfo &&
(cacheReads !== undefined ||
cacheWrites !== undefined ||
apiConfiguration?.apiProvider === "anthropic") && (
<div
style={{
display: "flex",
@@ -453,62 +421,50 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
gap: "4px",
flexWrap: "wrap",
}}>
<span style={{ fontWeight: "bold" }}>Cache:</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-database"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-1px",
}}
/>
+{formatLargeNumber(cacheWrites || 0)}
</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-arrow-right"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: 0,
}}
/>
{formatLargeNumber(cacheReads || 0)}
</span>
<div style={{ display: "flex", alignItems: "center" }}>
<span style={{ fontWeight: "bold" }}>Cache:</span>
</div>
{cacheWrites !== undefined && cacheWrites > 0 && (
<HeroTooltip content="Tokens written to cache">
<span className="flex items-center gap-[3px] cursor-pointer">
<i
className="codicon codicon-database"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-1px",
}}
/>
+{formatLargeNumber(cacheWrites || 0)}
</span>
</HeroTooltip>
)}
{cacheReads !== undefined && cacheReads > 0 && (
<HeroTooltip content="Tokens read from cache">
<span className="flex items-center gap-[3px] cursor-pointer">
<i
className={"codicon codicon-arrow-right"}
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: 0,
}}
/>
{formatLargeNumber(cacheReads || 0)}
</span>
</HeroTooltip>
)}
</div>
)}
{ContextWindowComponent}
{isCostAvailable && (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
height: 17,
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
}}>
<span style={{ fontWeight: "bold" }}>API Cost:</span>
<span>${totalCost?.toFixed(4)}</span>
<div className="flex items-center flex-wrap">
<CopyButton taskText={task.text} />
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
</div>
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
</div>
)}
<div className="flex flex-col">
<TaskTimeline messages={clineMessages} />
{ContextWindowComponent}
</div>
{checkpointTrackerErrorMessage && (
<div
style={{
@@ -669,27 +625,59 @@ export const highlightText = (text?: string, withShadow = true) => {
return [text]
}
const CopyButton: React.FC<{
taskText?: string
}> = ({ taskText }) => {
const [copied, setCopied] = useState(false)
const handleCopy = () => {
if (!taskText) return
navigator.clipboard.writeText(taskText).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
}
return (
<HeroTooltip content="Copy Task">
<VSCodeButton
appearance="icon"
onClick={handleCopy}
style={{ padding: "0px 0px" }}
className="p-0"
aria-label="Copy Task">
<div className="flex items-center gap-[3px] text-[8px] font-bold opacity-60">
<i className={`codicon codicon-${copied ? "check" : "copy"}`} />
</div>
</VSCodeButton>
</HeroTooltip>
)
}
const DeleteButton: React.FC<{
taskSize: string
taskId?: string
}> = ({ taskSize, taskId }) => (
<VSCodeButton
appearance="icon"
onClick={() => taskId && TaskServiceClient.deleteTasksWithIds({ value: [taskId] })}
style={{ padding: "0px 0px" }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "3px",
fontSize: "10px",
fontWeight: "bold",
opacity: 0.6,
}}>
<i className={`codicon codicon-trash`} />
{taskSize}
</div>
</VSCodeButton>
<HeroTooltip content="Delete Task & Checkpoints">
<VSCodeButton
appearance="icon"
onClick={() => taskId && TaskServiceClient.deleteTasksWithIds({ value: [taskId] })}
style={{ padding: "0px 0px" }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "3px",
fontSize: "10px",
fontWeight: "bold",
opacity: 0.6,
}}>
<i className={`codicon codicon-trash`} />
{taskSize}
</div>
</VSCodeButton>
</HeroTooltip>
)
// const ExportButton = () => (
+58 -21
View File
@@ -3,6 +3,13 @@ import { ClineMessage } from "@shared/ExtensionMessage"
import { combineApiRequests } from "@shared/combineApiRequests"
import { combineCommandSequences } from "@shared/combineCommandSequences"
import TaskTimelineTooltip from "./TaskTimelineTooltip"
import { COLOR_WHITE, COLOR_GRAY, COLOR_DARK_GRAY, COLOR_BEIGE, COLOR_BLUE, COLOR_RED, COLOR_PURPLE, COLOR_GREEN } from "./colors"
// Timeline dimensions and spacing
const TIMELINE_HEIGHT = "18px"
const BLOCK_WIDTH = "9px"
const BLOCK_GAP = "3px"
const TOOLTIP_MARGIN = 32 // 32px margin on each side
interface TaskTimelineProps {
messages: ClineMessage[]
@@ -12,9 +19,11 @@ const getBlockColor = (message: ClineMessage): string => {
if (message.type === "say") {
switch (message.say) {
case "task":
return "#FFFFFF" // White for system prompt
return COLOR_WHITE // White for system prompt
case "user_feedback":
return COLOR_WHITE // White for user feedback
case "text":
return "#AAAAAA" // Gray for assistant responses
return COLOR_GRAY // Gray for assistant responses
case "tool":
if (message.text) {
try {
@@ -26,43 +35,62 @@ const getBlockColor = (message: ClineMessage): string => {
toolData.tool === "listCodeDefinitionNames" ||
toolData.tool === "searchFiles"
) {
return "#F5F5DC" // Beige for file read operations
return COLOR_BEIGE // Beige for file read operations
} else if (toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") {
return "#3B82F6" // Blue for file edit/create operations
return COLOR_BLUE // Blue for file edit/create operations
}
} catch (e) {
// JSON parse error here
}
}
return "#F5F5DC" // Default beige for tool use
return COLOR_BEIGE // Default beige for tool use
case "command":
case "command_output":
return "#EF4444" // Red for terminal commands
return COLOR_PURPLE // Red for terminal commands
case "browser_action":
case "browser_action_result":
return "#8B5CF6" // Purple for browser actions
return COLOR_PURPLE // Purple for browser actions
case "completion_result":
return "#10B981" // Green for task success
return COLOR_GREEN // Green for task success
default:
return "#9E9E9E" // Grey for unknown
return COLOR_DARK_GRAY // Dark gray for unknown
}
} else if (message.type === "ask") {
switch (message.ask) {
case "followup":
return "#AAAAAA" // Gray for user messages
return COLOR_GRAY // Gray for user messages
case "plan_mode_respond":
return "#AAAAAA" // Gray for planning responses
return COLOR_GRAY // Gray for planning responses
case "tool":
return "#9E9E9E" // Gray for tool approvals
// Match the color of the tool approval with the tool type
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 COLOR_BEIGE // Beige for file read operations
} else if (toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") {
return COLOR_BLUE // Blue for file edit/create operations
}
} catch (e) {
// JSON parse error here
}
}
return COLOR_BEIGE // Default beige for tool approvals
case "command":
return "#9E9E9E" // Gray for command approvals
return COLOR_PURPLE // Red for command approvals (same as terminal commands)
case "browser_action_launch":
return "#9E9E9E" // Gray for browser launch approvals
return COLOR_PURPLE // Purple for browser launch approvals (same as browser actions)
default:
return "#9E9E9E" // Grey for unknown
return COLOR_DARK_GRAY // Dark gray for unknown
}
}
return "#9E9E9E" // Default grey
return COLOR_WHITE // Default color
}
const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
@@ -77,16 +105,27 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
const processed = combineApiRequests(combineCommandSequences(messages.slice(1)))
return processed.filter((msg) => {
// Filter out standard "say" events we don't want to show
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 === "checkpoint_created" ||
(msg.say === "text" && (!msg.text || msg.text.trim() === "")))
) {
return false
}
// Filter out "ask" events we don't want to show, including the duplicate completion_result
if (
msg.type === "ask" &&
(msg.ask === "resume_task" || msg.ask === "resume_completed_task" || msg.ask === "completion_result") // Filter out the duplicate completion_result "ask" message
) {
return false
}
return true
})
}, [messages])
@@ -101,8 +140,6 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
return null
}
const TOOLTIP_MARGIN = 32 // 32px margin on each side
const handleMouseEnter = (message: ClineMessage, event: React.MouseEvent<HTMLDivElement>) => {
setHoveredMessage(message)
@@ -134,12 +171,13 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
ref={scrollableRef}
style={{
display: "flex",
height: "10px",
height: TIMELINE_HEIGHT,
overflowX: "auto",
scrollbarWidth: "none",
msOverflowStyle: "none",
width: "100%",
WebkitOverflowScrolling: "touch",
gap: BLOCK_GAP, // Using flexbox gap instead of marginRight
}}>
<style>
{`
@@ -153,10 +191,9 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages }) => {
<div
key={index}
style={{
width: "5px",
width: BLOCK_WIDTH,
height: "100%",
backgroundColor: getBlockColor(message),
marginRight: "1px",
flexShrink: 0,
cursor: "pointer",
}}
@@ -1,5 +1,6 @@
import React from "react"
import { ClineMessage } from "@shared/ExtensionMessage"
import { COLOR_WHITE, COLOR_GRAY, COLOR_DARK_GRAY, COLOR_BEIGE, COLOR_BLUE, COLOR_RED, COLOR_PURPLE, COLOR_GREEN } from "./colors"
// Color mapping for different message types
@@ -14,6 +15,8 @@ const TaskTimelineTooltip: React.FC<TaskTimelineTooltipProps> = ({ message }) =>
// TODO: Need to confirm these classifcations with design
case "task":
return "Task Message"
case "user_feedback":
return "User Message"
case "text":
return "Assistant Response"
case "tool":
@@ -49,6 +52,8 @@ const TaskTimelineTooltip: React.FC<TaskTimelineTooltipProps> = ({ message }) =>
return "Browser Result"
case "completion_result":
return "Task Completed"
case "checkpoint_created":
return "Checkpoint Created"
default:
return message.say || "Unknown"
}
@@ -59,11 +64,32 @@ const TaskTimelineTooltip: React.FC<TaskTimelineTooltipProps> = ({ message }) =>
case "plan_mode_respond":
return "Planning 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 Approval: ${toolData.tool}`
} else if (toolData.tool === "editedExistingFile") {
return `File Edit Approval: ${toolData.path || "Unknown file"}`
} else if (toolData.tool === "newFileCreated") {
return `New File Approval: ${toolData.path || "Unknown file"}`
}
return `Tool Approval: ${toolData.tool}`
} catch (e) {
return "Tool Approval"
}
}
return "Tool Approval"
case "command":
return "Command Approval"
return "Terminal Command Approval"
case "browser_action_launch":
return "Browser Launch"
return "Browser Action Approval"
default:
return message.ask || "Unknown"
}
@@ -99,8 +125,24 @@ const TaskTimelineTooltip: React.FC<TaskTimelineTooltipProps> = ({ message }) =>
const getTimestamp = (message: ClineMessage): string => {
if (message.ts) {
const date = new Date(message.ts)
return date.toLocaleTimeString()
const messageDate = new Date(message.ts)
const today = new Date()
const todayDate = new Date(today.getFullYear(), today.getMonth(), today.getDate())
const messageDateOnly = new Date(messageDate.getFullYear(), messageDate.getMonth(), messageDate.getDate())
const time = messageDate.toLocaleTimeString([], { hour: "numeric", minute: "2-digit", hour12: true })
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
const monthName = monthNames[messageDate.getMonth()]
if (messageDateOnly.getTime() === todayDate.getTime()) {
return `${time}`
} else if (messageDate.getFullYear() === today.getFullYear()) {
return `${monthName} ${messageDate.getDate()} ${time}`
} else {
return `${monthName} ${messageDate.getDate()}, ${messageDate.getFullYear()} ${time}`
}
}
return ""
}
@@ -110,9 +152,11 @@ const TaskTimelineTooltip: React.FC<TaskTimelineTooltipProps> = ({ message }) =>
if (message.type === "say") {
switch (message.say) {
case "task":
return "#FFFFFF" // White for system prompt
return COLOR_WHITE // White for system prompt
case "user_feedback":
return COLOR_WHITE // White for user feedback
case "text":
return "#AAAAAA" // Gray for assistant responses
return COLOR_GRAY // Gray for assistant responses
case "tool":
if (message.text) {
try {
@@ -124,43 +168,62 @@ const TaskTimelineTooltip: React.FC<TaskTimelineTooltipProps> = ({ message }) =>
toolData.tool === "listCodeDefinitionNames" ||
toolData.tool === "searchFiles"
) {
return "#F5F5DC" // Beige for file read operations
return COLOR_BEIGE // Beige for file read operations
} else if (toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") {
return "#3B82F6" // Blue for file edit/create operations
return COLOR_BLUE // Blue for file edit/create operations
}
} catch (e) {
// JSON parse error here
}
}
return "#F5F5DC" // Default beige for tool use
return COLOR_BEIGE // Default beige for tool use
case "command":
case "command_output":
return "#EF4444" // Red for terminal commands
return COLOR_PURPLE // Red for terminal commands
case "browser_action":
case "browser_action_result":
return "#8B5CF6" // Purple for browser actions
return COLOR_PURPLE // Purple for browser actions
case "completion_result":
return "#10B981" // Green for task success
return COLOR_GREEN // Green for task success
default:
return "#9E9E9E" // Grey for unknown
return COLOR_DARK_GRAY // Dark gray for unknown
}
} else if (message.type === "ask") {
switch (message.ask) {
case "followup":
return "#AAAAAA" // Gray for user messages
return COLOR_GRAY // Gray for user messages
case "plan_mode_respond":
return "#AAAAAA" // Gray for planning responses
return COLOR_GRAY // Gray for planning responses
case "tool":
return "#9E9E9E" // Gray for tool approvals
// Match the color of the tool approval with the tool type
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 COLOR_BEIGE // Beige for file read operations
} else if (toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") {
return COLOR_BLUE // Blue for file edit/create operations
}
} catch (e) {
// JSON parse error here
}
}
return COLOR_BEIGE // Default beige for tool approvals
case "command":
return "#9E9E9E" // Gray for command approvals
return COLOR_PURPLE // Red for command approvals (same as terminal commands)
case "browser_action_launch":
return "#9E9E9E" // Gray for browser launch approvals
return COLOR_PURPLE // Purple for browser launch approvals (same as browser actions)
default:
return "#9E9E9E" // Grey for unknown
return COLOR_DARK_GRAY // Dark gray for unknown
}
}
return "#9E9E9E" // Default grey
return COLOR_DARK_GRAY // Default dark gray
}
return (
+9
View File
@@ -0,0 +1,9 @@
// Color constants for timeline and tooltips
export const COLOR_WHITE = "#FFFFFF" // White for system prompt and user feedback
export const COLOR_GRAY = "#AAAAAA" // Gray for assistant responses and user messages
export const COLOR_DARK_GRAY = "#9E9E9E" // Dark gray for unknown types
export const COLOR_BEIGE = "#FFFF54" // Beige for file read operations
export const COLOR_BLUE = "#3B82F6" // Blue for file edit/create operations
export const COLOR_RED = "#EF4444" // Red for terminal commands
export const COLOR_PURPLE = "#9E59FA" // Purple for browser actions
export const COLOR_GREEN = "#10B981" // Green for task success
@@ -0,0 +1,59 @@
import React from "react"
import { Tooltip } from "@heroui/react"
interface HeroTooltipProps {
content: React.ReactNode
children: React.ReactNode
className?: string
delay?: number
closeDelay?: number
placement?: "top" | "bottom" | "left" | "right"
}
/**
* HeroTooltip component that wraps the HeroUI tooltip with styling
* similar to TaskTimelineTooltip
*/
const HeroTooltip: React.FC<HeroTooltipProps> = ({
content,
children,
className,
delay = 0,
closeDelay = 500,
placement = "top",
}) => {
// If content is a simple string, wrap it in the tailwind styled divs
const formattedContent =
typeof content === "string" ? (
<div
className={`bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)]
border border-[var(--vscode-widget-border)] rounded p-2 w-full shadow-md text-xs max-w-[250px] ${className}`}>
<div
className="whitespace-pre-wrap break-words max-h-[150px] overflow-y-auto text-[11px]
font-[var(--vscode-editor-font-family)] p-1 rounded">
{content}
</div>
</div>
) : (
// If content is already a React node, assume it's pre-formatted
content
)
return (
<Tooltip
content={formattedContent}
delay={delay}
closeDelay={0} // Immediate close when cursor moves away
placement={placement}
isDisabled={false}
showArrow={false}
disableAnimation={true} // Disable animation for immediate appearance/disappearance
classNames={{
content: "hero-tooltip-content pointer-events-none", // Prevent hovering over tooltip
}}>
{children}
</Tooltip>
)
}
export default HeroTooltip
@@ -10,7 +10,6 @@ import { useEvent } from "react-use"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import BrowserSettingsSection from "./BrowserSettingsSection"
import TerminalSettingsSection from "./TerminalSettingsSection"
import { useFeatureFlag } from "@/hooks/useFeatureFlag"
import { FEATURE_FLAGS } from "@shared/services/feature-flags/feature-flags"
const { IS_DEV } = process.env
@@ -146,8 +145,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
handleSubmit(true)
}
const showCustomInstructions = useFeatureFlag(FEATURE_FLAGS.CUSTOM_INSTRUCTIONS)
return (
<div className="fixed top-0 left-0 right-0 bottom-0 pt-[10px] pr-0 pb-0 pl-5 flex flex-col overflow-hidden">
<div className="flex justify-between items-center mb-[13px] pr-[17px]">
@@ -186,24 +183,20 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
/>
)}
{showCustomInstructions && (
<div className="mb-[5px]">
<VSCodeTextArea
value={customInstructions ?? ""}
className="w-full"
resize="vertical"
rows={4}
placeholder={
'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
}
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
<span className="font-medium">Custom Instructions</span>
</VSCodeTextArea>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
These instructions are added to the end of the system prompt sent with every request.
</p>
</div>
)}
<div className="mb-[5px]">
<VSCodeTextArea
value={customInstructions ?? ""}
className="w-full"
resize="vertical"
rows={4}
placeholder={'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'}
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
<span className="font-medium">Custom Instructions</span>
</VSCodeTextArea>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
These instructions are added to the end of the system prompt sent with every request.
</p>
</div>
<div className="mb-[5px]">
<VSCodeCheckbox
+17 -4
View File
@@ -1,10 +1,23 @@
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useFeatureFlagPayload } from "posthog-js/react"
export const useFeatureFlag = (flagName: string): boolean => {
const payload = useFeatureFlagPayload(flagName) as { enabled: boolean }
if (payload && payload.enabled) {
return payload.enabled
const { telemetrySetting } = useExtensionState()
try {
const payload = useFeatureFlagPayload(flagName) as { enabled: boolean }
if (payload && typeof payload === "object") {
// Check if the enabled property exists and is a boolean
if ("enabled" in payload && typeof payload.enabled === "boolean") {
return payload.enabled
}
}
if (telemetrySetting === "enabled") {
console.warn(`Feature flag ${flagName} not found or missing enabled property.`)
}
} catch (error) {
console.error(`Error retrieving feature flag "${flagName}":`, error)
}
console.warn(`Feature flag ${flagName} not found or missing enabled property.`)
return false
}