mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Custom modes
This commit is contained in:
5
.changeset/pink-peaches-jump.md
Normal file
5
.changeset/pink-peaches-jump.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"roo-cline": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
v3.2
|
||||||
5
.changeset/plenty-suits-visit.md
Normal file
5
.changeset/plenty-suits-visit.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"roo-cline": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
debug from vscode and changed output channel to Roo-Code
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
# Ran Prettier on all files - https://github.com/RooVetGit/Roo-Cline/pull/404
|
# Ran Prettier on all files - https://github.com/RooVetGit/Roo-Code/pull/404
|
||||||
60a0a824b96a0b326af4d8871b6903f4ddcfe114
|
60a0a824b96a0b326af4d8871b6903f4ddcfe114
|
||||||
579bdd9dbf6d2d569e5e7adb5ff6292b1e42ea34
|
579bdd9dbf6d2d569e5e7adb5ff6292b1e42ea34
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +1,8 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Feature Request
|
- name: Feature Request
|
||||||
url: https://github.com/RooVetGit/Roo-Cline/discussions/categories/feature-requests
|
url: https://github.com/RooVetGit/Roo-Code/discussions/categories/feature-requests
|
||||||
about: Share and vote on feature requests for Roo Cline
|
about: Share and vote on feature requests for Roo Code
|
||||||
- name: Leave a Review
|
- name: Leave a Review
|
||||||
url: https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline&ssr=false#review-details
|
url: https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline&ssr=false#review-details
|
||||||
about: Enjoying Roo Cline? Leave a review here!
|
about: Enjoying Roo Code? Leave a review here!
|
||||||
|
|||||||
2
.github/workflows/code-qa.yml
vendored
2
.github/workflows/code-qa.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Code QA Roo Cline
|
name: Code QA Roo Code
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,57 +1,69 @@
|
|||||||
# Roo Cline Changelog
|
# Roo Code Changelog
|
||||||
|
|
||||||
|
## [3.2.0]
|
||||||
|
|
||||||
|
- **Name Change: From Roo Cline to Roo Code:** We're excited to announce our new name! After growing beyond 50,000 installations, we've rebranded from "Roo Cline" to "Roo Code" to better reflect our identity as we chart our own course.
|
||||||
|
|
||||||
|
- **Custom Modes:** Create your own personas for Roo Code! While our built-in modes (Code, Architect, Ask) are still here, you can now shape entirely new ones:
|
||||||
|
- Define custom prompts
|
||||||
|
- Choose which tools each mode can access
|
||||||
|
- Create specialized assistants for any workflow
|
||||||
|
- Just type "Create a new mode for <X>" or visit the Prompts tab in the top menu to get started
|
||||||
|
|
||||||
|
Join us at https://www.reddit.com/r/RooCode to share your custom modes and be part of our next chapter!
|
||||||
|
|
||||||
## [3.1.7]
|
## [3.1.7]
|
||||||
|
|
||||||
- DeepSeek-R1 support (thanks @philipnext!)
|
- DeepSeek-R1 support (thanks @philipnext!)
|
||||||
- Experimental new unified diff algorithm can be enabled in settings (thanks @daniel-lxs!)
|
- Experimental new unified diff algorithm can be enabled in settings (thanks @daniel-lxs!)
|
||||||
- More fixes to configuration profiles (thanks @samhvw8!)
|
- More fixes to configuration profiles (thanks @samhvw8!)
|
||||||
|
|
||||||
## [3.1.6]
|
## [3.1.6]
|
||||||
|
|
||||||
- Add Mistral (thanks Cline!)
|
- Add Mistral (thanks Cline!)
|
||||||
- Fix bug with VSCode LM configuration profile saving (thanks @samhvw8!)
|
- Fix bug with VSCode LM configuration profile saving (thanks @samhvw8!)
|
||||||
|
|
||||||
## [3.1.4 - 3.1.5]
|
## [3.1.4 - 3.1.5]
|
||||||
|
|
||||||
- Bug fixes to the auto approve menu
|
- Bug fixes to the auto approve menu
|
||||||
|
|
||||||
## [3.1.3]
|
## [3.1.3]
|
||||||
|
|
||||||
- Add auto-approve chat bar (thanks Cline!)
|
- Add auto-approve chat bar (thanks Cline!)
|
||||||
- Fix bug with VS Code Language Models integration
|
- Fix bug with VS Code Language Models integration
|
||||||
|
|
||||||
## [3.1.2]
|
## [3.1.2]
|
||||||
|
|
||||||
- Experimental support for VS Code Language Models including Copilot (thanks @RaySinner / @julesmons!)
|
- Experimental support for VS Code Language Models including Copilot (thanks @RaySinner / @julesmons!)
|
||||||
- Fix bug related to configuration profile switching (thanks @samhvw8!)
|
- Fix bug related to configuration profile switching (thanks @samhvw8!)
|
||||||
- Improvements to fuzzy search in mentions, history, and model lists (thanks @samhvw8!)
|
- Improvements to fuzzy search in mentions, history, and model lists (thanks @samhvw8!)
|
||||||
- PKCE support for Glama (thanks @punkpeye!)
|
- PKCE support for Glama (thanks @punkpeye!)
|
||||||
- Use 'developer' message for o1 system prompt
|
- Use 'developer' message for o1 system prompt
|
||||||
|
|
||||||
## [3.1.1]
|
## [3.1.1]
|
||||||
|
|
||||||
- Visual fixes to chat input and settings for the light+ themes
|
- Visual fixes to chat input and settings for the light+ themes
|
||||||
|
|
||||||
## [3.1.0]
|
## [3.1.0]
|
||||||
|
|
||||||
- You can now customize the role definition and instructions for each chat mode (Code, Architect, and Ask), either through the new Prompts tab in the top menu or mode-specific .clinerules-mode files. Prompt Enhancements have also been revamped: the "Enhance Prompt" button now works with any provider and API configuration, giving you the ability to craft messages with fully customizable prompts for even better results.
|
- You can now customize the role definition and instructions for each chat mode (Code, Architect, and Ask), either through the new Prompts tab in the top menu or mode-specific .clinerules-mode files. Prompt Enhancements have also been revamped: the "Enhance Prompt" button now works with any provider and API configuration, giving you the ability to craft messages with fully customizable prompts for even better results.
|
||||||
- Add a button to copy markdown out of the chat
|
- Add a button to copy markdown out of the chat
|
||||||
|
|
||||||
## [3.0.3]
|
## [3.0.3]
|
||||||
|
|
||||||
- Update required vscode engine to ^1.84.0 to match cline
|
- Update required vscode engine to ^1.84.0 to match cline
|
||||||
|
|
||||||
## [3.0.2]
|
## [3.0.2]
|
||||||
|
|
||||||
- A couple more tiny tweaks to the button alignment in the chat input
|
- A couple more tiny tweaks to the button alignment in the chat input
|
||||||
|
|
||||||
## [3.0.1]
|
## [3.0.1]
|
||||||
|
|
||||||
- Fix the reddit link and a small visual glitch in the chat input
|
- Fix the reddit link and a small visual glitch in the chat input
|
||||||
|
|
||||||
## [3.0.0]
|
## [3.0.0]
|
||||||
|
|
||||||
- This release adds chat modes! Now you can ask Roo Cline questions about system architecture or the codebase without immediately jumping into writing code. You can even assign different API configuration profiles to each mode if you prefer to use different models for thinking vs coding. Would love feedback in the new Roo Cline Reddit! https://www.reddit.com/r/roocline
|
- This release adds chat modes! Now you can ask Roo Code questions about system architecture or the codebase without immediately jumping into writing code. You can even assign different API configuration profiles to each mode if you prefer to use different models for thinking vs coding. Would love feedback in the new Roo Code Reddit! https://www.reddit.com/r/RooCode
|
||||||
|
|
||||||
## [2.2.46]
|
## [2.2.46]
|
||||||
|
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -186,7 +186,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2024 Cline Bot Inc.
|
Copyright 2025 Roo Veterinary Inc.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
340
README.md
340
README.md
@@ -1,8 +1,38 @@
|
|||||||
# Roo Cline
|
# Roo Code
|
||||||
|
|
||||||
A fork of Cline, an autonomous coding agent, with some additional experimental features. It’s been mainly writing itself recently, with a light touch of human guidance here and there.
|
**Roo Code** is an AI-powered **autonomous coding agent** that lives in your editor. It can:
|
||||||
|
|
||||||
You can track what's new at our [CHANGELOG](CHANGELOG.md), with some highlights below.
|
- Communicate in natural language
|
||||||
|
- Read and write files directly in your workspace
|
||||||
|
- Run terminal commands
|
||||||
|
- Automate browser actions
|
||||||
|
- Integrate with any OpenAI-compatible or custom API/model
|
||||||
|
- Adapt its “personality” and capabilities through **Custom Modes**
|
||||||
|
|
||||||
|
Whether you’re seeking a flexible coding partner, a system architect, or specialized roles like a QA engineer or product manager, Roo Code can help you build software more efficiently.
|
||||||
|
|
||||||
|
Check out the [CHANGELOG](CHANGELOG.md) for detailed updates and fixes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New in 3.2: Introducing Custom Modes, plus rebranding from Roo Cline → Roo Code! 🚀
|
||||||
|
|
||||||
|
### Introducing Roo Code
|
||||||
|
|
||||||
|
Our biggest update yet is here - we're officially changing our name from Roo Cline to Roo Code! After growing beyond 50,000 installations across VS Marketplace and Open VSX, we're ready to chart our own course. Our heartfelt thanks to everyone in the Cline community who helped us reach this milestone.
|
||||||
|
|
||||||
|
### Custom Modes
|
||||||
|
|
||||||
|
To mark this new chapter, we're introducing the power to shape Roo Code into any role you need. You can now create an entire team of agents with deeply customized prompts:
|
||||||
|
|
||||||
|
- QA Engineers who write thorough test cases and catch edge cases
|
||||||
|
- Product Managers who excel at user stories and feature prioritization
|
||||||
|
- UI/UX Designers who craft beautiful, accessible interfaces
|
||||||
|
- Code Reviewers who ensure quality and maintainability
|
||||||
|
|
||||||
|
The best part is that Roo can help you create these new modes! Just type "Create a new mode for <X>" in the chat to get started, and go into the Prompts tab or (carefully) edit the JSON representation to customize the prompt and allowed tools to your liking.
|
||||||
|
|
||||||
|
We can't wait to hear more about what you build and how we can continue to evolve the Roo Code platform to support you. Please join us in our new https://www.reddit.com/r/RooCode subreddit to share your custom modes and be part of our next chapter. 🚀
|
||||||
|
|
||||||
## New in 3.1: Chat Mode Prompt Customization & Prompt Enhancements
|
## New in 3.1: Chat Mode Prompt Customization & Prompt Enhancements
|
||||||
|
|
||||||
@@ -41,206 +71,168 @@ It’s super simple! There’s a dropdown in the bottom left of the chat input t
|
|||||||
|
|
||||||
Right now, switching modes is a manual process. In the future, we’d love to give Cline the ability to suggest mode switches based on context. For now, we’d really appreciate your feedback on this feature.
|
Right now, switching modes is a manual process. In the future, we’d love to give Cline the ability to suggest mode switches based on context. For now, we’d really appreciate your feedback on this feature.
|
||||||
|
|
||||||
## Disclaimer
|
---
|
||||||
|
|
||||||
**Please note** that Roo Veterinary, Inc does **not** make any representations or warranties regarding any code, models, or other tools provided or made available in connection with Roo-Cline, any associated third-party tools, or any resulting outputs. You assume **all risks** associated with the use of any such tools or outputs; such tools are provided on an **"AS IS"** and **"AS AVAILABLE"** basis. Such risks may include, without limitation, intellectual property infringement, cyber vulnerabilities or attacks, bias, inaccuracies, errors, defects, viruses, downtime, property loss or damage, and/or personal injury. You are solely responsible for your use of any such tools or outputs (including, without limitation, the legality, appropriateness, and results thereof).
|
## Key Features
|
||||||
|
|
||||||
## Demo
|
### Adaptive Autonomy
|
||||||
|
|
||||||
Here's an example of Roo-Cline autonomously creating a snake game with "Always approve write operations" and "Always approve browser actions" turned on:
|
Roo Code communicates in **natural language** and proposes actions—file edits, terminal commands, browser tests, etc. You choose how it behaves:
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/c2bb31dc-e9b2-4d73-885d-17f1471a4987
|
- **Manual Approval**: Review and approve every step to keep total control.
|
||||||
|
- **Autonomous/Auto-Approve**: Grant Roo Code the ability to run tasks without interruption, speeding up routine workflows.
|
||||||
|
- **Hybrid**: Auto-approve specific actions (e.g., file writes) but require confirmation for riskier tasks (like deploying code).
|
||||||
|
|
||||||
## Contributing
|
No matter your preference, you always have the final say on what Roo Code does.
|
||||||
|
|
||||||
To contribute to the project, start by exploring [open issues](https://github.com/RooVetGit/Roo-Cline/issues) or checking our [feature request board](https://github.com/RooVetGit/Roo-Cline/discussions/categories/feature-requests). We'd also love to have you join the [Roo Cline Reddit](https://www.reddit.com/r/roocline/) to share ideas and connect with other contributors.
|
---
|
||||||
|
|
||||||
### Local Setup
|
### Supports Any API or Model
|
||||||
|
|
||||||
1. Install dependencies:
|
Use Roo Code with:
|
||||||
|
|
||||||
|
- **OpenRouter**, Anthropic, Glama, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, or local models (LM Studio/Ollama)—anything **OpenAI-compatible**.
|
||||||
|
- Different models per mode. For instance, an advanced model for architecture vs. a cheaper model for daily coding tasks.
|
||||||
|
- **Usage Tracking**: Roo Code monitors token and cost usage for each session.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Custom Modes
|
||||||
|
|
||||||
|
**Custom Modes** let you shape Roo Code’s persona, instructions, and permissions:
|
||||||
|
|
||||||
|
- **Built-in**:
|
||||||
|
- **Code** – Default, multi-purpose coding assistant
|
||||||
|
- **Architect** – High-level system and design insights
|
||||||
|
- **Ask** – Research and Q&A for deeper exploration
|
||||||
|
- **User-Created**: Type `Create a new mode for <X>` and Roo Code generates a brand-new persona for that role—complete with tailored prompts and optional tool restrictions.
|
||||||
|
|
||||||
|
Modes can each have unique instructions and skill sets. Manage them in the **Prompts** tab.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### File & Editor Operations
|
||||||
|
|
||||||
|
Roo Code can:
|
||||||
|
|
||||||
|
- **Create and edit** files in your project (showing you diffs).
|
||||||
|
- **React** to linting or compile-time errors automatically (missing imports, syntax errors, etc.).
|
||||||
|
- **Track changes** via your editor’s timeline so you can review or revert if needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Command Line Integration
|
||||||
|
|
||||||
|
Easily run commands in your terminal—Roo Code:
|
||||||
|
|
||||||
|
- Installs packages, runs builds, or executes tests.
|
||||||
|
- Monitors output and adapts if it detects errors.
|
||||||
|
- Lets you keep dev servers running in the background while continuing to work.
|
||||||
|
|
||||||
|
You approve or decline each command, or set auto-approval for routine operations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Browser Automation
|
||||||
|
|
||||||
|
Roo Code can also open a **browser** session to:
|
||||||
|
|
||||||
|
- Launch your local or remote web app.
|
||||||
|
- Click, type, scroll, and capture screenshots.
|
||||||
|
- Collect console logs to debug runtime or UI/UX issues.
|
||||||
|
|
||||||
|
Ideal for **end-to-end testing** or visually verifying changes without constant copy-pasting.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Adding Tools with MCP
|
||||||
|
|
||||||
|
Extend Roo Code with the **Model Context Protocol (MCP)**:
|
||||||
|
|
||||||
|
- “Add a tool that manages AWS EC2 resources.”
|
||||||
|
- “Add a tool that queries the company Jira.”
|
||||||
|
- “Add a tool that pulls the latest PagerDuty incidents.”
|
||||||
|
|
||||||
|
Roo Code can build and configure new tools autonomously (with your approval) to expand its capabilities instantly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Context Mentions
|
||||||
|
|
||||||
|
When you need to provide extra context:
|
||||||
|
|
||||||
|
- **@file** – Embed a file’s contents in the conversation.
|
||||||
|
- **@folder** – Include entire folder structures.
|
||||||
|
- **@problems** – Pull in workspace errors/warnings for Roo Code to fix.
|
||||||
|
- **@url** – Fetch docs from a URL, converting them to markdown.
|
||||||
|
- **@git** – Supply a list of Git commits or diffs for Roo Code to analyze code history.
|
||||||
|
|
||||||
|
Help Roo Code focus on the most relevant details without blowing the token budget.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Roo Code is available on:
|
||||||
|
|
||||||
|
- **[VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline)**
|
||||||
|
- **[Open-VSX](https://open-vsx.org/extension/RooVeterinaryInc/roo-cline)**
|
||||||
|
|
||||||
|
1. **Search “Roo Code”** in your editor’s Extensions panel to install directly.
|
||||||
|
2. Or grab the `.vsix` file from Marketplace / Open-VSX and **drag-and-drop** into your editor.
|
||||||
|
3. **Open** Roo Code from the Activity Bar or Command Palette to start chatting.
|
||||||
|
|
||||||
|
> **Tip**: Use `Cmd/Ctrl + Shift + P` → “Roo Code: Open in New Tab” to dock the AI assistant alongside your file explorer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Local Setup & Development
|
||||||
|
|
||||||
|
1. **Clone** the repo:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/RooVetGit/Roo-Code.git
|
||||||
|
```
|
||||||
|
2. **Install dependencies**:
|
||||||
```bash
|
```bash
|
||||||
npm run install:all
|
npm run install:all
|
||||||
```
|
```
|
||||||
|
3. **Build** the extension:
|
||||||
2. Build the VSIX file:
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
3. The new VSIX file will be created in the `bin/` directory
|
- A `.vsix` file will appear in the `bin/` directory.
|
||||||
4. Install the extension from the VSIX file as described below:
|
4. **Install** the `.vsix` manually if desired:
|
||||||
|
|
||||||
- **Option 1:** Drag and drop the `.vsix` file into your VSCode-compatible editor's Extensions panel (Cmd/Ctrl+Shift+X).
|
|
||||||
|
|
||||||
- **Option 2:** Install the plugin using the CLI, make sure you have your VSCode-compatible CLI installed and in your `PATH` variable. Cursor example: `export PATH="$PATH:/Applications/Cursor.app/Contents/MacOS"`
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Ex: cursor --install-extension bin/roo-cline-2.0.1.vsix
|
code --install-extension bin/roo-code-4.0.0.vsix
|
||||||
# Ex: code --install-extension bin/roo-cline-2.0.1.vsix
|
|
||||||
```
|
```
|
||||||
|
5. **Debug**:
|
||||||
|
- Press `F5` (or **Run** → **Start Debugging**) in VSCode to open a new session with Roo Code loaded.
|
||||||
|
|
||||||
5. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
|
We use [changesets](https://github.com/changesets/changesets) for versioning and publishing. Check our `CHANGELOG.md` for release notes.
|
||||||
|
|
||||||
### Publishing
|
|
||||||
|
|
||||||
We use [changesets](https://github.com/changesets/changesets) for versioning and publishing this package. To make changes:
|
|
||||||
|
|
||||||
1. Create a PR with your changes
|
|
||||||
2. Create a new changeset by running `npm run changeset`
|
|
||||||
- Select the appropriate kind of change - `patch` for bug fixes, `minor` for new features, or `major` for breaking changes
|
|
||||||
- Write a clear description of your changes that will be included in the changelog
|
|
||||||
3. Get the PR approved and pass all checks
|
|
||||||
4. Merge it
|
|
||||||
|
|
||||||
Once your merge is successful:
|
|
||||||
|
|
||||||
- The release workflow will automatically create a new "Changeset version bump" PR
|
|
||||||
- This PR will:
|
|
||||||
- Update the version based on your changeset
|
|
||||||
- Update the `CHANGELOG.md` file
|
|
||||||
- Once the PR is approved and merged, a new version will be published
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Cline (prev. Claude Dev) – [#1 on OpenRouter](https://openrouter.ai/)
|
## Disclaimer
|
||||||
|
|
||||||
<p align="center">
|
**Please note** that Roo Veterinary, Inc does **not** make any representations or warranties regarding any code, models, or other tools provided or made available in connection with Roo Code, any associated third-party tools, or any resulting outputs. You assume **all risks** associated with the use of any such tools or outputs; such tools are provided on an **"AS IS"** and **"AS AVAILABLE"** basis. Such risks may include, without limitation, intellectual property infringement, cyber vulnerabilities or attacks, bias, inaccuracies, errors, defects, viruses, downtime, property loss or damage, and/or personal injury. You are solely responsible for your use of any such tools or outputs (including, without limitation, the legality, appropriateness, and results thereof).
|
||||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<td align="center">
|
|
||||||
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>Download on VS Marketplace</strong></a>
|
|
||||||
</td>
|
|
||||||
<td align="center">
|
|
||||||
<a href="https://discord.gg/cline" target="_blank"><strong>Join the Discord</strong></a>
|
|
||||||
</td>
|
|
||||||
<td align="center">
|
|
||||||
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
|
|
||||||
</td>
|
|
||||||
<td align="center">
|
|
||||||
<a href="https://cline.bot/join-us" target="_blank"><strong>We're Hiring!</strong></a>
|
|
||||||
</td>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Meet Cline, an AI assistant that can use your **CLI** a**N**d **E**ditor.
|
|
||||||
|
|
||||||
Thanks to [Claude 3.5 Sonnet's agentic coding capabilities](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf), Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support. Cline can even use the Model Context Protocol (MCP) to create new tools and extend his own capabilities. While autonomous AI scripts traditionally run in sandboxed environments, this extension provides a human-in-the-loop GUI to approve every file change and terminal command, providing a safe and accessible way to explore the potential of agentic AI.
|
|
||||||
|
|
||||||
1. Enter your task and add images to convert mockups into functional apps or fix bugs with screenshots.
|
|
||||||
2. Cline starts by analyzing your file structure & source code ASTs, running regex searches, and reading relevant files to get up to speed in existing projects. By carefully managing what information is added to context, Cline can provide valuable assistance even for large, complex projects without overwhelming the context window.
|
|
||||||
3. Once Cline has the information he needs, he can:
|
|
||||||
- Create and edit files + monitor linter/compiler errors along the way, letting him proactively fix issues like missing imports and syntax errors on his own.
|
|
||||||
- Execute commands directly in your terminal and monitor their output as he works, letting him e.g., react to dev server issues after editing a file.
|
|
||||||
- For web development tasks, Cline can launch the site in a headless browser, click, type, scroll, and capture screenshots + console logs, allowing him to fix runtime errors and visual bugs.
|
|
||||||
4. When a task is completed, Cline will present the result to you with a terminal command like `open -a "Google Chrome" index.html`, which you run with a click of a button.
|
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> Use the `CMD/CTRL + Shift + P` shortcut to open the command palette and type "Cline: Open In New Tab" to open the extension as a tab in your editor. This lets you use Cline side-by-side with your file explorer, and see how he changes your workspace more clearly.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
|
|
||||||
|
|
||||||
### Use any API and Model
|
|
||||||
|
|
||||||
Cline supports API providers like OpenRouter, Anthropic, Glama, OpenAI, Google Gemini, AWS Bedrock, Azure, and GCP Vertex. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
|
|
||||||
|
|
||||||
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
|
|
||||||
|
|
||||||
<!-- Transparent pixel to create line break after floating image -->
|
|
||||||
|
|
||||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
|
||||||
|
|
||||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
|
|
||||||
|
|
||||||
### Run Commands in Terminal
|
|
||||||
|
|
||||||
Thanks to the new [shell integration updates in VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api), Cline can execute commands directly in your terminal and receive the output. This allows him to perform a wide range of tasks, from installing packages and running build scripts to deploying applications, managing databases, and executing tests, all while adapting to your dev environment & toolchain to get the job done right.
|
|
||||||
|
|
||||||
For long running processes like dev servers, use the "Proceed While Running" button to let Cline continue in the task while the command runs in the background. As Cline works he’ll be notified of any new terminal output along the way, letting him react to issues that may come up, such as compile-time errors when editing files.
|
|
||||||
|
|
||||||
<!-- Transparent pixel to create line break after floating image -->
|
|
||||||
|
|
||||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
|
||||||
|
|
||||||
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
|
|
||||||
|
|
||||||
### Create and Edit Files
|
|
||||||
|
|
||||||
Cline can create and edit files directly in your editor, presenting you a diff view of the changes. You can edit or revert Cline's changes directly in the diff view editor, or provide feedback in chat until you're satisfied with the result. Cline also monitors linter/compiler errors (missing imports, syntax errors, etc.) so he can fix issues that come up along the way on his own.
|
|
||||||
|
|
||||||
All changes made by Cline are recorded in your file's Timeline, providing an easy way to track and revert modifications if needed.
|
|
||||||
|
|
||||||
<!-- Transparent pixel to create line break after floating image -->
|
|
||||||
|
|
||||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
|
||||||
|
|
||||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
|
|
||||||
|
|
||||||
### Use the Browser
|
|
||||||
|
|
||||||
With Claude 3.5 Sonnet's new [Computer Use](https://www.anthropic.com/news/3-5-models-and-computer-use) capability, Cline can launch a browser, click elements, type text, and scroll, capturing screenshots and console logs at each step. This allows for interactive debugging, end-to-end testing, and even general web use! This gives him autonomy to fixing visual bugs and runtime issues without you needing to handhold and copy-pasting error logs yourself.
|
|
||||||
|
|
||||||
Try asking Cline to "test the app", and watch as he runs a command like `npm run dev`, launches your locally running dev server in a browser, and performs a series of tests to confirm that everything works. [See a demo here.](https://x.com/sdrzn/status/1850880547825823989)
|
|
||||||
|
|
||||||
<!-- Transparent pixel to create line break after floating image -->
|
|
||||||
|
|
||||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
|
||||||
|
|
||||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
|
|
||||||
|
|
||||||
### "add a tool that..."
|
|
||||||
|
|
||||||
Thanks to the [Model Context Protocol](https://github.com/modelcontextprotocol), Cline can extend his capabilities through custom tools. While you can use [community-made servers](https://github.com/modelcontextprotocol/servers), Cline can instead create and install tools tailored to your specific workflow. Just ask Cline to "add a tool" and he will handle everything, from creating a new MCP server to installing it into the extension. These custom tools then become part of Cline's toolkit, ready to use in future tasks.
|
|
||||||
|
|
||||||
- "add a tool that fetches Jira tickets": Retrieve ticket ACs and put Cline to work
|
|
||||||
- "add a tool that manages AWS EC2s": Check server metrics and scale instances up or down
|
|
||||||
- "add a tool that pulls the latest PagerDuty incidents": Fetch details and ask Cline to fix bugs
|
|
||||||
|
|
||||||
<!-- Transparent pixel to create line break after floating image -->
|
|
||||||
|
|
||||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
|
||||||
|
|
||||||
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
|
|
||||||
|
|
||||||
### Add Context
|
|
||||||
|
|
||||||
**`@url`:** Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Cline the latest docs
|
|
||||||
|
|
||||||
**`@problems`:** Add workspace errors and warnings ('Problems' panel) for Cline to fix
|
|
||||||
|
|
||||||
**`@file`:** Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
|
|
||||||
|
|
||||||
**`@folder`:** Adds folder's files all at once to speed up your workflow even more
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
To contribute to the project, start by exploring [open issues](https://github.com/cline/cline/issues) or checking our [feature request board](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop). We'd also love to have you join our [Discord](https://discord.gg/cline) to share ideas and connect with other contributors. If you're interested in joining the team, check out our [careers page](https://cline.bot/join-us)!
|
We love community contributions! Here’s how to get involved:
|
||||||
|
|
||||||
<details>
|
1. **Check Issues & Requests**: See [open issues](https://github.com/RooVetGit/Roo-Code/issues) or [feature requests](https://github.com/RooVetGit/Roo-Code/discussions/categories/feature-requests).
|
||||||
<summary>Local Development Instructions</summary>
|
2. **Fork & branch** off `main`.
|
||||||
|
3. **Submit a Pull Request** once your feature or fix is ready.
|
||||||
|
4. **Join** our [Reddit community](https://www.reddit.com/r/RooCode/) for feedback, tips, and announcements.
|
||||||
|
|
||||||
1. Clone the repository _(Requires [git-lfs](https://git-lfs.com/))_:
|
---
|
||||||
```bash
|
|
||||||
git clone https://github.com/cline/cline.git
|
|
||||||
```
|
|
||||||
2. Open the project in VSCode:
|
|
||||||
```bash
|
|
||||||
code cline
|
|
||||||
```
|
|
||||||
3. Install the necessary dependencies for the extension and webview-gui:
|
|
||||||
```bash
|
|
||||||
npm run install:all
|
|
||||||
```
|
|
||||||
4. Launch by pressing `F5` (or `Run`->`Start Debugging`) to open a new VSCode window with the extension loaded. (You may need to install the [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) if you run into issues building the project.)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
|
[Apache 2.0 © 2025 Roo Veterinary, Inc.](./LICENSE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Enjoy Roo Code!** Whether you keep it on a short leash or let it roam autonomously, we can’t wait to see what you build. If you have questions or feature ideas, drop by our [Reddit community](https://www.reddit.com/r/RooCode/). Happy coding!
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "roo-cline",
|
"name": "roo-cline",
|
||||||
"displayName": "Roo Cline",
|
"displayName": "Roo Code (prev. Roo Cline)",
|
||||||
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
|
"description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.",
|
||||||
"publisher": "RooVeterinaryInc",
|
"publisher": "RooVeterinaryInc",
|
||||||
"version": "3.1.7",
|
"version": "3.1.7",
|
||||||
"icon": "assets/icons/rocket.png",
|
"icon": "assets/icons/rocket.png",
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/RooVetGit/Roo-Cline"
|
"url": "https://github.com/RooVetGit/Roo-Code"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/RooVetGit/Roo-Cline",
|
"homepage": "https://github.com/RooVetGit/Roo-Code",
|
||||||
"categories": [
|
"categories": [
|
||||||
"AI",
|
"AI",
|
||||||
"Chat",
|
"Chat",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"activitybar": [
|
"activitybar": [
|
||||||
{
|
{
|
||||||
"id": "roo-cline-ActivityBar",
|
"id": "roo-cline-ActivityBar",
|
||||||
"title": "Roo Cline",
|
"title": "Roo Code",
|
||||||
"icon": "$(rocket)"
|
"icon": "$(rocket)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
{
|
{
|
||||||
"command": "roo-cline.openInNewTab",
|
"command": "roo-cline.openInNewTab",
|
||||||
"title": "Open In New Tab",
|
"title": "Open In New Tab",
|
||||||
"category": "Roo Cline"
|
"category": "Roo Code"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"title": "RooCline",
|
"title": "Roo Code",
|
||||||
"properties": {
|
"properties": {
|
||||||
"roo-cline.allowedCommands": {
|
"roo-cline.allowedCommands": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|||||||
195
src/__mocks__/fs/promises.ts
Normal file
195
src/__mocks__/fs/promises.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
// Mock file system data
|
||||||
|
const mockFiles = new Map()
|
||||||
|
const mockDirectories = new Set()
|
||||||
|
|
||||||
|
// Initialize base test directories
|
||||||
|
const baseTestDirs = [
|
||||||
|
"/mock",
|
||||||
|
"/mock/extension",
|
||||||
|
"/mock/extension/path",
|
||||||
|
"/mock/storage",
|
||||||
|
"/mock/storage/path",
|
||||||
|
"/mock/settings",
|
||||||
|
"/mock/settings/path",
|
||||||
|
"/mock/mcp",
|
||||||
|
"/mock/mcp/path",
|
||||||
|
"/test",
|
||||||
|
"/test/path",
|
||||||
|
"/test/storage",
|
||||||
|
"/test/storage/path",
|
||||||
|
"/test/storage/path/settings",
|
||||||
|
"/test/extension",
|
||||||
|
"/test/extension/path",
|
||||||
|
"/test/global-storage",
|
||||||
|
"/test/log/path",
|
||||||
|
]
|
||||||
|
|
||||||
|
// Helper function to format instructions
|
||||||
|
const formatInstructions = (sections: string[]): string => {
|
||||||
|
const joinedSections = sections.filter(Boolean).join("\n\n")
|
||||||
|
return joinedSections
|
||||||
|
? `
|
||||||
|
====
|
||||||
|
|
||||||
|
USER'S CUSTOM INSTRUCTIONS
|
||||||
|
|
||||||
|
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
|
||||||
|
|
||||||
|
${joinedSections}`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to format rule content
|
||||||
|
const formatRuleContent = (ruleFile: string, content: string): string => {
|
||||||
|
return `Rules:\n# Rules from ${ruleFile}:\n${content}`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleFiles = {
|
||||||
|
".clinerules-code": string
|
||||||
|
".clinerules-ask": string
|
||||||
|
".clinerules-architect": string
|
||||||
|
".clinerules-test": string
|
||||||
|
".clinerules-review": string
|
||||||
|
".clinerules": string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to ensure directory exists
|
||||||
|
const ensureDirectoryExists = (path: string) => {
|
||||||
|
const parts = path.split("/")
|
||||||
|
let currentPath = ""
|
||||||
|
for (const part of parts) {
|
||||||
|
if (!part) continue
|
||||||
|
currentPath += "/" + part
|
||||||
|
mockDirectories.add(currentPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockFs = {
|
||||||
|
readFile: jest.fn().mockImplementation(async (filePath: string, encoding?: string) => {
|
||||||
|
// Return stored content if it exists
|
||||||
|
if (mockFiles.has(filePath)) {
|
||||||
|
return mockFiles.get(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle rule files
|
||||||
|
const ruleFiles: RuleFiles = {
|
||||||
|
".clinerules-code": "# Code Mode Rules\n1. Code specific rule",
|
||||||
|
".clinerules-ask": "# Ask Mode Rules\n1. Ask specific rule",
|
||||||
|
".clinerules-architect": "# Architect Mode Rules\n1. Architect specific rule",
|
||||||
|
".clinerules-test":
|
||||||
|
"# Test Engineer Rules\n1. Always write tests first\n2. Get approval before modifying non-test code",
|
||||||
|
".clinerules-review":
|
||||||
|
"# Code Reviewer Rules\n1. Provide specific examples in feedback\n2. Focus on maintainability and best practices",
|
||||||
|
".clinerules": "# Test Rules\n1. First rule\n2. Second rule",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for exact file name match
|
||||||
|
const fileName = filePath.split("/").pop()
|
||||||
|
if (fileName && fileName in ruleFiles) {
|
||||||
|
return ruleFiles[fileName as keyof RuleFiles]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for file name in path
|
||||||
|
for (const [ruleFile, content] of Object.entries(ruleFiles)) {
|
||||||
|
if (filePath.includes(ruleFile)) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle file not found
|
||||||
|
const error = new Error(`ENOENT: no such file or directory, open '${filePath}'`)
|
||||||
|
;(error as any).code = "ENOENT"
|
||||||
|
throw error
|
||||||
|
}),
|
||||||
|
|
||||||
|
writeFile: jest.fn().mockImplementation(async (path: string, content: string) => {
|
||||||
|
// Ensure parent directory exists
|
||||||
|
const parentDir = path.split("/").slice(0, -1).join("/")
|
||||||
|
ensureDirectoryExists(parentDir)
|
||||||
|
mockFiles.set(path, content)
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
mkdir: jest.fn().mockImplementation(async (path: string, options?: { recursive?: boolean }) => {
|
||||||
|
// Always handle recursive creation
|
||||||
|
const parts = path.split("/")
|
||||||
|
let currentPath = ""
|
||||||
|
|
||||||
|
// For recursive or test/mock paths, create all parent directories
|
||||||
|
if (options?.recursive || path.startsWith("/test") || path.startsWith("/mock")) {
|
||||||
|
for (const part of parts) {
|
||||||
|
if (!part) continue
|
||||||
|
currentPath += "/" + part
|
||||||
|
mockDirectories.add(currentPath)
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-recursive paths, verify parent exists
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
if (!parts[i]) continue
|
||||||
|
currentPath += "/" + parts[i]
|
||||||
|
if (!mockDirectories.has(currentPath)) {
|
||||||
|
const error = new Error(`ENOENT: no such file or directory, mkdir '${path}'`)
|
||||||
|
;(error as any).code = "ENOENT"
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the final directory
|
||||||
|
currentPath += "/" + parts[parts.length - 1]
|
||||||
|
mockDirectories.add(currentPath)
|
||||||
|
return Promise.resolve()
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
|
||||||
|
access: jest.fn().mockImplementation(async (path: string) => {
|
||||||
|
// Check if the path exists in either files or directories
|
||||||
|
if (mockFiles.has(path) || mockDirectories.has(path) || path.startsWith("/test")) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
const error = new Error(`ENOENT: no such file or directory, access '${path}'`)
|
||||||
|
;(error as any).code = "ENOENT"
|
||||||
|
throw error
|
||||||
|
}),
|
||||||
|
|
||||||
|
constants: jest.requireActual("fs").constants,
|
||||||
|
|
||||||
|
// Expose mock data for test assertions
|
||||||
|
_mockFiles: mockFiles,
|
||||||
|
_mockDirectories: mockDirectories,
|
||||||
|
|
||||||
|
// Helper to set up initial mock data
|
||||||
|
_setInitialMockData: () => {
|
||||||
|
// Set up default MCP settings
|
||||||
|
mockFiles.set(
|
||||||
|
"/mock/settings/path/cline_mcp_settings.json",
|
||||||
|
JSON.stringify({
|
||||||
|
mcpServers: {
|
||||||
|
"test-server": {
|
||||||
|
command: "node",
|
||||||
|
args: ["test.js"],
|
||||||
|
disabled: false,
|
||||||
|
alwaysAllow: ["existing-tool"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure all base directories exist
|
||||||
|
baseTestDirs.forEach((dir) => {
|
||||||
|
const parts = dir.split("/")
|
||||||
|
let currentPath = ""
|
||||||
|
for (const part of parts) {
|
||||||
|
if (!part) continue
|
||||||
|
currentPath += "/" + part
|
||||||
|
mockDirectories.add(currentPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize mock data
|
||||||
|
mockFs._setInitialMockData()
|
||||||
|
|
||||||
|
module.exports = mockFs
|
||||||
@@ -36,7 +36,7 @@ describe("OpenRouterHandler", () => {
|
|||||||
apiKey: mockOptions.openRouterApiKey,
|
apiKey: mockOptions.openRouterApiKey,
|
||||||
defaultHeaders: {
|
defaultHeaders: {
|
||||||
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
|
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
|
||||||
"X-Title": "Roo-Cline",
|
"X-Title": "Roo-Code",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export class LmStudioHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// LM Studio doesn't return an error code/body for now
|
// LM Studio doesn't return an error code/body for now
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Cline's prompts.",
|
"Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Roo Code's prompts.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ export class LmStudioHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
return response.choices[0]?.message.content || ""
|
return response.choices[0]?.message.content || ""
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Cline's prompts.",
|
"Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Roo Code's prompts.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
baseURL: "https://openrouter.ai/api/v1",
|
baseURL: "https://openrouter.ai/api/v1",
|
||||||
apiKey: this.options.openRouterApiKey,
|
apiKey: this.options.openRouterApiKey,
|
||||||
defaultHeaders: {
|
defaultHeaders: {
|
||||||
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline", // Optional, for including your app on openrouter.ai rankings.
|
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
|
||||||
"X-Title": "Roo-Cline", // Optional. Shows in rankings on openrouter.ai.
|
"X-Title": "Roo-Code",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
this.dispose()
|
this.dispose()
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Cline <Language Model API>: Failed to initialize handler: ${error instanceof Error ? error.message : "Unknown error"}`,
|
`Roo Code <Language Model API>: Failed to initialize handler: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||||
throw new Error(`Cline <Language Model API>: Failed to select model: ${errorMessage}`)
|
throw new Error(`Roo Code <Language Model API>: Failed to select model: ${errorMessage}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,18 +147,18 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise<number> {
|
private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise<number> {
|
||||||
// Check for required dependencies
|
// Check for required dependencies
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
console.warn("Cline <Language Model API>: No client available for token counting")
|
console.warn("Roo Code <Language Model API>: No client available for token counting")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.currentRequestCancellation) {
|
if (!this.currentRequestCancellation) {
|
||||||
console.warn("Cline <Language Model API>: No cancellation token available for token counting")
|
console.warn("Roo Code <Language Model API>: No cancellation token available for token counting")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate input
|
// Validate input
|
||||||
if (!text) {
|
if (!text) {
|
||||||
console.debug("Cline <Language Model API>: Empty text provided for token counting")
|
console.debug("Roo Code <Language Model API>: Empty text provided for token counting")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,23 +171,23 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
} else if (text instanceof vscode.LanguageModelChatMessage) {
|
} else if (text instanceof vscode.LanguageModelChatMessage) {
|
||||||
// For chat messages, ensure we have content
|
// For chat messages, ensure we have content
|
||||||
if (!text.content || (Array.isArray(text.content) && text.content.length === 0)) {
|
if (!text.content || (Array.isArray(text.content) && text.content.length === 0)) {
|
||||||
console.debug("Cline <Language Model API>: Empty chat message content")
|
console.debug("Roo Code <Language Model API>: Empty chat message content")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
|
tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
|
||||||
} else {
|
} else {
|
||||||
console.warn("Cline <Language Model API>: Invalid input type for token counting")
|
console.warn("Roo Code <Language Model API>: Invalid input type for token counting")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the result
|
// Validate the result
|
||||||
if (typeof tokenCount !== "number") {
|
if (typeof tokenCount !== "number") {
|
||||||
console.warn("Cline <Language Model API>: Non-numeric token count received:", tokenCount)
|
console.warn("Roo Code <Language Model API>: Non-numeric token count received:", tokenCount)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tokenCount < 0) {
|
if (tokenCount < 0) {
|
||||||
console.warn("Cline <Language Model API>: Negative token count received:", tokenCount)
|
console.warn("Roo Code <Language Model API>: Negative token count received:", tokenCount)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,12 +195,12 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle specific error types
|
// Handle specific error types
|
||||||
if (error instanceof vscode.CancellationError) {
|
if (error instanceof vscode.CancellationError) {
|
||||||
console.debug("Cline <Language Model API>: Token counting cancelled by user")
|
console.debug("Roo Code <Language Model API>: Token counting cancelled by user")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||||
console.warn("Cline <Language Model API>: Token counting failed:", errorMessage)
|
console.warn("Roo Code <Language Model API>: Token counting failed:", errorMessage)
|
||||||
|
|
||||||
// Log additional error details if available
|
// Log additional error details if available
|
||||||
if (error instanceof Error && error.stack) {
|
if (error instanceof Error && error.stack) {
|
||||||
@@ -232,7 +232,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
|
|
||||||
private async getClient(): Promise<vscode.LanguageModelChat> {
|
private async getClient(): Promise<vscode.LanguageModelChat> {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
console.debug("Cline <Language Model API>: Getting client with options:", {
|
console.debug("Roo Code <Language Model API>: Getting client with options:", {
|
||||||
vsCodeLmModelSelector: this.options.vsCodeLmModelSelector,
|
vsCodeLmModelSelector: this.options.vsCodeLmModelSelector,
|
||||||
hasOptions: !!this.options,
|
hasOptions: !!this.options,
|
||||||
selectorKeys: this.options.vsCodeLmModelSelector ? Object.keys(this.options.vsCodeLmModelSelector) : [],
|
selectorKeys: this.options.vsCodeLmModelSelector ? Object.keys(this.options.vsCodeLmModelSelector) : [],
|
||||||
@@ -241,12 +241,12 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
try {
|
try {
|
||||||
// Use default empty selector if none provided to get all available models
|
// Use default empty selector if none provided to get all available models
|
||||||
const selector = this.options?.vsCodeLmModelSelector || {}
|
const selector = this.options?.vsCodeLmModelSelector || {}
|
||||||
console.debug("Cline <Language Model API>: Creating client with selector:", selector)
|
console.debug("Roo Code <Language Model API>: Creating client with selector:", selector)
|
||||||
this.client = await this.createClient(selector)
|
this.client = await this.createClient(selector)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Unknown error"
|
const message = error instanceof Error ? error.message : "Unknown error"
|
||||||
console.error("Cline <Language Model API>: Client creation failed:", message)
|
console.error("Roo Code <Language Model API>: Client creation failed:", message)
|
||||||
throw new Error(`Cline <Language Model API>: Failed to create client: ${message}`)
|
throw new Error(`Roo Code <Language Model API>: Failed to create client: ${message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
try {
|
try {
|
||||||
// Create the response stream with minimal required options
|
// Create the response stream with minimal required options
|
||||||
const requestOptions: vscode.LanguageModelChatRequestOptions = {
|
const requestOptions: vscode.LanguageModelChatRequestOptions = {
|
||||||
justification: `Cline would like to use '${client.name}' from '${client.vendor}', Click 'Allow' to proceed.`,
|
justification: `Roo Code would like to use '${client.name}' from '${client.vendor}', Click 'Allow' to proceed.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Tool support is currently provided by the VSCode Language Model API directly
|
// Note: Tool support is currently provided by the VSCode Language Model API directly
|
||||||
@@ -365,7 +365,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
if (chunk instanceof vscode.LanguageModelTextPart) {
|
if (chunk instanceof vscode.LanguageModelTextPart) {
|
||||||
// Validate text part value
|
// Validate text part value
|
||||||
if (typeof chunk.value !== "string") {
|
if (typeof chunk.value !== "string") {
|
||||||
console.warn("Cline <Language Model API>: Invalid text part value received:", chunk.value)
|
console.warn("Roo Code <Language Model API>: Invalid text part value received:", chunk.value)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,18 +378,18 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
try {
|
try {
|
||||||
// Validate tool call parameters
|
// Validate tool call parameters
|
||||||
if (!chunk.name || typeof chunk.name !== "string") {
|
if (!chunk.name || typeof chunk.name !== "string") {
|
||||||
console.warn("Cline <Language Model API>: Invalid tool name received:", chunk.name)
|
console.warn("Roo Code <Language Model API>: Invalid tool name received:", chunk.name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chunk.callId || typeof chunk.callId !== "string") {
|
if (!chunk.callId || typeof chunk.callId !== "string") {
|
||||||
console.warn("Cline <Language Model API>: Invalid tool callId received:", chunk.callId)
|
console.warn("Roo Code <Language Model API>: Invalid tool callId received:", chunk.callId)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure input is a valid object
|
// Ensure input is a valid object
|
||||||
if (!chunk.input || typeof chunk.input !== "object") {
|
if (!chunk.input || typeof chunk.input !== "object") {
|
||||||
console.warn("Cline <Language Model API>: Invalid tool input received:", chunk.input)
|
console.warn("Roo Code <Language Model API>: Invalid tool input received:", chunk.input)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +405,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
accumulatedText += toolCallText
|
accumulatedText += toolCallText
|
||||||
|
|
||||||
// Log tool call for debugging
|
// Log tool call for debugging
|
||||||
console.debug("Cline <Language Model API>: Processing tool call:", {
|
console.debug("Roo Code <Language Model API>: Processing tool call:", {
|
||||||
name: chunk.name,
|
name: chunk.name,
|
||||||
callId: chunk.callId,
|
callId: chunk.callId,
|
||||||
inputSize: JSON.stringify(chunk.input).length,
|
inputSize: JSON.stringify(chunk.input).length,
|
||||||
@@ -416,12 +416,12 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
text: toolCallText,
|
text: toolCallText,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Cline <Language Model API>: Failed to process tool call:", error)
|
console.error("Roo Code <Language Model API>: Failed to process tool call:", error)
|
||||||
// Continue processing other chunks even if one fails
|
// Continue processing other chunks even if one fails
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn("Cline <Language Model API>: Unknown chunk type received:", chunk)
|
console.warn("Roo Code <Language Model API>: Unknown chunk type received:", chunk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,11 +439,11 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
this.ensureCleanState()
|
this.ensureCleanState()
|
||||||
|
|
||||||
if (error instanceof vscode.CancellationError) {
|
if (error instanceof vscode.CancellationError) {
|
||||||
throw new Error("Cline <Language Model API>: Request cancelled by user")
|
throw new Error("Roo Code <Language Model API>: Request cancelled by user")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error("Cline <Language Model API>: Stream error details:", {
|
console.error("Roo Code <Language Model API>: Stream error details:", {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
name: error.name,
|
name: error.name,
|
||||||
@@ -454,13 +454,13 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
} else if (typeof error === "object" && error !== null) {
|
} else if (typeof error === "object" && error !== null) {
|
||||||
// Handle error-like objects
|
// Handle error-like objects
|
||||||
const errorDetails = JSON.stringify(error, null, 2)
|
const errorDetails = JSON.stringify(error, null, 2)
|
||||||
console.error("Cline <Language Model API>: Stream error object:", errorDetails)
|
console.error("Roo Code <Language Model API>: Stream error object:", errorDetails)
|
||||||
throw new Error(`Cline <Language Model API>: Response stream error: ${errorDetails}`)
|
throw new Error(`Roo Code <Language Model API>: Response stream error: ${errorDetails}`)
|
||||||
} else {
|
} else {
|
||||||
// Fallback for unknown error types
|
// Fallback for unknown error types
|
||||||
const errorMessage = String(error)
|
const errorMessage = String(error)
|
||||||
console.error("Cline <Language Model API>: Unknown stream error:", errorMessage)
|
console.error("Roo Code <Language Model API>: Unknown stream error:", errorMessage)
|
||||||
throw new Error(`Cline <Language Model API>: Response stream error: ${errorMessage}`)
|
throw new Error(`Roo Code <Language Model API>: Response stream error: ${errorMessage}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -480,7 +480,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
// Log any missing properties for debugging
|
// Log any missing properties for debugging
|
||||||
for (const [prop, value] of Object.entries(requiredProps)) {
|
for (const [prop, value] of Object.entries(requiredProps)) {
|
||||||
if (!value && value !== 0) {
|
if (!value && value !== 0) {
|
||||||
console.warn(`Cline <Language Model API>: Client missing ${prop} property`)
|
console.warn(`Roo Code <Language Model API>: Client missing ${prop} property`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,7 +511,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
|||||||
? stringifyVsCodeLmModelSelector(this.options.vsCodeLmModelSelector)
|
? stringifyVsCodeLmModelSelector(this.options.vsCodeLmModelSelector)
|
||||||
: "vscode-lm"
|
: "vscode-lm"
|
||||||
|
|
||||||
console.debug("Cline <Language Model API>: No client available, using fallback model info")
|
console.debug("Roo Code <Language Model API>: No client available, using fallback model info")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: fallbackId,
|
id: fallbackId,
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ describe("vscode-lm-format", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await expect(convertToAnthropicMessage(vsCodeMessage as any)).rejects.toThrow(
|
await expect(convertToAnthropicMessage(vsCodeMessage as any)).rejects.toThrow(
|
||||||
"Cline <Language Model API>: Only assistant messages are supported.",
|
"Roo Code <Language Model API>: Only assistant messages are supported.",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function asObjectSafe(value: any): object {
|
|||||||
|
|
||||||
return {}
|
return {}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Cline <Language Model API>: Failed to parse object:", error)
|
console.warn("Roo Code <Language Model API>: Failed to parse object:", error)
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,7 @@ export async function convertToAnthropicMessage(
|
|||||||
): Promise<Anthropic.Messages.Message> {
|
): Promise<Anthropic.Messages.Message> {
|
||||||
const anthropicRole: string | null = convertToAnthropicRole(vsCodeLmMessage.role)
|
const anthropicRole: string | null = convertToAnthropicRole(vsCodeLmMessage.role)
|
||||||
if (anthropicRole !== "assistant") {
|
if (anthropicRole !== "assistant") {
|
||||||
throw new Error("Cline <Language Model API>: Only assistant messages are supported.")
|
throw new Error("Roo Code <Language Model API>: Only assistant messages are supported.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ import { arePathsEqual, getReadablePath } from "../utils/path"
|
|||||||
import { parseMentions } from "./mentions"
|
import { parseMentions } from "./mentions"
|
||||||
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
|
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
|
||||||
import { formatResponse } from "./prompts/responses"
|
import { formatResponse } from "./prompts/responses"
|
||||||
import { addCustomInstructions, SYSTEM_PROMPT } from "./prompts/system"
|
import { SYSTEM_PROMPT } from "./prompts/system"
|
||||||
import { modes, defaultModeSlug } from "../shared/modes"
|
import { modes, defaultModeSlug, getModeBySlug } from "../shared/modes"
|
||||||
import { truncateHalfConversation } from "./sliding-window"
|
import { truncateHalfConversation } from "./sliding-window"
|
||||||
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
|
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
|
||||||
import { detectCodeOmission } from "../integrations/editor/detect-omission"
|
import { detectCodeOmission } from "../integrations/editor/detect-omission"
|
||||||
@@ -264,7 +264,7 @@ export class Cline {
|
|||||||
): Promise<{ response: ClineAskResponse; text?: string; images?: string[] }> {
|
): Promise<{ response: ClineAskResponse; text?: string; images?: string[] }> {
|
||||||
// If this Cline instance was aborted by the provider, then the only thing keeping us alive is a promise still running in the background, in which case we don't want to send its result to the webview as it is attached to a new instance of Cline now. So we can safely ignore the result of any active promises, and this class will be deallocated. (Although we set Cline = undefined in provider, that simply removes the reference to this instance, but the instance is still alive until this promise resolves or rejects.)
|
// If this Cline instance was aborted by the provider, then the only thing keeping us alive is a promise still running in the background, in which case we don't want to send its result to the webview as it is attached to a new instance of Cline now. So we can safely ignore the result of any active promises, and this class will be deallocated. (Although we set Cline = undefined in provider, that simply removes the reference to this instance, but the instance is still alive until this promise resolves or rejects.)
|
||||||
if (this.abort) {
|
if (this.abort) {
|
||||||
throw new Error("Cline instance aborted")
|
throw new Error("Roo Code instance aborted")
|
||||||
}
|
}
|
||||||
let askTs: number
|
let askTs: number
|
||||||
if (partial !== undefined) {
|
if (partial !== undefined) {
|
||||||
@@ -360,7 +360,7 @@ export class Cline {
|
|||||||
|
|
||||||
async say(type: ClineSay, text?: string, images?: string[], partial?: boolean): Promise<undefined> {
|
async say(type: ClineSay, text?: string, images?: string[], partial?: boolean): Promise<undefined> {
|
||||||
if (this.abort) {
|
if (this.abort) {
|
||||||
throw new Error("Cline instance aborted")
|
throw new Error("Roo Code instance aborted")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (partial !== undefined) {
|
if (partial !== undefined) {
|
||||||
@@ -419,7 +419,7 @@ export class Cline {
|
|||||||
async sayAndCreateMissingParamError(toolName: ToolUseName, paramName: string, relPath?: string) {
|
async sayAndCreateMissingParamError(toolName: ToolUseName, paramName: string, relPath?: string) {
|
||||||
await this.say(
|
await this.say(
|
||||||
"error",
|
"error",
|
||||||
`Cline tried to use ${toolName}${
|
`Roo tried to use ${toolName}${
|
||||||
relPath ? ` for '${relPath.toPosix()}'` : ""
|
relPath ? ` for '${relPath.toPosix()}'` : ""
|
||||||
} without value for required parameter '${paramName}'. Retrying...`,
|
} without value for required parameter '${paramName}'. Retrying...`,
|
||||||
)
|
)
|
||||||
@@ -809,10 +809,15 @@ export class Cline {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const { browserViewportSize, preferredLanguage, mode, customPrompts } =
|
const { browserViewportSize, mode, customPrompts } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||||
(await this.providerRef.deref()?.getState()) ?? {}
|
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||||
const systemPrompt =
|
const systemPrompt = await (async () => {
|
||||||
(await SYSTEM_PROMPT(
|
const provider = this.providerRef.deref()
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error("Provider not available")
|
||||||
|
}
|
||||||
|
return SYSTEM_PROMPT(
|
||||||
|
provider.context,
|
||||||
cwd,
|
cwd,
|
||||||
this.api.getModel().info.supportsComputerUse ?? false,
|
this.api.getModel().info.supportsComputerUse ?? false,
|
||||||
mcpHub,
|
mcpHub,
|
||||||
@@ -820,16 +825,9 @@ export class Cline {
|
|||||||
browserViewportSize,
|
browserViewportSize,
|
||||||
mode,
|
mode,
|
||||||
customPrompts,
|
customPrompts,
|
||||||
)) +
|
customModes,
|
||||||
(await addCustomInstructions(
|
)
|
||||||
{
|
})()
|
||||||
customInstructions: this.customInstructions,
|
|
||||||
customPrompts,
|
|
||||||
preferredLanguage,
|
|
||||||
},
|
|
||||||
cwd,
|
|
||||||
mode,
|
|
||||||
))
|
|
||||||
|
|
||||||
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
|
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
|
||||||
if (previousApiReqIndex >= 0) {
|
if (previousApiReqIndex >= 0) {
|
||||||
@@ -923,7 +921,7 @@ export class Cline {
|
|||||||
|
|
||||||
async presentAssistantMessage() {
|
async presentAssistantMessage() {
|
||||||
if (this.abort) {
|
if (this.abort) {
|
||||||
throw new Error("Cline instance aborted")
|
throw new Error("Roo Code instance aborted")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.presentAssistantMessageLocked) {
|
if (this.presentAssistantMessageLocked) {
|
||||||
@@ -1142,8 +1140,9 @@ export class Cline {
|
|||||||
|
|
||||||
// Validate tool use based on current mode
|
// Validate tool use based on current mode
|
||||||
const { mode } = (await this.providerRef.deref()?.getState()) ?? {}
|
const { mode } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||||
|
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||||
try {
|
try {
|
||||||
validateToolUse(block.name, mode ?? defaultModeSlug)
|
validateToolUse(block.name, mode ?? defaultModeSlug, customModes)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.consecutiveMistakeCount++
|
this.consecutiveMistakeCount++
|
||||||
pushToolResult(formatResponse.toolError(error.message))
|
pushToolResult(formatResponse.toolError(error.message))
|
||||||
@@ -1264,7 +1263,9 @@ export class Cline {
|
|||||||
await this.diffViewProvider.revertChanges()
|
await this.diffViewProvider.revertChanges()
|
||||||
pushToolResult(
|
pushToolResult(
|
||||||
formatResponse.toolError(
|
formatResponse.toolError(
|
||||||
`Content appears to be truncated (file has ${newContent.split("\n").length} lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`,
|
`Content appears to be truncated (file has ${
|
||||||
|
newContent.split("\n").length
|
||||||
|
} lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
@@ -1317,7 +1318,9 @@ export class Cline {
|
|||||||
pushToolResult(
|
pushToolResult(
|
||||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||||
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
||||||
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || "")}\n</final_file_content>\n\n` +
|
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(
|
||||||
|
finalContent || "",
|
||||||
|
)}\n</final_file_content>\n\n` +
|
||||||
`Please note:\n` +
|
`Please note:\n` +
|
||||||
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
|
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
|
||||||
`2. Proceed with the task using this updated file content as the new baseline.\n` +
|
`2. Proceed with the task using this updated file content as the new baseline.\n` +
|
||||||
@@ -1396,7 +1399,9 @@ export class Cline {
|
|||||||
const errorDetails = diffResult.details
|
const errorDetails = diffResult.details
|
||||||
? JSON.stringify(diffResult.details, null, 2)
|
? JSON.stringify(diffResult.details, null, 2)
|
||||||
: ""
|
: ""
|
||||||
const formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${diffResult.error}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
const formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
|
||||||
|
diffResult.error
|
||||||
|
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
||||||
if (currentCount >= 2) {
|
if (currentCount >= 2) {
|
||||||
await this.say("error", formattedError)
|
await this.say("error", formattedError)
|
||||||
}
|
}
|
||||||
@@ -1438,7 +1443,9 @@ export class Cline {
|
|||||||
pushToolResult(
|
pushToolResult(
|
||||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||||
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
||||||
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || "")}\n</final_file_content>\n\n` +
|
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(
|
||||||
|
finalContent || "",
|
||||||
|
)}\n</final_file_content>\n\n` +
|
||||||
`Please note:\n` +
|
`Please note:\n` +
|
||||||
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
|
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
|
||||||
`2. Proceed with the task using this updated file content as the new baseline.\n` +
|
`2. Proceed with the task using this updated file content as the new baseline.\n` +
|
||||||
@@ -1853,7 +1860,7 @@ export class Cline {
|
|||||||
this.consecutiveMistakeCount++
|
this.consecutiveMistakeCount++
|
||||||
await this.say(
|
await this.say(
|
||||||
"error",
|
"error",
|
||||||
`Cline tried to use ${tool_name} with an invalid JSON argument. Retrying...`,
|
`Roo tried to use ${tool_name} with an invalid JSON argument. Retrying...`,
|
||||||
)
|
)
|
||||||
pushToolResult(
|
pushToolResult(
|
||||||
formatResponse.toolError(
|
formatResponse.toolError(
|
||||||
@@ -2164,7 +2171,7 @@ export class Cline {
|
|||||||
includeFileDetails: boolean = false,
|
includeFileDetails: boolean = false,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (this.abort) {
|
if (this.abort) {
|
||||||
throw new Error("Cline instance aborted")
|
throw new Error("Roo Code instance aborted")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.consecutiveMistakeCount >= 3) {
|
if (this.consecutiveMistakeCount >= 3) {
|
||||||
@@ -2172,7 +2179,7 @@ export class Cline {
|
|||||||
"mistake_limit_reached",
|
"mistake_limit_reached",
|
||||||
this.api.getModel().id.includes("claude")
|
this.api.getModel().id.includes("claude")
|
||||||
? `This may indicate a failure in his thought process or inability to use a tool properly, which can be mitigated with some user guidance (e.g. "Try breaking down the task into smaller steps").`
|
? `This may indicate a failure in his thought process or inability to use a tool properly, which can be mitigated with some user guidance (e.g. "Try breaking down the task into smaller steps").`
|
||||||
: "Cline uses complex prompts and iterative task execution that may be challenging for less capable models. For best results, it's recommended to use Claude 3.5 Sonnet for its advanced agentic coding capabilities.",
|
: "Roo Code uses complex prompts and iterative task execution that may be challenging for less capable models. For best results, it's recommended to use Claude 3.5 Sonnet for its advanced agentic coding capabilities.",
|
||||||
)
|
)
|
||||||
if (response === "messageResponse") {
|
if (response === "messageResponse") {
|
||||||
userContent.push(
|
userContent.push(
|
||||||
@@ -2366,7 +2373,7 @@ export class Cline {
|
|||||||
|
|
||||||
// need to call here in case the stream was aborted
|
// need to call here in case the stream was aborted
|
||||||
if (this.abort) {
|
if (this.abort) {
|
||||||
throw new Error("Cline instance aborted")
|
throw new Error("Roo Code instance aborted")
|
||||||
}
|
}
|
||||||
|
|
||||||
this.didCompleteReadingStream = true
|
this.didCompleteReadingStream = true
|
||||||
@@ -2622,16 +2629,18 @@ export class Cline {
|
|||||||
details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})`
|
details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})`
|
||||||
|
|
||||||
// Add current mode and any mode-specific warnings
|
// Add current mode and any mode-specific warnings
|
||||||
const { mode } = (await this.providerRef.deref()?.getState()) ?? {}
|
const { mode, customModes } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||||
const currentMode = mode ?? defaultModeSlug
|
const currentMode = mode ?? defaultModeSlug
|
||||||
details += `\n\n# Current Mode\n${currentMode}`
|
details += `\n\n# Current Mode\n${currentMode}`
|
||||||
|
|
||||||
// Add warning if not in code mode
|
// Add warning if not in code mode
|
||||||
if (
|
if (
|
||||||
!isToolAllowedForMode("write_to_file", currentMode) ||
|
!isToolAllowedForMode("write_to_file", currentMode, customModes ?? []) &&
|
||||||
!isToolAllowedForMode("execute_command", currentMode)
|
!isToolAllowedForMode("apply_diff", currentMode, customModes ?? [])
|
||||||
) {
|
) {
|
||||||
details += `\n\nNOTE: You are currently in '${currentMode}' mode which only allows read-only operations. To write files or execute commands, the user will need to switch to '${defaultModeSlug}' mode. Note that only the user can switch modes.`
|
const currentModeName = getModeBySlug(currentMode, customModes)?.name ?? currentMode
|
||||||
|
const defaultModeName = getModeBySlug(defaultModeSlug, customModes)?.name ?? defaultModeSlug
|
||||||
|
details += `\n\nNOTE: You are currently in '${currentModeName}' mode which only allows read-only operations. To write files or execute commands, the user will need to switch to '${defaultModeName}' mode. Note that only the user can switch modes.`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeFileDetails) {
|
if (includeFileDetails) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { ApiConfiguration, ModelInfo } from "../../shared/api"
|
|||||||
import { ApiStreamChunk } from "../../api/transform/stream"
|
import { ApiStreamChunk } from "../../api/transform/stream"
|
||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import * as vscode from "vscode"
|
import * as vscode from "vscode"
|
||||||
|
import * as os from "os"
|
||||||
|
import * as path from "path"
|
||||||
|
|
||||||
// Mock all MCP-related modules
|
// Mock all MCP-related modules
|
||||||
jest.mock(
|
jest.mock(
|
||||||
@@ -209,6 +211,9 @@ describe("Cline", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Setup mock extension context
|
// Setup mock extension context
|
||||||
|
const storageUri = {
|
||||||
|
fsPath: path.join(os.tmpdir(), "test-storage"),
|
||||||
|
}
|
||||||
mockExtensionContext = {
|
mockExtensionContext = {
|
||||||
globalState: {
|
globalState: {
|
||||||
get: jest.fn().mockImplementation((key) => {
|
get: jest.fn().mockImplementation((key) => {
|
||||||
@@ -231,6 +236,7 @@ describe("Cline", () => {
|
|||||||
update: jest.fn().mockImplementation((key, value) => Promise.resolve()),
|
update: jest.fn().mockImplementation((key, value) => Promise.resolve()),
|
||||||
keys: jest.fn().mockReturnValue([]),
|
keys: jest.fn().mockReturnValue([]),
|
||||||
},
|
},
|
||||||
|
globalStorageUri: storageUri,
|
||||||
workspaceState: {
|
workspaceState: {
|
||||||
get: jest.fn().mockImplementation((key) => undefined),
|
get: jest.fn().mockImplementation((key) => undefined),
|
||||||
update: jest.fn().mockImplementation((key, value) => Promise.resolve()),
|
update: jest.fn().mockImplementation((key, value) => Promise.resolve()),
|
||||||
@@ -244,9 +250,6 @@ describe("Cline", () => {
|
|||||||
extensionUri: {
|
extensionUri: {
|
||||||
fsPath: "/mock/extension/path",
|
fsPath: "/mock/extension/path",
|
||||||
},
|
},
|
||||||
globalStorageUri: {
|
|
||||||
fsPath: "/mock/storage/path",
|
|
||||||
},
|
|
||||||
extension: {
|
extension: {
|
||||||
packageJSON: {
|
packageJSON: {
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
@@ -425,27 +428,34 @@ describe("Cline", () => {
|
|||||||
|
|
||||||
// Mock the API's createMessage method to capture the conversation history
|
// Mock the API's createMessage method to capture the conversation history
|
||||||
const createMessageSpy = jest.fn()
|
const createMessageSpy = jest.fn()
|
||||||
const mockStream = {
|
// Set up mock stream
|
||||||
async *[Symbol.asyncIterator]() {
|
const mockStreamForClean = (async function* () {
|
||||||
yield { type: "text", text: "" }
|
yield { type: "text", text: "test response" }
|
||||||
},
|
})()
|
||||||
async next() {
|
|
||||||
return { done: true, value: undefined }
|
|
||||||
},
|
|
||||||
async return() {
|
|
||||||
return { done: true, value: undefined }
|
|
||||||
},
|
|
||||||
async throw(e: any) {
|
|
||||||
throw e
|
|
||||||
},
|
|
||||||
async [Symbol.asyncDispose]() {
|
|
||||||
// Cleanup
|
|
||||||
},
|
|
||||||
} as AsyncGenerator<ApiStreamChunk>
|
|
||||||
|
|
||||||
jest.spyOn(cline.api, "createMessage").mockImplementation((...args) => {
|
// Set up spy
|
||||||
createMessageSpy(...args)
|
const cleanMessageSpy = jest.fn().mockReturnValue(mockStreamForClean)
|
||||||
return mockStream
|
jest.spyOn(cline.api, "createMessage").mockImplementation(cleanMessageSpy)
|
||||||
|
|
||||||
|
// Mock getEnvironmentDetails to return empty details
|
||||||
|
jest.spyOn(cline as any, "getEnvironmentDetails").mockResolvedValue("")
|
||||||
|
|
||||||
|
// Mock loadContext to return unmodified content
|
||||||
|
jest.spyOn(cline as any, "loadContext").mockImplementation(async (content) => [content, ""])
|
||||||
|
|
||||||
|
// Add test message to conversation history
|
||||||
|
cline.apiConversationHistory = [
|
||||||
|
{
|
||||||
|
role: "user" as const,
|
||||||
|
content: [{ type: "text" as const, text: "test message" }],
|
||||||
|
ts: Date.now(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Mock abort state
|
||||||
|
Object.defineProperty(cline, "abort", {
|
||||||
|
get: () => false,
|
||||||
|
configurable: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add a message with extra properties to the conversation history
|
// Add a message with extra properties to the conversation history
|
||||||
@@ -458,30 +468,25 @@ describe("Cline", () => {
|
|||||||
cline.apiConversationHistory = [messageWithExtra]
|
cline.apiConversationHistory = [messageWithExtra]
|
||||||
|
|
||||||
// Trigger an API request
|
// Trigger an API request
|
||||||
await cline.recursivelyMakeClineRequests([{ type: "text", text: "test request" }])
|
await cline.recursivelyMakeClineRequests([{ type: "text", text: "test request" }], false)
|
||||||
|
|
||||||
// Get all calls to createMessage
|
// Get the conversation history from the first API call
|
||||||
const calls = createMessageSpy.mock.calls
|
const history = cleanMessageSpy.mock.calls[0][1]
|
||||||
|
expect(history).toBeDefined()
|
||||||
|
expect(history.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
// Find the call that includes our test message
|
// Find our test message
|
||||||
const relevantCall = calls.find((call) =>
|
const cleanedMessage = history.find((msg: { content?: Array<{ text: string }> }) =>
|
||||||
call[1]?.some((msg: any) => msg.content?.[0]?.text === "test message"),
|
msg.content?.some((content) => content.text === "test message"),
|
||||||
)
|
|
||||||
|
|
||||||
// Verify the conversation history was cleaned in the relevant call
|
|
||||||
expect(relevantCall?.[1]).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: [{ type: "text", text: "test message" }],
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
)
|
)
|
||||||
|
expect(cleanedMessage).toBeDefined()
|
||||||
|
expect(cleanedMessage).toEqual({
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "test message" }],
|
||||||
|
})
|
||||||
|
|
||||||
// Verify extra properties were removed
|
// Verify extra properties were removed
|
||||||
const passedMessage = relevantCall?.[1].find((msg: any) => msg.content?.[0]?.text === "test message")
|
expect(Object.keys(cleanedMessage!)).toEqual(["role", "content"])
|
||||||
expect(passedMessage).not.toHaveProperty("ts")
|
|
||||||
expect(passedMessage).not.toHaveProperty("extraProp")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle image blocks based on model capabilities", async () => {
|
it("should handle image blocks based on model capabilities", async () => {
|
||||||
@@ -573,41 +578,68 @@ describe("Cline", () => {
|
|||||||
})
|
})
|
||||||
clineWithoutImages.apiConversationHistory = conversationHistory
|
clineWithoutImages.apiConversationHistory = conversationHistory
|
||||||
|
|
||||||
// Create message spy for both instances
|
// Mock abort state for both instances
|
||||||
const createMessageSpyWithImages = jest.fn()
|
Object.defineProperty(clineWithImages, "abort", {
|
||||||
const createMessageSpyWithoutImages = jest.fn()
|
get: () => false,
|
||||||
const mockStream = {
|
configurable: true,
|
||||||
async *[Symbol.asyncIterator]() {
|
})
|
||||||
yield { type: "text", text: "" }
|
Object.defineProperty(clineWithoutImages, "abort", {
|
||||||
|
get: () => false,
|
||||||
|
configurable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock environment details and context loading
|
||||||
|
jest.spyOn(clineWithImages as any, "getEnvironmentDetails").mockResolvedValue("")
|
||||||
|
jest.spyOn(clineWithoutImages as any, "getEnvironmentDetails").mockResolvedValue("")
|
||||||
|
jest.spyOn(clineWithImages as any, "loadContext").mockImplementation(async (content) => [content, ""])
|
||||||
|
jest.spyOn(clineWithoutImages as any, "loadContext").mockImplementation(async (content) => [
|
||||||
|
content,
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
// Set up mock streams
|
||||||
|
const mockStreamWithImages = (async function* () {
|
||||||
|
yield { type: "text", text: "test response" }
|
||||||
|
})()
|
||||||
|
|
||||||
|
const mockStreamWithoutImages = (async function* () {
|
||||||
|
yield { type: "text", text: "test response" }
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Set up spies
|
||||||
|
const imagesSpy = jest.fn().mockReturnValue(mockStreamWithImages)
|
||||||
|
const noImagesSpy = jest.fn().mockReturnValue(mockStreamWithoutImages)
|
||||||
|
|
||||||
|
jest.spyOn(clineWithImages.api, "createMessage").mockImplementation(imagesSpy)
|
||||||
|
jest.spyOn(clineWithoutImages.api, "createMessage").mockImplementation(noImagesSpy)
|
||||||
|
|
||||||
|
// Set up conversation history with images
|
||||||
|
clineWithImages.apiConversationHistory = [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "Here is an image" },
|
||||||
|
{ type: "image", source: { type: "base64", media_type: "image/jpeg", data: "base64data" } },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
} as AsyncGenerator<ApiStreamChunk>
|
]
|
||||||
|
|
||||||
jest.spyOn(clineWithImages.api, "createMessage").mockImplementation((...args) => {
|
// Trigger API requests
|
||||||
createMessageSpyWithImages(...args)
|
await clineWithImages.recursivelyMakeClineRequests([{ type: "text", text: "test request" }])
|
||||||
return mockStream
|
await clineWithoutImages.recursivelyMakeClineRequests([{ type: "text", text: "test request" }])
|
||||||
})
|
|
||||||
jest.spyOn(clineWithoutImages.api, "createMessage").mockImplementation((...args) => {
|
|
||||||
createMessageSpyWithoutImages(...args)
|
|
||||||
return mockStream
|
|
||||||
})
|
|
||||||
|
|
||||||
// Trigger API requests for both instances
|
// Get the calls
|
||||||
await clineWithImages.recursivelyMakeClineRequests([{ type: "text", text: "test" }])
|
const imagesCalls = imagesSpy.mock.calls
|
||||||
await clineWithoutImages.recursivelyMakeClineRequests([{ type: "text", text: "test" }])
|
const noImagesCalls = noImagesSpy.mock.calls
|
||||||
|
|
||||||
// Verify model with image support preserves image blocks
|
// Verify model with image support preserves image blocks
|
||||||
const callsWithImages = createMessageSpyWithImages.mock.calls
|
expect(imagesCalls[0][1][0].content).toHaveLength(2)
|
||||||
const historyWithImages = callsWithImages[0][1][0]
|
expect(imagesCalls[0][1][0].content[0]).toEqual({ type: "text", text: "Here is an image" })
|
||||||
expect(historyWithImages.content).toHaveLength(2)
|
expect(imagesCalls[0][1][0].content[1]).toHaveProperty("type", "image")
|
||||||
expect(historyWithImages.content[0]).toEqual({ type: "text", text: "Here is an image" })
|
|
||||||
expect(historyWithImages.content[1]).toHaveProperty("type", "image")
|
|
||||||
|
|
||||||
// Verify model without image support converts image blocks to text
|
// Verify model without image support converts image blocks to text
|
||||||
const callsWithoutImages = createMessageSpyWithoutImages.mock.calls
|
expect(noImagesCalls[0][1][0].content).toHaveLength(2)
|
||||||
const historyWithoutImages = callsWithoutImages[0][1][0]
|
expect(noImagesCalls[0][1][0].content[0]).toEqual({ type: "text", text: "Here is an image" })
|
||||||
expect(historyWithoutImages.content).toHaveLength(2)
|
expect(noImagesCalls[0][1][0].content[1]).toEqual({
|
||||||
expect(historyWithoutImages.content[0]).toEqual({ type: "text", text: "Here is an image" })
|
|
||||||
expect(historyWithoutImages.content[1]).toEqual({
|
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "[Referenced image in conversation]",
|
text: "[Referenced image in conversation]",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Mode, isToolAllowedForMode, TestToolName, getModeConfig, modes } from "../../shared/modes"
|
import { Mode, isToolAllowedForMode, getModeConfig, modes } from "../../shared/modes"
|
||||||
import { validateToolUse } from "../mode-validator"
|
import { validateToolUse } from "../mode-validator"
|
||||||
|
import { TOOL_GROUPS } from "../../shared/tool-groups"
|
||||||
const asTestTool = (tool: string): TestToolName => tool as TestToolName
|
|
||||||
const [codeMode, architectMode, askMode] = modes.map((mode) => mode.slug)
|
const [codeMode, architectMode, askMode] = modes.map((mode) => mode.slug)
|
||||||
|
|
||||||
describe("mode-validator", () => {
|
describe("mode-validator", () => {
|
||||||
@@ -9,21 +8,26 @@ describe("mode-validator", () => {
|
|||||||
describe("code mode", () => {
|
describe("code mode", () => {
|
||||||
it("allows all code mode tools", () => {
|
it("allows all code mode tools", () => {
|
||||||
const mode = getModeConfig(codeMode)
|
const mode = getModeConfig(codeMode)
|
||||||
mode.tools.forEach(([tool]) => {
|
// Code mode has all groups
|
||||||
expect(isToolAllowedForMode(tool, codeMode)).toBe(true)
|
Object.entries(TOOL_GROUPS).forEach(([_, tools]) => {
|
||||||
|
tools.forEach((tool) => {
|
||||||
|
expect(isToolAllowedForMode(tool, codeMode, [])).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("disallows unknown tools", () => {
|
it("disallows unknown tools", () => {
|
||||||
expect(isToolAllowedForMode(asTestTool("unknown_tool"), codeMode)).toBe(false)
|
expect(isToolAllowedForMode("unknown_tool" as any, codeMode, [])).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("architect mode", () => {
|
describe("architect mode", () => {
|
||||||
it("allows configured tools", () => {
|
it("allows configured tools", () => {
|
||||||
const mode = getModeConfig(architectMode)
|
const mode = getModeConfig(architectMode)
|
||||||
mode.tools.forEach(([tool]) => {
|
// Architect mode has read, browser, and mcp groups
|
||||||
expect(isToolAllowedForMode(tool, architectMode)).toBe(true)
|
const architectTools = [...TOOL_GROUPS.read, ...TOOL_GROUPS.browser, ...TOOL_GROUPS.mcp]
|
||||||
|
architectTools.forEach((tool) => {
|
||||||
|
expect(isToolAllowedForMode(tool, architectMode, [])).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -31,22 +35,57 @@ describe("mode-validator", () => {
|
|||||||
describe("ask mode", () => {
|
describe("ask mode", () => {
|
||||||
it("allows configured tools", () => {
|
it("allows configured tools", () => {
|
||||||
const mode = getModeConfig(askMode)
|
const mode = getModeConfig(askMode)
|
||||||
mode.tools.forEach(([tool]) => {
|
// Ask mode has read, browser, and mcp groups
|
||||||
expect(isToolAllowedForMode(tool, askMode)).toBe(true)
|
const askTools = [...TOOL_GROUPS.read, ...TOOL_GROUPS.browser, ...TOOL_GROUPS.mcp]
|
||||||
|
askTools.forEach((tool) => {
|
||||||
|
expect(isToolAllowedForMode(tool, askMode, [])).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("custom modes", () => {
|
||||||
|
it("allows tools from custom mode configuration", () => {
|
||||||
|
const customModes = [
|
||||||
|
{
|
||||||
|
slug: "custom-mode",
|
||||||
|
name: "Custom Mode",
|
||||||
|
roleDefinition: "Custom role",
|
||||||
|
groups: ["read", "edit"] as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
// Should allow tools from read and edit groups
|
||||||
|
expect(isToolAllowedForMode("read_file", "custom-mode", customModes)).toBe(true)
|
||||||
|
expect(isToolAllowedForMode("write_to_file", "custom-mode", customModes)).toBe(true)
|
||||||
|
// Should not allow tools from other groups
|
||||||
|
expect(isToolAllowedForMode("execute_command", "custom-mode", customModes)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows custom mode to override built-in mode", () => {
|
||||||
|
const customModes = [
|
||||||
|
{
|
||||||
|
slug: codeMode,
|
||||||
|
name: "Custom Code Mode",
|
||||||
|
roleDefinition: "Custom role",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
// Should allow tools from read group
|
||||||
|
expect(isToolAllowedForMode("read_file", codeMode, customModes)).toBe(true)
|
||||||
|
// Should not allow tools from other groups
|
||||||
|
expect(isToolAllowedForMode("write_to_file", codeMode, customModes)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("validateToolUse", () => {
|
describe("validateToolUse", () => {
|
||||||
it("throws error for disallowed tools in architect mode", () => {
|
it("throws error for disallowed tools in architect mode", () => {
|
||||||
expect(() => validateToolUse("unknown_tool", "architect")).toThrow(
|
expect(() => validateToolUse("unknown_tool" as any, "architect", [])).toThrow(
|
||||||
'Tool "unknown_tool" is not allowed in architect mode.',
|
'Tool "unknown_tool" is not allowed in architect mode.',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("does not throw for allowed tools in architect mode", () => {
|
it("does not throw for allowed tools in architect mode", () => {
|
||||||
expect(() => validateToolUse("read_file", "architect")).not.toThrow()
|
expect(() => validateToolUse("read_file", "architect", [])).not.toThrow()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ExtensionContext } from "vscode"
|
import { ExtensionContext } from "vscode"
|
||||||
import { ApiConfiguration } from "../../shared/api"
|
import { ApiConfiguration } from "../../shared/api"
|
||||||
import { Mode } from "../prompts/types"
|
import { Mode } from "../../shared/modes"
|
||||||
import { ApiConfigMeta } from "../../shared/ExtensionMessage"
|
import { ApiConfigMeta } from "../../shared/ExtensionMessage"
|
||||||
|
|
||||||
export interface ApiConfigData {
|
export interface ApiConfigData {
|
||||||
|
|||||||
190
src/core/config/CustomModesManager.ts
Normal file
190
src/core/config/CustomModesManager.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import * as vscode from "vscode"
|
||||||
|
import * as path from "path"
|
||||||
|
import * as fs from "fs/promises"
|
||||||
|
import { CustomModesSettingsSchema } from "./CustomModesSchema"
|
||||||
|
import { ModeConfig } from "../../shared/modes"
|
||||||
|
import { fileExistsAtPath } from "../../utils/fs"
|
||||||
|
import { arePathsEqual } from "../../utils/path"
|
||||||
|
|
||||||
|
export class CustomModesManager {
|
||||||
|
private disposables: vscode.Disposable[] = []
|
||||||
|
private isWriting = false
|
||||||
|
private writeQueue: Array<() => Promise<void>> = []
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly context: vscode.ExtensionContext,
|
||||||
|
private readonly onUpdate: () => Promise<void>,
|
||||||
|
) {
|
||||||
|
this.watchCustomModesFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async queueWrite(operation: () => Promise<void>): Promise<void> {
|
||||||
|
this.writeQueue.push(operation)
|
||||||
|
if (!this.isWriting) {
|
||||||
|
await this.processWriteQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processWriteQueue(): Promise<void> {
|
||||||
|
if (this.isWriting || this.writeQueue.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isWriting = true
|
||||||
|
try {
|
||||||
|
while (this.writeQueue.length > 0) {
|
||||||
|
const operation = this.writeQueue.shift()
|
||||||
|
if (operation) {
|
||||||
|
await operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isWriting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomModesFilePath(): Promise<string> {
|
||||||
|
const settingsDir = await this.ensureSettingsDirectoryExists()
|
||||||
|
const filePath = path.join(settingsDir, "cline_custom_modes.json")
|
||||||
|
const fileExists = await fileExistsAtPath(filePath)
|
||||||
|
if (!fileExists) {
|
||||||
|
await this.queueWrite(async () => {
|
||||||
|
await fs.writeFile(filePath, JSON.stringify({ customModes: [] }, null, 2))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
private async watchCustomModesFile(): Promise<void> {
|
||||||
|
const settingsPath = await this.getCustomModesFilePath()
|
||||||
|
this.disposables.push(
|
||||||
|
vscode.workspace.onDidSaveTextDocument(async (document) => {
|
||||||
|
if (arePathsEqual(document.uri.fsPath, settingsPath)) {
|
||||||
|
const content = await fs.readFile(settingsPath, "utf-8")
|
||||||
|
const errorMessage =
|
||||||
|
"Invalid custom modes format. Please ensure your settings follow the correct JSON format."
|
||||||
|
let config: any
|
||||||
|
try {
|
||||||
|
config = JSON.parse(content)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
vscode.window.showErrorMessage(errorMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = CustomModesSettingsSchema.safeParse(config)
|
||||||
|
if (!result.success) {
|
||||||
|
vscode.window.showErrorMessage(errorMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await this.context.globalState.update("customModes", result.data.customModes)
|
||||||
|
await this.onUpdate()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomModes(): Promise<ModeConfig[]> {
|
||||||
|
const modes = await this.context.globalState.get<ModeConfig[]>("customModes")
|
||||||
|
|
||||||
|
// Always read from file to ensure we have the latest
|
||||||
|
try {
|
||||||
|
const settingsPath = await this.getCustomModesFilePath()
|
||||||
|
const content = await fs.readFile(settingsPath, "utf-8")
|
||||||
|
|
||||||
|
const settings = JSON.parse(content)
|
||||||
|
const result = CustomModesSettingsSchema.safeParse(settings)
|
||||||
|
if (result.success) {
|
||||||
|
await this.context.globalState.update("customModes", result.data.customModes)
|
||||||
|
return result.data.customModes
|
||||||
|
}
|
||||||
|
return modes ?? []
|
||||||
|
} catch (error) {
|
||||||
|
// Return empty array if there's an error reading the file
|
||||||
|
}
|
||||||
|
|
||||||
|
return modes ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCustomMode(slug: string, config: ModeConfig): Promise<void> {
|
||||||
|
try {
|
||||||
|
const settingsPath = await this.getCustomModesFilePath()
|
||||||
|
|
||||||
|
await this.queueWrite(async () => {
|
||||||
|
// Read and update file
|
||||||
|
const content = await fs.readFile(settingsPath, "utf-8")
|
||||||
|
const settings = JSON.parse(content)
|
||||||
|
const currentModes = settings.customModes || []
|
||||||
|
const updatedModes = currentModes.filter((m: ModeConfig) => m.slug !== slug)
|
||||||
|
updatedModes.push(config)
|
||||||
|
settings.customModes = updatedModes
|
||||||
|
|
||||||
|
const newContent = JSON.stringify(settings, null, 2)
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
await fs.writeFile(settingsPath, newContent)
|
||||||
|
|
||||||
|
// Update global state
|
||||||
|
await this.context.globalState.update("customModes", updatedModes)
|
||||||
|
|
||||||
|
// Notify about the update
|
||||||
|
await this.onUpdate()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Success, no need for message
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window.showErrorMessage(
|
||||||
|
`Failed to update custom mode: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCustomMode(slug: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const settingsPath = await this.getCustomModesFilePath()
|
||||||
|
|
||||||
|
await this.queueWrite(async () => {
|
||||||
|
const content = await fs.readFile(settingsPath, "utf-8")
|
||||||
|
const settings = JSON.parse(content)
|
||||||
|
|
||||||
|
settings.customModes = (settings.customModes || []).filter((m: ModeConfig) => m.slug !== slug)
|
||||||
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2))
|
||||||
|
|
||||||
|
await this.context.globalState.update("customModes", settings.customModes)
|
||||||
|
await this.onUpdate()
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window.showErrorMessage(
|
||||||
|
`Failed to delete custom mode: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureSettingsDirectoryExists(): Promise<string> {
|
||||||
|
const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
|
||||||
|
await fs.mkdir(settingsDir, { recursive: true })
|
||||||
|
return settingsDir
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the custom modes file and reset to default state
|
||||||
|
*/
|
||||||
|
async resetCustomModes(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const filePath = await this.getCustomModesFilePath()
|
||||||
|
await fs.writeFile(filePath, JSON.stringify({ customModes: [] }, null, 2))
|
||||||
|
await this.context.globalState.update("customModes", [])
|
||||||
|
await this.onUpdate()
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window.showErrorMessage(
|
||||||
|
`Failed to reset custom modes: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
for (const disposable of this.disposables) {
|
||||||
|
disposable.dispose()
|
||||||
|
}
|
||||||
|
this.disposables = []
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/core/config/CustomModesSchema.ts
Normal file
60
src/core/config/CustomModesSchema.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { ModeConfig } from "../../shared/modes"
|
||||||
|
import { TOOL_GROUPS, ToolGroup } from "../../shared/tool-groups"
|
||||||
|
|
||||||
|
// Create a schema for valid tool groups using the keys of TOOL_GROUPS
|
||||||
|
const ToolGroupSchema = z.enum(Object.keys(TOOL_GROUPS) as [ToolGroup, ...ToolGroup[]])
|
||||||
|
|
||||||
|
// Schema for array of groups
|
||||||
|
const GroupsArraySchema = z
|
||||||
|
.array(ToolGroupSchema)
|
||||||
|
.min(1, "At least one tool group is required")
|
||||||
|
.refine(
|
||||||
|
(groups) => {
|
||||||
|
const seen = new Set()
|
||||||
|
return groups.every((group) => {
|
||||||
|
if (seen.has(group)) return false
|
||||||
|
seen.add(group)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ message: "Duplicate groups are not allowed" },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Schema for mode configuration
|
||||||
|
export const CustomModeSchema = z.object({
|
||||||
|
slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
roleDefinition: z.string().min(1, "Role definition is required"),
|
||||||
|
customInstructions: z.string().optional(),
|
||||||
|
groups: GroupsArraySchema,
|
||||||
|
}) satisfies z.ZodType<ModeConfig>
|
||||||
|
|
||||||
|
// Schema for the entire custom modes settings file
|
||||||
|
export const CustomModesSettingsSchema = z.object({
|
||||||
|
customModes: z.array(CustomModeSchema).refine(
|
||||||
|
(modes) => {
|
||||||
|
const slugs = new Set()
|
||||||
|
return modes.every((mode) => {
|
||||||
|
if (slugs.has(mode.slug)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
slugs.add(mode.slug)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Duplicate mode slugs are not allowed",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type CustomModesSettings = z.infer<typeof CustomModesSettingsSchema>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a custom mode configuration against the schema
|
||||||
|
* @throws {z.ZodError} if validation fails
|
||||||
|
*/
|
||||||
|
export function validateCustomMode(mode: unknown): asserts mode is ModeConfig {
|
||||||
|
CustomModeSchema.parse(mode)
|
||||||
|
}
|
||||||
245
src/core/config/__tests__/CustomModesManager.test.ts
Normal file
245
src/core/config/__tests__/CustomModesManager.test.ts
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import { ModeConfig } from "../../../shared/modes"
|
||||||
|
import { CustomModesManager } from "../CustomModesManager"
|
||||||
|
import * as vscode from "vscode"
|
||||||
|
import * as fs from "fs/promises"
|
||||||
|
import * as path from "path"
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock("vscode")
|
||||||
|
jest.mock("fs/promises")
|
||||||
|
jest.mock("../../../utils/fs", () => ({
|
||||||
|
fileExistsAtPath: jest.fn().mockResolvedValue(false),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("CustomModesManager", () => {
|
||||||
|
let manager: CustomModesManager
|
||||||
|
let mockContext: vscode.ExtensionContext
|
||||||
|
let mockOnUpdate: jest.Mock
|
||||||
|
let mockStoragePath: string
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset mocks
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
// Mock storage path
|
||||||
|
mockStoragePath = "/test/storage/path"
|
||||||
|
|
||||||
|
// Mock context
|
||||||
|
mockContext = {
|
||||||
|
globalStorageUri: { fsPath: mockStoragePath },
|
||||||
|
globalState: {
|
||||||
|
get: jest.fn().mockResolvedValue([]),
|
||||||
|
update: jest.fn().mockResolvedValue(undefined),
|
||||||
|
},
|
||||||
|
} as unknown as vscode.ExtensionContext
|
||||||
|
|
||||||
|
// Mock onUpdate callback
|
||||||
|
mockOnUpdate = jest.fn().mockResolvedValue(undefined)
|
||||||
|
|
||||||
|
// Mock fs.mkdir to do nothing
|
||||||
|
;(fs.mkdir as jest.Mock).mockResolvedValue(undefined)
|
||||||
|
|
||||||
|
// Create manager instance
|
||||||
|
manager = new CustomModesManager(mockContext, mockOnUpdate)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Mode Configuration Validation", () => {
|
||||||
|
test("validates valid custom mode configuration", async () => {
|
||||||
|
const validMode = {
|
||||||
|
slug: "test-mode",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
// Mock file read/write operations
|
||||||
|
;(fs.readFile as jest.Mock).mockResolvedValue(JSON.stringify({ customModes: [] }))
|
||||||
|
;(fs.writeFile as jest.Mock).mockResolvedValue(undefined)
|
||||||
|
|
||||||
|
await manager.updateCustomMode(validMode.slug, validMode)
|
||||||
|
|
||||||
|
// Verify file was written with the new mode
|
||||||
|
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("cline_custom_modes.json"),
|
||||||
|
expect.stringContaining(validMode.name),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify global state was updated
|
||||||
|
expect(mockContext.globalState.update).toHaveBeenCalledWith(
|
||||||
|
"customModes",
|
||||||
|
expect.arrayContaining([validMode]),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify onUpdate was called
|
||||||
|
expect(mockOnUpdate).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("handles file read errors gracefully", async () => {
|
||||||
|
// Mock fs.readFile to throw error
|
||||||
|
;(fs.readFile as jest.Mock).mockRejectedValueOnce(new Error("Test error"))
|
||||||
|
|
||||||
|
const modes = await manager.getCustomModes()
|
||||||
|
|
||||||
|
// Should return empty array on error
|
||||||
|
expect(modes).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("handles file write errors gracefully", async () => {
|
||||||
|
const validMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
// Mock fs.writeFile to throw error
|
||||||
|
;(fs.writeFile as jest.Mock).mockRejectedValueOnce(new Error("Write error"))
|
||||||
|
|
||||||
|
const mockShowError = jest.fn()
|
||||||
|
;(vscode.window.showErrorMessage as jest.Mock) = mockShowError
|
||||||
|
|
||||||
|
await manager.updateCustomMode(validMode.slug, validMode)
|
||||||
|
|
||||||
|
// Should show error message
|
||||||
|
expect(mockShowError).toHaveBeenCalledWith(expect.stringContaining("Write error"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("File Operations", () => {
|
||||||
|
test("creates settings directory if it doesn't exist", async () => {
|
||||||
|
const configPath = path.join(mockStoragePath, "settings", "cline_custom_modes.json")
|
||||||
|
await manager.getCustomModesFilePath()
|
||||||
|
|
||||||
|
expect(fs.mkdir).toHaveBeenCalledWith(path.dirname(configPath), { recursive: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("creates default config if file doesn't exist", async () => {
|
||||||
|
const configPath = path.join(mockStoragePath, "settings", "cline_custom_modes.json")
|
||||||
|
await manager.getCustomModesFilePath()
|
||||||
|
|
||||||
|
expect(fs.writeFile).toHaveBeenCalledWith(configPath, JSON.stringify({ customModes: [] }, null, 2))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("watches file for changes", async () => {
|
||||||
|
// Mock file path resolution
|
||||||
|
const configPath = path.join(mockStoragePath, "settings", "cline_custom_modes.json")
|
||||||
|
;(fs.readFile as jest.Mock).mockResolvedValue(JSON.stringify({ customModes: [] }))
|
||||||
|
|
||||||
|
// Create manager and wait for initialization
|
||||||
|
const manager = new CustomModesManager(mockContext, mockOnUpdate)
|
||||||
|
await manager.getCustomModesFilePath() // This ensures watchCustomModesFile has completed
|
||||||
|
|
||||||
|
// Get the registered callback
|
||||||
|
const registerCall = (vscode.workspace.onDidSaveTextDocument as jest.Mock).mock.calls[0]
|
||||||
|
expect(registerCall).toBeDefined()
|
||||||
|
const [callback] = registerCall
|
||||||
|
|
||||||
|
// Simulate file save event
|
||||||
|
const mockDocument = {
|
||||||
|
uri: { fsPath: configPath },
|
||||||
|
}
|
||||||
|
await callback(mockDocument)
|
||||||
|
|
||||||
|
// Verify file was processed
|
||||||
|
expect(fs.readFile).toHaveBeenCalledWith(configPath, "utf-8")
|
||||||
|
expect(mockContext.globalState.update).toHaveBeenCalled()
|
||||||
|
expect(mockOnUpdate).toHaveBeenCalled()
|
||||||
|
|
||||||
|
// Verify file content was processed
|
||||||
|
expect(fs.readFile).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Mode Operations", () => {
|
||||||
|
const validMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock fs.readFile to return empty config
|
||||||
|
;(fs.readFile as jest.Mock).mockResolvedValue(JSON.stringify({ customModes: [] }))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("adds new custom mode", async () => {
|
||||||
|
await manager.updateCustomMode(validMode.slug, validMode)
|
||||||
|
|
||||||
|
expect(fs.writeFile).toHaveBeenCalledWith(expect.any(String), expect.stringContaining(validMode.name))
|
||||||
|
expect(mockOnUpdate).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("updates existing custom mode", async () => {
|
||||||
|
// Mock existing mode
|
||||||
|
;(fs.readFile as jest.Mock).mockResolvedValue(
|
||||||
|
JSON.stringify({
|
||||||
|
customModes: [validMode],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedMode = {
|
||||||
|
...validMode,
|
||||||
|
name: "Updated Name",
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.updateCustomMode(validMode.slug, updatedMode)
|
||||||
|
|
||||||
|
expect(fs.writeFile).toHaveBeenCalledWith(expect.any(String), expect.stringContaining("Updated Name"))
|
||||||
|
expect(mockOnUpdate).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("deletes custom mode", async () => {
|
||||||
|
// Mock existing mode
|
||||||
|
;(fs.readFile as jest.Mock).mockResolvedValue(
|
||||||
|
JSON.stringify({
|
||||||
|
customModes: [validMode],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await manager.deleteCustomMode(validMode.slug)
|
||||||
|
|
||||||
|
expect(fs.writeFile).toHaveBeenCalledWith(expect.any(String), expect.not.stringContaining(validMode.name))
|
||||||
|
expect(mockOnUpdate).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("queues write operations", async () => {
|
||||||
|
const mode1 = {
|
||||||
|
...validMode,
|
||||||
|
name: "Mode 1",
|
||||||
|
}
|
||||||
|
const mode2 = {
|
||||||
|
...validMode,
|
||||||
|
slug: "mode-2",
|
||||||
|
name: "Mode 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock initial empty state and track writes
|
||||||
|
let currentModes: ModeConfig[] = []
|
||||||
|
;(fs.readFile as jest.Mock).mockImplementation(() => JSON.stringify({ customModes: currentModes }))
|
||||||
|
;(fs.writeFile as jest.Mock).mockImplementation(async (path, content) => {
|
||||||
|
const data = JSON.parse(content)
|
||||||
|
currentModes = data.customModes
|
||||||
|
return Promise.resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start both updates simultaneously
|
||||||
|
await Promise.all([
|
||||||
|
manager.updateCustomMode(mode1.slug, mode1),
|
||||||
|
manager.updateCustomMode(mode2.slug, mode2),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Verify final state
|
||||||
|
expect(currentModes).toHaveLength(2)
|
||||||
|
expect(currentModes.map((m) => m.name)).toContain("Mode 1")
|
||||||
|
expect(currentModes.map((m) => m.name)).toContain("Mode 2")
|
||||||
|
|
||||||
|
// Verify write was called with both modes
|
||||||
|
const lastWriteCall = (fs.writeFile as jest.Mock).mock.calls.pop()
|
||||||
|
const finalContent = JSON.parse(lastWriteCall[1])
|
||||||
|
expect(finalContent.customModes).toHaveLength(2)
|
||||||
|
expect(finalContent.customModes.map((m: ModeConfig) => m.name)).toContain("Mode 1")
|
||||||
|
expect(finalContent.customModes.map((m: ModeConfig) => m.name)).toContain("Mode 2")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
122
src/core/config/__tests__/CustomModesSchema.test.ts
Normal file
122
src/core/config/__tests__/CustomModesSchema.test.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { validateCustomMode } from "../CustomModesSchema"
|
||||||
|
import { ModeConfig } from "../../../shared/modes"
|
||||||
|
import { ZodError } from "zod"
|
||||||
|
|
||||||
|
describe("CustomModesSchema", () => {
|
||||||
|
describe("validateCustomMode", () => {
|
||||||
|
test("accepts valid mode configuration", () => {
|
||||||
|
const validMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
expect(() => validateCustomMode(validMode)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("accepts mode with multiple groups", () => {
|
||||||
|
const validMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: ["read", "edit", "browser"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
expect(() => validateCustomMode(validMode)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("accepts mode with optional customInstructions", () => {
|
||||||
|
const validMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
customInstructions: "Custom instructions",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
expect(() => validateCustomMode(validMode)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects missing required fields", () => {
|
||||||
|
const invalidModes = [
|
||||||
|
{}, // All fields missing
|
||||||
|
{ name: "Test" }, // Missing most fields
|
||||||
|
{
|
||||||
|
name: "Test",
|
||||||
|
roleDefinition: "Role",
|
||||||
|
}, // Missing slug and groups
|
||||||
|
]
|
||||||
|
|
||||||
|
invalidModes.forEach((invalidMode) => {
|
||||||
|
expect(() => validateCustomMode(invalidMode)).toThrow(ZodError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects invalid slug format", () => {
|
||||||
|
const invalidMode = {
|
||||||
|
slug: "not@a@valid@slug",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies Omit<ModeConfig, "slug"> & { slug: string }
|
||||||
|
|
||||||
|
expect(() => validateCustomMode(invalidMode)).toThrow(ZodError)
|
||||||
|
expect(() => validateCustomMode(invalidMode)).toThrow("Slug must contain only letters numbers and dashes")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects empty strings in required fields", () => {
|
||||||
|
const emptyNameMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
const emptyRoleMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
expect(() => validateCustomMode(emptyNameMode)).toThrow("Name is required")
|
||||||
|
expect(() => validateCustomMode(emptyRoleMode)).toThrow("Role definition is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects invalid group configurations", () => {
|
||||||
|
const invalidGroupMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: ["not-a-valid-group"] as any,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => validateCustomMode(invalidGroupMode)).toThrow(ZodError)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects empty groups array", () => {
|
||||||
|
const invalidMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: [] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
expect(() => validateCustomMode(invalidMode)).toThrow("At least one tool group is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("handles null and undefined gracefully", () => {
|
||||||
|
expect(() => validateCustomMode(null)).toThrow(ZodError)
|
||||||
|
expect(() => validateCustomMode(undefined)).toThrow(ZodError)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects non-object inputs", () => {
|
||||||
|
const invalidInputs = [42, "string", true, [], () => {}]
|
||||||
|
|
||||||
|
invalidInputs.forEach((input) => {
|
||||||
|
expect(() => validateCustomMode(input)).toThrow(ZodError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
169
src/core/config/__tests__/CustomModesSettings.test.ts
Normal file
169
src/core/config/__tests__/CustomModesSettings.test.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { CustomModesSettingsSchema } from "../CustomModesSchema"
|
||||||
|
import { ModeConfig } from "../../../shared/modes"
|
||||||
|
import { ZodError } from "zod"
|
||||||
|
|
||||||
|
describe("CustomModesSettings", () => {
|
||||||
|
const validMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
describe("schema validation", () => {
|
||||||
|
test("accepts valid settings", () => {
|
||||||
|
const validSettings = {
|
||||||
|
customModes: [validMode],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
CustomModesSettingsSchema.parse(validSettings)
|
||||||
|
}).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("accepts empty custom modes array", () => {
|
||||||
|
const validSettings = {
|
||||||
|
customModes: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
CustomModesSettingsSchema.parse(validSettings)
|
||||||
|
}).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("accepts multiple custom modes", () => {
|
||||||
|
const validSettings = {
|
||||||
|
customModes: [
|
||||||
|
validMode,
|
||||||
|
{
|
||||||
|
...validMode,
|
||||||
|
slug: "987fcdeb-51a2-43e7-89ab-cdef01234567",
|
||||||
|
name: "Another Mode",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
CustomModesSettingsSchema.parse(validSettings)
|
||||||
|
}).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects missing customModes field", () => {
|
||||||
|
const invalidSettings = {} as any
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
CustomModesSettingsSchema.parse(invalidSettings)
|
||||||
|
}).toThrow(ZodError)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects invalid mode in array", () => {
|
||||||
|
const invalidSettings = {
|
||||||
|
customModes: [
|
||||||
|
validMode,
|
||||||
|
{
|
||||||
|
...validMode,
|
||||||
|
slug: "not@a@valid@slug", // Invalid slug
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
CustomModesSettingsSchema.parse(invalidSettings)
|
||||||
|
}).toThrow(ZodError)
|
||||||
|
expect(() => {
|
||||||
|
CustomModesSettingsSchema.parse(invalidSettings)
|
||||||
|
}).toThrow("Slug must contain only letters numbers and dashes")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects non-array customModes", () => {
|
||||||
|
const invalidSettings = {
|
||||||
|
customModes: "not an array",
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
CustomModesSettingsSchema.parse(invalidSettings)
|
||||||
|
}).toThrow(ZodError)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects null or undefined", () => {
|
||||||
|
expect(() => {
|
||||||
|
CustomModesSettingsSchema.parse(null)
|
||||||
|
}).toThrow(ZodError)
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
CustomModesSettingsSchema.parse(undefined)
|
||||||
|
}).toThrow(ZodError)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects duplicate mode slugs", () => {
|
||||||
|
const duplicateSettings = {
|
||||||
|
customModes: [
|
||||||
|
validMode,
|
||||||
|
{ ...validMode }, // Same slug
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
CustomModesSettingsSchema.parse(duplicateSettings)
|
||||||
|
}).toThrow("Duplicate mode slugs are not allowed")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects invalid group configurations in modes", () => {
|
||||||
|
const invalidSettings = {
|
||||||
|
customModes: [
|
||||||
|
{
|
||||||
|
...validMode,
|
||||||
|
groups: ["invalid_group"] as any,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
CustomModesSettingsSchema.parse(invalidSettings)
|
||||||
|
}).toThrow(ZodError)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("handles multiple groups", () => {
|
||||||
|
const validSettings = {
|
||||||
|
customModes: [
|
||||||
|
{
|
||||||
|
...validMode,
|
||||||
|
groups: ["read", "edit", "browser"] as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
CustomModesSettingsSchema.parse(validSettings)
|
||||||
|
}).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("type inference", () => {
|
||||||
|
test("inferred type includes all required fields", () => {
|
||||||
|
const settings = {
|
||||||
|
customModes: [validMode],
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeScript compilation will fail if the type is incorrect
|
||||||
|
expect(settings.customModes[0].slug).toBeDefined()
|
||||||
|
expect(settings.customModes[0].name).toBeDefined()
|
||||||
|
expect(settings.customModes[0].roleDefinition).toBeDefined()
|
||||||
|
expect(settings.customModes[0].groups).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("inferred type allows optional fields", () => {
|
||||||
|
const settings = {
|
||||||
|
customModes: [
|
||||||
|
{
|
||||||
|
...validMode,
|
||||||
|
customInstructions: "Optional instructions",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeScript compilation will fail if the type is incorrect
|
||||||
|
expect(settings.customModes[0].customInstructions).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
90
src/core/config/__tests__/GroupConfigSchema.test.ts
Normal file
90
src/core/config/__tests__/GroupConfigSchema.test.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { CustomModeSchema } from "../CustomModesSchema"
|
||||||
|
import { ModeConfig } from "../../../shared/modes"
|
||||||
|
|
||||||
|
describe("GroupConfigSchema", () => {
|
||||||
|
const validBaseMode = {
|
||||||
|
slug: "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Test role definition",
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("group format validation", () => {
|
||||||
|
test("accepts single group", () => {
|
||||||
|
const mode = {
|
||||||
|
...validBaseMode,
|
||||||
|
groups: ["read"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
expect(() => CustomModeSchema.parse(mode)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("accepts multiple groups", () => {
|
||||||
|
const mode = {
|
||||||
|
...validBaseMode,
|
||||||
|
groups: ["read", "edit", "browser"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
expect(() => CustomModeSchema.parse(mode)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("accepts all available groups", () => {
|
||||||
|
const mode = {
|
||||||
|
...validBaseMode,
|
||||||
|
groups: ["read", "edit", "browser", "command", "mcp"] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
expect(() => CustomModeSchema.parse(mode)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects non-array group format", () => {
|
||||||
|
const mode = {
|
||||||
|
...validBaseMode,
|
||||||
|
groups: "not-an-array" as any,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => CustomModeSchema.parse(mode)).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects empty groups array", () => {
|
||||||
|
const mode = {
|
||||||
|
...validBaseMode,
|
||||||
|
groups: [] as const,
|
||||||
|
} satisfies ModeConfig
|
||||||
|
|
||||||
|
expect(() => CustomModeSchema.parse(mode)).toThrow("At least one tool group is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects invalid group names", () => {
|
||||||
|
const mode = {
|
||||||
|
...validBaseMode,
|
||||||
|
groups: ["invalid_group"] as any,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => CustomModeSchema.parse(mode)).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects duplicate groups", () => {
|
||||||
|
const mode = {
|
||||||
|
...validBaseMode,
|
||||||
|
groups: ["read", "read"] as any,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => CustomModeSchema.parse(mode)).toThrow("Duplicate groups are not allowed")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects null or undefined groups", () => {
|
||||||
|
const modeWithNull = {
|
||||||
|
...validBaseMode,
|
||||||
|
groups: null as any,
|
||||||
|
}
|
||||||
|
|
||||||
|
const modeWithUndefined = {
|
||||||
|
...validBaseMode,
|
||||||
|
groups: undefined as any,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => CustomModeSchema.parse(modeWithNull)).toThrow()
|
||||||
|
expect(() => CustomModeSchema.parse(modeWithUndefined)).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -233,7 +233,7 @@ Your diff here
|
|||||||
originalContent: string,
|
originalContent: string,
|
||||||
diffContent: string,
|
diffContent: string,
|
||||||
startLine?: number,
|
startLine?: number,
|
||||||
endLine?: number
|
endLine?: number,
|
||||||
): Promise<DiffResult> {
|
): Promise<DiffResult> {
|
||||||
const parsedDiff = this.parseUnifiedDiff(diffContent)
|
const parsedDiff = this.parseUnifiedDiff(diffContent)
|
||||||
const originalLines = originalContent.split("\n")
|
const originalLines = originalContent.split("\n")
|
||||||
@@ -271,7 +271,7 @@ Your diff here
|
|||||||
subHunkResult,
|
subHunkResult,
|
||||||
subSearchResult.index,
|
subSearchResult.index,
|
||||||
subSearchResult.confidence,
|
subSearchResult.confidence,
|
||||||
this.confidenceThreshold
|
this.confidenceThreshold,
|
||||||
)
|
)
|
||||||
if (subEditResult.confidence >= this.confidenceThreshold) {
|
if (subEditResult.confidence >= this.confidenceThreshold) {
|
||||||
subHunkResult = subEditResult.result
|
subHunkResult = subEditResult.result
|
||||||
@@ -293,12 +293,12 @@ Your diff here
|
|||||||
const contextRatio = contextLines / totalLines
|
const contextRatio = contextLines / totalLines
|
||||||
|
|
||||||
let errorMsg = `Failed to find a matching location in the file (${Math.floor(
|
let errorMsg = `Failed to find a matching location in the file (${Math.floor(
|
||||||
confidence * 100
|
confidence * 100,
|
||||||
)}% confidence, needs ${Math.floor(this.confidenceThreshold * 100)}%)\n\n`
|
)}% confidence, needs ${Math.floor(this.confidenceThreshold * 100)}%)\n\n`
|
||||||
errorMsg += "Debug Info:\n"
|
errorMsg += "Debug Info:\n"
|
||||||
errorMsg += `- Search Strategy Used: ${strategy}\n`
|
errorMsg += `- Search Strategy Used: ${strategy}\n`
|
||||||
errorMsg += `- Context Lines: ${contextLines} out of ${totalLines} total lines (${Math.floor(
|
errorMsg += `- Context Lines: ${contextLines} out of ${totalLines} total lines (${Math.floor(
|
||||||
contextRatio * 100
|
contextRatio * 100,
|
||||||
)}%)\n`
|
)}%)\n`
|
||||||
errorMsg += `- Attempted to split into ${subHunks.length} sub-hunks but still failed\n`
|
errorMsg += `- Attempted to split into ${subHunks.length} sub-hunks but still failed\n`
|
||||||
|
|
||||||
@@ -330,7 +330,7 @@ Your diff here
|
|||||||
} else {
|
} else {
|
||||||
// Edit failure - likely due to content mismatch
|
// Edit failure - likely due to content mismatch
|
||||||
let errorMsg = `Failed to apply the edit using ${editResult.strategy} strategy (${Math.floor(
|
let errorMsg = `Failed to apply the edit using ${editResult.strategy} strategy (${Math.floor(
|
||||||
editResult.confidence * 100
|
editResult.confidence * 100,
|
||||||
)}% confidence)\n\n`
|
)}% confidence)\n\n`
|
||||||
errorMsg += "Debug Info:\n"
|
errorMsg += "Debug Info:\n"
|
||||||
errorMsg += "- The location was found but the content didn't match exactly\n"
|
errorMsg += "- The location was found but the content didn't match exactly\n"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Mode, isToolAllowedForMode, TestToolName, getModeConfig } from "../shared/modes"
|
import { Mode, isToolAllowedForMode, getModeConfig, ModeConfig } from "../shared/modes"
|
||||||
|
import { ToolName } from "../shared/tool-groups"
|
||||||
|
|
||||||
export { isToolAllowedForMode }
|
export { isToolAllowedForMode }
|
||||||
export type { TestToolName }
|
export type { ToolName }
|
||||||
|
|
||||||
export function validateToolUse(toolName: TestToolName, mode: Mode): void {
|
export function validateToolUse(toolName: ToolName, mode: Mode, customModes?: ModeConfig[]): void {
|
||||||
if (!isToolAllowedForMode(toolName, mode)) {
|
if (!isToolAllowedForMode(toolName, mode, customModes ?? [])) {
|
||||||
throw new Error(`Tool "${toolName}" is not allowed in ${mode} mode.`)
|
throw new Error(`Tool "${toolName}" is not allowed in ${mode} mode.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,62 @@
|
|||||||
import { SYSTEM_PROMPT, addCustomInstructions } from "../system"
|
import { SYSTEM_PROMPT } from "../system"
|
||||||
import { McpHub } from "../../../services/mcp/McpHub"
|
import { McpHub } from "../../../services/mcp/McpHub"
|
||||||
import { McpServer } from "../../../shared/mcp"
|
import { McpServer } from "../../../shared/mcp"
|
||||||
import { ClineProvider } from "../../../core/webview/ClineProvider"
|
import { ClineProvider } from "../../../core/webview/ClineProvider"
|
||||||
import { SearchReplaceDiffStrategy } from "../../../core/diff/strategies/search-replace"
|
import { SearchReplaceDiffStrategy } from "../../../core/diff/strategies/search-replace"
|
||||||
|
import * as vscode from "vscode"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import { defaultModeSlug, modes } from "../../../shared/modes"
|
import { defaultModeSlug, modes } from "../../../shared/modes"
|
||||||
// Import path utils to get access to toPosix string extension
|
// Import path utils to get access to toPosix string extension
|
||||||
import "../../../utils/path"
|
import "../../../utils/path"
|
||||||
|
import { addCustomInstructions } from "../sections/custom-instructions"
|
||||||
|
import * as modesSection from "../sections/modes"
|
||||||
|
|
||||||
|
// Mock the sections
|
||||||
|
jest.mock("../sections/modes", () => ({
|
||||||
|
getModesSection: jest.fn().mockImplementation(async () => `====\n\nMODES\n\n- Test modes section`),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock("../sections/custom-instructions", () => ({
|
||||||
|
addCustomInstructions: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(async (modeCustomInstructions, globalCustomInstructions, cwd, mode, options) => {
|
||||||
|
const sections = []
|
||||||
|
|
||||||
|
// Add language preference if provided
|
||||||
|
if (options?.preferredLanguage) {
|
||||||
|
sections.push(
|
||||||
|
`Language Preference:\nYou should always speak and think in the ${options.preferredLanguage} language.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add global instructions first
|
||||||
|
if (globalCustomInstructions?.trim()) {
|
||||||
|
sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add mode-specific instructions after
|
||||||
|
if (modeCustomInstructions?.trim()) {
|
||||||
|
sections.push(`Mode-specific Instructions:\n${modeCustomInstructions}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add rules
|
||||||
|
const rules = []
|
||||||
|
if (mode) {
|
||||||
|
rules.push(`# Rules from .clinerules-${mode}:\nMock mode-specific rules`)
|
||||||
|
}
|
||||||
|
rules.push(`# Rules from .clinerules:\nMock generic rules`)
|
||||||
|
|
||||||
|
if (rules.length > 0) {
|
||||||
|
sections.push(`Rules:\n${rules.join("\n")}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinedSections = sections.join("\n\n")
|
||||||
|
return joinedSections
|
||||||
|
? `\n====\n\nUSER'S CUSTOM INSTRUCTIONS\n\nThe following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.\n\n${joinedSections}`
|
||||||
|
: ""
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
// Mock environment-specific values for consistent tests
|
// Mock environment-specific values for consistent tests
|
||||||
jest.mock("os", () => ({
|
jest.mock("os", () => ({
|
||||||
@@ -19,42 +68,38 @@ jest.mock("default-shell", () => "/bin/bash")
|
|||||||
|
|
||||||
jest.mock("os-name", () => () => "Linux")
|
jest.mock("os-name", () => () => "Linux")
|
||||||
|
|
||||||
// Mock fs.readFile to return empty mcpServers config and mock rules files
|
// Create a mock ExtensionContext
|
||||||
jest.mock("fs/promises", () => ({
|
const mockContext = {
|
||||||
...jest.requireActual("fs/promises"),
|
extensionPath: "/mock/extension/path",
|
||||||
readFile: jest.fn().mockImplementation(async (path: string) => {
|
globalStoragePath: "/mock/storage/path",
|
||||||
if (path.endsWith("mcpSettings.json")) {
|
storagePath: "/mock/storage/path",
|
||||||
return '{"mcpServers": {}}'
|
logPath: "/mock/log/path",
|
||||||
}
|
subscriptions: [],
|
||||||
if (path.endsWith(".clinerules-code")) {
|
workspaceState: {
|
||||||
return "# Code Mode Rules\n1. Code specific rule"
|
get: () => undefined,
|
||||||
}
|
update: () => Promise.resolve(),
|
||||||
if (path.endsWith(".clinerules-ask")) {
|
},
|
||||||
return "# Ask Mode Rules\n1. Ask specific rule"
|
globalState: {
|
||||||
}
|
get: () => undefined,
|
||||||
if (path.endsWith(".clinerules-architect")) {
|
update: () => Promise.resolve(),
|
||||||
return "# Architect Mode Rules\n1. Architect specific rule"
|
setKeysForSync: () => {},
|
||||||
}
|
},
|
||||||
if (path.endsWith(".clinerules")) {
|
extensionUri: { fsPath: "/mock/extension/path" },
|
||||||
return "# Test Rules\n1. First rule\n2. Second rule"
|
globalStorageUri: { fsPath: "/mock/settings/path" },
|
||||||
}
|
asAbsolutePath: (relativePath: string) => `/mock/extension/path/${relativePath}`,
|
||||||
return ""
|
extension: {
|
||||||
}),
|
packageJSON: {
|
||||||
writeFile: jest.fn().mockResolvedValue(undefined),
|
version: "1.0.0",
|
||||||
}))
|
},
|
||||||
|
},
|
||||||
|
} as unknown as vscode.ExtensionContext
|
||||||
|
|
||||||
// Create a minimal mock of ClineProvider
|
// Create a minimal mock of ClineProvider
|
||||||
const mockProvider = {
|
const mockProvider = {
|
||||||
ensureMcpServersDirectoryExists: async () => "/mock/mcp/path",
|
ensureMcpServersDirectoryExists: async () => "/mock/mcp/path",
|
||||||
ensureSettingsDirectoryExists: async () => "/mock/settings/path",
|
ensureSettingsDirectoryExists: async () => "/mock/settings/path",
|
||||||
postMessageToWebview: async () => {},
|
postMessageToWebview: async () => {},
|
||||||
context: {
|
context: mockContext,
|
||||||
extension: {
|
|
||||||
packageJSON: {
|
|
||||||
version: "1.0.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as ClineProvider
|
} as unknown as ClineProvider
|
||||||
|
|
||||||
// Instead of extending McpHub, create a mock that implements just what we need
|
// Instead of extending McpHub, create a mock that implements just what we need
|
||||||
@@ -77,6 +122,26 @@ const createMockMcpHub = (): McpHub =>
|
|||||||
describe("SYSTEM_PROMPT", () => {
|
describe("SYSTEM_PROMPT", () => {
|
||||||
let mockMcpHub: McpHub
|
let mockMcpHub: McpHub
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
// Ensure fs mock is properly initialized
|
||||||
|
const mockFs = jest.requireMock("fs/promises")
|
||||||
|
mockFs._setInitialMockData()
|
||||||
|
|
||||||
|
// Initialize all required directories
|
||||||
|
const dirs = [
|
||||||
|
"/mock",
|
||||||
|
"/mock/extension",
|
||||||
|
"/mock/extension/path",
|
||||||
|
"/mock/storage",
|
||||||
|
"/mock/storage/path",
|
||||||
|
"/mock/settings",
|
||||||
|
"/mock/settings/path",
|
||||||
|
"/mock/mcp",
|
||||||
|
"/mock/mcp/path",
|
||||||
|
]
|
||||||
|
dirs.forEach((dir) => mockFs._mockDirectories.add(dir))
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
@@ -90,18 +155,32 @@ describe("SYSTEM_PROMPT", () => {
|
|||||||
|
|
||||||
it("should maintain consistent system prompt", async () => {
|
it("should maintain consistent system prompt", async () => {
|
||||||
const prompt = await SYSTEM_PROMPT(
|
const prompt = await SYSTEM_PROMPT(
|
||||||
|
mockContext,
|
||||||
"/test/path",
|
"/test/path",
|
||||||
false, // supportsComputerUse
|
false, // supportsComputerUse
|
||||||
undefined, // mcpHub
|
undefined, // mcpHub
|
||||||
undefined, // diffStrategy
|
undefined, // diffStrategy
|
||||||
undefined, // browserViewportSize
|
undefined, // browserViewportSize
|
||||||
|
defaultModeSlug, // mode
|
||||||
|
undefined, // customPrompts
|
||||||
|
undefined, // customModes
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(prompt).toMatchSnapshot()
|
expect(prompt).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should include browser actions when supportsComputerUse is true", async () => {
|
it("should include browser actions when supportsComputerUse is true", async () => {
|
||||||
const prompt = await SYSTEM_PROMPT("/test/path", true, undefined, undefined, "1280x800")
|
const prompt = await SYSTEM_PROMPT(
|
||||||
|
mockContext,
|
||||||
|
"/test/path",
|
||||||
|
true, // supportsComputerUse
|
||||||
|
undefined, // mcpHub
|
||||||
|
undefined, // diffStrategy
|
||||||
|
"1280x800", // browserViewportSize
|
||||||
|
defaultModeSlug, // mode
|
||||||
|
undefined, // customPrompts
|
||||||
|
undefined, // customModes
|
||||||
|
)
|
||||||
|
|
||||||
expect(prompt).toMatchSnapshot()
|
expect(prompt).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
@@ -109,18 +188,32 @@ describe("SYSTEM_PROMPT", () => {
|
|||||||
it("should include MCP server info when mcpHub is provided", async () => {
|
it("should include MCP server info when mcpHub is provided", async () => {
|
||||||
mockMcpHub = createMockMcpHub()
|
mockMcpHub = createMockMcpHub()
|
||||||
|
|
||||||
const prompt = await SYSTEM_PROMPT("/test/path", false, mockMcpHub)
|
const prompt = await SYSTEM_PROMPT(
|
||||||
|
mockContext,
|
||||||
|
"/test/path",
|
||||||
|
false, // supportsComputerUse
|
||||||
|
mockMcpHub, // mcpHub
|
||||||
|
undefined, // diffStrategy
|
||||||
|
undefined, // browserViewportSize
|
||||||
|
defaultModeSlug, // mode
|
||||||
|
undefined, // customPrompts
|
||||||
|
undefined, // customModes
|
||||||
|
)
|
||||||
|
|
||||||
expect(prompt).toMatchSnapshot()
|
expect(prompt).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should explicitly handle undefined mcpHub", async () => {
|
it("should explicitly handle undefined mcpHub", async () => {
|
||||||
const prompt = await SYSTEM_PROMPT(
|
const prompt = await SYSTEM_PROMPT(
|
||||||
|
mockContext,
|
||||||
"/test/path",
|
"/test/path",
|
||||||
false,
|
false, // supportsComputerUse
|
||||||
undefined, // explicitly undefined mcpHub
|
undefined, // explicitly undefined mcpHub
|
||||||
undefined,
|
undefined, // diffStrategy
|
||||||
undefined,
|
undefined, // browserViewportSize
|
||||||
|
defaultModeSlug, // mode
|
||||||
|
undefined, // customPrompts
|
||||||
|
undefined, // customModes
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(prompt).toMatchSnapshot()
|
expect(prompt).toMatchSnapshot()
|
||||||
@@ -128,11 +221,15 @@ describe("SYSTEM_PROMPT", () => {
|
|||||||
|
|
||||||
it("should handle different browser viewport sizes", async () => {
|
it("should handle different browser viewport sizes", async () => {
|
||||||
const prompt = await SYSTEM_PROMPT(
|
const prompt = await SYSTEM_PROMPT(
|
||||||
|
mockContext,
|
||||||
"/test/path",
|
"/test/path",
|
||||||
true,
|
true, // supportsComputerUse
|
||||||
undefined,
|
undefined, // mcpHub
|
||||||
undefined,
|
undefined, // diffStrategy
|
||||||
"900x600", // different viewport size
|
"900x600", // different viewport size
|
||||||
|
defaultModeSlug, // mode
|
||||||
|
undefined, // customPrompts
|
||||||
|
undefined, // customModes
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(prompt).toMatchSnapshot()
|
expect(prompt).toMatchSnapshot()
|
||||||
@@ -140,187 +237,198 @@ describe("SYSTEM_PROMPT", () => {
|
|||||||
|
|
||||||
it("should include diff strategy tool description", async () => {
|
it("should include diff strategy tool description", async () => {
|
||||||
const prompt = await SYSTEM_PROMPT(
|
const prompt = await SYSTEM_PROMPT(
|
||||||
|
mockContext,
|
||||||
"/test/path",
|
"/test/path",
|
||||||
false,
|
false, // supportsComputerUse
|
||||||
undefined,
|
undefined, // mcpHub
|
||||||
new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase
|
new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase
|
||||||
undefined,
|
undefined, // browserViewportSize
|
||||||
|
defaultModeSlug, // mode
|
||||||
|
undefined, // customPrompts
|
||||||
|
undefined, // customModes
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(prompt).toMatchSnapshot()
|
expect(prompt).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should include custom mode role definition at top and instructions at bottom", async () => {
|
||||||
|
const modeCustomInstructions = "Custom mode instructions"
|
||||||
|
const customModes = [
|
||||||
|
{
|
||||||
|
slug: "custom-mode",
|
||||||
|
name: "Custom Mode",
|
||||||
|
roleDefinition: "Custom role definition",
|
||||||
|
customInstructions: modeCustomInstructions,
|
||||||
|
groups: ["read"] as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const prompt = await SYSTEM_PROMPT(
|
||||||
|
mockContext,
|
||||||
|
"/test/path",
|
||||||
|
false, // supportsComputerUse
|
||||||
|
undefined, // mcpHub
|
||||||
|
undefined, // diffStrategy
|
||||||
|
undefined, // browserViewportSize
|
||||||
|
"custom-mode", // mode
|
||||||
|
undefined, // customPrompts
|
||||||
|
customModes, // customModes
|
||||||
|
"Global instructions", // globalCustomInstructions
|
||||||
|
)
|
||||||
|
|
||||||
|
// Role definition should be at the top
|
||||||
|
expect(prompt.indexOf("Custom role definition")).toBeLessThan(prompt.indexOf("TOOL USE"))
|
||||||
|
|
||||||
|
// Custom instructions should be at the bottom
|
||||||
|
const customInstructionsIndex = prompt.indexOf("Custom mode instructions")
|
||||||
|
const userInstructionsHeader = prompt.indexOf("USER'S CUSTOM INSTRUCTIONS")
|
||||||
|
expect(customInstructionsIndex).toBeGreaterThan(-1)
|
||||||
|
expect(userInstructionsHeader).toBeGreaterThan(-1)
|
||||||
|
expect(customInstructionsIndex).toBeGreaterThan(userInstructionsHeader)
|
||||||
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
jest.restoreAllMocks()
|
jest.restoreAllMocks()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("addCustomInstructions", () => {
|
describe("addCustomInstructions", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
// Ensure fs mock is properly initialized
|
||||||
|
const mockFs = jest.requireMock("fs/promises")
|
||||||
|
mockFs._setInitialMockData()
|
||||||
|
mockFs.mkdir.mockImplementation(async (path: string) => {
|
||||||
|
if (path.startsWith("/test")) {
|
||||||
|
mockFs._mockDirectories.add(path)
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
throw new Error(`ENOENT: no such file or directory, mkdir '${path}'`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should generate correct prompt for architect mode", async () => {
|
it("should generate correct prompt for architect mode", async () => {
|
||||||
const prompt = await SYSTEM_PROMPT("/test/path", false, undefined, undefined, undefined, "architect")
|
const prompt = await SYSTEM_PROMPT(
|
||||||
|
mockContext,
|
||||||
|
"/test/path",
|
||||||
|
false, // supportsComputerUse
|
||||||
|
undefined, // mcpHub
|
||||||
|
undefined, // diffStrategy
|
||||||
|
undefined, // browserViewportSize
|
||||||
|
"architect", // mode
|
||||||
|
undefined, // customPrompts
|
||||||
|
undefined, // customModes
|
||||||
|
)
|
||||||
|
|
||||||
expect(prompt).toMatchSnapshot()
|
expect(prompt).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should generate correct prompt for ask mode", async () => {
|
it("should generate correct prompt for ask mode", async () => {
|
||||||
const prompt = await SYSTEM_PROMPT("/test/path", false, undefined, undefined, undefined, "ask")
|
const prompt = await SYSTEM_PROMPT(
|
||||||
|
mockContext,
|
||||||
|
"/test/path",
|
||||||
|
false, // supportsComputerUse
|
||||||
|
undefined, // mcpHub
|
||||||
|
undefined, // diffStrategy
|
||||||
|
undefined, // browserViewportSize
|
||||||
|
"ask", // mode
|
||||||
|
undefined, // customPrompts
|
||||||
|
undefined, // customModes
|
||||||
|
)
|
||||||
|
|
||||||
expect(prompt).toMatchSnapshot()
|
expect(prompt).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should prioritize mode-specific rules for code mode", async () => {
|
it("should prioritize mode-specific rules for code mode", async () => {
|
||||||
const instructions = await addCustomInstructions({}, "/test/path", defaultModeSlug)
|
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
|
||||||
expect(instructions).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should prioritize mode-specific rules for ask mode", async () => {
|
it("should prioritize mode-specific rules for ask mode", async () => {
|
||||||
const instructions = await addCustomInstructions({}, "/test/path", modes[2].slug)
|
const instructions = await addCustomInstructions("", "", "/test/path", modes[2].slug)
|
||||||
expect(instructions).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should prioritize mode-specific rules for architect mode", async () => {
|
it("should prioritize mode-specific rules for architect mode", async () => {
|
||||||
const instructions = await addCustomInstructions({}, "/test/path", modes[1].slug)
|
const instructions = await addCustomInstructions("", "", "/test/path", modes[1].slug)
|
||||||
|
|
||||||
expect(instructions).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should prioritize mode-specific rules for test engineer mode", async () => {
|
it("should prioritize mode-specific rules for test engineer mode", async () => {
|
||||||
// Mock readFile to include test engineer rules
|
const instructions = await addCustomInstructions("", "", "/test/path", "test")
|
||||||
const mockReadFile = jest.fn().mockImplementation(async (path: string) => {
|
|
||||||
if (path.endsWith(".clinerules-test")) {
|
|
||||||
return "# Test Engineer Rules\n1. Always write tests first\n2. Get approval before modifying non-test code"
|
|
||||||
}
|
|
||||||
if (path.endsWith(".clinerules")) {
|
|
||||||
return "# Test Rules\n1. First rule\n2. Second rule"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
})
|
|
||||||
jest.spyOn(fs, "readFile").mockImplementation(mockReadFile)
|
|
||||||
|
|
||||||
const instructions = await addCustomInstructions({}, "/test/path", "test")
|
|
||||||
expect(instructions).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should prioritize mode-specific rules for code reviewer mode", async () => {
|
it("should prioritize mode-specific rules for code reviewer mode", async () => {
|
||||||
// Mock readFile to include code reviewer rules
|
const instructions = await addCustomInstructions("", "", "/test/path", "review")
|
||||||
const mockReadFile = jest.fn().mockImplementation(async (path: string) => {
|
|
||||||
if (path.endsWith(".clinerules-review")) {
|
|
||||||
return "# Code Reviewer Rules\n1. Provide specific examples in feedback\n2. Focus on maintainability and best practices"
|
|
||||||
}
|
|
||||||
if (path.endsWith(".clinerules")) {
|
|
||||||
return "# Test Rules\n1. First rule\n2. Second rule"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
})
|
|
||||||
jest.spyOn(fs, "readFile").mockImplementation(mockReadFile)
|
|
||||||
|
|
||||||
const instructions = await addCustomInstructions({}, "/test/path", "review")
|
|
||||||
expect(instructions).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should fall back to generic rules when mode-specific rules not found", async () => {
|
it("should fall back to generic rules when mode-specific rules not found", async () => {
|
||||||
// Mock readFile to return ENOENT for mode-specific file
|
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
|
||||||
const mockReadFile = jest.fn().mockImplementation(async (path: string) => {
|
|
||||||
if (
|
|
||||||
path.endsWith(".clinerules-code") ||
|
|
||||||
path.endsWith(".clinerules-test") ||
|
|
||||||
path.endsWith(".clinerules-review")
|
|
||||||
) {
|
|
||||||
const error = new Error("ENOENT") as NodeJS.ErrnoException
|
|
||||||
error.code = "ENOENT"
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
if (path.endsWith(".clinerules")) {
|
|
||||||
return "# Test Rules\n1. First rule\n2. Second rule"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
})
|
|
||||||
jest.spyOn(fs, "readFile").mockImplementation(mockReadFile)
|
|
||||||
|
|
||||||
const instructions = await addCustomInstructions({}, "/test/path", defaultModeSlug)
|
|
||||||
|
|
||||||
expect(instructions).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should include preferred language when provided", async () => {
|
it("should include preferred language when provided", async () => {
|
||||||
const instructions = await addCustomInstructions(
|
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug, {
|
||||||
{ preferredLanguage: "Spanish" },
|
preferredLanguage: "Spanish",
|
||||||
"/test/path",
|
})
|
||||||
defaultModeSlug,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(instructions).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should include custom instructions when provided", async () => {
|
it("should include custom instructions when provided", async () => {
|
||||||
const instructions = await addCustomInstructions(
|
const instructions = await addCustomInstructions("Custom test instructions", "", "/test/path", defaultModeSlug)
|
||||||
{ customInstructions: "Custom test instructions" },
|
|
||||||
"/test/path",
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(instructions).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should combine all custom instructions", async () => {
|
it("should combine all custom instructions", async () => {
|
||||||
const instructions = await addCustomInstructions(
|
const instructions = await addCustomInstructions(
|
||||||
{
|
"Custom test instructions",
|
||||||
customInstructions: "Custom test instructions",
|
"",
|
||||||
preferredLanguage: "French",
|
|
||||||
},
|
|
||||||
"/test/path",
|
"/test/path",
|
||||||
defaultModeSlug,
|
defaultModeSlug,
|
||||||
|
{ preferredLanguage: "French" },
|
||||||
)
|
)
|
||||||
expect(instructions).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle undefined mode-specific instructions", async () => {
|
it("should handle undefined mode-specific instructions", async () => {
|
||||||
const instructions = await addCustomInstructions({}, "/test/path")
|
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
|
||||||
|
|
||||||
expect(instructions).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should trim mode-specific instructions", async () => {
|
it("should trim mode-specific instructions", async () => {
|
||||||
const instructions = await addCustomInstructions(
|
const instructions = await addCustomInstructions(
|
||||||
{ customInstructions: " Custom mode instructions " },
|
" Custom mode instructions ",
|
||||||
|
"",
|
||||||
"/test/path",
|
"/test/path",
|
||||||
|
defaultModeSlug,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(instructions).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle empty mode-specific instructions", async () => {
|
it("should handle empty mode-specific instructions", async () => {
|
||||||
const instructions = await addCustomInstructions({ customInstructions: "" }, "/test/path")
|
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
|
||||||
|
|
||||||
expect(instructions).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should combine global and mode-specific instructions", async () => {
|
it("should combine global and mode-specific instructions", async () => {
|
||||||
const instructions = await addCustomInstructions(
|
const instructions = await addCustomInstructions(
|
||||||
{
|
"Mode-specific instructions",
|
||||||
customInstructions: "Global instructions",
|
"Global instructions",
|
||||||
customPrompts: {
|
|
||||||
code: { customInstructions: "Mode-specific instructions" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"/test/path",
|
"/test/path",
|
||||||
defaultModeSlug,
|
defaultModeSlug,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(instructions).toMatchSnapshot()
|
expect(instructions).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should prioritize mode-specific instructions after global ones", async () => {
|
it("should prioritize mode-specific instructions after global ones", async () => {
|
||||||
const instructions = await addCustomInstructions(
|
const instructions = await addCustomInstructions(
|
||||||
{
|
"Second instruction",
|
||||||
customInstructions: "First instruction",
|
"First instruction",
|
||||||
customPrompts: {
|
|
||||||
code: { customInstructions: "Second instruction" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"/test/path",
|
"/test/path",
|
||||||
defaultModeSlug,
|
defaultModeSlug,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,28 +23,70 @@ export async function loadRuleFiles(cwd: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function addCustomInstructions(
|
export async function addCustomInstructions(
|
||||||
customInstructions: string,
|
modeCustomInstructions: string,
|
||||||
|
globalCustomInstructions: string,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
preferredLanguage?: string,
|
mode: string,
|
||||||
|
options: { preferredLanguage?: string } = {},
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const ruleFileContent = await loadRuleFiles(cwd)
|
const sections = []
|
||||||
const allInstructions = []
|
|
||||||
|
|
||||||
if (preferredLanguage) {
|
// Load mode-specific rules if mode is provided
|
||||||
allInstructions.push(`You should always speak and think in the ${preferredLanguage} language.`)
|
let modeRuleContent = ""
|
||||||
|
if (mode) {
|
||||||
|
try {
|
||||||
|
const modeRuleFile = `.clinerules-${mode}`
|
||||||
|
const content = await fs.readFile(path.join(cwd, modeRuleFile), "utf-8")
|
||||||
|
if (content.trim()) {
|
||||||
|
modeRuleContent = content.trim()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Silently skip if file doesn't exist
|
||||||
|
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customInstructions.trim()) {
|
// Add language preference if provided
|
||||||
allInstructions.push(customInstructions.trim())
|
if (options.preferredLanguage) {
|
||||||
|
sections.push(
|
||||||
|
`Language Preference:\nYou should always speak and think in the ${options.preferredLanguage} language.`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ruleFileContent && ruleFileContent.trim()) {
|
// Add global instructions first
|
||||||
allInstructions.push(ruleFileContent.trim())
|
if (typeof globalCustomInstructions === "string" && globalCustomInstructions.trim()) {
|
||||||
|
sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const joinedInstructions = allInstructions.join("\n\n")
|
// Add mode-specific instructions after
|
||||||
|
if (typeof modeCustomInstructions === "string" && modeCustomInstructions.trim()) {
|
||||||
|
sections.push(`Mode-specific Instructions:\n${modeCustomInstructions.trim()}`)
|
||||||
|
}
|
||||||
|
|
||||||
return joinedInstructions
|
// Add rules - include both mode-specific and generic rules if they exist
|
||||||
|
const rules = []
|
||||||
|
|
||||||
|
// Add mode-specific rules first if they exist
|
||||||
|
if (modeRuleContent && modeRuleContent.trim()) {
|
||||||
|
const modeRuleFile = `.clinerules-${mode}`
|
||||||
|
rules.push(`# Rules from ${modeRuleFile}:\n${modeRuleContent}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add generic rules
|
||||||
|
const genericRuleContent = await loadRuleFiles(cwd)
|
||||||
|
if (genericRuleContent && genericRuleContent.trim()) {
|
||||||
|
rules.push(genericRuleContent.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules.length > 0) {
|
||||||
|
sections.push(`Rules:\n\n${rules.join("\n\n")}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinedSections = sections.join("\n\n")
|
||||||
|
|
||||||
|
return joinedSections
|
||||||
? `
|
? `
|
||||||
====
|
====
|
||||||
|
|
||||||
@@ -52,6 +94,6 @@ USER'S CUSTOM INSTRUCTIONS
|
|||||||
|
|
||||||
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
|
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
|
||||||
|
|
||||||
${joinedInstructions}`
|
${joinedSections}`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export { getSharedToolUseSection } from "./tool-use"
|
|||||||
export { getMcpServersSection } from "./mcp-servers"
|
export { getMcpServersSection } from "./mcp-servers"
|
||||||
export { getToolUseGuidelinesSection } from "./tool-use-guidelines"
|
export { getToolUseGuidelinesSection } from "./tool-use-guidelines"
|
||||||
export { getCapabilitiesSection } from "./capabilities"
|
export { getCapabilitiesSection } from "./capabilities"
|
||||||
|
export { getModesSection } from "./modes"
|
||||||
|
|||||||
45
src/core/prompts/sections/modes.ts
Normal file
45
src/core/prompts/sections/modes.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import * as path from "path"
|
||||||
|
import * as vscode from "vscode"
|
||||||
|
import { promises as fs } from "fs"
|
||||||
|
import { modes, ModeConfig } from "../../../shared/modes"
|
||||||
|
|
||||||
|
export async function getModesSection(context: vscode.ExtensionContext): Promise<string> {
|
||||||
|
const settingsDir = path.join(context.globalStorageUri.fsPath, "settings")
|
||||||
|
await fs.mkdir(settingsDir, { recursive: true })
|
||||||
|
const customModesPath = path.join(settingsDir, "cline_custom_modes.json")
|
||||||
|
|
||||||
|
return `====
|
||||||
|
|
||||||
|
MODES
|
||||||
|
|
||||||
|
- When referring to modes, always use their display names. The built-in modes are:
|
||||||
|
${modes.map((mode: ModeConfig) => ` * "${mode.name}" mode - ${mode.roleDefinition.split(".")[0]}`).join("\n")}
|
||||||
|
Custom modes will be referred to by their configured name property.
|
||||||
|
|
||||||
|
- Custom modes can be configured by creating or editing the custom modes file at '${customModesPath}'. The following fields are required and must not be empty:
|
||||||
|
* slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better.
|
||||||
|
* name: The display name for the mode
|
||||||
|
* roleDefinition: A detailed description of the mode's role and capabilities
|
||||||
|
* groups: Array of allowed tool groups (can be empty)
|
||||||
|
|
||||||
|
The customInstructions field is optional.
|
||||||
|
|
||||||
|
The file should follow this structure:
|
||||||
|
{
|
||||||
|
"customModes": [
|
||||||
|
{
|
||||||
|
"slug": "designer", // Required: unique slug with lowercase letters, numbers, and hyphens
|
||||||
|
"name": "Designer", // Required: mode display name
|
||||||
|
"roleDefinition": "You are Roo, a UI/UX expert specializing in design systems and frontend development. Your expertise includes:\n- Creating and maintaining design systems\n- Implementing responsive and accessible web interfaces\n- Working with CSS, HTML, and modern frontend frameworks\n- Ensuring consistent user experiences across platforms", // Required: non-empty
|
||||||
|
"groups": [ // Required: array of tool groups (can be empty)
|
||||||
|
"read", // Read files group (read_file, search_files, list_files, list_code_definition_names)
|
||||||
|
"edit", // Edit files group (write_to_file, apply_diff)
|
||||||
|
"browser", // Browser group (browser_action)
|
||||||
|
"command", // Command group (execute_command)
|
||||||
|
"mcp" // MCP group (use_mcp_tool, access_mcp_resource)
|
||||||
|
],
|
||||||
|
"customInstructions": "Additional instructions for the Designer mode" // Optional
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
}
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||||
|
import { modes, ModeConfig } from "../../../shared/modes"
|
||||||
|
import * as vscode from "vscode"
|
||||||
|
import * as path from "path"
|
||||||
|
|
||||||
export function getRulesSection(cwd: string, supportsComputerUse: boolean, diffStrategy?: DiffStrategy): string {
|
export function getRulesSection(
|
||||||
|
cwd: string,
|
||||||
|
supportsComputerUse: boolean,
|
||||||
|
diffStrategy?: DiffStrategy,
|
||||||
|
context?: vscode.ExtensionContext,
|
||||||
|
): string {
|
||||||
|
const settingsDir = context ? path.join(context.globalStorageUri.fsPath, "settings") : "<settings directory>"
|
||||||
|
const customModesPath = path.join(settingsDir, "cline_custom_modes.json")
|
||||||
return `====
|
return `====
|
||||||
|
|
||||||
RULES
|
RULES
|
||||||
@@ -11,7 +21,11 @@ RULES
|
|||||||
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '${cwd.toPosix()}', and if so prepend with \`cd\`'ing into that directory && then executing the command (as one command since you are stuck operating from '${cwd.toPosix()}'). For example, if you needed to run \`npm install\` in a project outside of '${cwd.toPosix()}', you would need to prepend with a \`cd\` i.e. pseudocode for this would be \`cd (path to project) && (command, in this case npm install)\`.
|
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '${cwd.toPosix()}', and if so prepend with \`cd\`'ing into that directory && then executing the command (as one command since you are stuck operating from '${cwd.toPosix()}'). For example, if you needed to run \`npm install\` in a project outside of '${cwd.toPosix()}', you would need to prepend with a \`cd\` i.e. pseudocode for this would be \`cd (path to project) && (command, in this case npm install)\`.
|
||||||
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.
|
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.
|
||||||
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
|
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
|
||||||
${diffStrategy ? "- You should use apply_diff instead of write_to_file when making changes to existing files since it is much faster and easier to apply a diff than to write the entire file again. Only use write_to_file to edit files when apply_diff has failed repeatedly to apply the diff." : "- When you want to modify a file, use the write_to_file tool directly with the desired content. You do not need to display the content before using the tool."}
|
${
|
||||||
|
diffStrategy
|
||||||
|
? "- You should use apply_diff instead of write_to_file when making changes to existing files since it is much faster and easier to apply a diff than to write the entire file again. Only use write_to_file to edit files when apply_diff has failed repeatedly to apply the diff."
|
||||||
|
: "- When you want to modify a file, use the write_to_file tool directly with the desired content. You do not need to display the content before using the tool."
|
||||||
|
}
|
||||||
- Be sure to consider the type of project (e.g. Python, JavaScript, web application) when determining the appropriate structure and files to include. Also consider what files may be most relevant to accomplishing the task, for example looking at a project's manifest file would help you understand the project's dependencies, which you could incorporate into any code you write.
|
- Be sure to consider the type of project (e.g. Python, JavaScript, web application) when determining the appropriate structure and files to include. Also consider what files may be most relevant to accomplishing the task, for example looking at a project's manifest file would help you understand the project's dependencies, which you could incorporate into any code you write.
|
||||||
- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.
|
- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.
|
||||||
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
|
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import defaultShell from "default-shell"
|
import defaultShell from "default-shell"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import osName from "os-name"
|
import osName from "os-name"
|
||||||
|
import { Mode, ModeConfig, getModeBySlug, defaultModeSlug, isToolAllowedForMode } from "../../../shared/modes"
|
||||||
|
|
||||||
export function getSystemInfoSection(cwd: string): string {
|
export function getSystemInfoSection(cwd: string, currentMode: Mode, customModes?: ModeConfig[]): string {
|
||||||
return `====
|
const findModeBySlug = (slug: string, modes?: ModeConfig[]) => modes?.find((m) => m.slug === slug)
|
||||||
|
|
||||||
|
const currentModeName = findModeBySlug(currentMode, customModes)?.name || currentMode
|
||||||
|
const codeModeName = findModeBySlug(defaultModeSlug, customModes)?.name || "Code"
|
||||||
|
|
||||||
|
let details = `====
|
||||||
|
|
||||||
SYSTEM INFORMATION
|
SYSTEM INFORMATION
|
||||||
|
|
||||||
@@ -13,4 +19,6 @@ Home Directory: ${os.homedir().toPosix()}
|
|||||||
Current Working Directory: ${cwd.toPosix()}
|
Current Working Directory: ${cwd.toPosix()}
|
||||||
|
|
||||||
When the user initially gives you a task, a recursive list of all filepaths in the current working directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current working directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.`
|
When the user initially gives you a task, a recursive list of all filepaths in the current working directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current working directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.`
|
||||||
|
|
||||||
|
return details
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { Mode, modes, CustomPrompts, PromptComponent, getRoleDefinition, defaultModeSlug } from "../../shared/modes"
|
import {
|
||||||
|
Mode,
|
||||||
|
modes,
|
||||||
|
CustomPrompts,
|
||||||
|
PromptComponent,
|
||||||
|
getRoleDefinition,
|
||||||
|
defaultModeSlug,
|
||||||
|
ModeConfig,
|
||||||
|
getModeBySlug,
|
||||||
|
} from "../../shared/modes"
|
||||||
import { DiffStrategy } from "../diff/DiffStrategy"
|
import { DiffStrategy } from "../diff/DiffStrategy"
|
||||||
import { McpHub } from "../../services/mcp/McpHub"
|
import { McpHub } from "../../services/mcp/McpHub"
|
||||||
import { getToolDescriptionsForMode } from "./tools"
|
import { getToolDescriptionsForMode } from "./tools"
|
||||||
|
import * as vscode from "vscode"
|
||||||
import {
|
import {
|
||||||
getRulesSection,
|
getRulesSection,
|
||||||
getSystemInfoSection,
|
getSystemInfoSection,
|
||||||
@@ -10,88 +20,14 @@ import {
|
|||||||
getMcpServersSection,
|
getMcpServersSection,
|
||||||
getToolUseGuidelinesSection,
|
getToolUseGuidelinesSection,
|
||||||
getCapabilitiesSection,
|
getCapabilitiesSection,
|
||||||
|
getModesSection,
|
||||||
|
addCustomInstructions,
|
||||||
} from "./sections"
|
} from "./sections"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
async function loadRuleFiles(cwd: string, mode: Mode): Promise<string> {
|
|
||||||
let combinedRules = ""
|
|
||||||
|
|
||||||
// First try mode-specific rules
|
|
||||||
const modeSpecificFile = `.clinerules-${mode}`
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(path.join(cwd, modeSpecificFile), "utf-8")
|
|
||||||
if (content.trim()) {
|
|
||||||
combinedRules += `\n# Rules from ${modeSpecificFile}:\n${content.trim()}\n`
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Silently skip if file doesn't exist
|
|
||||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then try generic rules files
|
|
||||||
const genericRuleFiles = [".clinerules"]
|
|
||||||
for (const file of genericRuleFiles) {
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(path.join(cwd, file), "utf-8")
|
|
||||||
if (content.trim()) {
|
|
||||||
combinedRules += `\n# Rules from ${file}:\n${content.trim()}\n`
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Silently skip if file doesn't exist
|
|
||||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return combinedRules
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
customInstructions?: string
|
|
||||||
customPrompts?: CustomPrompts
|
|
||||||
preferredLanguage?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addCustomInstructions(state: State, cwd: string, mode: Mode = defaultModeSlug): Promise<string> {
|
|
||||||
const ruleFileContent = await loadRuleFiles(cwd, mode)
|
|
||||||
const allInstructions = []
|
|
||||||
|
|
||||||
if (state.preferredLanguage) {
|
|
||||||
allInstructions.push(`You should always speak and think in the ${state.preferredLanguage} language.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.customInstructions?.trim()) {
|
|
||||||
allInstructions.push(state.customInstructions.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
const customPrompt = state.customPrompts?.[mode]
|
|
||||||
if (typeof customPrompt === "object" && customPrompt?.customInstructions?.trim()) {
|
|
||||||
allInstructions.push(customPrompt.customInstructions.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ruleFileContent && ruleFileContent.trim()) {
|
|
||||||
allInstructions.push(ruleFileContent.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
const joinedInstructions = allInstructions.join("\n\n")
|
|
||||||
|
|
||||||
return joinedInstructions
|
|
||||||
? `
|
|
||||||
====
|
|
||||||
|
|
||||||
USER'S CUSTOM INSTRUCTIONS
|
|
||||||
|
|
||||||
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
|
|
||||||
|
|
||||||
${joinedInstructions}`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generatePrompt(
|
async function generatePrompt(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
supportsComputerUse: boolean,
|
supportsComputerUse: boolean,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
@@ -99,29 +35,57 @@ async function generatePrompt(
|
|||||||
diffStrategy?: DiffStrategy,
|
diffStrategy?: DiffStrategy,
|
||||||
browserViewportSize?: string,
|
browserViewportSize?: string,
|
||||||
promptComponent?: PromptComponent,
|
promptComponent?: PromptComponent,
|
||||||
|
customModeConfigs?: ModeConfig[],
|
||||||
|
globalCustomInstructions?: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const basePrompt = `${promptComponent?.roleDefinition || getRoleDefinition(mode)}
|
if (!context) {
|
||||||
|
throw new Error("Extension context is required for generating system prompt")
|
||||||
|
}
|
||||||
|
|
||||||
|
const [mcpServersSection, modesSection] = await Promise.all([
|
||||||
|
getMcpServersSection(mcpHub, diffStrategy),
|
||||||
|
getModesSection(context),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Get the full mode config to ensure we have the role definition
|
||||||
|
const modeConfig = getModeBySlug(mode, customModeConfigs) || modes.find((m) => m.slug === mode) || modes[0]
|
||||||
|
const roleDefinition = modeConfig.roleDefinition
|
||||||
|
|
||||||
|
const basePrompt = `${roleDefinition}
|
||||||
|
|
||||||
${getSharedToolUseSection()}
|
${getSharedToolUseSection()}
|
||||||
|
|
||||||
${getToolDescriptionsForMode(mode, cwd, supportsComputerUse, diffStrategy, browserViewportSize, mcpHub)}
|
${getToolDescriptionsForMode(
|
||||||
|
mode,
|
||||||
|
cwd,
|
||||||
|
supportsComputerUse,
|
||||||
|
diffStrategy,
|
||||||
|
browserViewportSize,
|
||||||
|
mcpHub,
|
||||||
|
customModeConfigs,
|
||||||
|
)}
|
||||||
|
|
||||||
${getToolUseGuidelinesSection()}
|
${getToolUseGuidelinesSection()}
|
||||||
|
|
||||||
${await getMcpServersSection(mcpHub, diffStrategy)}
|
${mcpServersSection}
|
||||||
|
|
||||||
${getCapabilitiesSection(cwd, supportsComputerUse, mcpHub, diffStrategy)}
|
${getCapabilitiesSection(cwd, supportsComputerUse, mcpHub, diffStrategy)}
|
||||||
|
|
||||||
${getRulesSection(cwd, supportsComputerUse, diffStrategy)}
|
${modesSection}
|
||||||
|
|
||||||
${getSystemInfoSection(cwd)}
|
${getRulesSection(cwd, supportsComputerUse, diffStrategy, context)}
|
||||||
|
|
||||||
${getObjectiveSection()}`
|
${getSystemInfoSection(cwd, mode, customModeConfigs)}
|
||||||
|
|
||||||
|
${getObjectiveSection()}
|
||||||
|
|
||||||
|
${await addCustomInstructions(modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, {})}`
|
||||||
|
|
||||||
return basePrompt
|
return basePrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SYSTEM_PROMPT = async (
|
export const SYSTEM_PROMPT = async (
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
supportsComputerUse: boolean,
|
supportsComputerUse: boolean,
|
||||||
mcpHub?: McpHub,
|
mcpHub?: McpHub,
|
||||||
@@ -129,7 +93,13 @@ export const SYSTEM_PROMPT = async (
|
|||||||
browserViewportSize?: string,
|
browserViewportSize?: string,
|
||||||
mode: Mode = defaultModeSlug,
|
mode: Mode = defaultModeSlug,
|
||||||
customPrompts?: CustomPrompts,
|
customPrompts?: CustomPrompts,
|
||||||
) => {
|
customModes?: ModeConfig[],
|
||||||
|
globalCustomInstructions?: string,
|
||||||
|
): Promise<string> => {
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("Extension context is required for generating system prompt")
|
||||||
|
}
|
||||||
|
|
||||||
const getPromptComponent = (value: unknown) => {
|
const getPromptComponent = (value: unknown) => {
|
||||||
if (typeof value === "object" && value !== null) {
|
if (typeof value === "object" && value !== null) {
|
||||||
return value as PromptComponent
|
return value as PromptComponent
|
||||||
@@ -137,11 +107,13 @@ export const SYSTEM_PROMPT = async (
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use default mode if not found
|
// Check if it's a custom mode
|
||||||
const currentMode = modes.find((m) => m.slug === mode) || modes[0]
|
const promptComponent = getPromptComponent(customPrompts?.[mode])
|
||||||
const promptComponent = getPromptComponent(customPrompts?.[currentMode.slug])
|
// Get full mode config from custom modes or fall back to built-in modes
|
||||||
|
const currentMode = getModeBySlug(mode, customModes) || modes.find((m) => m.slug === mode) || modes[0]
|
||||||
|
|
||||||
return generatePrompt(
|
return generatePrompt(
|
||||||
|
context,
|
||||||
cwd,
|
cwd,
|
||||||
supportsComputerUse,
|
supportsComputerUse,
|
||||||
currentMode.slug,
|
currentMode.slug,
|
||||||
@@ -149,5 +121,7 @@ export const SYSTEM_PROMPT = async (
|
|||||||
diffStrategy,
|
diffStrategy,
|
||||||
browserViewportSize,
|
browserViewportSize,
|
||||||
promptComponent,
|
promptComponent,
|
||||||
|
customModes,
|
||||||
|
globalCustomInstructions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import { getUseMcpToolDescription } from "./use-mcp-tool"
|
|||||||
import { getAccessMcpResourceDescription } from "./access-mcp-resource"
|
import { getAccessMcpResourceDescription } from "./access-mcp-resource"
|
||||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||||
import { McpHub } from "../../../services/mcp/McpHub"
|
import { McpHub } from "../../../services/mcp/McpHub"
|
||||||
import { Mode, ToolName, getModeConfig, isToolAllowedForMode } from "../../../shared/modes"
|
import { Mode, ModeConfig, getModeConfig, isToolAllowedForMode } from "../../../shared/modes"
|
||||||
|
import { ToolName, getToolName, getToolOptions, TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "../../../shared/tool-groups"
|
||||||
import { ToolArgs } from "./types"
|
import { ToolArgs } from "./types"
|
||||||
|
|
||||||
// Map of tool names to their description functions
|
// Map of tool names to their description functions
|
||||||
@@ -38,8 +39,9 @@ export function getToolDescriptionsForMode(
|
|||||||
diffStrategy?: DiffStrategy,
|
diffStrategy?: DiffStrategy,
|
||||||
browserViewportSize?: string,
|
browserViewportSize?: string,
|
||||||
mcpHub?: McpHub,
|
mcpHub?: McpHub,
|
||||||
|
customModes?: ModeConfig[],
|
||||||
): string {
|
): string {
|
||||||
const config = getModeConfig(mode)
|
const config = getModeConfig(mode, customModes)
|
||||||
const args: ToolArgs = {
|
const args: ToolArgs = {
|
||||||
cwd,
|
cwd,
|
||||||
supportsComputerUse,
|
supportsComputerUse,
|
||||||
@@ -48,16 +50,27 @@ export function getToolDescriptionsForMode(
|
|||||||
mcpHub,
|
mcpHub,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map tool descriptions in the exact order specified in the mode's tools array
|
// Get all tools from the mode's groups and always available tools
|
||||||
const descriptions = config.tools.map(([toolName, toolOptions]) => {
|
const tools = new Set<string>()
|
||||||
|
|
||||||
|
// Add tools from mode's groups
|
||||||
|
config.groups.forEach((group) => {
|
||||||
|
TOOL_GROUPS[group].forEach((tool) => tools.add(tool))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add always available tools
|
||||||
|
ALWAYS_AVAILABLE_TOOLS.forEach((tool) => tools.add(tool))
|
||||||
|
|
||||||
|
// Map tool descriptions for all allowed tools
|
||||||
|
const descriptions = Array.from(tools).map((toolName) => {
|
||||||
const descriptionFn = toolDescriptionMap[toolName]
|
const descriptionFn = toolDescriptionMap[toolName]
|
||||||
if (!descriptionFn || !isToolAllowedForMode(toolName as ToolName, mode)) {
|
if (!descriptionFn || !isToolAllowedForMode(toolName as ToolName, mode, customModes ?? [])) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return descriptionFn({
|
return descriptionFn({
|
||||||
...args,
|
...args,
|
||||||
toolOptions,
|
toolOptions: undefined, // No tool options in group-based approach
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import { Mode } from "../../shared/modes"
|
|
||||||
|
|
||||||
export type { Mode }
|
|
||||||
|
|
||||||
export type ToolName =
|
|
||||||
| "execute_command"
|
|
||||||
| "read_file"
|
|
||||||
| "write_to_file"
|
|
||||||
| "apply_diff"
|
|
||||||
| "search_files"
|
|
||||||
| "list_files"
|
|
||||||
| "list_code_definition_names"
|
|
||||||
| "browser_action"
|
|
||||||
| "use_mcp_tool"
|
|
||||||
| "access_mcp_resource"
|
|
||||||
| "ask_followup_question"
|
|
||||||
| "attempt_completion"
|
|
||||||
|
|
||||||
export const CODE_TOOLS: ToolName[] = [
|
|
||||||
"execute_command",
|
|
||||||
"read_file",
|
|
||||||
"write_to_file",
|
|
||||||
"apply_diff",
|
|
||||||
"search_files",
|
|
||||||
"list_files",
|
|
||||||
"list_code_definition_names",
|
|
||||||
"browser_action",
|
|
||||||
"use_mcp_tool",
|
|
||||||
"access_mcp_resource",
|
|
||||||
"ask_followup_question",
|
|
||||||
"attempt_completion",
|
|
||||||
]
|
|
||||||
|
|
||||||
export const ARCHITECT_TOOLS: ToolName[] = [
|
|
||||||
"read_file",
|
|
||||||
"search_files",
|
|
||||||
"list_files",
|
|
||||||
"list_code_definition_names",
|
|
||||||
"ask_followup_question",
|
|
||||||
"attempt_completion",
|
|
||||||
]
|
|
||||||
|
|
||||||
export const ASK_TOOLS: ToolName[] = [
|
|
||||||
"read_file",
|
|
||||||
"search_files",
|
|
||||||
"list_files",
|
|
||||||
"browser_action",
|
|
||||||
"use_mcp_tool",
|
|
||||||
"access_mcp_resource",
|
|
||||||
"ask_followup_question",
|
|
||||||
"attempt_completion",
|
|
||||||
]
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// Shared tools for architect and ask modes - read-only operations plus MCP and browser tools
|
|
||||||
export const READONLY_ALLOWED_TOOLS = [
|
|
||||||
"read_file",
|
|
||||||
"search_files",
|
|
||||||
"list_files",
|
|
||||||
"list_code_definition_names",
|
|
||||||
"browser_action",
|
|
||||||
"use_mcp_tool",
|
|
||||||
"access_mcp_resource",
|
|
||||||
"ask_followup_question",
|
|
||||||
"attempt_completion",
|
|
||||||
] as const
|
|
||||||
|
|
||||||
// Code mode has access to all tools
|
|
||||||
export const CODE_ALLOWED_TOOLS = [
|
|
||||||
"execute_command",
|
|
||||||
"read_file",
|
|
||||||
"write_to_file",
|
|
||||||
"apply_diff",
|
|
||||||
"search_files",
|
|
||||||
"list_files",
|
|
||||||
"list_code_definition_names",
|
|
||||||
"browser_action",
|
|
||||||
"use_mcp_tool",
|
|
||||||
"access_mcp_resource",
|
|
||||||
"ask_followup_question",
|
|
||||||
"attempt_completion",
|
|
||||||
] as const
|
|
||||||
|
|
||||||
// Tool name types for type safety
|
|
||||||
export type ReadOnlyToolName = (typeof READONLY_ALLOWED_TOOLS)[number]
|
|
||||||
export type ToolName = (typeof CODE_ALLOWED_TOOLS)[number]
|
|
||||||
@@ -16,9 +16,9 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
|
|||||||
import { findLast } from "../../shared/array"
|
import { findLast } from "../../shared/array"
|
||||||
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
|
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||||
import { HistoryItem } from "../../shared/HistoryItem"
|
import { HistoryItem } from "../../shared/HistoryItem"
|
||||||
import { WebviewMessage, PromptMode } from "../../shared/WebviewMessage"
|
import { WebviewMessage } from "../../shared/WebviewMessage"
|
||||||
import { defaultModeSlug, defaultPrompts } from "../../shared/modes"
|
import { defaultModeSlug } from "../../shared/modes"
|
||||||
import { SYSTEM_PROMPT, addCustomInstructions } from "../prompts/system"
|
import { SYSTEM_PROMPT } from "../prompts/system"
|
||||||
import { fileExistsAtPath } from "../../utils/fs"
|
import { fileExistsAtPath } from "../../utils/fs"
|
||||||
import { Cline } from "../Cline"
|
import { Cline } from "../Cline"
|
||||||
import { openMention } from "../mentions"
|
import { openMention } from "../mentions"
|
||||||
@@ -29,7 +29,8 @@ import { checkExistKey } from "../../shared/checkExistApiConfig"
|
|||||||
import { enhancePrompt } from "../../utils/enhance-prompt"
|
import { enhancePrompt } from "../../utils/enhance-prompt"
|
||||||
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
|
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
|
||||||
import { ConfigManager } from "../config/ConfigManager"
|
import { ConfigManager } from "../config/ConfigManager"
|
||||||
import { Mode, modes, CustomPrompts, PromptComponent, enhance } from "../../shared/modes"
|
import { CustomModesManager } from "../config/CustomModesManager"
|
||||||
|
import { Mode, modes, CustomPrompts, PromptComponent, enhance, ModeConfig } from "../../shared/modes"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||||
@@ -100,6 +101,7 @@ type GlobalStateKey =
|
|||||||
| "enhancementApiConfigId"
|
| "enhancementApiConfigId"
|
||||||
| "experimentalDiffStrategy"
|
| "experimentalDiffStrategy"
|
||||||
| "autoApprovalEnabled"
|
| "autoApprovalEnabled"
|
||||||
|
| "customModes" // Array of custom modes
|
||||||
|
|
||||||
export const GlobalFileNames = {
|
export const GlobalFileNames = {
|
||||||
apiConversationHistory: "api_conversation_history.json",
|
apiConversationHistory: "api_conversation_history.json",
|
||||||
@@ -118,8 +120,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
private cline?: Cline
|
private cline?: Cline
|
||||||
private workspaceTracker?: WorkspaceTracker
|
private workspaceTracker?: WorkspaceTracker
|
||||||
mcpHub?: McpHub
|
mcpHub?: McpHub
|
||||||
private latestAnnouncementId = "jan-13-2025-custom-prompt" // update to some unique identifier when we add a new announcement
|
private latestAnnouncementId = "jan-21-2025-custom-modes" // update to some unique identifier when we add a new announcement
|
||||||
configManager: ConfigManager
|
configManager: ConfigManager
|
||||||
|
customModesManager: CustomModesManager
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly context: vscode.ExtensionContext,
|
readonly context: vscode.ExtensionContext,
|
||||||
@@ -130,6 +133,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
this.workspaceTracker = new WorkspaceTracker(this)
|
this.workspaceTracker = new WorkspaceTracker(this)
|
||||||
this.mcpHub = new McpHub(this)
|
this.mcpHub = new McpHub(this)
|
||||||
this.configManager = new ConfigManager(this.context)
|
this.configManager = new ConfigManager(this.context)
|
||||||
|
this.customModesManager = new CustomModesManager(this.context, async () => {
|
||||||
|
await this.postStateToWebview()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -155,6 +161,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
this.workspaceTracker = undefined
|
this.workspaceTracker = undefined
|
||||||
this.mcpHub?.dispose()
|
this.mcpHub?.dispose()
|
||||||
this.mcpHub = undefined
|
this.mcpHub = undefined
|
||||||
|
this.customModesManager?.dispose()
|
||||||
this.outputChannel.appendLine("Disposed all disposables")
|
this.outputChannel.appendLine("Disposed all disposables")
|
||||||
ClineProvider.activeInstances.delete(this)
|
ClineProvider.activeInstances.delete(this)
|
||||||
}
|
}
|
||||||
@@ -258,8 +265,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
const modePrompt = customPrompts?.[mode]
|
const modePrompt = customPrompts?.[mode]
|
||||||
const modeInstructions = typeof modePrompt === "object" ? modePrompt.customInstructions : undefined
|
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
|
||||||
const effectiveInstructions = [globalInstructions, modeInstructions].filter(Boolean).join("\n\n")
|
|
||||||
|
|
||||||
this.cline = new Cline(
|
this.cline = new Cline(
|
||||||
this,
|
this,
|
||||||
@@ -287,8 +293,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
const modePrompt = customPrompts?.[mode]
|
const modePrompt = customPrompts?.[mode]
|
||||||
const modeInstructions = typeof modePrompt === "object" ? modePrompt.customInstructions : undefined
|
const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
|
||||||
const effectiveInstructions = [globalInstructions, modeInstructions].filter(Boolean).join("\n\n")
|
|
||||||
|
|
||||||
this.cline = new Cline(
|
this.cline = new Cline(
|
||||||
this,
|
this,
|
||||||
@@ -377,7 +382,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} data:; script-src 'nonce-${nonce}';">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} data:; script-src 'nonce-${nonce}';">
|
||||||
<link rel="stylesheet" type="text/css" href="${stylesUri}">
|
<link rel="stylesheet" type="text/css" href="${stylesUri}">
|
||||||
<link href="${codiconsUri}" rel="stylesheet" />
|
<link href="${codiconsUri}" rel="stylesheet" />
|
||||||
<title>Cline</title>
|
<title>Roo Code</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
@@ -399,6 +404,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
async (message: WebviewMessage) => {
|
async (message: WebviewMessage) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "webviewDidLaunch":
|
case "webviewDidLaunch":
|
||||||
|
// Load custom modes first
|
||||||
|
const customModes = await this.customModesManager.getCustomModes()
|
||||||
|
await this.updateGlobalState("customModes", customModes)
|
||||||
|
|
||||||
this.postStateToWebview()
|
this.postStateToWebview()
|
||||||
this.workspaceTracker?.initializeFilePaths() // don't await
|
this.workspaceTracker?.initializeFilePaths() // don't await
|
||||||
getTheme().then((theme) =>
|
getTheme().then((theme) =>
|
||||||
@@ -958,26 +967,35 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ""
|
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ""
|
||||||
|
|
||||||
const mode = message.mode ?? defaultModeSlug
|
const mode = message.mode ?? defaultModeSlug
|
||||||
const instructions = await addCustomInstructions(
|
const customModes = await this.customModesManager.getCustomModes()
|
||||||
{ customInstructions, customPrompts, preferredLanguage },
|
|
||||||
cwd,
|
const modePrompt = customPrompts?.[mode]
|
||||||
mode,
|
const effectiveInstructions = [customInstructions, modePrompt?.customInstructions]
|
||||||
)
|
.filter(Boolean)
|
||||||
|
.join("\n\n")
|
||||||
|
|
||||||
const systemPrompt = await SYSTEM_PROMPT(
|
const systemPrompt = await SYSTEM_PROMPT(
|
||||||
|
this.context,
|
||||||
cwd,
|
cwd,
|
||||||
apiConfiguration.openRouterModelInfo?.supportsComputerUse ?? false,
|
apiConfiguration.openRouterModelInfo?.supportsComputerUse ?? false,
|
||||||
mcpEnabled ? this.mcpHub : undefined,
|
mcpEnabled ? this.mcpHub : undefined,
|
||||||
undefined,
|
undefined,
|
||||||
browserViewportSize ?? "900x600",
|
browserViewportSize ?? "900x600",
|
||||||
mode,
|
mode,
|
||||||
customPrompts,
|
{
|
||||||
|
...customPrompts,
|
||||||
|
[mode]: {
|
||||||
|
...(modePrompt ?? {}),
|
||||||
|
customInstructions: undefined, // Prevent double-inclusion
|
||||||
|
},
|
||||||
|
},
|
||||||
|
customModes,
|
||||||
|
effectiveInstructions || undefined,
|
||||||
)
|
)
|
||||||
const fullPrompt = instructions ? `${systemPrompt}${instructions}` : systemPrompt
|
|
||||||
|
|
||||||
await this.postMessageToWebview({
|
await this.postMessageToWebview({
|
||||||
type: "systemPrompt",
|
type: "systemPrompt",
|
||||||
text: fullPrompt,
|
text: systemPrompt,
|
||||||
mode: message.mode,
|
mode: message.mode,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1115,6 +1133,34 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
await this.cline.updateDiffStrategy(message.bool ?? false)
|
await this.cline.updateDiffStrategy(message.bool ?? false)
|
||||||
}
|
}
|
||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
|
break
|
||||||
|
case "updateCustomMode":
|
||||||
|
if (message.modeConfig) {
|
||||||
|
await this.customModesManager.updateCustomMode(message.modeConfig.slug, message.modeConfig)
|
||||||
|
// Update state after saving the mode
|
||||||
|
const customModes = await this.customModesManager.getCustomModes()
|
||||||
|
await this.updateGlobalState("customModes", customModes)
|
||||||
|
await this.updateGlobalState("mode", message.modeConfig.slug)
|
||||||
|
await this.postStateToWebview()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "deleteCustomMode":
|
||||||
|
if (message.slug) {
|
||||||
|
const answer = await vscode.window.showInformationMessage(
|
||||||
|
"Are you sure you want to delete this custom mode?",
|
||||||
|
{ modal: true },
|
||||||
|
"Yes",
|
||||||
|
)
|
||||||
|
|
||||||
|
if (answer !== "Yes") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.customModesManager.deleteCustomMode(message.slug)
|
||||||
|
// Switch back to default mode after deletion
|
||||||
|
await this.updateGlobalState("mode", defaultModeSlug)
|
||||||
|
await this.postStateToWebview()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
@@ -1727,6 +1773,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
enhancementApiConfigId,
|
enhancementApiConfigId,
|
||||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||||
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
||||||
|
customModes: await this.customModesManager.getCustomModes(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1844,6 +1891,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
enhancementApiConfigId,
|
enhancementApiConfigId,
|
||||||
experimentalDiffStrategy,
|
experimentalDiffStrategy,
|
||||||
autoApprovalEnabled,
|
autoApprovalEnabled,
|
||||||
|
customModes,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||||
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
||||||
@@ -1906,6 +1954,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
this.getGlobalState("enhancementApiConfigId") as Promise<string | undefined>,
|
this.getGlobalState("enhancementApiConfigId") as Promise<string | undefined>,
|
||||||
this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>,
|
this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>,
|
||||||
this.getGlobalState("autoApprovalEnabled") as Promise<boolean | undefined>,
|
this.getGlobalState("autoApprovalEnabled") as Promise<boolean | undefined>,
|
||||||
|
this.customModesManager.getCustomModes(),
|
||||||
])
|
])
|
||||||
|
|
||||||
let apiProvider: ApiProvider
|
let apiProvider: ApiProvider
|
||||||
@@ -2014,6 +2063,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
enhancementApiConfigId,
|
enhancementApiConfigId,
|
||||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||||
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
||||||
|
customModes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2107,6 +2157,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
await this.storeSecret(key, undefined)
|
await this.storeSecret(key, undefined)
|
||||||
}
|
}
|
||||||
await this.configManager.resetAllConfigs()
|
await this.configManager.resetAllConfigs()
|
||||||
|
await this.customModesManager.resetCustomModes()
|
||||||
if (this.cline) {
|
if (this.cline) {
|
||||||
this.cline.abortTask()
|
this.cline.abortTask()
|
||||||
this.cline = undefined
|
this.cline = undefined
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import * as vscode from "vscode"
|
|||||||
import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
|
import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
|
||||||
import { setSoundEnabled } from "../../../utils/sound"
|
import { setSoundEnabled } from "../../../utils/sound"
|
||||||
import { defaultModeSlug, modes } from "../../../shared/modes"
|
import { defaultModeSlug, modes } from "../../../shared/modes"
|
||||||
|
import { addCustomInstructions } from "../../prompts/sections/custom-instructions"
|
||||||
|
|
||||||
|
// Mock custom-instructions module
|
||||||
|
const mockAddCustomInstructions = jest.fn()
|
||||||
|
jest.mock("../../prompts/sections/custom-instructions", () => ({
|
||||||
|
addCustomInstructions: mockAddCustomInstructions,
|
||||||
|
}))
|
||||||
|
|
||||||
// Mock delay module
|
// Mock delay module
|
||||||
jest.mock("delay", () => {
|
jest.mock("delay", () => {
|
||||||
@@ -130,7 +137,6 @@ jest.mock("../../../api", () => ({
|
|||||||
jest.mock("../../prompts/system", () => ({
|
jest.mock("../../prompts/system", () => ({
|
||||||
SYSTEM_PROMPT: jest.fn().mockImplementation(async () => "mocked system prompt"),
|
SYSTEM_PROMPT: jest.fn().mockImplementation(async () => "mocked system prompt"),
|
||||||
codeMode: "code",
|
codeMode: "code",
|
||||||
addCustomInstructions: jest.fn().mockImplementation(async () => ""),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock WorkspaceTracker
|
// Mock WorkspaceTracker
|
||||||
@@ -221,6 +227,13 @@ describe("ClineProvider", () => {
|
|||||||
},
|
},
|
||||||
} as unknown as vscode.ExtensionContext
|
} as unknown as vscode.ExtensionContext
|
||||||
|
|
||||||
|
// Mock CustomModesManager
|
||||||
|
const mockCustomModesManager = {
|
||||||
|
updateCustomMode: jest.fn().mockResolvedValue(undefined),
|
||||||
|
getCustomModes: jest.fn().mockResolvedValue({}),
|
||||||
|
dispose: jest.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
// Mock output channel
|
// Mock output channel
|
||||||
mockOutputChannel = {
|
mockOutputChannel = {
|
||||||
appendLine: jest.fn(),
|
appendLine: jest.fn(),
|
||||||
@@ -250,6 +263,8 @@ describe("ClineProvider", () => {
|
|||||||
} as unknown as vscode.WebviewView
|
} as unknown as vscode.WebviewView
|
||||||
|
|
||||||
provider = new ClineProvider(mockContext, mockOutputChannel)
|
provider = new ClineProvider(mockContext, mockOutputChannel)
|
||||||
|
// @ts-ignore - accessing private property for testing
|
||||||
|
provider.customModesManager = mockCustomModesManager
|
||||||
})
|
})
|
||||||
|
|
||||||
test("constructor initializes correctly", () => {
|
test("constructor initializes correctly", () => {
|
||||||
@@ -297,6 +312,7 @@ describe("ClineProvider", () => {
|
|||||||
mcpEnabled: true,
|
mcpEnabled: true,
|
||||||
requestDelaySeconds: 5,
|
requestDelaySeconds: 5,
|
||||||
mode: defaultModeSlug,
|
mode: defaultModeSlug,
|
||||||
|
customModes: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const message: ExtensionMessage = {
|
const message: ExtensionMessage = {
|
||||||
@@ -831,6 +847,13 @@ describe("ClineProvider", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockPostMessage.mockClear()
|
mockPostMessage.mockClear()
|
||||||
provider.resolveWebviewView(mockWebviewView)
|
provider.resolveWebviewView(mockWebviewView)
|
||||||
|
// Reset and setup mock
|
||||||
|
mockAddCustomInstructions.mockClear()
|
||||||
|
mockAddCustomInstructions.mockImplementation(
|
||||||
|
(modeInstructions: string, globalInstructions: string, cwd: string) => {
|
||||||
|
return Promise.resolve(modeInstructions || globalInstructions || "")
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const getMessageHandler = () => {
|
const getMessageHandler = () => {
|
||||||
@@ -913,77 +936,132 @@ describe("ClineProvider", () => {
|
|||||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to get system prompt")
|
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to get system prompt")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("uses mode-specific custom instructions in system prompt", async () => {
|
test("uses code mode custom instructions", async () => {
|
||||||
const systemPrompt = require("../../prompts/system")
|
// Get the mock function
|
||||||
const { addCustomInstructions } = systemPrompt
|
const mockAddCustomInstructions = (jest.requireMock("../../prompts/sections/custom-instructions") as any)
|
||||||
|
.addCustomInstructions
|
||||||
|
|
||||||
// Mock getState to return mode-specific custom instructions
|
// Clear any previous calls
|
||||||
jest.spyOn(provider, "getState").mockResolvedValue({
|
mockAddCustomInstructions.mockClear()
|
||||||
apiConfiguration: {
|
|
||||||
apiProvider: "openrouter",
|
|
||||||
openRouterModelInfo: { supportsComputerUse: true },
|
|
||||||
},
|
|
||||||
customPrompts: {
|
|
||||||
code: { customInstructions: "Code mode specific instructions" },
|
|
||||||
},
|
|
||||||
mode: "code",
|
|
||||||
mcpEnabled: false,
|
|
||||||
browserViewportSize: "900x600",
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
// Mock SYSTEM_PROMPT
|
||||||
await messageHandler({ type: "getSystemPrompt", mode: "code" })
|
const systemPromptModule = require("../../prompts/system")
|
||||||
|
jest.spyOn(systemPromptModule, "SYSTEM_PROMPT").mockImplementation(async () => {
|
||||||
|
await mockAddCustomInstructions("Code mode specific instructions", "", "/mock/path")
|
||||||
|
return "mocked system prompt"
|
||||||
|
})
|
||||||
|
|
||||||
// Verify addCustomInstructions was called with mode-specific instructions
|
// Trigger getSystemPrompt
|
||||||
expect(addCustomInstructions).toHaveBeenCalledWith(
|
const promptHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||||
{
|
await promptHandler({ type: "getSystemPrompt" })
|
||||||
customInstructions: undefined,
|
|
||||||
customPrompts: {
|
// Verify mock was called with code mode instructions
|
||||||
code: { customInstructions: "Code mode specific instructions" },
|
expect(mockAddCustomInstructions).toHaveBeenCalledWith(
|
||||||
},
|
"Code mode specific instructions",
|
||||||
preferredLanguage: undefined,
|
"",
|
||||||
},
|
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
"code",
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("uses correct mode-specific instructions when mode is specified", async () => {
|
test("uses correct mode-specific instructions when mode is specified", async () => {
|
||||||
const systemPrompt = require("../../prompts/system")
|
// Mock getState to return architect mode instructions
|
||||||
const { addCustomInstructions } = systemPrompt
|
|
||||||
|
|
||||||
// Mock getState to return instructions for multiple modes
|
|
||||||
jest.spyOn(provider, "getState").mockResolvedValue({
|
jest.spyOn(provider, "getState").mockResolvedValue({
|
||||||
apiConfiguration: {
|
apiConfiguration: {
|
||||||
apiProvider: "openrouter",
|
apiProvider: "openrouter",
|
||||||
openRouterModelInfo: { supportsComputerUse: true },
|
openRouterModelInfo: { supportsComputerUse: true },
|
||||||
},
|
},
|
||||||
customPrompts: {
|
customPrompts: {
|
||||||
code: { customInstructions: "Code mode instructions" },
|
|
||||||
architect: { customInstructions: "Architect mode instructions" },
|
architect: { customInstructions: "Architect mode instructions" },
|
||||||
},
|
},
|
||||||
mode: "code",
|
mode: "architect",
|
||||||
mcpEnabled: false,
|
mcpEnabled: false,
|
||||||
browserViewportSize: "900x600",
|
browserViewportSize: "900x600",
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
// Mock SYSTEM_PROMPT to call addCustomInstructions
|
||||||
|
const systemPromptModule = require("../../prompts/system")
|
||||||
|
jest.spyOn(systemPromptModule, "SYSTEM_PROMPT").mockImplementation(async () => {
|
||||||
|
await mockAddCustomInstructions("Architect mode instructions", "", "/mock/path")
|
||||||
|
return "mocked system prompt"
|
||||||
|
})
|
||||||
|
|
||||||
// Request architect mode prompt
|
// Resolve webview and trigger getSystemPrompt
|
||||||
await messageHandler({ type: "getSystemPrompt", mode: "architect" })
|
provider.resolveWebviewView(mockWebviewView)
|
||||||
|
const architectHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||||
|
await architectHandler({ type: "getSystemPrompt" })
|
||||||
|
|
||||||
// Verify architect mode instructions were used
|
// Verify architect mode instructions were used
|
||||||
expect(addCustomInstructions).toHaveBeenCalledWith(
|
expect(mockAddCustomInstructions).toHaveBeenCalledWith(
|
||||||
{
|
"Architect mode instructions",
|
||||||
customInstructions: undefined,
|
"",
|
||||||
customPrompts: {
|
|
||||||
code: { customInstructions: "Code mode instructions" },
|
|
||||||
architect: { customInstructions: "Architect mode instructions" },
|
|
||||||
},
|
|
||||||
preferredLanguage: undefined,
|
|
||||||
},
|
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
"architect",
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("updateCustomMode", () => {
|
||||||
|
test("updates both file and state when updating custom mode", async () => {
|
||||||
|
provider.resolveWebviewView(mockWebviewView)
|
||||||
|
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||||
|
|
||||||
|
// Mock CustomModesManager methods
|
||||||
|
provider.customModesManager = {
|
||||||
|
updateCustomMode: jest.fn().mockResolvedValue(undefined),
|
||||||
|
getCustomModes: jest.fn().mockResolvedValue({
|
||||||
|
"test-mode": {
|
||||||
|
slug: "test-mode",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Updated role definition",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
dispose: jest.fn(),
|
||||||
|
} as any
|
||||||
|
|
||||||
|
// Test updating a custom mode
|
||||||
|
await messageHandler({
|
||||||
|
type: "updateCustomMode",
|
||||||
|
modeConfig: {
|
||||||
|
slug: "test-mode",
|
||||||
|
name: "Test Mode",
|
||||||
|
roleDefinition: "Updated role definition",
|
||||||
|
groups: ["read"] as const,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify CustomModesManager.updateCustomMode was called
|
||||||
|
expect(provider.customModesManager.updateCustomMode).toHaveBeenCalledWith(
|
||||||
|
"test-mode",
|
||||||
|
expect.objectContaining({
|
||||||
|
slug: "test-mode",
|
||||||
|
roleDefinition: "Updated role definition",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify state was updated
|
||||||
|
expect(mockContext.globalState.update).toHaveBeenCalledWith(
|
||||||
|
"customModes",
|
||||||
|
expect.objectContaining({
|
||||||
|
"test-mode": expect.objectContaining({
|
||||||
|
slug: "test-mode",
|
||||||
|
roleDefinition: "Updated role definition",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify state was posted to webview
|
||||||
|
expect(mockPostMessage).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "state",
|
||||||
|
state: expect.objectContaining({
|
||||||
|
customModes: expect.objectContaining({
|
||||||
|
"test-mode": expect.objectContaining({
|
||||||
|
slug: "test-mode",
|
||||||
|
roleDefinition: "Updated role definition",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ let outputChannel: vscode.OutputChannel
|
|||||||
// This method is called when your extension is activated
|
// This method is called when your extension is activated
|
||||||
// Your extension is activated the very first time the command is executed
|
// Your extension is activated the very first time the command is executed
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
outputChannel = vscode.window.createOutputChannel("Roo-Cline")
|
outputChannel = vscode.window.createOutputChannel("Roo-Code")
|
||||||
context.subscriptions.push(outputChannel)
|
context.subscriptions.push(outputChannel)
|
||||||
|
|
||||||
outputChannel.appendLine("Roo-Cline extension activated")
|
outputChannel.appendLine("Roo-Code extension activated")
|
||||||
|
|
||||||
// Get default commands from configuration
|
// Get default commands from configuration
|
||||||
const defaultCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
|
const defaultCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
|
||||||
@@ -64,7 +64,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const openClineInNewTab = async () => {
|
const openClineInNewTab = async () => {
|
||||||
outputChannel.appendLine("Opening Cline in new tab")
|
outputChannel.appendLine("Opening Roo Code in new tab")
|
||||||
// (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event)
|
// (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event)
|
||||||
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
|
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
|
||||||
const tabProvider = new ClineProvider(context, outputChannel)
|
const tabProvider = new ClineProvider(context, outputChannel)
|
||||||
@@ -78,7 +78,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
}
|
}
|
||||||
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
|
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
|
||||||
|
|
||||||
const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Cline", targetCol, {
|
const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, {
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
retainContextWhenHidden: true,
|
retainContextWhenHidden: true,
|
||||||
localResourceRoots: [context.extensionUri],
|
localResourceRoots: [context.extensionUri],
|
||||||
@@ -163,5 +163,5 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
// This method is called when your extension is deactivated
|
// This method is called when your extension is deactivated
|
||||||
export function deactivate() {
|
export function deactivate() {
|
||||||
outputChannel.appendLine("Roo-Cline extension deactivated")
|
outputChannel.appendLine("Roo-Code extension deactivated")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,17 +161,17 @@ export class DiffViewProvider {
|
|||||||
Getting diagnostics before and after the file edit is a better approach than
|
Getting diagnostics before and after the file edit is a better approach than
|
||||||
automatically tracking problems in real-time. This method ensures we only
|
automatically tracking problems in real-time. This method ensures we only
|
||||||
report new problems that are a direct result of this specific edit.
|
report new problems that are a direct result of this specific edit.
|
||||||
Since these are new problems resulting from Cline's edit, we know they're
|
Since these are new problems resulting from Roo's edit, we know they're
|
||||||
directly related to the work he's doing. This eliminates the risk of Cline
|
directly related to the work he's doing. This eliminates the risk of Roo
|
||||||
going off-task or getting distracted by unrelated issues, which was a problem
|
going off-task or getting distracted by unrelated issues, which was a problem
|
||||||
with the previous auto-debug approach. Some users' machines may be slow to
|
with the previous auto-debug approach. Some users' machines may be slow to
|
||||||
update diagnostics, so this approach provides a good balance between automation
|
update diagnostics, so this approach provides a good balance between automation
|
||||||
and avoiding potential issues where Cline might get stuck in loops due to
|
and avoiding potential issues where Roo might get stuck in loops due to
|
||||||
outdated problem information. If no new problems show up by the time the user
|
outdated problem information. If no new problems show up by the time the user
|
||||||
accepts the changes, they can always debug later using the '@problems' mention.
|
accepts the changes, they can always debug later using the '@problems' mention.
|
||||||
This way, Cline only becomes aware of new problems resulting from his edits
|
This way, Roo only becomes aware of new problems resulting from his edits
|
||||||
and can address them accordingly. If problems don't change immediately after
|
and can address them accordingly. If problems don't change immediately after
|
||||||
applying a fix, Cline won't be notified, which is generally fine since the
|
applying a fix, won't be notified, which is generally fine since the
|
||||||
initial fix is usually correct and it may just take time for linters to catch up.
|
initial fix is usually correct and it may just take time for linters to catch up.
|
||||||
*/
|
*/
|
||||||
const postDiagnostics = vscode.languages.getDiagnostics()
|
const postDiagnostics = vscode.languages.getDiagnostics()
|
||||||
@@ -297,7 +297,7 @@ export class DiffViewProvider {
|
|||||||
query: Buffer.from(this.originalContent ?? "").toString("base64"),
|
query: Buffer.from(this.originalContent ?? "").toString("base64"),
|
||||||
}),
|
}),
|
||||||
uri,
|
uri,
|
||||||
`${fileName}: ${fileExists ? "Original ↔ Cline's Changes" : "New File"} (Editable)`,
|
`${fileName}: ${fileExists ? "Original ↔ Roo's Changes" : "New File"} (Editable)`,
|
||||||
)
|
)
|
||||||
// This may happen on very slow machines ie project idx
|
// This may happen on very slow machines ie project idx
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export class TerminalRegistry {
|
|||||||
static createTerminal(cwd?: string | vscode.Uri | undefined): TerminalInfo {
|
static createTerminal(cwd?: string | vscode.Uri | undefined): TerminalInfo {
|
||||||
const terminal = vscode.window.createTerminal({
|
const terminal = vscode.window.createTerminal({
|
||||||
cwd,
|
cwd,
|
||||||
name: "Roo Cline",
|
name: "Roo Code",
|
||||||
iconPath: new vscode.ThemeIcon("rocket"),
|
iconPath: new vscode.ThemeIcon("rocket"),
|
||||||
env: {
|
env: {
|
||||||
PAGER: "cat",
|
PAGER: "cat",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ describe("TerminalRegistry", () => {
|
|||||||
|
|
||||||
expect(mockCreateTerminal).toHaveBeenCalledWith({
|
expect(mockCreateTerminal).toHaveBeenCalledWith({
|
||||||
cwd: "/test/path",
|
cwd: "/test/path",
|
||||||
name: "Roo Cline",
|
name: "Roo Code",
|
||||||
iconPath: expect.any(Object),
|
iconPath: expect.any(Object),
|
||||||
env: {
|
env: {
|
||||||
PAGER: "cat",
|
PAGER: "cat",
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export class McpHub {
|
|||||||
// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
|
// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
|
||||||
const client = new Client(
|
const client = new Client(
|
||||||
{
|
{
|
||||||
name: "Cline",
|
name: "Roo Code",
|
||||||
version: this.providerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
|
version: this.providerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,14 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "./api"
|
|||||||
import { HistoryItem } from "./HistoryItem"
|
import { HistoryItem } from "./HistoryItem"
|
||||||
import { McpServer } from "./mcp"
|
import { McpServer } from "./mcp"
|
||||||
import { GitCommit } from "../utils/git"
|
import { GitCommit } from "../utils/git"
|
||||||
import { Mode, CustomPrompts } from "./modes"
|
import { Mode, CustomPrompts, ModeConfig } from "./modes"
|
||||||
|
|
||||||
|
export interface LanguageModelChatSelector {
|
||||||
|
vendor?: string
|
||||||
|
family?: string
|
||||||
|
version?: string
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
// webview will hold state
|
// webview will hold state
|
||||||
export interface ExtensionMessage {
|
export interface ExtensionMessage {
|
||||||
@@ -31,6 +38,8 @@ export interface ExtensionMessage {
|
|||||||
| "updatePrompt"
|
| "updatePrompt"
|
||||||
| "systemPrompt"
|
| "systemPrompt"
|
||||||
| "autoApprovalEnabled"
|
| "autoApprovalEnabled"
|
||||||
|
| "updateCustomMode"
|
||||||
|
| "deleteCustomMode"
|
||||||
text?: string
|
text?: string
|
||||||
action?:
|
action?:
|
||||||
| "chatButtonClicked"
|
| "chatButtonClicked"
|
||||||
@@ -54,6 +63,8 @@ export interface ExtensionMessage {
|
|||||||
commits?: GitCommit[]
|
commits?: GitCommit[]
|
||||||
listApiConfig?: ApiConfigMeta[]
|
listApiConfig?: ApiConfigMeta[]
|
||||||
mode?: Mode
|
mode?: Mode
|
||||||
|
customMode?: ModeConfig
|
||||||
|
slug?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiConfigMeta {
|
export interface ApiConfigMeta {
|
||||||
@@ -96,6 +107,7 @@ export interface ExtensionState {
|
|||||||
enhancementApiConfigId?: string
|
enhancementApiConfigId?: string
|
||||||
experimentalDiffStrategy?: boolean
|
experimentalDiffStrategy?: boolean
|
||||||
autoApprovalEnabled?: boolean
|
autoApprovalEnabled?: boolean
|
||||||
|
customModes: ModeConfig[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClineMessage {
|
export interface ClineMessage {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiConfiguration, ApiProvider } from "./api"
|
import { ApiConfiguration, ApiProvider } from "./api"
|
||||||
import { Mode, PromptComponent } from "./modes"
|
import { Mode, PromptComponent, ModeConfig } from "./modes"
|
||||||
|
|
||||||
export type PromptMode = Mode | "enhance"
|
export type PromptMode = Mode | "enhance"
|
||||||
|
|
||||||
@@ -74,6 +74,8 @@ export interface WebviewMessage {
|
|||||||
| "enhancementApiConfigId"
|
| "enhancementApiConfigId"
|
||||||
| "experimentalDiffStrategy"
|
| "experimentalDiffStrategy"
|
||||||
| "autoApprovalEnabled"
|
| "autoApprovalEnabled"
|
||||||
|
| "updateCustomMode"
|
||||||
|
| "deleteCustomMode"
|
||||||
text?: string
|
text?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
askResponse?: ClineAskResponse
|
askResponse?: ClineAskResponse
|
||||||
@@ -92,6 +94,8 @@ export interface WebviewMessage {
|
|||||||
dataUrls?: string[]
|
dataUrls?: string[]
|
||||||
values?: Record<string, any>
|
values?: Record<string, any>
|
||||||
query?: string
|
query?: string
|
||||||
|
slug?: string
|
||||||
|
modeConfig?: ModeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
// Tool options for specific tools
|
import { TOOL_GROUPS, ToolGroup, ALWAYS_AVAILABLE_TOOLS } from "./tool-groups"
|
||||||
export type ToolOptions = {
|
|
||||||
string: readonly string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tool configuration tuple type
|
|
||||||
export type ToolConfig = readonly [string] | readonly [string, ToolOptions]
|
|
||||||
|
|
||||||
// Mode types
|
// Mode types
|
||||||
export type Mode = string
|
export type Mode = string
|
||||||
@@ -14,7 +8,124 @@ export type ModeConfig = {
|
|||||||
slug: string
|
slug: string
|
||||||
name: string
|
name: string
|
||||||
roleDefinition: string
|
roleDefinition: string
|
||||||
tools: readonly ToolConfig[]
|
customInstructions?: string
|
||||||
|
groups: readonly ToolGroup[] // Now uses groups instead of tools array
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get all tools for a mode
|
||||||
|
export function getToolsForMode(groups: readonly ToolGroup[]): string[] {
|
||||||
|
const tools = new Set<string>()
|
||||||
|
|
||||||
|
// Add tools from each group
|
||||||
|
groups.forEach((group) => {
|
||||||
|
TOOL_GROUPS[group].forEach((tool) => tools.add(tool))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Always add required tools
|
||||||
|
ALWAYS_AVAILABLE_TOOLS.forEach((tool) => tools.add(tool))
|
||||||
|
|
||||||
|
return Array.from(tools)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main modes configuration as an ordered array
|
||||||
|
export const modes: readonly ModeConfig[] = [
|
||||||
|
{
|
||||||
|
slug: "code",
|
||||||
|
name: "Code",
|
||||||
|
roleDefinition:
|
||||||
|
"You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.",
|
||||||
|
groups: ["read", "edit", "browser", "command", "mcp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "architect",
|
||||||
|
name: "Architect",
|
||||||
|
roleDefinition:
|
||||||
|
"You are Roo, a software architecture expert specializing in analyzing codebases, identifying patterns, and providing high-level technical guidance. You excel at understanding complex systems, evaluating architectural decisions, and suggesting improvements while maintaining a read-only approach to the codebase. Make sure to help the user come up with a solid implementation plan for their project and don't rush to switch to implementing code.",
|
||||||
|
groups: ["read", "browser", "mcp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "ask",
|
||||||
|
name: "Ask",
|
||||||
|
roleDefinition:
|
||||||
|
"You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. You can analyze code, explain concepts, and access external resources while maintaining a read-only approach to the codebase. Make sure to answer the user's questions and don't rush to switch to implementing code.",
|
||||||
|
groups: ["read", "browser", "mcp"],
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
// Export the default mode slug
|
||||||
|
export const defaultModeSlug = modes[0].slug
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
export function getModeBySlug(slug: string, customModes?: ModeConfig[]): ModeConfig | undefined {
|
||||||
|
// Check custom modes first
|
||||||
|
const customMode = customModes?.find((mode) => mode.slug === slug)
|
||||||
|
if (customMode) {
|
||||||
|
return customMode
|
||||||
|
}
|
||||||
|
// Then check built-in modes
|
||||||
|
return modes.find((mode) => mode.slug === slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getModeConfig(slug: string, customModes?: ModeConfig[]): ModeConfig {
|
||||||
|
const mode = getModeBySlug(slug, customModes)
|
||||||
|
if (!mode) {
|
||||||
|
throw new Error(`No mode found for slug: ${slug}`)
|
||||||
|
}
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all available modes, with custom modes overriding built-in modes
|
||||||
|
export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] {
|
||||||
|
if (!customModes?.length) {
|
||||||
|
return [...modes]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with built-in modes
|
||||||
|
const allModes = [...modes]
|
||||||
|
|
||||||
|
// Process custom modes
|
||||||
|
customModes.forEach((customMode) => {
|
||||||
|
const index = allModes.findIndex((mode) => mode.slug === customMode.slug)
|
||||||
|
if (index !== -1) {
|
||||||
|
// Override existing mode
|
||||||
|
allModes[index] = customMode
|
||||||
|
} else {
|
||||||
|
// Add new mode
|
||||||
|
allModes.push(customMode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return allModes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a mode is custom or an override
|
||||||
|
export function isCustomMode(slug: string, customModes?: ModeConfig[]): boolean {
|
||||||
|
return !!customModes?.some((mode) => mode.slug === slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isToolAllowedForMode(tool: string, modeSlug: string, customModes: ModeConfig[]): boolean {
|
||||||
|
// Always allow these tools
|
||||||
|
if (ALWAYS_AVAILABLE_TOOLS.includes(tool as any)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const mode = getModeBySlug(modeSlug, customModes)
|
||||||
|
if (!mode) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if tool is in any of the mode's groups
|
||||||
|
return mode.groups.some((group) => TOOL_GROUPS[group].includes(tool as string))
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PromptComponent = {
|
||||||
|
roleDefinition?: string
|
||||||
|
customInstructions?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode-specific prompts only
|
||||||
|
export type CustomPrompts = {
|
||||||
|
[key: string]: PromptComponent | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate enhance prompt type and definition
|
// Separate enhance prompt type and definition
|
||||||
@@ -26,127 +137,25 @@ export const enhance: EnhanceConfig = {
|
|||||||
prompt: "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):",
|
prompt: "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// Main modes configuration as an ordered array
|
// Completely separate enhance prompt handling
|
||||||
export const modes: readonly ModeConfig[] = [
|
export const enhancePrompt = {
|
||||||
{
|
default: enhance.prompt,
|
||||||
slug: "code",
|
get: (customPrompts: Record<string, any> | undefined): string => {
|
||||||
name: "Code",
|
return customPrompts?.enhance ?? enhance.prompt
|
||||||
roleDefinition:
|
|
||||||
"You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.",
|
|
||||||
tools: [
|
|
||||||
["execute_command"],
|
|
||||||
["read_file"],
|
|
||||||
["write_to_file"],
|
|
||||||
["apply_diff"],
|
|
||||||
["search_files"],
|
|
||||||
["list_files"],
|
|
||||||
["list_code_definition_names"],
|
|
||||||
["browser_action"],
|
|
||||||
["use_mcp_tool"],
|
|
||||||
["access_mcp_resource"],
|
|
||||||
["ask_followup_question"],
|
|
||||||
["attempt_completion"],
|
|
||||||
] as const,
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
slug: "architect",
|
|
||||||
name: "Architect",
|
|
||||||
roleDefinition:
|
|
||||||
"You are Cline, a software architecture expert specializing in analyzing codebases, identifying patterns, and providing high-level technical guidance. You excel at understanding complex systems, evaluating architectural decisions, and suggesting improvements while maintaining a read-only approach to the codebase. Make sure to help the user come up with a solid implementation plan for their project and don't rush to switch to implementing code.",
|
|
||||||
tools: [
|
|
||||||
["read_file"],
|
|
||||||
["search_files"],
|
|
||||||
["list_files"],
|
|
||||||
["list_code_definition_names"],
|
|
||||||
["browser_action"],
|
|
||||||
["use_mcp_tool"],
|
|
||||||
["access_mcp_resource"],
|
|
||||||
["ask_followup_question"],
|
|
||||||
["attempt_completion"],
|
|
||||||
] as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: "ask",
|
|
||||||
name: "Ask",
|
|
||||||
roleDefinition:
|
|
||||||
"You are Cline, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. You can analyze code, explain concepts, and access external resources while maintaining a read-only approach to the codebase. Make sure to answer the user's questions and don't rush to switch to implementing code.",
|
|
||||||
tools: [
|
|
||||||
["read_file"],
|
|
||||||
["search_files"],
|
|
||||||
["list_files"],
|
|
||||||
["list_code_definition_names"],
|
|
||||||
["browser_action"],
|
|
||||||
["use_mcp_tool"],
|
|
||||||
["access_mcp_resource"],
|
|
||||||
["ask_followup_question"],
|
|
||||||
["attempt_completion"],
|
|
||||||
] as const,
|
|
||||||
},
|
|
||||||
] as const
|
|
||||||
|
|
||||||
// Export the default mode slug
|
|
||||||
export const defaultModeSlug = modes[0].slug
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
export function getModeBySlug(slug: string): ModeConfig | undefined {
|
|
||||||
return modes.find((mode) => mode.slug === slug)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getModeConfig(slug: string): ModeConfig {
|
|
||||||
const mode = getModeBySlug(slug)
|
|
||||||
if (!mode) {
|
|
||||||
throw new Error(`No mode found for slug: ${slug}`)
|
|
||||||
}
|
|
||||||
return mode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derive tool names from the modes configuration
|
|
||||||
export type ToolName = (typeof modes)[number]["tools"][number][0]
|
|
||||||
export type TestToolName = ToolName | "unknown_tool"
|
|
||||||
|
|
||||||
export function isToolAllowedForMode(tool: TestToolName, modeSlug: string): boolean {
|
|
||||||
if (tool === "unknown_tool") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const mode = getModeBySlug(modeSlug)
|
|
||||||
if (!mode) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return mode.tools.some(([toolName]) => toolName === tool)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getToolOptions(tool: ToolName, modeSlug: string): ToolOptions | undefined {
|
|
||||||
const mode = getModeBySlug(modeSlug)
|
|
||||||
if (!mode) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
const toolConfig = mode.tools.find(([toolName]) => toolName === tool)
|
|
||||||
return toolConfig?.[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PromptComponent = {
|
|
||||||
roleDefinition?: string
|
|
||||||
customInstructions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CustomPrompts = {
|
|
||||||
[key: string]: PromptComponent | string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the defaultPrompts object with the correct type
|
|
||||||
export const defaultPrompts: CustomPrompts = {
|
|
||||||
...Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])),
|
|
||||||
enhance: enhance.prompt,
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
// Create the mode-specific default prompts
|
||||||
|
export const defaultPrompts: Readonly<CustomPrompts> = Object.freeze(
|
||||||
|
Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])),
|
||||||
|
)
|
||||||
|
|
||||||
// Helper function to safely get role definition
|
// Helper function to safely get role definition
|
||||||
export function getRoleDefinition(modeSlug: string): string {
|
export function getRoleDefinition(modeSlug: string, customModes?: ModeConfig[]): string {
|
||||||
const prompt = defaultPrompts[modeSlug]
|
const mode = getModeBySlug(modeSlug, customModes)
|
||||||
if (!prompt || typeof prompt === "string") {
|
if (!mode) {
|
||||||
throw new Error(`Invalid mode slug: ${modeSlug}`)
|
console.warn(`No mode found for slug: ${modeSlug}`)
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
if (!prompt.roleDefinition) {
|
return mode.roleDefinition
|
||||||
throw new Error(`No role definition found for mode: ${modeSlug}`)
|
|
||||||
}
|
|
||||||
return prompt.roleDefinition
|
|
||||||
}
|
}
|
||||||
|
|||||||
53
src/shared/tool-groups.ts
Normal file
53
src/shared/tool-groups.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Define tool group values
|
||||||
|
export type ToolGroupValues = readonly string[]
|
||||||
|
|
||||||
|
// Map of tool slugs to their display names
|
||||||
|
export const TOOL_DISPLAY_NAMES = {
|
||||||
|
execute_command: "run commands",
|
||||||
|
read_file: "read files",
|
||||||
|
write_to_file: "write files",
|
||||||
|
apply_diff: "apply changes",
|
||||||
|
search_files: "search files",
|
||||||
|
list_files: "list files",
|
||||||
|
list_code_definition_names: "list definitions",
|
||||||
|
browser_action: "use a browser",
|
||||||
|
use_mcp_tool: "use mcp tools",
|
||||||
|
access_mcp_resource: "access mcp resources",
|
||||||
|
ask_followup_question: "ask questions",
|
||||||
|
attempt_completion: "complete tasks",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// Define available tool groups
|
||||||
|
export const TOOL_GROUPS: Record<string, ToolGroupValues> = {
|
||||||
|
read: ["read_file", "search_files", "list_files", "list_code_definition_names"],
|
||||||
|
edit: ["write_to_file", "apply_diff"],
|
||||||
|
browser: ["browser_action"],
|
||||||
|
command: ["execute_command"],
|
||||||
|
mcp: ["use_mcp_tool", "access_mcp_resource"],
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ToolGroup = keyof typeof TOOL_GROUPS
|
||||||
|
|
||||||
|
// Tools that are always available to all modes
|
||||||
|
export const ALWAYS_AVAILABLE_TOOLS = ["ask_followup_question", "attempt_completion"] as const
|
||||||
|
|
||||||
|
// Tool name types for type safety
|
||||||
|
export type ToolName = keyof typeof TOOL_DISPLAY_NAMES
|
||||||
|
|
||||||
|
// Tool helper functions
|
||||||
|
export function getToolName(toolConfig: string | readonly [ToolName, ...any[]]): ToolName {
|
||||||
|
return typeof toolConfig === "string" ? (toolConfig as ToolName) : toolConfig[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToolOptions(toolConfig: string | readonly [ToolName, ...any[]]): any {
|
||||||
|
return typeof toolConfig === "string" ? undefined : toolConfig[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display names for groups in UI
|
||||||
|
export const GROUP_DISPLAY_NAMES: Record<ToolGroup, string> = {
|
||||||
|
read: "Read Files",
|
||||||
|
edit: "Edit Files",
|
||||||
|
browser: "Use Browser",
|
||||||
|
command: "Run Commands",
|
||||||
|
mcp: "Use MCP",
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ interface AnnouncementProps {
|
|||||||
You must update the latestAnnouncementId in ClineProvider for new announcements to show to users. This new id will be compared with whats in state for the 'last announcement shown', and if it's different then the announcement will render. As soon as an announcement is shown, the id will be updated in state. This ensures that announcements are not shown more than once, even if the user doesn't close it themselves.
|
You must update the latestAnnouncementId in ClineProvider for new announcements to show to users. This new id will be compared with whats in state for the 'last announcement shown', and if it's different then the announcement will render. As soon as an announcement is shown, the id will be updated in state. This ensures that announcements are not shown more than once, even if the user doesn't close it themselves.
|
||||||
*/
|
*/
|
||||||
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||||
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
|
// const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -29,42 +29,36 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
|||||||
style={{ position: "absolute", top: "8px", right: "8px" }}>
|
style={{ position: "absolute", top: "8px", right: "8px" }}>
|
||||||
<span className="codicon codicon-close"></span>
|
<span className="codicon codicon-close"></span>
|
||||||
</VSCodeButton>
|
</VSCodeButton>
|
||||||
<h2 style={{ margin: "0 0 8px" }}>
|
<h2 style={{ margin: "0 0 8px" }}>🎉{" "}Introducing Roo Code 4.0</h2>
|
||||||
🎉{" "}Introducing Roo Cline v{minorVersion}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<h3 style={{ margin: "0 0 8px" }}>Agent Modes Customization</h3>
|
|
||||||
<p style={{ margin: "5px 0px" }}>
|
<p style={{ margin: "5px 0px" }}>
|
||||||
Click the new <span className="codicon codicon-notebook" style={{ fontSize: "10px" }}></span> icon in
|
Our biggest update yet is here - we're officially changing our name from "Roo Cline" to "Roo Code"!
|
||||||
the menu bar to open the Prompts Settings and customize Agent Modes for new levels of productivity.
|
After growing beyond 50,000 installations, we're ready to chart our own course. Our heartfelt thanks to
|
||||||
<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
|
everyone in the Cline community who helped us reach this milestone.
|
||||||
<li>Tailor how Roo Cline behaves in different modes: Code, Architect, and Ask.</li>
|
|
||||||
<li>Preview and verify your changes using the Preview System Prompt button.</li>
|
|
||||||
</ul>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 style={{ margin: "0 0 8px" }}>Prompt Enhancement Configuration</h3>
|
<h3 style={{ margin: "12px 0 8px" }}>Custom Modes: Celebrating Our New Identity</h3>
|
||||||
<p style={{ margin: "5px 0px" }}>
|
<p style={{ margin: "5px 0px" }}>
|
||||||
Now available for all providers! Access it directly in the chat box by clicking the{" "}
|
To mark this new chapter, we're introducing the power to shape Roo Code into any role you need! Create
|
||||||
<span className="codicon codicon-sparkle" style={{ fontSize: "10px" }}></span> sparkle icon next to the
|
specialized personas and create an entire team of agents with deeply customized prompts:
|
||||||
input field. From there, you can customize the enhancement logic and provider to best suit your
|
|
||||||
workflow.
|
|
||||||
<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
|
<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
|
||||||
<li>Customize how prompts are enhanced for better results in your workflow.</li>
|
<li>QA Engineers who write thorough test cases and catch edge cases</li>
|
||||||
<li>
|
<li>Product Managers who excel at user stories and feature prioritization</li>
|
||||||
Use the sparkle icon in the chat box to select a API configuration and provider (e.g., GPT-4)
|
<li>UI/UX Designers who craft beautiful, accessible interfaces</li>
|
||||||
and configure your own enhancement logic.
|
<li>Code Reviewers who ensure quality and maintainability</li>
|
||||||
</li>
|
|
||||||
<li>Test your changes instantly with the Preview Prompt Enhancement tool.</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
Just click the <span className="codicon codicon-notebook" style={{ fontSize: "10px" }}></span> icon to
|
||||||
|
get started with Custom Modes!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h3 style={{ margin: "12px 0 8px" }}>Join Us for the Next Chapter</h3>
|
||||||
<p style={{ margin: "5px 0px" }}>
|
<p style={{ margin: "5px 0px" }}>
|
||||||
We're very excited to see what you build with this new feature! Join us at
|
We can't wait to see how you'll push Roo Code's potential even further! Share your custom modes and join
|
||||||
<VSCodeLink href="https://www.reddit.com/r/roocline" style={{ display: "inline" }}>
|
the discussion at{" "}
|
||||||
reddit.com/r/roocline
|
<VSCodeLink href="https://www.reddit.com/r/RooCode" style={{ display: "inline" }}>
|
||||||
|
reddit.com/r/RooCode
|
||||||
</VSCodeLink>
|
</VSCodeLink>
|
||||||
to discuss and share feedback.
|
.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
|
|||||||
color: "var(--vscode-descriptionForeground)",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
fontSize: "12px",
|
fontSize: "12px",
|
||||||
}}>
|
}}>
|
||||||
Auto-approve allows Cline to perform actions without asking for permission. Only enable for
|
Auto-approve allows Roo Code to perform actions without asking for permission. Only enable for
|
||||||
actions you fully trust.
|
actions you fully trust.
|
||||||
</div>
|
</div>
|
||||||
{actions.map((action) => (
|
{actions.map((action) => (
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||||||
style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}></span>
|
style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}></span>
|
||||||
)}
|
)}
|
||||||
<span style={{ fontWeight: "bold" }}>
|
<span style={{ fontWeight: "bold" }}>
|
||||||
<>Cline wants to use the browser:</>
|
<>Roo wants to use the browser:</>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export const ChatRowContent = ({
|
|||||||
<span
|
<span
|
||||||
className="codicon codicon-error"
|
className="codicon codicon-error"
|
||||||
style={{ color: errorColor, marginBottom: "-1.5px" }}></span>,
|
style={{ color: errorColor, marginBottom: "-1.5px" }}></span>,
|
||||||
<span style={{ color: errorColor, fontWeight: "bold" }}>Cline is having trouble...</span>,
|
<span style={{ color: errorColor, fontWeight: "bold" }}>Roo is having trouble...</span>,
|
||||||
]
|
]
|
||||||
case "command":
|
case "command":
|
||||||
return [
|
return [
|
||||||
@@ -128,9 +128,7 @@ export const ChatRowContent = ({
|
|||||||
className="codicon codicon-terminal"
|
className="codicon codicon-terminal"
|
||||||
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
|
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
|
||||||
),
|
),
|
||||||
<span style={{ color: normalColor, fontWeight: "bold" }}>
|
<span style={{ color: normalColor, fontWeight: "bold" }}>Roo wants to execute this command:</span>,
|
||||||
Cline wants to execute this command:
|
|
||||||
</span>,
|
|
||||||
]
|
]
|
||||||
case "use_mcp_server":
|
case "use_mcp_server":
|
||||||
const mcpServerUse = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
|
const mcpServerUse = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
|
||||||
@@ -143,8 +141,8 @@ export const ChatRowContent = ({
|
|||||||
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
|
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
|
||||||
),
|
),
|
||||||
<span style={{ color: normalColor, fontWeight: "bold" }}>
|
<span style={{ color: normalColor, fontWeight: "bold" }}>
|
||||||
Cline wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on
|
Roo wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "}
|
||||||
the <code>{mcpServerUse.serverName}</code> MCP server:
|
<code>{mcpServerUse.serverName}</code> MCP server:
|
||||||
</span>,
|
</span>,
|
||||||
]
|
]
|
||||||
case "completion_result":
|
case "completion_result":
|
||||||
@@ -208,7 +206,7 @@ export const ChatRowContent = ({
|
|||||||
<span
|
<span
|
||||||
className="codicon codicon-question"
|
className="codicon codicon-question"
|
||||||
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>,
|
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>,
|
||||||
<span style={{ color: normalColor, fontWeight: "bold" }}>Cline has a question:</span>,
|
<span style={{ color: normalColor, fontWeight: "bold" }}>Roo has a question:</span>,
|
||||||
]
|
]
|
||||||
default:
|
default:
|
||||||
return [null, null]
|
return [null, null]
|
||||||
@@ -250,7 +248,7 @@ export const ChatRowContent = ({
|
|||||||
<>
|
<>
|
||||||
<div style={headerStyle}>
|
<div style={headerStyle}>
|
||||||
{toolIcon(tool.tool === "appliedDiff" ? "diff" : "edit")}
|
{toolIcon(tool.tool === "appliedDiff" ? "diff" : "edit")}
|
||||||
<span style={{ fontWeight: "bold" }}>Cline wants to edit this file:</span>
|
<span style={{ fontWeight: "bold" }}>Roo wants to edit this file:</span>
|
||||||
</div>
|
</div>
|
||||||
<CodeAccordian
|
<CodeAccordian
|
||||||
isLoading={message.partial}
|
isLoading={message.partial}
|
||||||
@@ -266,7 +264,7 @@ export const ChatRowContent = ({
|
|||||||
<>
|
<>
|
||||||
<div style={headerStyle}>
|
<div style={headerStyle}>
|
||||||
{toolIcon("new-file")}
|
{toolIcon("new-file")}
|
||||||
<span style={{ fontWeight: "bold" }}>Cline wants to create a new file:</span>
|
<span style={{ fontWeight: "bold" }}>Roo wants to create a new file:</span>
|
||||||
</div>
|
</div>
|
||||||
<CodeAccordian
|
<CodeAccordian
|
||||||
isLoading={message.partial}
|
isLoading={message.partial}
|
||||||
@@ -283,7 +281,7 @@ export const ChatRowContent = ({
|
|||||||
<div style={headerStyle}>
|
<div style={headerStyle}>
|
||||||
{toolIcon("file-code")}
|
{toolIcon("file-code")}
|
||||||
<span style={{ fontWeight: "bold" }}>
|
<span style={{ fontWeight: "bold" }}>
|
||||||
{message.type === "ask" ? "Cline wants to read this file:" : "Cline read this file:"}
|
{message.type === "ask" ? "Roo wants to read this file:" : "Roo read this file:"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* <CodeAccordian
|
{/* <CodeAccordian
|
||||||
@@ -341,8 +339,8 @@ export const ChatRowContent = ({
|
|||||||
{toolIcon("folder-opened")}
|
{toolIcon("folder-opened")}
|
||||||
<span style={{ fontWeight: "bold" }}>
|
<span style={{ fontWeight: "bold" }}>
|
||||||
{message.type === "ask"
|
{message.type === "ask"
|
||||||
? "Cline wants to view the top level files in this directory:"
|
? "Roo wants to view the top level files in this directory:"
|
||||||
: "Cline viewed the top level files in this directory:"}
|
: "Roo viewed the top level files in this directory:"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<CodeAccordian
|
<CodeAccordian
|
||||||
@@ -361,8 +359,8 @@ export const ChatRowContent = ({
|
|||||||
{toolIcon("folder-opened")}
|
{toolIcon("folder-opened")}
|
||||||
<span style={{ fontWeight: "bold" }}>
|
<span style={{ fontWeight: "bold" }}>
|
||||||
{message.type === "ask"
|
{message.type === "ask"
|
||||||
? "Cline wants to recursively view all files in this directory:"
|
? "Roo wants to recursively view all files in this directory:"
|
||||||
: "Cline recursively viewed all files in this directory:"}
|
: "Roo recursively viewed all files in this directory:"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<CodeAccordian
|
<CodeAccordian
|
||||||
@@ -381,8 +379,8 @@ export const ChatRowContent = ({
|
|||||||
{toolIcon("file-code")}
|
{toolIcon("file-code")}
|
||||||
<span style={{ fontWeight: "bold" }}>
|
<span style={{ fontWeight: "bold" }}>
|
||||||
{message.type === "ask"
|
{message.type === "ask"
|
||||||
? "Cline wants to view source code definition names used in this directory:"
|
? "Roo wants to view source code definition names used in this directory:"
|
||||||
: "Cline viewed source code definition names used in this directory:"}
|
: "Roo viewed source code definition names used in this directory:"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<CodeAccordian
|
<CodeAccordian
|
||||||
@@ -401,11 +399,11 @@ export const ChatRowContent = ({
|
|||||||
<span style={{ fontWeight: "bold" }}>
|
<span style={{ fontWeight: "bold" }}>
|
||||||
{message.type === "ask" ? (
|
{message.type === "ask" ? (
|
||||||
<>
|
<>
|
||||||
Cline wants to search this directory for <code>{tool.regex}</code>:
|
Roo wants to search this directory for <code>{tool.regex}</code>:
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
Cline searched this directory for <code>{tool.regex}</code>:
|
Roo searched this directory for <code>{tool.regex}</code>:
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -428,9 +426,9 @@ export const ChatRowContent = ({
|
|||||||
// {isInspecting ? <ProgressIndicator /> : toolIcon("inspect")}
|
// {isInspecting ? <ProgressIndicator /> : toolIcon("inspect")}
|
||||||
// <span style={{ fontWeight: "bold" }}>
|
// <span style={{ fontWeight: "bold" }}>
|
||||||
// {message.type === "ask" ? (
|
// {message.type === "ask" ? (
|
||||||
// <>Cline wants to inspect this website:</>
|
// <>Roo wants to inspect this website:</>
|
||||||
// ) : (
|
// ) : (
|
||||||
// <>Cline is inspecting this website:</>
|
// <>Roo is inspecting this website:</>
|
||||||
// )}
|
// )}
|
||||||
// </span>
|
// </span>
|
||||||
// </div>
|
// </div>
|
||||||
@@ -663,7 +661,7 @@ export const ChatRowContent = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Cline won't be able to view the command's output. Please update VSCode (
|
Roo won't be able to view the command's output. Please update VSCode (
|
||||||
<code>CMD/CTRL + Shift + P</code> → "Update") and make sure you're using a supported
|
<code>CMD/CTRL + Shift + P</code> → "Update") and make sure you're using a supported
|
||||||
shell: zsh, bash, fish, or PowerShell (<code>CMD/CTRL + Shift + P</code> →
|
shell: zsh, bash, fish, or PowerShell (<code>CMD/CTRL + Shift + P</code> →
|
||||||
"Terminal: Select Default Profile").{" "}
|
"Terminal: Select Default Profile").{" "}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import ContextMenu from "./ContextMenu"
|
|||||||
import Thumbnails from "../common/Thumbnails"
|
import Thumbnails from "../common/Thumbnails"
|
||||||
import { vscode } from "../../utils/vscode"
|
import { vscode } from "../../utils/vscode"
|
||||||
import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
|
import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
|
||||||
import { Mode, modes } from "../../../../src/shared/modes"
|
import { Mode, getAllModes } from "../../../../src/shared/modes"
|
||||||
import { CaretIcon } from "../common/CaretIcon"
|
import { CaretIcon } from "../common/CaretIcon"
|
||||||
|
|
||||||
interface ChatTextAreaProps {
|
interface ChatTextAreaProps {
|
||||||
@@ -50,7 +50,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const { filePaths, currentApiConfigName, listApiConfigMeta } = useExtensionState()
|
const { filePaths, currentApiConfigName, listApiConfigMeta, customModes } = useExtensionState()
|
||||||
const [gitCommits, setGitCommits] = useState<any[]>([])
|
const [gitCommits, setGitCommits] = useState<any[]>([])
|
||||||
const [showDropdown, setShowDropdown] = useState(false)
|
const [showDropdown, setShowDropdown] = useState(false)
|
||||||
|
|
||||||
@@ -730,7 +730,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
minWidth: "70px",
|
minWidth: "70px",
|
||||||
flex: "0 0 auto",
|
flex: "0 0 auto",
|
||||||
}}>
|
}}>
|
||||||
{modes.map((mode) => (
|
{getAllModes(customModes).map((mode) => (
|
||||||
<option
|
<option
|
||||||
key={mode.slug}
|
key={mode.slug}
|
||||||
value={mode.slug}
|
value={mode.slug}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ const McpEnabledToggle = () => {
|
|||||||
marginTop: "5px",
|
marginTop: "5px",
|
||||||
color: "var(--vscode-descriptionForeground)",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
}}>
|
}}>
|
||||||
When enabled, Cline will be able to interact with MCP servers for advanced functionality. If you're not
|
When enabled, Roo will be able to interact with MCP servers for advanced functionality. If you're not
|
||||||
using MCP, you can disable this to reduce Cline's token usage.
|
using MCP, you can disable this to reduce Roo's token usage.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -115,12 +115,12 @@ const McpView = ({ onDone }: McpViewProps) => {
|
|||||||
Model Context Protocol
|
Model Context Protocol
|
||||||
</VSCodeLink>{" "}
|
</VSCodeLink>{" "}
|
||||||
enables communication with locally running MCP servers that provide additional tools and resources
|
enables communication with locally running MCP servers that provide additional tools and resources
|
||||||
to extend Cline's capabilities. You can use{" "}
|
to extend Roo's capabilities. You can use{" "}
|
||||||
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
|
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
|
||||||
community-made servers
|
community-made servers
|
||||||
</VSCodeLink>{" "}
|
</VSCodeLink>{" "}
|
||||||
or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest
|
or ask Roo to create new tools specific to your workflow (e.g., "add a tool that gets the latest npm
|
||||||
npm docs").
|
docs").
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<McpEnabledToggle />
|
<McpEnabledToggle />
|
||||||
|
|||||||
@@ -1,18 +1,31 @@
|
|||||||
import { VSCodeButton, VSCodeTextArea, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
import React, { useState, useEffect, useMemo, useCallback } from "react"
|
||||||
|
import {
|
||||||
|
VSCodeButton,
|
||||||
|
VSCodeTextArea,
|
||||||
|
VSCodeDropdown,
|
||||||
|
VSCodeOption,
|
||||||
|
VSCodeTextField,
|
||||||
|
VSCodeCheckbox,
|
||||||
|
} from "@vscode/webview-ui-toolkit/react"
|
||||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
import { defaultPrompts, modes, Mode, PromptComponent, getRoleDefinition } from "../../../../src/shared/modes"
|
import {
|
||||||
|
Mode,
|
||||||
|
PromptComponent,
|
||||||
|
getRoleDefinition,
|
||||||
|
getAllModes,
|
||||||
|
ModeConfig,
|
||||||
|
enhancePrompt,
|
||||||
|
} from "../../../../src/shared/modes"
|
||||||
|
import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups"
|
||||||
import { vscode } from "../../utils/vscode"
|
import { vscode } from "../../utils/vscode"
|
||||||
import React, { useState, useEffect } from "react"
|
|
||||||
|
// Get all available groups from GROUP_DISPLAY_NAMES
|
||||||
|
const availableGroups = Object.keys(TOOL_GROUPS) as ToolGroup[]
|
||||||
|
|
||||||
type PromptsViewProps = {
|
type PromptsViewProps = {
|
||||||
onDone: () => void
|
onDone: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AGENT_MODES = modes.map((mode) => ({
|
|
||||||
id: mode.slug,
|
|
||||||
label: mode.name,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const PromptsView = ({ onDone }: PromptsViewProps) => {
|
const PromptsView = ({ onDone }: PromptsViewProps) => {
|
||||||
const {
|
const {
|
||||||
customPrompts,
|
customPrompts,
|
||||||
@@ -24,13 +37,201 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
setCustomInstructions,
|
setCustomInstructions,
|
||||||
preferredLanguage,
|
preferredLanguage,
|
||||||
setPreferredLanguage,
|
setPreferredLanguage,
|
||||||
|
customModes,
|
||||||
} = useExtensionState()
|
} = useExtensionState()
|
||||||
|
|
||||||
|
// Memoize modes to preserve array order
|
||||||
|
const modes = useMemo(() => getAllModes(customModes), [customModes])
|
||||||
|
|
||||||
const [testPrompt, setTestPrompt] = useState("")
|
const [testPrompt, setTestPrompt] = useState("")
|
||||||
const [isEnhancing, setIsEnhancing] = useState(false)
|
const [isEnhancing, setIsEnhancing] = useState(false)
|
||||||
const [activeTab, setActiveTab] = useState<Mode>(mode)
|
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||||
const [selectedPromptContent, setSelectedPromptContent] = useState("")
|
const [selectedPromptContent, setSelectedPromptContent] = useState("")
|
||||||
const [selectedPromptTitle, setSelectedPromptTitle] = useState("")
|
const [selectedPromptTitle, setSelectedPromptTitle] = useState("")
|
||||||
|
const [isToolsEditMode, setIsToolsEditMode] = useState(false)
|
||||||
|
const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false)
|
||||||
|
|
||||||
|
// Direct update functions
|
||||||
|
const updateAgentPrompt = useCallback(
|
||||||
|
(mode: Mode, promptData: PromptComponent) => {
|
||||||
|
const existingPrompt = customPrompts?.[mode]
|
||||||
|
const updatedPrompt = { ...existingPrompt, ...promptData }
|
||||||
|
|
||||||
|
// Only include properties that differ from defaults
|
||||||
|
if (updatedPrompt.roleDefinition === getRoleDefinition(mode)) {
|
||||||
|
delete updatedPrompt.roleDefinition
|
||||||
|
}
|
||||||
|
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "updatePrompt",
|
||||||
|
promptMode: mode,
|
||||||
|
customPrompt: updatedPrompt,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[customPrompts],
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateCustomMode = useCallback((slug: string, modeConfig: ModeConfig) => {
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "updateCustomMode",
|
||||||
|
slug,
|
||||||
|
modeConfig,
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Helper function to find a mode by slug
|
||||||
|
const findModeBySlug = useCallback(
|
||||||
|
(searchSlug: string, modes: readonly ModeConfig[] | undefined): ModeConfig | undefined => {
|
||||||
|
if (!modes) return undefined
|
||||||
|
const isModeWithSlug = (mode: ModeConfig): mode is ModeConfig => mode.slug === searchSlug
|
||||||
|
return modes.find(isModeWithSlug)
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const switchMode = useCallback((slug: string) => {
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "mode",
|
||||||
|
text: slug,
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Handle mode switching with explicit state initialization
|
||||||
|
const handleModeSwitch = useCallback(
|
||||||
|
(modeConfig: ModeConfig) => {
|
||||||
|
if (modeConfig.slug === mode) return // Prevent unnecessary updates
|
||||||
|
|
||||||
|
// First switch the mode
|
||||||
|
switchMode(modeConfig.slug)
|
||||||
|
|
||||||
|
// Exit tools edit mode when switching modes
|
||||||
|
setIsToolsEditMode(false)
|
||||||
|
},
|
||||||
|
[mode, switchMode, setIsToolsEditMode],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to get current mode's config
|
||||||
|
const getCurrentMode = useCallback((): ModeConfig | undefined => {
|
||||||
|
const findMode = (m: ModeConfig): boolean => m.slug === mode
|
||||||
|
return customModes?.find(findMode) || modes.find(findMode)
|
||||||
|
}, [mode, customModes, modes])
|
||||||
|
|
||||||
|
// Helper function to safely access mode properties
|
||||||
|
const getModeProperty = <T extends keyof ModeConfig>(
|
||||||
|
mode: ModeConfig | undefined,
|
||||||
|
property: T,
|
||||||
|
): ModeConfig[T] | undefined => {
|
||||||
|
return mode?.[property]
|
||||||
|
}
|
||||||
|
|
||||||
|
// State for create mode dialog
|
||||||
|
const [newModeName, setNewModeName] = useState("")
|
||||||
|
const [newModeSlug, setNewModeSlug] = useState("")
|
||||||
|
const [newModeRoleDefinition, setNewModeRoleDefinition] = useState("")
|
||||||
|
const [newModeCustomInstructions, setNewModeCustomInstructions] = useState("")
|
||||||
|
const [newModeGroups, setNewModeGroups] = useState<readonly ToolGroup[]>(availableGroups)
|
||||||
|
|
||||||
|
// Reset form fields when dialog opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCreateModeDialogOpen) {
|
||||||
|
setNewModeGroups(availableGroups)
|
||||||
|
setNewModeRoleDefinition("")
|
||||||
|
setNewModeCustomInstructions("")
|
||||||
|
}
|
||||||
|
}, [isCreateModeDialogOpen])
|
||||||
|
|
||||||
|
// Helper function to generate a unique slug from a name
|
||||||
|
const generateSlug = useCallback((name: string, attempt = 0): string => {
|
||||||
|
const baseSlug = name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9-]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "")
|
||||||
|
return attempt === 0 ? baseSlug : `${baseSlug}-${attempt}`
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Handler for name changes
|
||||||
|
const handleNameChange = useCallback(
|
||||||
|
(name: string) => {
|
||||||
|
setNewModeName(name)
|
||||||
|
setNewModeSlug(generateSlug(name))
|
||||||
|
},
|
||||||
|
[generateSlug],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleCreateMode = useCallback(() => {
|
||||||
|
if (!newModeName.trim() || !newModeSlug.trim()) return
|
||||||
|
|
||||||
|
const newMode: ModeConfig = {
|
||||||
|
slug: newModeSlug,
|
||||||
|
name: newModeName,
|
||||||
|
roleDefinition: newModeRoleDefinition.trim() || "",
|
||||||
|
customInstructions: newModeCustomInstructions.trim() || undefined,
|
||||||
|
groups: newModeGroups,
|
||||||
|
}
|
||||||
|
updateCustomMode(newModeSlug, newMode)
|
||||||
|
switchMode(newModeSlug)
|
||||||
|
setIsCreateModeDialogOpen(false)
|
||||||
|
setNewModeName("")
|
||||||
|
setNewModeSlug("")
|
||||||
|
setNewModeRoleDefinition("")
|
||||||
|
setNewModeCustomInstructions("")
|
||||||
|
setNewModeGroups(availableGroups)
|
||||||
|
}, [
|
||||||
|
newModeName,
|
||||||
|
newModeSlug,
|
||||||
|
newModeRoleDefinition,
|
||||||
|
newModeCustomInstructions,
|
||||||
|
newModeGroups,
|
||||||
|
updateCustomMode,
|
||||||
|
switchMode,
|
||||||
|
])
|
||||||
|
|
||||||
|
const isNameOrSlugTaken = useCallback(
|
||||||
|
(name: string, slug: string) => {
|
||||||
|
return modes.some((m) => m.slug === slug || m.name === name)
|
||||||
|
},
|
||||||
|
[modes],
|
||||||
|
)
|
||||||
|
|
||||||
|
const openCreateModeDialog = useCallback(() => {
|
||||||
|
const baseNamePrefix = "New Custom Mode"
|
||||||
|
// Find unique name and slug
|
||||||
|
let attempt = 0
|
||||||
|
let name = baseNamePrefix
|
||||||
|
let slug = generateSlug(name)
|
||||||
|
while (isNameOrSlugTaken(name, slug)) {
|
||||||
|
attempt++
|
||||||
|
name = `${baseNamePrefix} ${attempt + 1}`
|
||||||
|
slug = generateSlug(name)
|
||||||
|
}
|
||||||
|
setNewModeName(name)
|
||||||
|
setNewModeSlug(slug)
|
||||||
|
setIsCreateModeDialogOpen(true)
|
||||||
|
}, [generateSlug, isNameOrSlugTaken])
|
||||||
|
|
||||||
|
// Handler for group checkbox changes
|
||||||
|
const handleGroupChange = useCallback(
|
||||||
|
(group: ToolGroup, isCustomMode: boolean, customMode: ModeConfig | undefined) =>
|
||||||
|
(e: Event | React.FormEvent<HTMLElement>) => {
|
||||||
|
if (!isCustomMode) return // Prevent changes to built-in modes
|
||||||
|
const target = (e as CustomEvent)?.detail?.target || (e.target as HTMLInputElement)
|
||||||
|
const checked = target.checked
|
||||||
|
const oldGroups = customMode?.groups || []
|
||||||
|
let newGroups: readonly ToolGroup[]
|
||||||
|
if (checked) {
|
||||||
|
newGroups = [...oldGroups, group]
|
||||||
|
} else {
|
||||||
|
newGroups = oldGroups.filter((g) => g !== group)
|
||||||
|
}
|
||||||
|
if (customMode) {
|
||||||
|
updateCustomMode(customMode.slug, {
|
||||||
|
...customMode,
|
||||||
|
groups: newGroups,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[updateCustomMode],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (event: MessageEvent) => {
|
const handler = (event: MessageEvent) => {
|
||||||
@@ -53,24 +254,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
return () => window.removeEventListener("message", handler)
|
return () => window.removeEventListener("message", handler)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
type AgentMode = string
|
|
||||||
|
|
||||||
const updateAgentPrompt = (mode: Mode, promptData: PromptComponent) => {
|
|
||||||
const existingPrompt = customPrompts?.[mode]
|
|
||||||
const updatedPrompt = typeof existingPrompt === "object" ? { ...existingPrompt, ...promptData } : promptData
|
|
||||||
|
|
||||||
// Only include properties that differ from defaults
|
|
||||||
if (updatedPrompt.roleDefinition === getRoleDefinition(mode)) {
|
|
||||||
delete updatedPrompt.roleDefinition
|
|
||||||
}
|
|
||||||
|
|
||||||
vscode.postMessage({
|
|
||||||
type: "updatePrompt",
|
|
||||||
promptMode: mode,
|
|
||||||
customPrompt: updatedPrompt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateEnhancePrompt = (value: string | undefined) => {
|
const updateEnhancePrompt = (value: string | undefined) => {
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
type: "updateEnhancedPrompt",
|
type: "updateEnhancedPrompt",
|
||||||
@@ -78,23 +261,19 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAgentPromptChange = (mode: AgentMode, e: Event | React.FormEvent<HTMLElement>) => {
|
const handleEnhancePromptChange = (e: Event | React.FormEvent<HTMLElement>): void => {
|
||||||
const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
|
|
||||||
updateAgentPrompt(mode, { roleDefinition: value.trim() || undefined })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEnhancePromptChange = (e: Event | React.FormEvent<HTMLElement>) => {
|
|
||||||
const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
|
const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value
|
||||||
const trimmedValue = value.trim()
|
const trimmedValue = value.trim()
|
||||||
if (trimmedValue !== defaultPrompts.enhance) {
|
if (trimmedValue !== enhancePrompt.default) {
|
||||||
updateEnhancePrompt(trimmedValue || undefined)
|
updateEnhancePrompt(trimmedValue || enhancePrompt.default)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAgentReset = (mode: AgentMode) => {
|
const handleAgentReset = (modeSlug: string) => {
|
||||||
const existingPrompt = customPrompts?.[mode]
|
// Only reset role definition for built-in modes
|
||||||
updateAgentPrompt(mode, {
|
const existingPrompt = customPrompts?.[modeSlug]
|
||||||
...(typeof existingPrompt === "object" ? existingPrompt : {}),
|
updateAgentPrompt(modeSlug, {
|
||||||
|
...existingPrompt,
|
||||||
roleDefinition: undefined,
|
roleDefinition: undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -103,15 +282,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
updateEnhancePrompt(undefined)
|
updateEnhancePrompt(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAgentPromptValue = (mode: Mode): string => {
|
|
||||||
const prompt = customPrompts?.[mode]
|
|
||||||
return typeof prompt === "object" ? (prompt.roleDefinition ?? getRoleDefinition(mode)) : getRoleDefinition(mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getEnhancePromptValue = (): string => {
|
const getEnhancePromptValue = (): string => {
|
||||||
const enhance = customPrompts?.enhance
|
return enhancePrompt.get(customPrompts)
|
||||||
const defaultEnhance = typeof defaultPrompts.enhance === "string" ? defaultPrompts.enhance : ""
|
|
||||||
return typeof enhance === "string" ? enhance : defaultEnhance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTestEnhancement = () => {
|
const handleTestEnhancement = () => {
|
||||||
@@ -244,42 +416,185 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 20px 0" }}>Mode-Specific Prompts</h3>
|
<div style={{ marginBottom: "20px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: "12px",
|
||||||
|
}}>
|
||||||
|
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Mode-Specific Prompts</h3>
|
||||||
|
<div style={{ display: "flex", gap: "8px" }}>
|
||||||
|
<VSCodeButton appearance="icon" onClick={openCreateModeDialog} title="Create new mode">
|
||||||
|
<span className="codicon codicon-add"></span>
|
||||||
|
</VSCodeButton>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="icon"
|
||||||
|
title="Edit modes configuration"
|
||||||
|
onClick={() => {
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "openFile",
|
||||||
|
text: "settings/cline_custom_modes.json",
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<span className="codicon codicon-json"></span>
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
fontSize: "13px",
|
||||||
gap: "16px",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
alignItems: "center",
|
marginBottom: "12px",
|
||||||
marginBottom: "12px",
|
}}>
|
||||||
}}>
|
Hit the + to create a new custom mode, or just ask Roo in chat to create one for you!
|
||||||
{AGENT_MODES.map((tab) => (
|
</div>
|
||||||
<button
|
|
||||||
key={tab.id}
|
<div
|
||||||
data-testid={`${tab.id}-tab`}
|
style={{
|
||||||
data-active={activeTab === tab.id ? "true" : "false"}
|
display: "flex",
|
||||||
onClick={() => setActiveTab(tab.id)}
|
gap: "16px",
|
||||||
style={{
|
alignItems: "center",
|
||||||
padding: "4px 8px",
|
marginBottom: "12px",
|
||||||
border: "none",
|
overflowX: "auto",
|
||||||
background: activeTab === tab.id ? "var(--vscode-button-background)" : "none",
|
flexWrap: "nowrap",
|
||||||
color:
|
paddingBottom: "4px",
|
||||||
activeTab === tab.id
|
paddingRight: "20px",
|
||||||
? "var(--vscode-button-foreground)"
|
}}>
|
||||||
: "var(--vscode-foreground)",
|
{modes.map((modeConfig) => {
|
||||||
cursor: "pointer",
|
const isActive = mode === modeConfig.slug
|
||||||
opacity: activeTab === tab.id ? 1 : 0.8,
|
return (
|
||||||
borderRadius: "3px",
|
<button
|
||||||
fontWeight: "bold",
|
key={modeConfig.slug}
|
||||||
}}>
|
data-testid={`${modeConfig.slug}-tab`}
|
||||||
{tab.label}
|
data-active={isActive ? "true" : "false"}
|
||||||
</button>
|
onClick={() => handleModeSwitch(modeConfig)}
|
||||||
))}
|
style={{
|
||||||
|
padding: "4px 8px",
|
||||||
|
border: "none",
|
||||||
|
background: isActive ? "var(--vscode-button-background)" : "none",
|
||||||
|
color: isActive
|
||||||
|
? "var(--vscode-button-foreground)"
|
||||||
|
: "var(--vscode-foreground)",
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: isActive ? 1 : 0.8,
|
||||||
|
borderRadius: "3px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}>
|
||||||
|
{modeConfig.name}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: "20px" }}>
|
<div style={{ marginBottom: "20px" }}>
|
||||||
<div style={{ marginBottom: "8px" }}>
|
{/* Only show name and delete for custom modes */}
|
||||||
<div>
|
{mode && findModeBySlug(mode, customModes) && (
|
||||||
|
<div style={{ display: "flex", gap: "12px", marginBottom: "16px" }}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Name</div>
|
||||||
|
<div style={{ display: "flex", gap: "8px" }}>
|
||||||
|
<VSCodeTextField
|
||||||
|
value={getModeProperty(findModeBySlug(mode, customModes), "name") ?? ""}
|
||||||
|
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
|
||||||
|
const target =
|
||||||
|
(e as CustomEvent)?.detail?.target ||
|
||||||
|
((e as any).target as HTMLInputElement)
|
||||||
|
const customMode = findModeBySlug(mode, customModes)
|
||||||
|
if (customMode) {
|
||||||
|
updateCustomMode(mode, {
|
||||||
|
...customMode,
|
||||||
|
name: target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="icon"
|
||||||
|
title="Delete mode"
|
||||||
|
onClick={() => {
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "deleteCustomMode",
|
||||||
|
slug: mode,
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<span className="codicon codicon-trash"></span>
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ marginBottom: "16px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: "4px",
|
||||||
|
}}>
|
||||||
|
<div style={{ fontWeight: "bold" }}>Role Definition</div>
|
||||||
|
{!findModeBySlug(mode, customModes) && (
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const currentMode = getCurrentMode()
|
||||||
|
if (currentMode?.slug) {
|
||||||
|
handleAgentReset(currentMode.slug)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Reset to default"
|
||||||
|
data-testid="role-definition-reset">
|
||||||
|
<span className="codicon codicon-discard"></span>
|
||||||
|
</VSCodeButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "13px",
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
marginBottom: "8px",
|
||||||
|
}}>
|
||||||
|
Define Roo's expertise and personality for this mode. This description shapes how Roo
|
||||||
|
presents itself and approaches tasks.
|
||||||
|
</div>
|
||||||
|
<VSCodeTextArea
|
||||||
|
value={(() => {
|
||||||
|
const customMode = findModeBySlug(mode, customModes)
|
||||||
|
const prompt = customPrompts?.[mode]
|
||||||
|
return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode)
|
||||||
|
})()}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value =
|
||||||
|
(e as CustomEvent)?.detail?.target?.value ||
|
||||||
|
((e as any).target as HTMLTextAreaElement).value
|
||||||
|
const customMode = findModeBySlug(mode, customModes)
|
||||||
|
if (customMode) {
|
||||||
|
// For custom modes, update the JSON file
|
||||||
|
updateCustomMode(mode, {
|
||||||
|
...customMode,
|
||||||
|
roleDefinition: value.trim() || "",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// For built-in modes, update the prompts
|
||||||
|
updateAgentPrompt(mode, {
|
||||||
|
roleDefinition: value.trim() || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
rows={4}
|
||||||
|
resize="vertical"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
data-testid={`${getCurrentMode()?.slug || "code"}-prompt-textarea`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Mode settings */}
|
||||||
|
<>
|
||||||
|
{/* Show tools for all modes */}
|
||||||
|
<div style={{ marginBottom: "16px" }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -287,34 +602,72 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginBottom: "4px",
|
marginBottom: "4px",
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontWeight: "bold" }}>Role Definition</div>
|
<div style={{ fontWeight: "bold" }}>Available Tools</div>
|
||||||
<VSCodeButton
|
{findModeBySlug(mode, customModes) && (
|
||||||
appearance="icon"
|
<VSCodeButton
|
||||||
onClick={() => handleAgentReset(activeTab)}
|
appearance="icon"
|
||||||
data-testid="reset-prompt-button"
|
onClick={() => setIsToolsEditMode(!isToolsEditMode)}
|
||||||
title="Revert to default">
|
title={isToolsEditMode ? "Done editing" : "Edit tools"}>
|
||||||
<span className="codicon codicon-discard"></span>
|
<span
|
||||||
</VSCodeButton>
|
className={`codicon codicon-${isToolsEditMode ? "check" : "edit"}`}></span>
|
||||||
</div>
|
</VSCodeButton>
|
||||||
<div
|
)}
|
||||||
style={{
|
|
||||||
fontSize: "13px",
|
|
||||||
color: "var(--vscode-descriptionForeground)",
|
|
||||||
marginBottom: "8px",
|
|
||||||
}}>
|
|
||||||
Define Cline's expertise and personality for this mode. This description shapes how
|
|
||||||
Cline presents itself and approaches tasks.
|
|
||||||
</div>
|
</div>
|
||||||
|
{!findModeBySlug(mode, customModes) && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "13px",
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
marginBottom: "8px",
|
||||||
|
}}>
|
||||||
|
Tools for built-in modes cannot be modified
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isToolsEditMode && findModeBySlug(mode, customModes) ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
|
||||||
|
gap: "8px",
|
||||||
|
}}>
|
||||||
|
{availableGroups.map((group) => {
|
||||||
|
const currentMode = getCurrentMode()
|
||||||
|
const isCustomMode = findModeBySlug(mode, customModes)
|
||||||
|
const customMode = isCustomMode
|
||||||
|
const isGroupEnabled = isCustomMode
|
||||||
|
? customMode?.groups?.includes(group)
|
||||||
|
: currentMode?.groups?.includes(group)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VSCodeCheckbox
|
||||||
|
key={group}
|
||||||
|
checked={isGroupEnabled}
|
||||||
|
onChange={handleGroupChange(group, Boolean(isCustomMode), customMode)}
|
||||||
|
disabled={!isCustomMode}>
|
||||||
|
{GROUP_DISPLAY_NAMES[group]}
|
||||||
|
</VSCodeCheckbox>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "13px",
|
||||||
|
color: "var(--vscode-foreground)",
|
||||||
|
marginBottom: "8px",
|
||||||
|
lineHeight: "1.4",
|
||||||
|
}}>
|
||||||
|
{(() => {
|
||||||
|
const currentMode = getCurrentMode()
|
||||||
|
const enabledGroups = currentMode?.groups || []
|
||||||
|
return enabledGroups.map((group) => GROUP_DISPLAY_NAMES[group]).join(", ")
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<VSCodeTextArea
|
</>
|
||||||
value={getAgentPromptValue(activeTab)}
|
|
||||||
onChange={(e) => handleAgentPromptChange(activeTab, e)}
|
{/* Role definition for both built-in and custom modes */}
|
||||||
rows={4}
|
|
||||||
resize="vertical"
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
data-testid={`${activeTab}-prompt-textarea`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginBottom: "8px" }}>
|
<div style={{ marginBottom: "8px" }}>
|
||||||
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Mode-specific Custom Instructions</div>
|
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Mode-specific Custom Instructions</div>
|
||||||
<div
|
<div
|
||||||
@@ -323,28 +676,38 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
color: "var(--vscode-descriptionForeground)",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
marginBottom: "8px",
|
marginBottom: "8px",
|
||||||
}}>
|
}}>
|
||||||
Add behavioral guidelines specific to {activeTab} mode. These instructions enhance the base
|
Add behavioral guidelines specific to {getCurrentMode()?.name || "Code"} mode.
|
||||||
behaviors defined above.
|
|
||||||
</div>
|
</div>
|
||||||
<VSCodeTextArea
|
<VSCodeTextArea
|
||||||
value={(() => {
|
value={(() => {
|
||||||
const prompt = customPrompts?.[activeTab]
|
const customMode = findModeBySlug(mode, customModes)
|
||||||
return typeof prompt === "object" ? (prompt.customInstructions ?? "") : ""
|
const prompt = customPrompts?.[mode]
|
||||||
|
return customMode?.customInstructions ?? prompt?.customInstructions ?? ""
|
||||||
})()}
|
})()}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value =
|
const value =
|
||||||
(e as CustomEvent)?.detail?.target?.value ||
|
(e as CustomEvent)?.detail?.target?.value ||
|
||||||
((e as any).target as HTMLTextAreaElement).value
|
((e as any).target as HTMLTextAreaElement).value
|
||||||
const existingPrompt = customPrompts?.[activeTab]
|
const customMode = findModeBySlug(mode, customModes)
|
||||||
updateAgentPrompt(activeTab, {
|
if (customMode) {
|
||||||
...(typeof existingPrompt === "object" ? existingPrompt : {}),
|
// For custom modes, update the JSON file
|
||||||
customInstructions: value.trim() || undefined,
|
updateCustomMode(mode, {
|
||||||
})
|
...customMode,
|
||||||
|
customInstructions: value.trim() || undefined,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// For built-in modes, update the prompts
|
||||||
|
const existingPrompt = customPrompts?.[mode]
|
||||||
|
updateAgentPrompt(mode, {
|
||||||
|
...existingPrompt,
|
||||||
|
customInstructions: value.trim() || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
rows={4}
|
rows={4}
|
||||||
resize="vertical"
|
resize="vertical"
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
data-testid={`${activeTab}-custom-instructions-textarea`}
|
data-testid={`${getCurrentMode()?.slug || "code"}-custom-instructions-textarea`}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -352,7 +715,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
color: "var(--vscode-descriptionForeground)",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
marginTop: "5px",
|
marginTop: "5px",
|
||||||
}}>
|
}}>
|
||||||
Custom instructions specific to {activeTab} mode can also be loaded from{" "}
|
Custom instructions specific to {getCurrentMode()?.name || "Code"} mode can also be loaded
|
||||||
|
from{" "}
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
color: "var(--vscode-textLink-foreground)",
|
color: "var(--vscode-textLink-foreground)",
|
||||||
@@ -360,32 +724,20 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
textDecoration: "underline",
|
textDecoration: "underline",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// First create/update the file with current custom instructions
|
const currentMode = getCurrentMode()
|
||||||
const defaultContent = `# ${activeTab} Mode Rules\n\nAdd mode-specific rules and guidelines here.`
|
if (!currentMode) return
|
||||||
const existingPrompt = customPrompts?.[activeTab]
|
|
||||||
const existingInstructions =
|
// Open or create an empty file
|
||||||
typeof existingPrompt === "object"
|
|
||||||
? existingPrompt.customInstructions
|
|
||||||
: undefined
|
|
||||||
vscode.postMessage({
|
|
||||||
type: "updatePrompt",
|
|
||||||
promptMode: activeTab,
|
|
||||||
customPrompt: {
|
|
||||||
...(typeof existingPrompt === "object" ? existingPrompt : {}),
|
|
||||||
customInstructions: existingInstructions || defaultContent,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// Then open the file
|
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
type: "openFile",
|
type: "openFile",
|
||||||
text: `./.clinerules-${activeTab}`,
|
text: `./.clinerules-${currentMode.slug}`,
|
||||||
values: {
|
values: {
|
||||||
create: true,
|
create: true,
|
||||||
content: "",
|
content: "",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}}>
|
}}>
|
||||||
.clinerules-{activeTab}
|
.clinerules-{getCurrentMode()?.slug || "code"}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
in your workspace.
|
in your workspace.
|
||||||
</div>
|
</div>
|
||||||
@@ -395,10 +747,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
<VSCodeButton
|
<VSCodeButton
|
||||||
appearance="primary"
|
appearance="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
vscode.postMessage({
|
const currentMode = getCurrentMode()
|
||||||
type: "getSystemPrompt",
|
if (currentMode) {
|
||||||
mode: activeTab,
|
vscode.postMessage({
|
||||||
})
|
type: "getSystemPrompt",
|
||||||
|
mode: currentMode.slug,
|
||||||
|
})
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
data-testid="preview-prompt-button">
|
data-testid="preview-prompt-button">
|
||||||
Preview System Prompt
|
Preview System Prompt
|
||||||
@@ -414,8 +769,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
marginBottom: "20px",
|
marginBottom: "20px",
|
||||||
marginTop: "5px",
|
marginTop: "5px",
|
||||||
}}>
|
}}>
|
||||||
Use prompt enhancement to get tailored suggestions or improvements for your inputs. This ensures
|
Use prompt enhancement to get tailored suggestions or improvements for your inputs. This ensures Roo
|
||||||
Cline understands your intent and provides the best possible responses.
|
understands your intent and provides the best possible responses.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
|
||||||
@@ -517,6 +872,181 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||||||
<div style={{ height: "20px" }} />
|
<div style={{ height: "20px" }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isCreateModeDialogOpen && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
|
zIndex: 1000,
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "calc(100vw - 100px)",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "var(--vscode-editor-background)",
|
||||||
|
boxShadow: "-2px 0 5px rgba(0, 0, 0, 0.2)",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
position: "relative",
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: "20px",
|
||||||
|
overflowY: "auto",
|
||||||
|
minHeight: 0,
|
||||||
|
}}>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="icon"
|
||||||
|
onClick={() => setIsCreateModeDialogOpen(false)}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "20px",
|
||||||
|
right: "20px",
|
||||||
|
}}>
|
||||||
|
<span className="codicon codicon-close"></span>
|
||||||
|
</VSCodeButton>
|
||||||
|
<h2 style={{ margin: "0 0 16px" }}>Create New Mode</h2>
|
||||||
|
<div style={{ marginBottom: "16px" }}>
|
||||||
|
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Name</div>
|
||||||
|
<VSCodeTextField
|
||||||
|
value={newModeName}
|
||||||
|
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
|
||||||
|
const target =
|
||||||
|
(e as CustomEvent)?.detail?.target ||
|
||||||
|
((e as any).target as HTMLInputElement)
|
||||||
|
handleNameChange(target.value)
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: "16px" }}>
|
||||||
|
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Slug</div>
|
||||||
|
<VSCodeTextField
|
||||||
|
value={newModeSlug}
|
||||||
|
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
|
||||||
|
const target =
|
||||||
|
(e as CustomEvent)?.detail?.target ||
|
||||||
|
((e as any).target as HTMLInputElement)
|
||||||
|
setNewModeSlug(target.value)
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
marginTop: "4px",
|
||||||
|
}}>
|
||||||
|
The slug is used in URLs and file names. It should be lowercase and contain only
|
||||||
|
letters, numbers, and hyphens.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: "16px" }}>
|
||||||
|
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Role Definition</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "13px",
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
marginBottom: "8px",
|
||||||
|
}}>
|
||||||
|
Define Roo's expertise and personality for this mode.
|
||||||
|
</div>
|
||||||
|
<VSCodeTextArea
|
||||||
|
value={newModeRoleDefinition}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value =
|
||||||
|
(e as CustomEvent)?.detail?.target?.value ||
|
||||||
|
((e as any).target as HTMLTextAreaElement).value
|
||||||
|
setNewModeRoleDefinition(value)
|
||||||
|
}}
|
||||||
|
rows={4}
|
||||||
|
resize="vertical"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: "16px" }}>
|
||||||
|
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Available Tools</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "13px",
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
marginBottom: "8px",
|
||||||
|
}}>
|
||||||
|
Select which tools this mode can use.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
|
||||||
|
gap: "8px",
|
||||||
|
}}>
|
||||||
|
{availableGroups.map((group) => (
|
||||||
|
<VSCodeCheckbox
|
||||||
|
key={group}
|
||||||
|
checked={newModeGroups.includes(group)}
|
||||||
|
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
|
||||||
|
const target =
|
||||||
|
(e as CustomEvent)?.detail?.target || (e.target as HTMLInputElement)
|
||||||
|
const checked = target.checked
|
||||||
|
if (checked) {
|
||||||
|
setNewModeGroups([...newModeGroups, group])
|
||||||
|
} else {
|
||||||
|
setNewModeGroups(newModeGroups.filter((g) => g !== group))
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{GROUP_DISPLAY_NAMES[group]}
|
||||||
|
</VSCodeCheckbox>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: "16px" }}>
|
||||||
|
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Custom Instructions</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "13px",
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
marginBottom: "8px",
|
||||||
|
}}>
|
||||||
|
Add behavioral guidelines specific to this mode.
|
||||||
|
</div>
|
||||||
|
<VSCodeTextArea
|
||||||
|
value={newModeCustomInstructions}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value =
|
||||||
|
(e as CustomEvent)?.detail?.target?.value ||
|
||||||
|
((e as any).target as HTMLTextAreaElement).value
|
||||||
|
setNewModeCustomInstructions(value)
|
||||||
|
}}
|
||||||
|
rows={4}
|
||||||
|
resize="vertical"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
padding: "12px 20px",
|
||||||
|
gap: "8px",
|
||||||
|
borderTop: "1px solid var(--vscode-editor-lineHighlightBorder)",
|
||||||
|
backgroundColor: "var(--vscode-editor-background)",
|
||||||
|
}}>
|
||||||
|
<VSCodeButton onClick={() => setIsCreateModeDialogOpen(false)}>Cancel</VSCodeButton>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="primary"
|
||||||
|
onClick={handleCreateMode}
|
||||||
|
disabled={!newModeName.trim() || !newModeSlug.trim()}>
|
||||||
|
Create Mode
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{isDialogOpen && (
|
{isDialogOpen && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -57,8 +57,12 @@ describe("PromptsView", () => {
|
|||||||
expect(architectTab).toHaveAttribute("data-active", "false")
|
expect(architectTab).toHaveAttribute("data-active", "false")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("switches between tabs correctly", () => {
|
it("switches between tabs correctly", async () => {
|
||||||
renderPromptsView({ mode: "code" })
|
const { rerender } = render(
|
||||||
|
<ExtensionStateContext.Provider value={{ ...mockExtensionState, mode: "code" } as any}>
|
||||||
|
<PromptsView onDone={jest.fn()} />
|
||||||
|
</ExtensionStateContext.Provider>,
|
||||||
|
)
|
||||||
|
|
||||||
const codeTab = screen.getByTestId("code-tab")
|
const codeTab = screen.getByTestId("code-tab")
|
||||||
const askTab = screen.getByTestId("ask-tab")
|
const askTab = screen.getByTestId("ask-tab")
|
||||||
@@ -68,16 +72,27 @@ describe("PromptsView", () => {
|
|||||||
expect(codeTab).toHaveAttribute("data-active", "true")
|
expect(codeTab).toHaveAttribute("data-active", "true")
|
||||||
expect(askTab).toHaveAttribute("data-active", "false")
|
expect(askTab).toHaveAttribute("data-active", "false")
|
||||||
expect(architectTab).toHaveAttribute("data-active", "false")
|
expect(architectTab).toHaveAttribute("data-active", "false")
|
||||||
expect(architectTab).toHaveAttribute("data-active", "false")
|
|
||||||
|
|
||||||
// Click Ask tab
|
// Click Ask tab and update context
|
||||||
fireEvent.click(askTab)
|
fireEvent.click(askTab)
|
||||||
|
rerender(
|
||||||
|
<ExtensionStateContext.Provider value={{ ...mockExtensionState, mode: "ask" } as any}>
|
||||||
|
<PromptsView onDone={jest.fn()} />
|
||||||
|
</ExtensionStateContext.Provider>,
|
||||||
|
)
|
||||||
|
|
||||||
expect(askTab).toHaveAttribute("data-active", "true")
|
expect(askTab).toHaveAttribute("data-active", "true")
|
||||||
expect(codeTab).toHaveAttribute("data-active", "false")
|
expect(codeTab).toHaveAttribute("data-active", "false")
|
||||||
expect(architectTab).toHaveAttribute("data-active", "false")
|
expect(architectTab).toHaveAttribute("data-active", "false")
|
||||||
|
|
||||||
// Click Architect tab
|
// Click Architect tab and update context
|
||||||
fireEvent.click(architectTab)
|
fireEvent.click(architectTab)
|
||||||
|
rerender(
|
||||||
|
<ExtensionStateContext.Provider value={{ ...mockExtensionState, mode: "architect" } as any}>
|
||||||
|
<PromptsView onDone={jest.fn()} />
|
||||||
|
</ExtensionStateContext.Provider>,
|
||||||
|
)
|
||||||
|
|
||||||
expect(architectTab).toHaveAttribute("data-active", "true")
|
expect(architectTab).toHaveAttribute("data-active", "true")
|
||||||
expect(askTab).toHaveAttribute("data-active", "false")
|
expect(askTab).toHaveAttribute("data-active", "false")
|
||||||
expect(codeTab).toHaveAttribute("data-active", "false")
|
expect(codeTab).toHaveAttribute("data-active", "false")
|
||||||
@@ -105,17 +120,47 @@ describe("PromptsView", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("resets prompt to default value", () => {
|
it("resets role definition only for built-in modes", async () => {
|
||||||
renderPromptsView()
|
const customMode = {
|
||||||
|
slug: "custom-mode",
|
||||||
|
name: "Custom Mode",
|
||||||
|
roleDefinition: "Custom role",
|
||||||
|
groups: [],
|
||||||
|
}
|
||||||
|
|
||||||
const resetButton = screen.getByTestId("reset-prompt-button")
|
// Test with built-in mode (code)
|
||||||
fireEvent.click(resetButton)
|
const { unmount } = render(
|
||||||
|
<ExtensionStateContext.Provider
|
||||||
|
value={{ ...mockExtensionState, mode: "code", customModes: [customMode] } as any}>
|
||||||
|
<PromptsView onDone={jest.fn()} />
|
||||||
|
</ExtensionStateContext.Provider>,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Find and click the role definition reset button
|
||||||
|
const resetButton = screen.getByTestId("role-definition-reset")
|
||||||
|
expect(resetButton).toBeInTheDocument()
|
||||||
|
await fireEvent.click(resetButton)
|
||||||
|
|
||||||
|
// Verify it only resets role definition
|
||||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
type: "updatePrompt",
|
type: "updatePrompt",
|
||||||
promptMode: "code",
|
promptMode: "code",
|
||||||
customPrompt: { roleDefinition: undefined },
|
customPrompt: { roleDefinition: undefined },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Cleanup before testing custom mode
|
||||||
|
unmount()
|
||||||
|
|
||||||
|
// Test with custom mode
|
||||||
|
render(
|
||||||
|
<ExtensionStateContext.Provider
|
||||||
|
value={{ ...mockExtensionState, mode: "custom-mode", customModes: [customMode] } as any}>
|
||||||
|
<PromptsView onDone={jest.fn()} />
|
||||||
|
</ExtensionStateContext.Provider>,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify reset button is not present for custom mode
|
||||||
|
expect(screen.queryByTestId("role-definition-reset")).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("handles API configuration selection", () => {
|
it("handles API configuration selection", () => {
|
||||||
|
|||||||
@@ -557,7 +557,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
|||||||
color: "var(--vscode-descriptionForeground)",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
}}>
|
}}>
|
||||||
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
||||||
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best
|
(<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
|
||||||
with Claude models. Less capable models may not work as expected.)
|
with Claude models. Less capable models may not work as expected.)
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -626,7 +626,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
|||||||
</VSCodeLink>{" "}
|
</VSCodeLink>{" "}
|
||||||
feature to use it with this extension.{" "}
|
feature to use it with this extension.{" "}
|
||||||
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
||||||
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best
|
(<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
|
||||||
with Claude models. Less capable models may not work as expected.)
|
with Claude models. Less capable models may not work as expected.)
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -717,7 +717,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
|||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
}}>
|
}}>
|
||||||
Note: This is a very experimental integration and may not work as expected. Please report
|
Note: This is a very experimental integration and may not work as expected. Please report
|
||||||
any issues to the Roo-Cline GitHub repository.
|
any issues to the Roo-Code GitHub repository.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -780,7 +780,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
|||||||
quickstart guide.
|
quickstart guide.
|
||||||
</VSCodeLink>
|
</VSCodeLink>
|
||||||
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
||||||
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best
|
(<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
|
||||||
with Claude models. Less capable models may not work as expected.)
|
with Claude models. Less capable models may not work as expected.)
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ const GlamaModelPicker: React.FC = () => {
|
|||||||
<VSCodeLink style={{ display: "inline", fontSize: "inherit" }} href="https://glama.ai/models">
|
<VSCodeLink style={{ display: "inline", fontSize: "inherit" }} href="https://glama.ai/models">
|
||||||
Glama.
|
Glama.
|
||||||
</VSCodeLink>
|
</VSCodeLink>
|
||||||
If you're unsure which model to choose, Cline works best with{" "}
|
If you're unsure which model to choose, Roo Code works best with{" "}
|
||||||
<VSCodeLink
|
<VSCodeLink
|
||||||
style={{ display: "inline", fontSize: "inherit" }}
|
style={{ display: "inline", fontSize: "inherit" }}
|
||||||
onClick={() => handleModelChange("anthropic/claude-3.5-sonnet")}>
|
onClick={() => handleModelChange("anthropic/claude-3.5-sonnet")}>
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ const OpenRouterModelPicker: React.FC = () => {
|
|||||||
<VSCodeLink style={{ display: "inline", fontSize: "inherit" }} href="https://openrouter.ai/models">
|
<VSCodeLink style={{ display: "inline", fontSize: "inherit" }} href="https://openrouter.ai/models">
|
||||||
OpenRouter.
|
OpenRouter.
|
||||||
</VSCodeLink>
|
</VSCodeLink>
|
||||||
If you're unsure which model to choose, Cline works best with{" "}
|
If you're unsure which model to choose, Roo Code works best with{" "}
|
||||||
<VSCodeLink
|
<VSCodeLink
|
||||||
style={{ display: "inline", fontSize: "inherit" }}
|
style={{ display: "inline", fontSize: "inherit" }}
|
||||||
onClick={() => handleModelChange("anthropic/claude-3.5-sonnet:beta")}>
|
onClick={() => handleModelChange("anthropic/claude-3.5-sonnet:beta")}>
|
||||||
|
|||||||
@@ -193,9 +193,9 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
<div style={{ marginBottom: 40 }}>
|
<div style={{ marginBottom: 40 }}>
|
||||||
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Auto-Approve Settings</h3>
|
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Auto-Approve Settings</h3>
|
||||||
<p style={{ fontSize: "12px", marginBottom: 15, color: "var(--vscode-descriptionForeground)" }}>
|
<p style={{ fontSize: "12px", marginBottom: 15, color: "var(--vscode-descriptionForeground)" }}>
|
||||||
The following settings allow Cline to automatically perform operations without requiring
|
The following settings allow Roo to automatically perform operations without requiring approval.
|
||||||
approval. Enable these settings only if you fully trust the AI and understand the associated
|
Enable these settings only if you fully trust the AI and understand the associated security
|
||||||
security risks.
|
risks.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style={{ marginBottom: 15 }}>
|
<div style={{ marginBottom: 15 }}>
|
||||||
@@ -210,7 +210,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
marginTop: "5px",
|
marginTop: "5px",
|
||||||
color: "var(--vscode-descriptionForeground)",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
}}>
|
}}>
|
||||||
When enabled, Cline will automatically view directory contents and read files without
|
When enabled, Roo will automatically view directory contents and read files without
|
||||||
requiring you to click the Approve button.
|
requiring you to click the Approve button.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -485,7 +485,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
marginTop: "5px",
|
marginTop: "5px",
|
||||||
color: "var(--vscode-descriptionForeground)",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
}}>
|
}}>
|
||||||
When enabled, Cline will play sound effects for notifications and events.
|
When enabled, Roo will play sound effects for notifications and events.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{soundEnabled && (
|
{soundEnabled && (
|
||||||
@@ -560,7 +560,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
marginTop: "5px",
|
marginTop: "5px",
|
||||||
color: "var(--vscode-descriptionForeground)",
|
color: "var(--vscode-descriptionForeground)",
|
||||||
}}>
|
}}>
|
||||||
When enabled, Cline will be able to edit files more quickly and will automatically reject
|
When enabled, Roo will be able to edit files more quickly and will automatically reject
|
||||||
truncated full-file writes. Works best with the latest Claude 3.5 Sonnet model.
|
truncated full-file writes. Works best with the latest Claude 3.5 Sonnet model.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -635,12 +635,12 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
}}>
|
}}>
|
||||||
<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
|
<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
|
||||||
If you have any questions or feedback, feel free to open an issue at{" "}
|
If you have any questions or feedback, feel free to open an issue at{" "}
|
||||||
<VSCodeLink href="https://github.com/RooVetGit/Roo-Cline" style={{ display: "inline" }}>
|
<VSCodeLink href="https://github.com/RooVetGit/Roo-Code" style={{ display: "inline" }}>
|
||||||
github.com/RooVetGit/Roo-Cline
|
github.com/RooVetGit/Roo-Code
|
||||||
</VSCodeLink>{" "}
|
</VSCodeLink>{" "}
|
||||||
or join{" "}
|
or join{" "}
|
||||||
<VSCodeLink href="https://www.reddit.com/r/roocline/" style={{ display: "inline" }}>
|
<VSCodeLink href="https://www.reddit.com/r/RooCode/" style={{ display: "inline" }}>
|
||||||
reddit.com/r/roocline
|
reddit.com/r/RooCode
|
||||||
</VSCodeLink>
|
</VSCodeLink>
|
||||||
</p>
|
</p>
|
||||||
<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0, marginBottom: 100 }}>
|
<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0, marginBottom: 100 }}>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const WelcomeView = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, padding: "0 20px" }}>
|
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, padding: "0 20px" }}>
|
||||||
<h2>Hi, I'm Cline</h2>
|
<h2>Hi, I'm Roo!</h2>
|
||||||
<p>
|
<p>
|
||||||
I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities and access
|
I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities and access
|
||||||
to tools that let me create & edit files, explore complex projects, use the browser, and execute
|
to tools that let me create & edit files, explore complex projects, use the browser, and execute
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
|||||||
import { findLastIndex } from "../../../src/shared/array"
|
import { findLastIndex } from "../../../src/shared/array"
|
||||||
import { McpServer } from "../../../src/shared/mcp"
|
import { McpServer } from "../../../src/shared/mcp"
|
||||||
import { checkExistKey } from "../../../src/shared/checkExistApiConfig"
|
import { checkExistKey } from "../../../src/shared/checkExistApiConfig"
|
||||||
import { Mode } from "../../../src/core/prompts/types"
|
import { Mode, CustomPrompts, defaultModeSlug, defaultPrompts, ModeConfig } from "../../../src/shared/modes"
|
||||||
import { CustomPrompts, defaultModeSlug, defaultPrompts } from "../../../src/shared/modes"
|
|
||||||
|
|
||||||
export interface ExtensionStateContextType extends ExtensionState {
|
export interface ExtensionStateContextType extends ExtensionState {
|
||||||
didHydrateState: boolean
|
didHydrateState: boolean
|
||||||
@@ -66,6 +65,8 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||||||
autoApprovalEnabled?: boolean
|
autoApprovalEnabled?: boolean
|
||||||
setAutoApprovalEnabled: (value: boolean) => void
|
setAutoApprovalEnabled: (value: boolean) => void
|
||||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||||
|
customModes: ModeConfig[]
|
||||||
|
setCustomModes: (value: ModeConfig[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||||
@@ -96,7 +97,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
enhancementApiConfigId: "",
|
enhancementApiConfigId: "",
|
||||||
experimentalDiffStrategy: false,
|
experimentalDiffStrategy: false,
|
||||||
autoApprovalEnabled: false,
|
autoApprovalEnabled: false,
|
||||||
|
customModes: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||||
const [showWelcome, setShowWelcome] = useState(false)
|
const [showWelcome, setShowWelcome] = useState(false)
|
||||||
const [theme, setTheme] = useState<any>(undefined)
|
const [theme, setTheme] = useState<any>(undefined)
|
||||||
@@ -274,6 +277,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
setState((prevState) => ({ ...prevState, experimentalDiffStrategy: value })),
|
setState((prevState) => ({ ...prevState, experimentalDiffStrategy: value })),
|
||||||
setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
|
setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
|
||||||
handleInputChange,
|
handleInputChange,
|
||||||
|
setCustomModes: (value) => setState((prevState) => ({ ...prevState, customModes: value })),
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||||
|
|||||||
Reference in New Issue
Block a user