From d995a6a3a2380a5e67c55aefaf8d8831506fbc8d Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 20 Jan 2025 20:48:18 -0600 Subject: [PATCH 01/66] fix: correct X-Glama-Metadata placement --- src/api/providers/glama.ts | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/api/providers/glama.ts b/src/api/providers/glama.ts index b475438..c27161a 100644 --- a/src/api/providers/glama.ts +++ b/src/api/providers/glama.ts @@ -73,13 +73,27 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler { } const { data: completion, response } = await this.client.chat.completions - .create({ - model: this.getModel().id, - max_tokens: maxTokens, - temperature: 0, - messages: openAiMessages, - stream: true, - }) + .create( + { + model: this.getModel().id, + max_tokens: maxTokens, + temperature: 0, + messages: openAiMessages, + stream: true, + }, + { + headers: { + "X-Glama-Metadata": JSON.stringify({ + labels: [ + { + key: "app", + value: "vscode.rooveterinaryinc.roo-cline", + }, + ], + }), + }, + }, + ) .withResponse() const completionRequestId = response.headers.get("x-completion-request-id") @@ -101,14 +115,6 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler { { headers: { Authorization: `Bearer ${this.options.glamaApiKey}`, - "X-Glama-Metadata": JSON.stringify({ - labels: [ - { - key: "app", - value: "vscode.rooveterinaryinc.roo-cline", - }, - ], - }), }, }, ) From b8e0aa0cde58b7799fac856a23aa22829d9c5558 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sat, 18 Jan 2025 03:39:26 -0500 Subject: [PATCH 02/66] Custom modes --- .changeset/pink-peaches-jump.md | 5 + .changeset/plenty-suits-visit.md | 5 + .git-blame-ignore-revs | 2 +- .github/ISSUE_TEMPLATE/config.yml | 6 +- .github/workflows/code-qa.yml | 2 +- CHANGELOG.md | 56 +- LICENSE | 2 +- README.md | 340 +++---- package.json | 14 +- src/__mocks__/fs/promises.ts | 195 ++++ .../providers/__tests__/openrouter.test.ts | 2 +- src/api/providers/lmstudio.ts | 4 +- src/api/providers/openrouter.ts | 4 +- src/api/providers/vscode-lm.ts | 62 +- .../__tests__/vscode-lm-format.test.ts | 2 +- src/api/transform/vscode-lm-format.ts | 4 +- src/core/Cline.ts | 75 +- src/core/__tests__/Cline.test.ts | 172 ++-- src/core/__tests__/mode-validator.test.ts | 63 +- src/core/config/ConfigManager.ts | 2 +- src/core/config/CustomModesManager.ts | 190 ++++ src/core/config/CustomModesSchema.ts | 60 ++ .../__tests__/CustomModesManager.test.ts | 245 +++++ .../__tests__/CustomModesSchema.test.ts | 122 +++ .../__tests__/CustomModesSettings.test.ts | 169 ++++ .../__tests__/GroupConfigSchema.test.ts | 90 ++ src/core/diff/strategies/new-unified/index.ts | 10 +- src/core/mode-validator.ts | 9 +- .../__snapshots__/system.test.ts.snap | 957 ++++++++++-------- src/core/prompts/__tests__/system.test.ts | 364 ++++--- .../prompts/sections/custom-instructions.ts | 68 +- src/core/prompts/sections/index.ts | 1 + src/core/prompts/sections/modes.ts | 45 + src/core/prompts/sections/rules.ts | 18 +- src/core/prompts/sections/system-info.ts | 12 +- src/core/prompts/system.ts | 150 ++- src/core/prompts/tools/index.ts | 25 +- src/core/prompts/types.ts | 52 - src/core/tool-lists.ts | 32 - src/core/webview/ClineProvider.ts | 87 +- .../webview/__tests__/ClineProvider.test.ts | 174 +++- src/extension.ts | 10 +- src/integrations/editor/DiffViewProvider.ts | 12 +- src/integrations/terminal/TerminalRegistry.ts | 2 +- .../__tests__/TerminalRegistry.test.ts | 2 +- src/services/mcp/McpHub.ts | 2 +- src/shared/ExtensionMessage.ts | 14 +- src/shared/WebviewMessage.ts | 6 +- src/shared/modes.ts | 261 ++--- src/shared/tool-groups.ts | 53 + .../src/components/chat/Announcement.tsx | 46 +- .../src/components/chat/AutoApproveMenu.tsx | 2 +- .../src/components/chat/BrowserSessionRow.tsx | 2 +- webview-ui/src/components/chat/ChatRow.tsx | 40 +- .../src/components/chat/ChatTextArea.tsx | 6 +- .../src/components/mcp/McpEnabledToggle.tsx | 4 +- webview-ui/src/components/mcp/McpView.tsx | 6 +- .../src/components/prompts/PromptsView.tsx | 810 ++++++++++++--- .../prompts/__tests__/PromptsView.test.tsx | 63 +- .../src/components/settings/ApiOptions.tsx | 8 +- .../components/settings/GlamaModelPicker.tsx | 2 +- .../settings/OpenRouterModelPicker.tsx | 2 +- .../src/components/settings/SettingsView.tsx | 20 +- .../src/components/welcome/WelcomeView.tsx | 2 +- .../src/context/ExtensionStateContext.tsx | 8 +- 65 files changed, 3749 insertions(+), 1531 deletions(-) create mode 100644 .changeset/pink-peaches-jump.md create mode 100644 .changeset/plenty-suits-visit.md create mode 100644 src/__mocks__/fs/promises.ts create mode 100644 src/core/config/CustomModesManager.ts create mode 100644 src/core/config/CustomModesSchema.ts create mode 100644 src/core/config/__tests__/CustomModesManager.test.ts create mode 100644 src/core/config/__tests__/CustomModesSchema.test.ts create mode 100644 src/core/config/__tests__/CustomModesSettings.test.ts create mode 100644 src/core/config/__tests__/GroupConfigSchema.test.ts create mode 100644 src/core/prompts/sections/modes.ts delete mode 100644 src/core/prompts/types.ts delete mode 100644 src/core/tool-lists.ts create mode 100644 src/shared/tool-groups.ts diff --git a/.changeset/pink-peaches-jump.md b/.changeset/pink-peaches-jump.md new file mode 100644 index 0000000..6c4f416 --- /dev/null +++ b/.changeset/pink-peaches-jump.md @@ -0,0 +1,5 @@ +--- +"roo-cline": minor +--- + +v3.2 diff --git a/.changeset/plenty-suits-visit.md b/.changeset/plenty-suits-visit.md new file mode 100644 index 0000000..b5bdeb7 --- /dev/null +++ b/.changeset/plenty-suits-visit.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +debug from vscode and changed output channel to Roo-Code diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 74f6645..93f9eea 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -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 579bdd9dbf6d2d569e5e7adb5ff6292b1e42ea34 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e9033a4..70472c2 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - name: Feature Request - url: https://github.com/RooVetGit/Roo-Cline/discussions/categories/feature-requests - about: Share and vote on feature requests for Roo Cline + url: https://github.com/RooVetGit/Roo-Code/discussions/categories/feature-requests + about: Share and vote on feature requests for Roo Code - name: Leave a Review 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! diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index de3f054..19682b6 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -1,4 +1,4 @@ -name: Code QA Roo Cline +name: Code QA Roo Code on: push: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1447293..ab4ad37 100644 --- a/CHANGELOG.md +++ b/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 " 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] -- DeepSeek-R1 support (thanks @philipnext!) -- Experimental new unified diff algorithm can be enabled in settings (thanks @daniel-lxs!) -- More fixes to configuration profiles (thanks @samhvw8!) +- DeepSeek-R1 support (thanks @philipnext!) +- Experimental new unified diff algorithm can be enabled in settings (thanks @daniel-lxs!) +- More fixes to configuration profiles (thanks @samhvw8!) ## [3.1.6] -- Add Mistral (thanks Cline!) -- Fix bug with VSCode LM configuration profile saving (thanks @samhvw8!) +- Add Mistral (thanks Cline!) +- Fix bug with VSCode LM configuration profile saving (thanks @samhvw8!) ## [3.1.4 - 3.1.5] -- Bug fixes to the auto approve menu +- Bug fixes to the auto approve menu ## [3.1.3] -- Add auto-approve chat bar (thanks Cline!) -- Fix bug with VS Code Language Models integration +- Add auto-approve chat bar (thanks Cline!) +- Fix bug with VS Code Language Models integration ## [3.1.2] -- Experimental support for VS Code Language Models including Copilot (thanks @RaySinner / @julesmons!) -- Fix bug related to configuration profile switching (thanks @samhvw8!) -- Improvements to fuzzy search in mentions, history, and model lists (thanks @samhvw8!) -- PKCE support for Glama (thanks @punkpeye!) -- Use 'developer' message for o1 system prompt +- Experimental support for VS Code Language Models including Copilot (thanks @RaySinner / @julesmons!) +- Fix bug related to configuration profile switching (thanks @samhvw8!) +- Improvements to fuzzy search in mentions, history, and model lists (thanks @samhvw8!) +- PKCE support for Glama (thanks @punkpeye!) +- Use 'developer' message for o1 system prompt ## [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] -- 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 +- 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 ## [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] -- 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] -- 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] -- 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] @@ -305,4 +317,4 @@ ## [2.1.2] - Support for auto-approval of write operations and command execution -- Support for .clinerules custom instructions +- Support for .clinerules custom instructions \ No newline at end of file diff --git a/LICENSE b/LICENSE index b8f1f99..194125d 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier 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"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 2918f27..5ebbd66 100644 --- a/README.md +++ b/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 " 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 @@ -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. -## 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 ` 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 npm run install:all ``` - -2. Build the VSIX file: +3. **Build** the extension: ```bash npm run build ``` -3. The new VSIX file will be created in the `bin/` directory -4. Install the extension from the VSIX file as described below: - - - **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"` - + - A `.vsix` file will appear in the `bin/` directory. +4. **Install** the `.vsix` manually if desired: ```bash - # Ex: cursor --install-extension bin/roo-cline-2.0.1.vsix - # Ex: code --install-extension bin/roo-cline-2.0.1.vsix + code --install-extension bin/roo-code-4.0.0.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.) - -### 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 +We use [changesets](https://github.com/changesets/changesets) for versioning and publishing. Check our `CHANGELOG.md` for release notes. --- -# Cline (prev. Claude Dev) – [#1 on OpenRouter](https://openrouter.ai/) +## Disclaimer -

- -

- - - -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. +**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). --- - - -### 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. - - - -
- - - -### 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. - - - -
- - - -### 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. - - - -
- - - -### 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) - - - -
- - - -### "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 - - - -
- - - -### 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 -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: -
-Local Development Instructions +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). +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.) - -
+--- ## 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! diff --git a/package.json b/package.json index 763bcfa..dcb0888 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "roo-cline", - "displayName": "Roo Cline", - "description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.", + "displayName": "Roo Code (prev. Roo Cline)", + "description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.", "publisher": "RooVeterinaryInc", "version": "3.1.7", "icon": "assets/icons/rocket.png", @@ -17,9 +17,9 @@ }, "repository": { "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": [ "AI", "Chat", @@ -52,7 +52,7 @@ "activitybar": [ { "id": "roo-cline-ActivityBar", - "title": "Roo Cline", + "title": "Roo Code", "icon": "$(rocket)" } ] @@ -100,7 +100,7 @@ { "command": "roo-cline.openInNewTab", "title": "Open In New Tab", - "category": "Roo Cline" + "category": "Roo Code" } ], "menus": { @@ -138,7 +138,7 @@ ] }, "configuration": { - "title": "RooCline", + "title": "Roo Code", "properties": { "roo-cline.allowedCommands": { "type": "array", diff --git a/src/__mocks__/fs/promises.ts b/src/__mocks__/fs/promises.ts new file mode 100644 index 0000000..d5f0762 --- /dev/null +++ b/src/__mocks__/fs/promises.ts @@ -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 diff --git a/src/api/providers/__tests__/openrouter.test.ts b/src/api/providers/__tests__/openrouter.test.ts index b395e27..039bf97 100644 --- a/src/api/providers/__tests__/openrouter.test.ts +++ b/src/api/providers/__tests__/openrouter.test.ts @@ -36,7 +36,7 @@ describe("OpenRouterHandler", () => { apiKey: mockOptions.openRouterApiKey, defaultHeaders: { "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline", - "X-Title": "Roo-Cline", + "X-Title": "Roo-Code", }, }) }) diff --git a/src/api/providers/lmstudio.ts b/src/api/providers/lmstudio.ts index d07b9c2..81cec81 100644 --- a/src/api/providers/lmstudio.ts +++ b/src/api/providers/lmstudio.ts @@ -42,7 +42,7 @@ export class LmStudioHandler implements ApiHandler, SingleCompletionHandler { } catch (error) { // LM Studio doesn't return an error code/body for now 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 || "" } catch (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.", ) } } diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index c69d6fe..3f0b88b 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -29,8 +29,8 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { baseURL: "https://openrouter.ai/api/v1", apiKey: this.options.openRouterApiKey, defaultHeaders: { - "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline", // Optional, for including your app on openrouter.ai rankings. - "X-Title": "Roo-Cline", // Optional. Shows in rankings on openrouter.ai. + "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline", + "X-Title": "Roo-Code", }, }) } diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts index 6ddc6bd..e2bf860 100644 --- a/src/api/providers/vscode-lm.ts +++ b/src/api/providers/vscode-lm.ts @@ -63,7 +63,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { this.dispose() throw new Error( - `Cline : Failed to initialize handler: ${error instanceof Error ? error.message : "Unknown error"}`, + `Roo Code : Failed to initialize handler: ${error instanceof Error ? error.message : "Unknown error"}`, ) } } @@ -113,7 +113,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error" - throw new Error(`Cline : Failed to select model: ${errorMessage}`) + throw new Error(`Roo Code : Failed to select model: ${errorMessage}`) } } @@ -147,18 +147,18 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise { // Check for required dependencies if (!this.client) { - console.warn("Cline : No client available for token counting") + console.warn("Roo Code : No client available for token counting") return 0 } if (!this.currentRequestCancellation) { - console.warn("Cline : No cancellation token available for token counting") + console.warn("Roo Code : No cancellation token available for token counting") return 0 } // Validate input if (!text) { - console.debug("Cline : Empty text provided for token counting") + console.debug("Roo Code : Empty text provided for token counting") return 0 } @@ -171,23 +171,23 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { } else if (text instanceof vscode.LanguageModelChatMessage) { // For chat messages, ensure we have content if (!text.content || (Array.isArray(text.content) && text.content.length === 0)) { - console.debug("Cline : Empty chat message content") + console.debug("Roo Code : Empty chat message content") return 0 } tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token) } else { - console.warn("Cline : Invalid input type for token counting") + console.warn("Roo Code : Invalid input type for token counting") return 0 } // Validate the result if (typeof tokenCount !== "number") { - console.warn("Cline : Non-numeric token count received:", tokenCount) + console.warn("Roo Code : Non-numeric token count received:", tokenCount) return 0 } if (tokenCount < 0) { - console.warn("Cline : Negative token count received:", tokenCount) + console.warn("Roo Code : Negative token count received:", tokenCount) return 0 } @@ -195,12 +195,12 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { } catch (error) { // Handle specific error types if (error instanceof vscode.CancellationError) { - console.debug("Cline : Token counting cancelled by user") + console.debug("Roo Code : Token counting cancelled by user") return 0 } const errorMessage = error instanceof Error ? error.message : "Unknown error" - console.warn("Cline : Token counting failed:", errorMessage) + console.warn("Roo Code : Token counting failed:", errorMessage) // Log additional error details if available if (error instanceof Error && error.stack) { @@ -232,7 +232,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { private async getClient(): Promise { if (!this.client) { - console.debug("Cline : Getting client with options:", { + console.debug("Roo Code : Getting client with options:", { vsCodeLmModelSelector: this.options.vsCodeLmModelSelector, hasOptions: !!this.options, selectorKeys: this.options.vsCodeLmModelSelector ? Object.keys(this.options.vsCodeLmModelSelector) : [], @@ -241,12 +241,12 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { try { // Use default empty selector if none provided to get all available models const selector = this.options?.vsCodeLmModelSelector || {} - console.debug("Cline : Creating client with selector:", selector) + console.debug("Roo Code : Creating client with selector:", selector) this.client = await this.createClient(selector) } catch (error) { const message = error instanceof Error ? error.message : "Unknown error" - console.error("Cline : Client creation failed:", message) - throw new Error(`Cline : Failed to create client: ${message}`) + console.error("Roo Code : Client creation failed:", message) + throw new Error(`Roo Code : Failed to create client: ${message}`) } } @@ -348,7 +348,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { try { // Create the response stream with minimal required options 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 @@ -365,7 +365,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { if (chunk instanceof vscode.LanguageModelTextPart) { // Validate text part value if (typeof chunk.value !== "string") { - console.warn("Cline : Invalid text part value received:", chunk.value) + console.warn("Roo Code : Invalid text part value received:", chunk.value) continue } @@ -378,18 +378,18 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { try { // Validate tool call parameters if (!chunk.name || typeof chunk.name !== "string") { - console.warn("Cline : Invalid tool name received:", chunk.name) + console.warn("Roo Code : Invalid tool name received:", chunk.name) continue } if (!chunk.callId || typeof chunk.callId !== "string") { - console.warn("Cline : Invalid tool callId received:", chunk.callId) + console.warn("Roo Code : Invalid tool callId received:", chunk.callId) continue } // Ensure input is a valid object if (!chunk.input || typeof chunk.input !== "object") { - console.warn("Cline : Invalid tool input received:", chunk.input) + console.warn("Roo Code : Invalid tool input received:", chunk.input) continue } @@ -405,7 +405,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { accumulatedText += toolCallText // Log tool call for debugging - console.debug("Cline : Processing tool call:", { + console.debug("Roo Code : Processing tool call:", { name: chunk.name, callId: chunk.callId, inputSize: JSON.stringify(chunk.input).length, @@ -416,12 +416,12 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { text: toolCallText, } } catch (error) { - console.error("Cline : Failed to process tool call:", error) + console.error("Roo Code : Failed to process tool call:", error) // Continue processing other chunks even if one fails continue } } else { - console.warn("Cline : Unknown chunk type received:", chunk) + console.warn("Roo Code : Unknown chunk type received:", chunk) } } @@ -439,11 +439,11 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { this.ensureCleanState() if (error instanceof vscode.CancellationError) { - throw new Error("Cline : Request cancelled by user") + throw new Error("Roo Code : Request cancelled by user") } if (error instanceof Error) { - console.error("Cline : Stream error details:", { + console.error("Roo Code : Stream error details:", { message: error.message, stack: error.stack, name: error.name, @@ -454,13 +454,13 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { } else if (typeof error === "object" && error !== null) { // Handle error-like objects const errorDetails = JSON.stringify(error, null, 2) - console.error("Cline : Stream error object:", errorDetails) - throw new Error(`Cline : Response stream error: ${errorDetails}`) + console.error("Roo Code : Stream error object:", errorDetails) + throw new Error(`Roo Code : Response stream error: ${errorDetails}`) } else { // Fallback for unknown error types const errorMessage = String(error) - console.error("Cline : Unknown stream error:", errorMessage) - throw new Error(`Cline : Response stream error: ${errorMessage}`) + console.error("Roo Code : Unknown stream error:", errorMessage) + throw new Error(`Roo Code : Response stream error: ${errorMessage}`) } } } @@ -480,7 +480,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { // Log any missing properties for debugging for (const [prop, value] of Object.entries(requiredProps)) { if (!value && value !== 0) { - console.warn(`Cline : Client missing ${prop} property`) + console.warn(`Roo Code : Client missing ${prop} property`) } } @@ -511,7 +511,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { ? stringifyVsCodeLmModelSelector(this.options.vsCodeLmModelSelector) : "vscode-lm" - console.debug("Cline : No client available, using fallback model info") + console.debug("Roo Code : No client available, using fallback model info") return { id: fallbackId, diff --git a/src/api/transform/__tests__/vscode-lm-format.test.ts b/src/api/transform/__tests__/vscode-lm-format.test.ts index bc70da8..b27097f 100644 --- a/src/api/transform/__tests__/vscode-lm-format.test.ts +++ b/src/api/transform/__tests__/vscode-lm-format.test.ts @@ -249,7 +249,7 @@ describe("vscode-lm-format", () => { } await expect(convertToAnthropicMessage(vsCodeMessage as any)).rejects.toThrow( - "Cline : Only assistant messages are supported.", + "Roo Code : Only assistant messages are supported.", ) }) }) diff --git a/src/api/transform/vscode-lm-format.ts b/src/api/transform/vscode-lm-format.ts index acec365..6d7bea9 100644 --- a/src/api/transform/vscode-lm-format.ts +++ b/src/api/transform/vscode-lm-format.ts @@ -23,7 +23,7 @@ function asObjectSafe(value: any): object { return {} } catch (error) { - console.warn("Cline : Failed to parse object:", error) + console.warn("Roo Code : Failed to parse object:", error) return {} } } @@ -161,7 +161,7 @@ export async function convertToAnthropicMessage( ): Promise { const anthropicRole: string | null = convertToAnthropicRole(vsCodeLmMessage.role) if (anthropicRole !== "assistant") { - throw new Error("Cline : Only assistant messages are supported.") + throw new Error("Roo Code : Only assistant messages are supported.") } return { diff --git a/src/core/Cline.ts b/src/core/Cline.ts index eb78cc4..f26b43b 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -51,8 +51,8 @@ import { arePathsEqual, getReadablePath } from "../utils/path" import { parseMentions } from "./mentions" import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message" import { formatResponse } from "./prompts/responses" -import { addCustomInstructions, SYSTEM_PROMPT } from "./prompts/system" -import { modes, defaultModeSlug } from "../shared/modes" +import { SYSTEM_PROMPT } from "./prompts/system" +import { modes, defaultModeSlug, getModeBySlug } from "../shared/modes" import { truncateHalfConversation } from "./sliding-window" import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider" import { detectCodeOmission } from "../integrations/editor/detect-omission" @@ -264,7 +264,7 @@ export class Cline { ): 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.abort) { - throw new Error("Cline instance aborted") + throw new Error("Roo Code instance aborted") } let askTs: number if (partial !== undefined) { @@ -360,7 +360,7 @@ export class Cline { async say(type: ClineSay, text?: string, images?: string[], partial?: boolean): Promise { if (this.abort) { - throw new Error("Cline instance aborted") + throw new Error("Roo Code instance aborted") } if (partial !== undefined) { @@ -419,7 +419,7 @@ export class Cline { async sayAndCreateMissingParamError(toolName: ToolUseName, paramName: string, relPath?: string) { await this.say( "error", - `Cline tried to use ${toolName}${ + `Roo tried to use ${toolName}${ relPath ? ` for '${relPath.toPosix()}'` : "" } without value for required parameter '${paramName}'. Retrying...`, ) @@ -809,10 +809,15 @@ export class Cline { }) } - const { browserViewportSize, preferredLanguage, mode, customPrompts } = - (await this.providerRef.deref()?.getState()) ?? {} - const systemPrompt = - (await SYSTEM_PROMPT( + const { browserViewportSize, mode, customPrompts } = (await this.providerRef.deref()?.getState()) ?? {} + const { customModes } = (await this.providerRef.deref()?.getState()) ?? {} + const systemPrompt = await (async () => { + const provider = this.providerRef.deref() + if (!provider) { + throw new Error("Provider not available") + } + return SYSTEM_PROMPT( + provider.context, cwd, this.api.getModel().info.supportsComputerUse ?? false, mcpHub, @@ -820,16 +825,9 @@ export class Cline { browserViewportSize, mode, customPrompts, - )) + - (await addCustomInstructions( - { - customInstructions: this.customInstructions, - customPrompts, - preferredLanguage, - }, - cwd, - mode, - )) + customModes, + ) + })() // 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) { @@ -923,7 +921,7 @@ export class Cline { async presentAssistantMessage() { if (this.abort) { - throw new Error("Cline instance aborted") + throw new Error("Roo Code instance aborted") } if (this.presentAssistantMessageLocked) { @@ -1142,8 +1140,9 @@ export class Cline { // Validate tool use based on current mode const { mode } = (await this.providerRef.deref()?.getState()) ?? {} + const { customModes } = (await this.providerRef.deref()?.getState()) ?? {} try { - validateToolUse(block.name, mode ?? defaultModeSlug) + validateToolUse(block.name, mode ?? defaultModeSlug, customModes) } catch (error) { this.consecutiveMistakeCount++ pushToolResult(formatResponse.toolError(error.message)) @@ -1264,7 +1263,9 @@ export class Cline { await this.diffViewProvider.revertChanges() pushToolResult( 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 @@ -1317,7 +1318,9 @@ export class Cline { pushToolResult( `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` + - `\n${addLineNumbers(finalContent || "")}\n\n\n` + + `\n${addLineNumbers( + finalContent || "", + )}\n\n\n` + `Please note:\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` + @@ -1396,7 +1399,9 @@ export class Cline { const errorDetails = diffResult.details ? JSON.stringify(diffResult.details, null, 2) : "" - const formattedError = `Unable to apply diff to file: ${absolutePath}\n\n\n${diffResult.error}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n` + const formattedError = `Unable to apply diff to file: ${absolutePath}\n\n\n${ + diffResult.error + }${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n` if (currentCount >= 2) { await this.say("error", formattedError) } @@ -1438,7 +1443,9 @@ export class Cline { pushToolResult( `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` + - `\n${addLineNumbers(finalContent || "")}\n\n\n` + + `\n${addLineNumbers( + finalContent || "", + )}\n\n\n` + `Please note:\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` + @@ -1853,7 +1860,7 @@ export class Cline { this.consecutiveMistakeCount++ await this.say( "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( formatResponse.toolError( @@ -2164,7 +2171,7 @@ export class Cline { includeFileDetails: boolean = false, ): Promise { if (this.abort) { - throw new Error("Cline instance aborted") + throw new Error("Roo Code instance aborted") } if (this.consecutiveMistakeCount >= 3) { @@ -2172,7 +2179,7 @@ export class Cline { "mistake_limit_reached", 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").` - : "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") { userContent.push( @@ -2366,7 +2373,7 @@ export class Cline { // need to call here in case the stream was aborted if (this.abort) { - throw new Error("Cline instance aborted") + throw new Error("Roo Code instance aborted") } this.didCompleteReadingStream = true @@ -2622,16 +2629,18 @@ export class Cline { details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})` // 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 details += `\n\n# Current Mode\n${currentMode}` // Add warning if not in code mode if ( - !isToolAllowedForMode("write_to_file", currentMode) || - !isToolAllowedForMode("execute_command", currentMode) + !isToolAllowedForMode("write_to_file", currentMode, customModes ?? []) && + !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) { diff --git a/src/core/__tests__/Cline.test.ts b/src/core/__tests__/Cline.test.ts index 4cf0def..2f46456 100644 --- a/src/core/__tests__/Cline.test.ts +++ b/src/core/__tests__/Cline.test.ts @@ -4,6 +4,8 @@ import { ApiConfiguration, ModelInfo } from "../../shared/api" import { ApiStreamChunk } from "../../api/transform/stream" import { Anthropic } from "@anthropic-ai/sdk" import * as vscode from "vscode" +import * as os from "os" +import * as path from "path" // Mock all MCP-related modules jest.mock( @@ -209,6 +211,9 @@ describe("Cline", () => { beforeEach(() => { // Setup mock extension context + const storageUri = { + fsPath: path.join(os.tmpdir(), "test-storage"), + } mockExtensionContext = { globalState: { get: jest.fn().mockImplementation((key) => { @@ -231,6 +236,7 @@ describe("Cline", () => { update: jest.fn().mockImplementation((key, value) => Promise.resolve()), keys: jest.fn().mockReturnValue([]), }, + globalStorageUri: storageUri, workspaceState: { get: jest.fn().mockImplementation((key) => undefined), update: jest.fn().mockImplementation((key, value) => Promise.resolve()), @@ -244,9 +250,6 @@ describe("Cline", () => { extensionUri: { fsPath: "/mock/extension/path", }, - globalStorageUri: { - fsPath: "/mock/storage/path", - }, extension: { packageJSON: { version: "1.0.0", @@ -425,27 +428,34 @@ describe("Cline", () => { // Mock the API's createMessage method to capture the conversation history const createMessageSpy = jest.fn() - const mockStream = { - async *[Symbol.asyncIterator]() { - yield { type: "text", text: "" } - }, - 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 + // Set up mock stream + const mockStreamForClean = (async function* () { + yield { type: "text", text: "test response" } + })() - jest.spyOn(cline.api, "createMessage").mockImplementation((...args) => { - createMessageSpy(...args) - return mockStream + // Set up spy + const cleanMessageSpy = jest.fn().mockReturnValue(mockStreamForClean) + 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 @@ -458,30 +468,25 @@ describe("Cline", () => { cline.apiConversationHistory = [messageWithExtra] // 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 - const calls = createMessageSpy.mock.calls + // Get the conversation history from the first API call + const history = cleanMessageSpy.mock.calls[0][1] + expect(history).toBeDefined() + expect(history.length).toBeGreaterThan(0) - // Find the call that includes our test message - const relevantCall = calls.find((call) => - call[1]?.some((msg: any) => msg.content?.[0]?.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" }], - }, - ]), + // Find our test message + const cleanedMessage = history.find((msg: { content?: Array<{ text: string }> }) => + msg.content?.some((content) => content.text === "test message"), ) + expect(cleanedMessage).toBeDefined() + expect(cleanedMessage).toEqual({ + role: "user", + content: [{ type: "text", text: "test message" }], + }) // Verify extra properties were removed - const passedMessage = relevantCall?.[1].find((msg: any) => msg.content?.[0]?.text === "test message") - expect(passedMessage).not.toHaveProperty("ts") - expect(passedMessage).not.toHaveProperty("extraProp") + expect(Object.keys(cleanedMessage!)).toEqual(["role", "content"]) }) it("should handle image blocks based on model capabilities", async () => { @@ -573,41 +578,68 @@ describe("Cline", () => { }) clineWithoutImages.apiConversationHistory = conversationHistory - // Create message spy for both instances - const createMessageSpyWithImages = jest.fn() - const createMessageSpyWithoutImages = jest.fn() - const mockStream = { - async *[Symbol.asyncIterator]() { - yield { type: "text", text: "" } + // Mock abort state for both instances + Object.defineProperty(clineWithImages, "abort", { + get: () => false, + configurable: true, + }) + 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 + ] - jest.spyOn(clineWithImages.api, "createMessage").mockImplementation((...args) => { - createMessageSpyWithImages(...args) - return mockStream - }) - jest.spyOn(clineWithoutImages.api, "createMessage").mockImplementation((...args) => { - createMessageSpyWithoutImages(...args) - return mockStream - }) + // Trigger API requests + await clineWithImages.recursivelyMakeClineRequests([{ type: "text", text: "test request" }]) + await clineWithoutImages.recursivelyMakeClineRequests([{ type: "text", text: "test request" }]) - // Trigger API requests for both instances - await clineWithImages.recursivelyMakeClineRequests([{ type: "text", text: "test" }]) - await clineWithoutImages.recursivelyMakeClineRequests([{ type: "text", text: "test" }]) + // Get the calls + const imagesCalls = imagesSpy.mock.calls + const noImagesCalls = noImagesSpy.mock.calls // Verify model with image support preserves image blocks - const callsWithImages = createMessageSpyWithImages.mock.calls - const historyWithImages = callsWithImages[0][1][0] - expect(historyWithImages.content).toHaveLength(2) - expect(historyWithImages.content[0]).toEqual({ type: "text", text: "Here is an image" }) - expect(historyWithImages.content[1]).toHaveProperty("type", "image") + expect(imagesCalls[0][1][0].content).toHaveLength(2) + expect(imagesCalls[0][1][0].content[0]).toEqual({ type: "text", text: "Here is an image" }) + expect(imagesCalls[0][1][0].content[1]).toHaveProperty("type", "image") // Verify model without image support converts image blocks to text - const callsWithoutImages = createMessageSpyWithoutImages.mock.calls - const historyWithoutImages = callsWithoutImages[0][1][0] - expect(historyWithoutImages.content).toHaveLength(2) - expect(historyWithoutImages.content[0]).toEqual({ type: "text", text: "Here is an image" }) - expect(historyWithoutImages.content[1]).toEqual({ + expect(noImagesCalls[0][1][0].content).toHaveLength(2) + expect(noImagesCalls[0][1][0].content[0]).toEqual({ type: "text", text: "Here is an image" }) + expect(noImagesCalls[0][1][0].content[1]).toEqual({ type: "text", text: "[Referenced image in conversation]", }) diff --git a/src/core/__tests__/mode-validator.test.ts b/src/core/__tests__/mode-validator.test.ts index 6842e52..635ae77 100644 --- a/src/core/__tests__/mode-validator.test.ts +++ b/src/core/__tests__/mode-validator.test.ts @@ -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" - -const asTestTool = (tool: string): TestToolName => tool as TestToolName +import { TOOL_GROUPS } from "../../shared/tool-groups" const [codeMode, architectMode, askMode] = modes.map((mode) => mode.slug) describe("mode-validator", () => { @@ -9,21 +8,26 @@ describe("mode-validator", () => { describe("code mode", () => { it("allows all code mode tools", () => { const mode = getModeConfig(codeMode) - mode.tools.forEach(([tool]) => { - expect(isToolAllowedForMode(tool, codeMode)).toBe(true) + // Code mode has all groups + Object.entries(TOOL_GROUPS).forEach(([_, tools]) => { + tools.forEach((tool) => { + expect(isToolAllowedForMode(tool, codeMode, [])).toBe(true) + }) }) }) it("disallows unknown tools", () => { - expect(isToolAllowedForMode(asTestTool("unknown_tool"), codeMode)).toBe(false) + expect(isToolAllowedForMode("unknown_tool" as any, codeMode, [])).toBe(false) }) }) describe("architect mode", () => { it("allows configured tools", () => { const mode = getModeConfig(architectMode) - mode.tools.forEach(([tool]) => { - expect(isToolAllowedForMode(tool, architectMode)).toBe(true) + // Architect mode has read, browser, and mcp groups + 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", () => { it("allows configured tools", () => { const mode = getModeConfig(askMode) - mode.tools.forEach(([tool]) => { - expect(isToolAllowedForMode(tool, askMode)).toBe(true) + // Ask mode has read, browser, and mcp groups + 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", () => { 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.', ) }) it("does not throw for allowed tools in architect mode", () => { - expect(() => validateToolUse("read_file", "architect")).not.toThrow() + expect(() => validateToolUse("read_file", "architect", [])).not.toThrow() }) }) }) diff --git a/src/core/config/ConfigManager.ts b/src/core/config/ConfigManager.ts index 0299955..467eeb3 100644 --- a/src/core/config/ConfigManager.ts +++ b/src/core/config/ConfigManager.ts @@ -1,6 +1,6 @@ import { ExtensionContext } from "vscode" import { ApiConfiguration } from "../../shared/api" -import { Mode } from "../prompts/types" +import { Mode } from "../../shared/modes" import { ApiConfigMeta } from "../../shared/ExtensionMessage" export interface ApiConfigData { diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts new file mode 100644 index 0000000..ad3a2b8 --- /dev/null +++ b/src/core/config/CustomModesManager.ts @@ -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> = [] + + constructor( + private readonly context: vscode.ExtensionContext, + private readonly onUpdate: () => Promise, + ) { + this.watchCustomModesFile() + } + + private async queueWrite(operation: () => Promise): Promise { + this.writeQueue.push(operation) + if (!this.isWriting) { + await this.processWriteQueue() + } + } + + private async processWriteQueue(): Promise { + 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 { + 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 { + 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 { + const modes = await this.context.globalState.get("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 { + 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 { + 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 { + 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 { + 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 = [] + } +} diff --git a/src/core/config/CustomModesSchema.ts b/src/core/config/CustomModesSchema.ts new file mode 100644 index 0000000..386b99f --- /dev/null +++ b/src/core/config/CustomModesSchema.ts @@ -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 + +// 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 + +/** + * 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) +} diff --git a/src/core/config/__tests__/CustomModesManager.test.ts b/src/core/config/__tests__/CustomModesManager.test.ts new file mode 100644 index 0000000..384f0a2 --- /dev/null +++ b/src/core/config/__tests__/CustomModesManager.test.ts @@ -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") + }) + }) +}) diff --git a/src/core/config/__tests__/CustomModesSchema.test.ts b/src/core/config/__tests__/CustomModesSchema.test.ts new file mode 100644 index 0000000..338550b --- /dev/null +++ b/src/core/config/__tests__/CustomModesSchema.test.ts @@ -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 & { 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) + }) + }) + }) +}) diff --git a/src/core/config/__tests__/CustomModesSettings.test.ts b/src/core/config/__tests__/CustomModesSettings.test.ts new file mode 100644 index 0000000..322cd83 --- /dev/null +++ b/src/core/config/__tests__/CustomModesSettings.test.ts @@ -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() + }) + }) +}) diff --git a/src/core/config/__tests__/GroupConfigSchema.test.ts b/src/core/config/__tests__/GroupConfigSchema.test.ts new file mode 100644 index 0000000..3b1452b --- /dev/null +++ b/src/core/config/__tests__/GroupConfigSchema.test.ts @@ -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() + }) + }) +}) diff --git a/src/core/diff/strategies/new-unified/index.ts b/src/core/diff/strategies/new-unified/index.ts index d256614..d82a05a 100644 --- a/src/core/diff/strategies/new-unified/index.ts +++ b/src/core/diff/strategies/new-unified/index.ts @@ -233,7 +233,7 @@ Your diff here originalContent: string, diffContent: string, startLine?: number, - endLine?: number + endLine?: number, ): Promise { const parsedDiff = this.parseUnifiedDiff(diffContent) const originalLines = originalContent.split("\n") @@ -271,7 +271,7 @@ Your diff here subHunkResult, subSearchResult.index, subSearchResult.confidence, - this.confidenceThreshold + this.confidenceThreshold, ) if (subEditResult.confidence >= this.confidenceThreshold) { subHunkResult = subEditResult.result @@ -293,12 +293,12 @@ Your diff here const contextRatio = contextLines / totalLines 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` errorMsg += "Debug Info:\n" errorMsg += `- Search Strategy Used: ${strategy}\n` errorMsg += `- Context Lines: ${contextLines} out of ${totalLines} total lines (${Math.floor( - contextRatio * 100 + contextRatio * 100, )}%)\n` errorMsg += `- Attempted to split into ${subHunks.length} sub-hunks but still failed\n` @@ -330,7 +330,7 @@ Your diff here } else { // Edit failure - likely due to content mismatch let errorMsg = `Failed to apply the edit using ${editResult.strategy} strategy (${Math.floor( - editResult.confidence * 100 + editResult.confidence * 100, )}% confidence)\n\n` errorMsg += "Debug Info:\n" errorMsg += "- The location was found but the content didn't match exactly\n" diff --git a/src/core/mode-validator.ts b/src/core/mode-validator.ts index c432c73..e2f38e2 100644 --- a/src/core/mode-validator.ts +++ b/src/core/mode-validator.ts @@ -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 type { TestToolName } +export type { ToolName } -export function validateToolUse(toolName: TestToolName, mode: Mode): void { - if (!isToolAllowedForMode(toolName, mode)) { +export function validateToolUse(toolName: ToolName, mode: Mode, customModes?: ModeConfig[]): void { + if (!isToolAllowedForMode(toolName, mode, customModes ?? [])) { throw new Error(`Tool "${toolName}" is not allowed in ${mode} mode.`) } } diff --git a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap index b7f97fe..ca390c8 100644 --- a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap +++ b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SYSTEM_PROMPT should explicitly handle undefined mcpHub 1`] = ` -"You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +"You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. ==== @@ -29,20 +29,6 @@ Always adhere to this format for the tool use to ensure proper parsing and execu # Tools -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -Usage: - -Your command here - - -Example: Requesting to execute npm run dev - -npm run dev - - ## read_file Description: Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. The output includes line numbers prefixed to each line (e.g. "1 | const x = 1"), making it easier to reference specific lines when creating diffs or discussing code. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string. Parameters: @@ -57,43 +43,6 @@ Example: Requesting to read frontend-config.json frontend-config.json -## write_to_file -Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file. -Parameters: -- path: (required) The path of the file to write to (relative to the current working directory /test/path) -- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. -Usage: - -File path here - -Your file content here - -total number of lines in the file, including empty lines - - -Example: Requesting to write to frontend-config.json - -frontend-config.json - -{ - "apiEndpoint": "https://api.example.com", - "theme": { - "primaryColor": "#007bff", - "secondaryColor": "#6c757d", - "fontFamily": "Arial, sans-serif" - }, - "features": { - "darkMode": true, - "notifications": true, - "analytics": false - }, - "version": "1.0.0" -} - -14 - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -145,6 +94,57 @@ Example: Requesting to list all top level source code definitions in the current . +## write_to_file +Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file. +Parameters: +- path: (required) The path of the file to write to (relative to the current working directory /test/path) +- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. +- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. +Usage: + +File path here + +Your file content here + +total number of lines in the file, including empty lines + + +Example: Requesting to write to frontend-config.json + +frontend-config.json + +{ + "apiEndpoint": "https://api.example.com", + "theme": { + "primaryColor": "#007bff", + "secondaryColor": "#6c757d", + "fontFamily": "Arial, sans-serif" + }, + "features": { + "darkMode": true, + "notifications": true, + "analytics": false + }, + "version": "1.0.0" +} + +14 + + +## execute_command +Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +Parameters: +- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. +Usage: + +Your command here + + +Example: Requesting to execute npm run dev + +npm run dev + + ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: @@ -217,6 +217,12 @@ CAPABILITIES ==== +MODES + +- Test modes section + +==== + RULES - Your current working directory is: /test/path @@ -263,11 +269,24 @@ You accomplish a given task iteratively, breaking it down into clear steps and w 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go. 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided. 4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built. -5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance." +5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance. + + +==== + +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. + +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules +# Rules from .clinerules: +Mock generic rules" `; exports[`SYSTEM_PROMPT should handle different browser viewport sizes 1`] = ` -"You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +"You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. ==== @@ -295,20 +314,6 @@ Always adhere to this format for the tool use to ensure proper parsing and execu # Tools -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -Usage: - -Your command here - - -Example: Requesting to execute npm run dev - -npm run dev - - ## read_file Description: Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. The output includes line numbers prefixed to each line (e.g. "1 | const x = 1"), making it easier to reference specific lines when creating diffs or discussing code. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string. Parameters: @@ -323,43 +328,6 @@ Example: Requesting to read frontend-config.json frontend-config.json -## write_to_file -Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file. -Parameters: -- path: (required) The path of the file to write to (relative to the current working directory /test/path) -- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. -Usage: - -File path here - -Your file content here - -total number of lines in the file, including empty lines - - -Example: Requesting to write to frontend-config.json - -frontend-config.json - -{ - "apiEndpoint": "https://api.example.com", - "theme": { - "primaryColor": "#007bff", - "secondaryColor": "#6c757d", - "fontFamily": "Arial, sans-serif" - }, - "features": { - "darkMode": true, - "notifications": true, - "analytics": false - }, - "version": "1.0.0" -} - -14 - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -411,6 +379,43 @@ Example: Requesting to list all top level source code definitions in the current . +## write_to_file +Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file. +Parameters: +- path: (required) The path of the file to write to (relative to the current working directory /test/path) +- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. +- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. +Usage: + +File path here + +Your file content here + +total number of lines in the file, including empty lines + + +Example: Requesting to write to frontend-config.json + +frontend-config.json + +{ + "apiEndpoint": "https://api.example.com", + "theme": { + "primaryColor": "#007bff", + "secondaryColor": "#6c757d", + "fontFamily": "Arial, sans-serif" + }, + "features": { + "darkMode": true, + "notifications": true, + "analytics": false + }, + "version": "1.0.0" +} + +14 + + ## browser_action Description: Request to interact with a Puppeteer-controlled browser. Every action, except \`close\`, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action. - The sequence of actions **must always start with** launching the browser at a URL, and **must always end with** closing the browser. If you need to visit a new URL that is not possible to navigate to from the current webpage, you must first close the browser, then launch again at the new URL. @@ -457,6 +462,20 @@ Example: Requesting to click on the element at coordinates 450,300 450,300 +## execute_command +Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +Parameters: +- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. +Usage: + +Your command here + + +Example: Requesting to execute npm run dev + +npm run dev + + ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: @@ -531,6 +550,12 @@ CAPABILITIES ==== +MODES + +- Test modes section + +==== + RULES - Your current working directory is: /test/path @@ -578,11 +603,24 @@ You accomplish a given task iteratively, breaking it down into clear steps and w 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go. 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided. 4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built. -5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance." +5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance. + + +==== + +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. + +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules +# Rules from .clinerules: +Mock generic rules" `; exports[`SYSTEM_PROMPT should include MCP server info when mcpHub is provided 1`] = ` -"You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +"You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. ==== @@ -610,20 +648,6 @@ Always adhere to this format for the tool use to ensure proper parsing and execu # Tools -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -Usage: - -Your command here - - -Example: Requesting to execute npm run dev - -npm run dev - - ## read_file Description: Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. The output includes line numbers prefixed to each line (e.g. "1 | const x = 1"), making it easier to reference specific lines when creating diffs or discussing code. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string. Parameters: @@ -638,43 +662,6 @@ Example: Requesting to read frontend-config.json frontend-config.json -## write_to_file -Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file. -Parameters: -- path: (required) The path of the file to write to (relative to the current working directory /test/path) -- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. -Usage: - -File path here - -Your file content here - -total number of lines in the file, including empty lines - - -Example: Requesting to write to frontend-config.json - -frontend-config.json - -{ - "apiEndpoint": "https://api.example.com", - "theme": { - "primaryColor": "#007bff", - "secondaryColor": "#6c757d", - "fontFamily": "Arial, sans-serif" - }, - "features": { - "darkMode": true, - "notifications": true, - "analytics": false - }, - "version": "1.0.0" -} - -14 - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -726,6 +713,57 @@ Example: Requesting to list all top level source code definitions in the current . +## write_to_file +Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file. +Parameters: +- path: (required) The path of the file to write to (relative to the current working directory /test/path) +- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. +- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. +Usage: + +File path here + +Your file content here + +total number of lines in the file, including empty lines + + +Example: Requesting to write to frontend-config.json + +frontend-config.json + +{ + "apiEndpoint": "https://api.example.com", + "theme": { + "primaryColor": "#007bff", + "secondaryColor": "#6c757d", + "fontFamily": "Arial, sans-serif" + }, + "features": { + "darkMode": true, + "notifications": true, + "analytics": false + }, + "version": "1.0.0" +} + +14 + + +## execute_command +Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +Parameters: +- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. +Usage: + +Your command here + + +Example: Requesting to execute npm run dev + +npm run dev + + ## use_mcp_tool Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters. Parameters: @@ -1209,6 +1247,12 @@ CAPABILITIES - You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively. +==== + +MODES + +- Test modes section + ==== RULES @@ -1257,11 +1301,24 @@ You accomplish a given task iteratively, breaking it down into clear steps and w 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go. 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided. 4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built. -5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance." +5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance. + + +==== + +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. + +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules +# Rules from .clinerules: +Mock generic rules" `; exports[`SYSTEM_PROMPT should include browser actions when supportsComputerUse is true 1`] = ` -"You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +"You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. ==== @@ -1289,20 +1346,6 @@ Always adhere to this format for the tool use to ensure proper parsing and execu # Tools -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -Usage: - -Your command here - - -Example: Requesting to execute npm run dev - -npm run dev - - ## read_file Description: Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. The output includes line numbers prefixed to each line (e.g. "1 | const x = 1"), making it easier to reference specific lines when creating diffs or discussing code. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string. Parameters: @@ -1317,43 +1360,6 @@ Example: Requesting to read frontend-config.json frontend-config.json -## write_to_file -Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file. -Parameters: -- path: (required) The path of the file to write to (relative to the current working directory /test/path) -- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. -- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. -Usage: - -File path here - -Your file content here - -total number of lines in the file, including empty lines - - -Example: Requesting to write to frontend-config.json - -frontend-config.json - -{ - "apiEndpoint": "https://api.example.com", - "theme": { - "primaryColor": "#007bff", - "secondaryColor": "#6c757d", - "fontFamily": "Arial, sans-serif" - }, - "features": { - "darkMode": true, - "notifications": true, - "analytics": false - }, - "version": "1.0.0" -} - -14 - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -1405,6 +1411,43 @@ Example: Requesting to list all top level source code definitions in the current . +## write_to_file +Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file. +Parameters: +- path: (required) The path of the file to write to (relative to the current working directory /test/path) +- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. +- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. +Usage: + +File path here + +Your file content here + +total number of lines in the file, including empty lines + + +Example: Requesting to write to frontend-config.json + +frontend-config.json + +{ + "apiEndpoint": "https://api.example.com", + "theme": { + "primaryColor": "#007bff", + "secondaryColor": "#6c757d", + "fontFamily": "Arial, sans-serif" + }, + "features": { + "darkMode": true, + "notifications": true, + "analytics": false + }, + "version": "1.0.0" +} + +14 + + ## browser_action Description: Request to interact with a Puppeteer-controlled browser. Every action, except \`close\`, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action. - The sequence of actions **must always start with** launching the browser at a URL, and **must always end with** closing the browser. If you need to visit a new URL that is not possible to navigate to from the current webpage, you must first close the browser, then launch again at the new URL. @@ -1451,6 +1494,20 @@ Example: Requesting to click on the element at coordinates 450,300 450,300 +## execute_command +Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +Parameters: +- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. +Usage: + +Your command here + + +Example: Requesting to execute npm run dev + +npm run dev + + ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: @@ -1525,6 +1582,12 @@ CAPABILITIES ==== +MODES + +- Test modes section + +==== + RULES - Your current working directory is: /test/path @@ -1572,11 +1635,24 @@ You accomplish a given task iteratively, breaking it down into clear steps and w 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go. 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided. 4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built. -5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance." +5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance. + + +==== + +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. + +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules +# Rules from .clinerules: +Mock generic rules" `; exports[`SYSTEM_PROMPT should include diff strategy tool description 1`] = ` -"You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +"You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. ==== @@ -1604,20 +1680,6 @@ Always adhere to this format for the tool use to ensure proper parsing and execu # Tools -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -Usage: - -Your command here - - -Example: Requesting to execute npm run dev - -npm run dev - - ## read_file Description: Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. The output includes line numbers prefixed to each line (e.g. "1 | const x = 1"), making it easier to reference specific lines when creating diffs or discussing code. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string. Parameters: @@ -1632,6 +1694,57 @@ Example: Requesting to read frontend-config.json frontend-config.json +## search_files +Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. +Parameters: +- path: (required) The path of the directory to search in (relative to the current working directory /test/path). This directory will be recursively searched. +- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax. +- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*). +Usage: + +Directory path here +Your regex pattern here +file pattern here (optional) + + +Example: Requesting to search for all .ts files in the current directory + +. +.* +*.ts + + +## list_files +Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not. +Parameters: +- path: (required) The path of the directory to list contents for (relative to the current working directory /test/path) +- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only. +Usage: + +Directory path here +true or false (optional) + + +Example: Requesting to list all files in the current directory + +. +false + + +## list_code_definition_names +Description: Request to list definition names (classes, functions, methods, etc.) used in source code files at the top level of the specified directory. This tool provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture. +Parameters: +- path: (required) The path of the directory (relative to the current working directory /test/path) to list top level source code definitions for. +Usage: + +Directory path here + + +Example: Requesting to list all top level source code definitions in the current directory + +. + + ## write_to_file Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file. Parameters: @@ -1729,56 +1842,19 @@ Your search/replace content here 5 -## search_files -Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. +## execute_command +Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path Parameters: -- path: (required) The path of the directory to search in (relative to the current working directory /test/path). This directory will be recursively searched. -- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax. -- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*). +- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Usage: - -Directory path here -Your regex pattern here -file pattern here (optional) - + +Your command here + -Example: Requesting to search for all .ts files in the current directory - -. -.* -*.ts - - -## list_files -Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not. -Parameters: -- path: (required) The path of the directory to list contents for (relative to the current working directory /test/path) -- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only. -Usage: - -Directory path here -true or false (optional) - - -Example: Requesting to list all files in the current directory - -. -false - - -## list_code_definition_names -Description: Request to list definition names (classes, functions, methods, etc.) used in source code files at the top level of the specified directory. This tool provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture. -Parameters: -- path: (required) The path of the directory (relative to the current working directory /test/path) to list top level source code definitions for. -Usage: - -Directory path here - - -Example: Requesting to list all top level source code definitions in the current directory - -. - +Example: Requesting to execute npm run dev + +npm run dev + ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. @@ -1852,6 +1928,12 @@ CAPABILITIES ==== +MODES + +- Test modes section + +==== + RULES - Your current working directory is: /test/path @@ -1898,11 +1980,24 @@ You accomplish a given task iteratively, breaking it down into clear steps and w 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go. 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided. 4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built. -5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance." +5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance. + + +==== + +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. + +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules +# Rules from .clinerules: +Mock generic rules" `; exports[`SYSTEM_PROMPT should maintain consistent system prompt 1`] = ` -"You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +"You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. ==== @@ -1930,20 +2025,6 @@ Always adhere to this format for the tool use to ensure proper parsing and execu # Tools -## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path -Parameters: -- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. -Usage: - -Your command here - - -Example: Requesting to execute npm run dev - -npm run dev - - ## read_file Description: Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. The output includes line numbers prefixed to each line (e.g. "1 | const x = 1"), making it easier to reference specific lines when creating diffs or discussing code. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string. Parameters: @@ -1958,6 +2039,57 @@ Example: Requesting to read frontend-config.json frontend-config.json +## search_files +Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. +Parameters: +- path: (required) The path of the directory to search in (relative to the current working directory /test/path). This directory will be recursively searched. +- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax. +- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*). +Usage: + +Directory path here +Your regex pattern here +file pattern here (optional) + + +Example: Requesting to search for all .ts files in the current directory + +. +.* +*.ts + + +## list_files +Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not. +Parameters: +- path: (required) The path of the directory to list contents for (relative to the current working directory /test/path) +- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only. +Usage: + +Directory path here +true or false (optional) + + +Example: Requesting to list all files in the current directory + +. +false + + +## list_code_definition_names +Description: Request to list definition names (classes, functions, methods, etc.) used in source code files at the top level of the specified directory. This tool provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture. +Parameters: +- path: (required) The path of the directory (relative to the current working directory /test/path) to list top level source code definitions for. +Usage: + +Directory path here + + +Example: Requesting to list all top level source code definitions in the current directory + +. + + ## write_to_file Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file. Parameters: @@ -1995,56 +2127,19 @@ Example: Requesting to write to frontend-config.json 14 -## search_files -Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. +## execute_command +Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path Parameters: -- path: (required) The path of the directory to search in (relative to the current working directory /test/path). This directory will be recursively searched. -- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax. -- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*). +- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Usage: - -Directory path here -Your regex pattern here -file pattern here (optional) - + +Your command here + -Example: Requesting to search for all .ts files in the current directory - -. -.* -*.ts - - -## list_files -Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not. -Parameters: -- path: (required) The path of the directory to list contents for (relative to the current working directory /test/path) -- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only. -Usage: - -Directory path here -true or false (optional) - - -Example: Requesting to list all files in the current directory - -. -false - - -## list_code_definition_names -Description: Request to list definition names (classes, functions, methods, etc.) used in source code files at the top level of the specified directory. This tool provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture. -Parameters: -- path: (required) The path of the directory (relative to the current working directory /test/path) to list top level source code definitions for. -Usage: - -Directory path here - - -Example: Requesting to list all top level source code definitions in the current directory - -. - +Example: Requesting to execute npm run dev + +npm run dev + ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. @@ -2118,6 +2213,12 @@ CAPABILITIES ==== +MODES + +- Test modes section + +==== + RULES - Your current working directory is: /test/path @@ -2164,7 +2265,20 @@ You accomplish a given task iteratively, breaking it down into clear steps and w 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go. 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided. 4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built. -5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance." +5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance. + + +==== + +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. + +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules +# Rules from .clinerules: +Mock generic rules" `; exports[`addCustomInstructions should combine all custom instructions 1`] = ` @@ -2175,14 +2289,17 @@ 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. +Language Preference: You should always speak and think in the French language. +Mode-specific Instructions: Custom test instructions +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; exports[`addCustomInstructions should combine global and mode-specific instructions 1`] = ` @@ -2193,14 +2310,17 @@ 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. +Global Instructions: Global instructions +Mode-specific Instructions: Mode-specific instructions +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; exports[`addCustomInstructions should fall back to generic rules when mode-specific rules not found 1`] = ` @@ -2211,14 +2331,15 @@ 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. +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; exports[`addCustomInstructions should generate correct prompt for architect mode 1`] = ` -"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. +"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. ==== @@ -2383,6 +2504,12 @@ CAPABILITIES ==== +MODES + +- Test modes section + +==== + RULES - Your current working directory is: /test/path @@ -2429,11 +2556,24 @@ You accomplish a given task iteratively, breaking it down into clear steps and w 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go. 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided. 4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built. -5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance." +5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance. + + +==== + +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. + +Rules: +# Rules from .clinerules-architect: +Mock mode-specific rules +# Rules from .clinerules: +Mock generic rules" `; exports[`addCustomInstructions should generate correct prompt for ask mode 1`] = ` -"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. +"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. ==== @@ -2598,6 +2738,12 @@ CAPABILITIES ==== +MODES + +- Test modes section + +==== + RULES - Your current working directory is: /test/path @@ -2644,7 +2790,20 @@ You accomplish a given task iteratively, breaking it down into clear steps and w 2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go. 3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided. 4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built. -5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance." +5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance. + + +==== + +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. + +Rules: +# Rules from .clinerules-ask: +Mock mode-specific rules +# Rules from .clinerules: +Mock generic rules" `; exports[`addCustomInstructions should handle empty mode-specific instructions 1`] = ` @@ -2655,10 +2814,11 @@ 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. +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; exports[`addCustomInstructions should handle undefined mode-specific instructions 1`] = ` @@ -2669,10 +2829,11 @@ 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. +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; exports[`addCustomInstructions should include custom instructions when provided 1`] = ` @@ -2683,12 +2844,14 @@ 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. +Mode-specific Instructions: Custom test instructions +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; exports[`addCustomInstructions should include preferred language when provided 1`] = ` @@ -2699,12 +2862,14 @@ 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. +Language Preference: You should always speak and think in the Spanish language. +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; exports[`addCustomInstructions should prioritize mode-specific instructions after global ones 1`] = ` @@ -2715,14 +2880,17 @@ 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. +Global Instructions: First instruction +Mode-specific Instructions: Second instruction +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; exports[`addCustomInstructions should prioritize mode-specific rules for architect mode 1`] = ` @@ -2733,14 +2901,11 @@ 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. +Rules: # Rules from .clinerules-architect: -# Architect Mode Rules -1. Architect specific rule - +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; exports[`addCustomInstructions should prioritize mode-specific rules for ask mode 1`] = ` @@ -2751,14 +2916,11 @@ 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. +Rules: # Rules from .clinerules-ask: -# Ask Mode Rules -1. Ask specific rule - +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; exports[`addCustomInstructions should prioritize mode-specific rules for code mode 1`] = ` @@ -2769,14 +2931,11 @@ 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. +Rules: # Rules from .clinerules-code: -# Code Mode Rules -1. Code specific rule - +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; exports[`addCustomInstructions should prioritize mode-specific rules for code reviewer mode 1`] = ` @@ -2787,15 +2946,11 @@ 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. +Rules: # Rules from .clinerules-review: -# Code Reviewer Rules -1. Provide specific examples in feedback -2. Focus on maintainability and best practices - +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; exports[`addCustomInstructions should prioritize mode-specific rules for test engineer mode 1`] = ` @@ -2806,15 +2961,11 @@ 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. +Rules: # Rules from .clinerules-test: -# Test Engineer Rules -1. Always write tests first -2. Get approval before modifying non-test code - +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; exports[`addCustomInstructions should trim mode-specific instructions 1`] = ` @@ -2825,10 +2976,12 @@ 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. -Custom mode instructions +Mode-specific Instructions: + Custom mode instructions +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules # Rules from .clinerules: -# Test Rules -1. First rule -2. Second rule" +Mock generic rules" `; diff --git a/src/core/prompts/__tests__/system.test.ts b/src/core/prompts/__tests__/system.test.ts index 3487f98..5a87a18 100644 --- a/src/core/prompts/__tests__/system.test.ts +++ b/src/core/prompts/__tests__/system.test.ts @@ -1,13 +1,62 @@ -import { SYSTEM_PROMPT, addCustomInstructions } from "../system" +import { SYSTEM_PROMPT } from "../system" import { McpHub } from "../../../services/mcp/McpHub" import { McpServer } from "../../../shared/mcp" import { ClineProvider } from "../../../core/webview/ClineProvider" import { SearchReplaceDiffStrategy } from "../../../core/diff/strategies/search-replace" +import * as vscode from "vscode" import fs from "fs/promises" import os from "os" import { defaultModeSlug, modes } from "../../../shared/modes" // Import path utils to get access to toPosix string extension 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 jest.mock("os", () => ({ @@ -19,42 +68,38 @@ jest.mock("default-shell", () => "/bin/bash") jest.mock("os-name", () => () => "Linux") -// Mock fs.readFile to return empty mcpServers config and mock rules files -jest.mock("fs/promises", () => ({ - ...jest.requireActual("fs/promises"), - readFile: jest.fn().mockImplementation(async (path: string) => { - if (path.endsWith("mcpSettings.json")) { - return '{"mcpServers": {}}' - } - if (path.endsWith(".clinerules-code")) { - return "# Code Mode Rules\n1. Code specific rule" - } - if (path.endsWith(".clinerules-ask")) { - return "# Ask Mode Rules\n1. Ask specific rule" - } - if (path.endsWith(".clinerules-architect")) { - return "# Architect Mode Rules\n1. Architect specific rule" - } - if (path.endsWith(".clinerules")) { - return "# Test Rules\n1. First rule\n2. Second rule" - } - return "" - }), - writeFile: jest.fn().mockResolvedValue(undefined), -})) +// Create a mock ExtensionContext +const mockContext = { + extensionPath: "/mock/extension/path", + globalStoragePath: "/mock/storage/path", + storagePath: "/mock/storage/path", + logPath: "/mock/log/path", + subscriptions: [], + workspaceState: { + get: () => undefined, + update: () => Promise.resolve(), + }, + globalState: { + get: () => undefined, + update: () => Promise.resolve(), + setKeysForSync: () => {}, + }, + extensionUri: { fsPath: "/mock/extension/path" }, + globalStorageUri: { fsPath: "/mock/settings/path" }, + asAbsolutePath: (relativePath: string) => `/mock/extension/path/${relativePath}`, + extension: { + packageJSON: { + version: "1.0.0", + }, + }, +} as unknown as vscode.ExtensionContext // Create a minimal mock of ClineProvider const mockProvider = { ensureMcpServersDirectoryExists: async () => "/mock/mcp/path", ensureSettingsDirectoryExists: async () => "/mock/settings/path", postMessageToWebview: async () => {}, - context: { - extension: { - packageJSON: { - version: "1.0.0", - }, - }, - }, + context: mockContext, } as unknown as ClineProvider // Instead of extending McpHub, create a mock that implements just what we need @@ -77,6 +122,26 @@ const createMockMcpHub = (): McpHub => describe("SYSTEM_PROMPT", () => { 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(() => { jest.clearAllMocks() }) @@ -90,18 +155,32 @@ describe("SYSTEM_PROMPT", () => { it("should maintain consistent system prompt", async () => { const prompt = await SYSTEM_PROMPT( + mockContext, "/test/path", false, // supportsComputerUse undefined, // mcpHub undefined, // diffStrategy undefined, // browserViewportSize + defaultModeSlug, // mode + undefined, // customPrompts + undefined, // customModes ) expect(prompt).toMatchSnapshot() }) 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() }) @@ -109,18 +188,32 @@ describe("SYSTEM_PROMPT", () => { it("should include MCP server info when mcpHub is provided", async () => { 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() }) it("should explicitly handle undefined mcpHub", async () => { const prompt = await SYSTEM_PROMPT( + mockContext, "/test/path", - false, + false, // supportsComputerUse undefined, // explicitly undefined mcpHub - undefined, - undefined, + undefined, // diffStrategy + undefined, // browserViewportSize + defaultModeSlug, // mode + undefined, // customPrompts + undefined, // customModes ) expect(prompt).toMatchSnapshot() @@ -128,11 +221,15 @@ describe("SYSTEM_PROMPT", () => { it("should handle different browser viewport sizes", async () => { const prompt = await SYSTEM_PROMPT( + mockContext, "/test/path", - true, - undefined, - undefined, + true, // supportsComputerUse + undefined, // mcpHub + undefined, // diffStrategy "900x600", // different viewport size + defaultModeSlug, // mode + undefined, // customPrompts + undefined, // customModes ) expect(prompt).toMatchSnapshot() @@ -140,187 +237,198 @@ describe("SYSTEM_PROMPT", () => { it("should include diff strategy tool description", async () => { const prompt = await SYSTEM_PROMPT( + mockContext, "/test/path", - false, - undefined, + false, // supportsComputerUse + undefined, // mcpHub new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase - undefined, + undefined, // browserViewportSize + defaultModeSlug, // mode + undefined, // customPrompts + undefined, // customModes ) 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(() => { jest.restoreAllMocks() }) }) 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(() => { jest.clearAllMocks() }) 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() }) 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() }) 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() }) 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() }) 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() }) it("should prioritize mode-specific rules for test engineer mode", async () => { - // Mock readFile to include test engineer rules - 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") + const instructions = await addCustomInstructions("", "", "/test/path", "test") expect(instructions).toMatchSnapshot() }) it("should prioritize mode-specific rules for code reviewer mode", async () => { - // Mock readFile to include code reviewer rules - 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") + const instructions = await addCustomInstructions("", "", "/test/path", "review") expect(instructions).toMatchSnapshot() }) it("should fall back to generic rules when mode-specific rules not found", async () => { - // Mock readFile to return ENOENT for mode-specific file - 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) - + const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug) expect(instructions).toMatchSnapshot() }) it("should include preferred language when provided", async () => { - const instructions = await addCustomInstructions( - { preferredLanguage: "Spanish" }, - "/test/path", - defaultModeSlug, - ) - + const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug, { + preferredLanguage: "Spanish", + }) expect(instructions).toMatchSnapshot() }) it("should include custom instructions when provided", async () => { - const instructions = await addCustomInstructions( - { customInstructions: "Custom test instructions" }, - "/test/path", - ) - + const instructions = await addCustomInstructions("Custom test instructions", "", "/test/path", defaultModeSlug) expect(instructions).toMatchSnapshot() }) it("should combine all custom instructions", async () => { const instructions = await addCustomInstructions( - { - customInstructions: "Custom test instructions", - preferredLanguage: "French", - }, + "Custom test instructions", + "", "/test/path", defaultModeSlug, + { preferredLanguage: "French" }, ) expect(instructions).toMatchSnapshot() }) it("should handle undefined mode-specific instructions", async () => { - const instructions = await addCustomInstructions({}, "/test/path") - + const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug) expect(instructions).toMatchSnapshot() }) it("should trim mode-specific instructions", async () => { const instructions = await addCustomInstructions( - { customInstructions: " Custom mode instructions " }, + " Custom mode instructions ", + "", "/test/path", + defaultModeSlug, ) - expect(instructions).toMatchSnapshot() }) 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() }) it("should combine global and mode-specific instructions", async () => { const instructions = await addCustomInstructions( - { - customInstructions: "Global instructions", - customPrompts: { - code: { customInstructions: "Mode-specific instructions" }, - }, - }, + "Mode-specific instructions", + "Global instructions", "/test/path", defaultModeSlug, ) - expect(instructions).toMatchSnapshot() }) it("should prioritize mode-specific instructions after global ones", async () => { const instructions = await addCustomInstructions( - { - customInstructions: "First instruction", - customPrompts: { - code: { customInstructions: "Second instruction" }, - }, - }, + "Second instruction", + "First instruction", "/test/path", defaultModeSlug, ) diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index b55e472..0808c4b 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -23,28 +23,70 @@ export async function loadRuleFiles(cwd: string): Promise { } export async function addCustomInstructions( - customInstructions: string, + modeCustomInstructions: string, + globalCustomInstructions: string, cwd: string, - preferredLanguage?: string, + mode: string, + options: { preferredLanguage?: string } = {}, ): Promise { - const ruleFileContent = await loadRuleFiles(cwd) - const allInstructions = [] + const sections = [] - if (preferredLanguage) { - allInstructions.push(`You should always speak and think in the ${preferredLanguage} language.`) + // Load mode-specific rules if mode is provided + 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()) { - allInstructions.push(customInstructions.trim()) + // Add language preference if provided + if (options.preferredLanguage) { + sections.push( + `Language Preference:\nYou should always speak and think in the ${options.preferredLanguage} language.`, + ) } - if (ruleFileContent && ruleFileContent.trim()) { - allInstructions.push(ruleFileContent.trim()) + // Add global instructions first + 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. -${joinedInstructions}` +${joinedSections}` : "" } diff --git a/src/core/prompts/sections/index.ts b/src/core/prompts/sections/index.ts index 06cfcb6..a9f8c9e 100644 --- a/src/core/prompts/sections/index.ts +++ b/src/core/prompts/sections/index.ts @@ -6,3 +6,4 @@ export { getSharedToolUseSection } from "./tool-use" export { getMcpServersSection } from "./mcp-servers" export { getToolUseGuidelinesSection } from "./tool-use-guidelines" export { getCapabilitiesSection } from "./capabilities" +export { getModesSection } from "./modes" diff --git a/src/core/prompts/sections/modes.ts b/src/core/prompts/sections/modes.ts new file mode 100644 index 0000000..36ba413 --- /dev/null +++ b/src/core/prompts/sections/modes.ts @@ -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 { + 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 + } + ] +}` +} diff --git a/src/core/prompts/sections/rules.ts b/src/core/prompts/sections/rules.ts index df6da0c..18caa48 100644 --- a/src/core/prompts/sections/rules.ts +++ b/src/core/prompts/sections/rules.ts @@ -1,6 +1,16 @@ 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") : "" + const customModesPath = path.join(settingsDir, "cline_custom_modes.json") return `==== 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)\`. - 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. -${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. - 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. diff --git a/src/core/prompts/sections/system-info.ts b/src/core/prompts/sections/system-info.ts index 5721b1b..2e04a6f 100644 --- a/src/core/prompts/sections/system-info.ts +++ b/src/core/prompts/sections/system-info.ts @@ -1,9 +1,15 @@ import defaultShell from "default-shell" import os from "os" import osName from "os-name" +import { Mode, ModeConfig, getModeBySlug, defaultModeSlug, isToolAllowedForMode } from "../../../shared/modes" -export function getSystemInfoSection(cwd: string): string { - return `==== +export function getSystemInfoSection(cwd: string, currentMode: Mode, customModes?: ModeConfig[]): string { + 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 @@ -13,4 +19,6 @@ Home Directory: ${os.homedir().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.` + + return details } diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 8a77f0d..3d30a96 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -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 { McpHub } from "../../services/mcp/McpHub" import { getToolDescriptionsForMode } from "./tools" +import * as vscode from "vscode" import { getRulesSection, getSystemInfoSection, @@ -10,88 +20,14 @@ import { getMcpServersSection, getToolUseGuidelinesSection, getCapabilitiesSection, + getModesSection, + addCustomInstructions, } from "./sections" import fs from "fs/promises" import path from "path" -async function loadRuleFiles(cwd: string, mode: Mode): Promise { - 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 { - 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( + context: vscode.ExtensionContext, cwd: string, supportsComputerUse: boolean, mode: Mode, @@ -99,29 +35,57 @@ async function generatePrompt( diffStrategy?: DiffStrategy, browserViewportSize?: string, promptComponent?: PromptComponent, + customModeConfigs?: ModeConfig[], + globalCustomInstructions?: string, ): Promise { - 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()} -${getToolDescriptionsForMode(mode, cwd, supportsComputerUse, diffStrategy, browserViewportSize, mcpHub)} +${getToolDescriptionsForMode( + mode, + cwd, + supportsComputerUse, + diffStrategy, + browserViewportSize, + mcpHub, + customModeConfigs, +)} ${getToolUseGuidelinesSection()} -${await getMcpServersSection(mcpHub, diffStrategy)} +${mcpServersSection} ${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 } export const SYSTEM_PROMPT = async ( + context: vscode.ExtensionContext, cwd: string, supportsComputerUse: boolean, mcpHub?: McpHub, @@ -129,7 +93,13 @@ export const SYSTEM_PROMPT = async ( browserViewportSize?: string, mode: Mode = defaultModeSlug, customPrompts?: CustomPrompts, -) => { + customModes?: ModeConfig[], + globalCustomInstructions?: string, +): Promise => { + if (!context) { + throw new Error("Extension context is required for generating system prompt") + } + const getPromptComponent = (value: unknown) => { if (typeof value === "object" && value !== null) { return value as PromptComponent @@ -137,11 +107,13 @@ export const SYSTEM_PROMPT = async ( return undefined } - // Use default mode if not found - const currentMode = modes.find((m) => m.slug === mode) || modes[0] - const promptComponent = getPromptComponent(customPrompts?.[currentMode.slug]) + // Check if it's a custom mode + const promptComponent = getPromptComponent(customPrompts?.[mode]) + // 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( + context, cwd, supportsComputerUse, currentMode.slug, @@ -149,5 +121,7 @@ export const SYSTEM_PROMPT = async ( diffStrategy, browserViewportSize, promptComponent, + customModes, + globalCustomInstructions, ) } diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index 9627a32..001942c 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -11,7 +11,8 @@ import { getUseMcpToolDescription } from "./use-mcp-tool" import { getAccessMcpResourceDescription } from "./access-mcp-resource" import { DiffStrategy } from "../../diff/DiffStrategy" 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" // Map of tool names to their description functions @@ -38,8 +39,9 @@ export function getToolDescriptionsForMode( diffStrategy?: DiffStrategy, browserViewportSize?: string, mcpHub?: McpHub, + customModes?: ModeConfig[], ): string { - const config = getModeConfig(mode) + const config = getModeConfig(mode, customModes) const args: ToolArgs = { cwd, supportsComputerUse, @@ -48,16 +50,27 @@ export function getToolDescriptionsForMode( mcpHub, } - // Map tool descriptions in the exact order specified in the mode's tools array - const descriptions = config.tools.map(([toolName, toolOptions]) => { + // Get all tools from the mode's groups and always available tools + const tools = new Set() + + // 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] - if (!descriptionFn || !isToolAllowedForMode(toolName as ToolName, mode)) { + if (!descriptionFn || !isToolAllowedForMode(toolName as ToolName, mode, customModes ?? [])) { return undefined } return descriptionFn({ ...args, - toolOptions, + toolOptions: undefined, // No tool options in group-based approach }) }) diff --git a/src/core/prompts/types.ts b/src/core/prompts/types.ts deleted file mode 100644 index 1ac1a18..0000000 --- a/src/core/prompts/types.ts +++ /dev/null @@ -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", -] diff --git a/src/core/tool-lists.ts b/src/core/tool-lists.ts deleted file mode 100644 index 862106b..0000000 --- a/src/core/tool-lists.ts +++ /dev/null @@ -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] diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 04c24a1..72e7e27 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -16,9 +16,9 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api" import { findLast } from "../../shared/array" import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage" import { HistoryItem } from "../../shared/HistoryItem" -import { WebviewMessage, PromptMode } from "../../shared/WebviewMessage" -import { defaultModeSlug, defaultPrompts } from "../../shared/modes" -import { SYSTEM_PROMPT, addCustomInstructions } from "../prompts/system" +import { WebviewMessage } from "../../shared/WebviewMessage" +import { defaultModeSlug } from "../../shared/modes" +import { SYSTEM_PROMPT } from "../prompts/system" import { fileExistsAtPath } from "../../utils/fs" import { Cline } from "../Cline" import { openMention } from "../mentions" @@ -29,7 +29,8 @@ import { checkExistKey } from "../../shared/checkExistApiConfig" import { enhancePrompt } from "../../utils/enhance-prompt" import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git" 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 @@ -100,6 +101,7 @@ type GlobalStateKey = | "enhancementApiConfigId" | "experimentalDiffStrategy" | "autoApprovalEnabled" + | "customModes" // Array of custom modes export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -118,8 +120,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { private cline?: Cline private workspaceTracker?: WorkspaceTracker 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 + customModesManager: CustomModesManager constructor( readonly context: vscode.ExtensionContext, @@ -130,6 +133,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.workspaceTracker = new WorkspaceTracker(this) this.mcpHub = new McpHub(this) 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.mcpHub?.dispose() this.mcpHub = undefined + this.customModesManager?.dispose() this.outputChannel.appendLine("Disposed all disposables") ClineProvider.activeInstances.delete(this) } @@ -258,8 +265,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { } = await this.getState() const modePrompt = customPrompts?.[mode] - const modeInstructions = typeof modePrompt === "object" ? modePrompt.customInstructions : undefined - const effectiveInstructions = [globalInstructions, modeInstructions].filter(Boolean).join("\n\n") + const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") this.cline = new Cline( this, @@ -287,8 +293,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { } = await this.getState() const modePrompt = customPrompts?.[mode] - const modeInstructions = typeof modePrompt === "object" ? modePrompt.customInstructions : undefined - const effectiveInstructions = [globalInstructions, modeInstructions].filter(Boolean).join("\n\n") + const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") this.cline = new Cline( this, @@ -377,7 +382,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { - Cline + Roo Code @@ -399,6 +404,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { async (message: WebviewMessage) => { switch (message.type) { case "webviewDidLaunch": + // Load custom modes first + const customModes = await this.customModesManager.getCustomModes() + await this.updateGlobalState("customModes", customModes) + this.postStateToWebview() this.workspaceTracker?.initializeFilePaths() // don't await getTheme().then((theme) => @@ -958,26 +967,35 @@ export class ClineProvider implements vscode.WebviewViewProvider { vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || "" const mode = message.mode ?? defaultModeSlug - const instructions = await addCustomInstructions( - { customInstructions, customPrompts, preferredLanguage }, - cwd, - mode, - ) + const customModes = await this.customModesManager.getCustomModes() + + const modePrompt = customPrompts?.[mode] + const effectiveInstructions = [customInstructions, modePrompt?.customInstructions] + .filter(Boolean) + .join("\n\n") const systemPrompt = await SYSTEM_PROMPT( + this.context, cwd, apiConfiguration.openRouterModelInfo?.supportsComputerUse ?? false, mcpEnabled ? this.mcpHub : undefined, undefined, browserViewportSize ?? "900x600", mode, - customPrompts, + { + ...customPrompts, + [mode]: { + ...(modePrompt ?? {}), + customInstructions: undefined, // Prevent double-inclusion + }, + }, + customModes, + effectiveInstructions || undefined, ) - const fullPrompt = instructions ? `${systemPrompt}${instructions}` : systemPrompt await this.postMessageToWebview({ type: "systemPrompt", - text: fullPrompt, + text: systemPrompt, mode: message.mode, }) } catch (error) { @@ -1115,6 +1133,34 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.cline.updateDiffStrategy(message.bool ?? false) } 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, @@ -1727,6 +1773,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { enhancementApiConfigId, experimentalDiffStrategy: experimentalDiffStrategy ?? false, autoApprovalEnabled: autoApprovalEnabled ?? false, + customModes: await this.customModesManager.getCustomModes(), } } @@ -1844,6 +1891,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { enhancementApiConfigId, experimentalDiffStrategy, autoApprovalEnabled, + customModes, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -1906,6 +1954,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("enhancementApiConfigId") as Promise, this.getGlobalState("experimentalDiffStrategy") as Promise, this.getGlobalState("autoApprovalEnabled") as Promise, + this.customModesManager.getCustomModes(), ]) let apiProvider: ApiProvider @@ -2014,6 +2063,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { enhancementApiConfigId, experimentalDiffStrategy: experimentalDiffStrategy ?? false, autoApprovalEnabled: autoApprovalEnabled ?? false, + customModes, } } @@ -2107,6 +2157,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.storeSecret(key, undefined) } await this.configManager.resetAllConfigs() + await this.customModesManager.resetCustomModes() if (this.cline) { this.cline.abortTask() this.cline = undefined diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index ed194dd..ff427a9 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -3,6 +3,13 @@ import * as vscode from "vscode" import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage" import { setSoundEnabled } from "../../../utils/sound" 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 jest.mock("delay", () => { @@ -130,7 +137,6 @@ jest.mock("../../../api", () => ({ jest.mock("../../prompts/system", () => ({ SYSTEM_PROMPT: jest.fn().mockImplementation(async () => "mocked system prompt"), codeMode: "code", - addCustomInstructions: jest.fn().mockImplementation(async () => ""), })) // Mock WorkspaceTracker @@ -221,6 +227,13 @@ describe("ClineProvider", () => { }, } as unknown as vscode.ExtensionContext + // Mock CustomModesManager + const mockCustomModesManager = { + updateCustomMode: jest.fn().mockResolvedValue(undefined), + getCustomModes: jest.fn().mockResolvedValue({}), + dispose: jest.fn(), + } + // Mock output channel mockOutputChannel = { appendLine: jest.fn(), @@ -250,6 +263,8 @@ describe("ClineProvider", () => { } as unknown as vscode.WebviewView provider = new ClineProvider(mockContext, mockOutputChannel) + // @ts-ignore - accessing private property for testing + provider.customModesManager = mockCustomModesManager }) test("constructor initializes correctly", () => { @@ -297,6 +312,7 @@ describe("ClineProvider", () => { mcpEnabled: true, requestDelaySeconds: 5, mode: defaultModeSlug, + customModes: [], } const message: ExtensionMessage = { @@ -831,6 +847,13 @@ describe("ClineProvider", () => { beforeEach(() => { mockPostMessage.mockClear() provider.resolveWebviewView(mockWebviewView) + // Reset and setup mock + mockAddCustomInstructions.mockClear() + mockAddCustomInstructions.mockImplementation( + (modeInstructions: string, globalInstructions: string, cwd: string) => { + return Promise.resolve(modeInstructions || globalInstructions || "") + }, + ) }) const getMessageHandler = () => { @@ -913,77 +936,132 @@ describe("ClineProvider", () => { expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to get system prompt") }) - test("uses mode-specific custom instructions in system prompt", async () => { - const systemPrompt = require("../../prompts/system") - const { addCustomInstructions } = systemPrompt + test("uses code mode custom instructions", async () => { + // Get the mock function + const mockAddCustomInstructions = (jest.requireMock("../../prompts/sections/custom-instructions") as any) + .addCustomInstructions - // Mock getState to return mode-specific custom instructions - jest.spyOn(provider, "getState").mockResolvedValue({ - apiConfiguration: { - apiProvider: "openrouter", - openRouterModelInfo: { supportsComputerUse: true }, - }, - customPrompts: { - code: { customInstructions: "Code mode specific instructions" }, - }, - mode: "code", - mcpEnabled: false, - browserViewportSize: "900x600", - } as any) + // Clear any previous calls + mockAddCustomInstructions.mockClear() - const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] - await messageHandler({ type: "getSystemPrompt", mode: "code" }) + // Mock SYSTEM_PROMPT + 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 - expect(addCustomInstructions).toHaveBeenCalledWith( - { - customInstructions: undefined, - customPrompts: { - code: { customInstructions: "Code mode specific instructions" }, - }, - preferredLanguage: undefined, - }, + // Trigger getSystemPrompt + const promptHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] + await promptHandler({ type: "getSystemPrompt" }) + + // Verify mock was called with code mode instructions + expect(mockAddCustomInstructions).toHaveBeenCalledWith( + "Code mode specific instructions", + "", expect.any(String), - "code", ) }) test("uses correct mode-specific instructions when mode is specified", async () => { - const systemPrompt = require("../../prompts/system") - const { addCustomInstructions } = systemPrompt - - // Mock getState to return instructions for multiple modes + // Mock getState to return architect mode instructions jest.spyOn(provider, "getState").mockResolvedValue({ apiConfiguration: { apiProvider: "openrouter", openRouterModelInfo: { supportsComputerUse: true }, }, customPrompts: { - code: { customInstructions: "Code mode instructions" }, architect: { customInstructions: "Architect mode instructions" }, }, - mode: "code", + mode: "architect", mcpEnabled: false, browserViewportSize: "900x600", } 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 - await messageHandler({ type: "getSystemPrompt", mode: "architect" }) + // Resolve webview and trigger getSystemPrompt + 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 - expect(addCustomInstructions).toHaveBeenCalledWith( - { - customInstructions: undefined, - customPrompts: { - code: { customInstructions: "Code mode instructions" }, - architect: { customInstructions: "Architect mode instructions" }, - }, - preferredLanguage: undefined, - }, + expect(mockAddCustomInstructions).toHaveBeenCalledWith( + "Architect mode instructions", + "", 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", + }), + }), + }), + }), ) }) }) diff --git a/src/extension.ts b/src/extension.ts index 04e911f..0cf3053 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,10 +21,10 @@ let outputChannel: vscode.OutputChannel // This method is called when your extension is activated // Your extension is activated the very first time the command is executed export function activate(context: vscode.ExtensionContext) { - outputChannel = vscode.window.createOutputChannel("Roo-Cline") + outputChannel = vscode.window.createOutputChannel("Roo-Code") context.subscriptions.push(outputChannel) - outputChannel.appendLine("Roo-Cline extension activated") + outputChannel.appendLine("Roo-Code extension activated") // Get default commands from configuration const defaultCommands = vscode.workspace.getConfiguration("roo-cline").get("allowedCommands") || [] @@ -64,7 +64,7 @@ export function activate(context: vscode.ExtensionContext) { ) 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) // https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts 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 panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Cline", targetCol, { + const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, { enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [context.extensionUri], @@ -163,5 +163,5 @@ export function activate(context: vscode.ExtensionContext) { // This method is called when your extension is deactivated export function deactivate() { - outputChannel.appendLine("Roo-Cline extension deactivated") + outputChannel.appendLine("Roo-Code extension deactivated") } diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index a145a22..61c4e76 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -161,17 +161,17 @@ export class DiffViewProvider { Getting diagnostics before and after the file edit is a better approach than automatically tracking problems in real-time. This method ensures we only 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 - directly related to the work he's doing. This eliminates the risk of Cline + 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 Roo 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 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 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 - 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. */ const postDiagnostics = vscode.languages.getDiagnostics() @@ -297,7 +297,7 @@ export class DiffViewProvider { query: Buffer.from(this.originalContent ?? "").toString("base64"), }), 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 setTimeout(() => { diff --git a/src/integrations/terminal/TerminalRegistry.ts b/src/integrations/terminal/TerminalRegistry.ts index e016147..2fb49e4 100644 --- a/src/integrations/terminal/TerminalRegistry.ts +++ b/src/integrations/terminal/TerminalRegistry.ts @@ -16,7 +16,7 @@ export class TerminalRegistry { static createTerminal(cwd?: string | vscode.Uri | undefined): TerminalInfo { const terminal = vscode.window.createTerminal({ cwd, - name: "Roo Cline", + name: "Roo Code", iconPath: new vscode.ThemeIcon("rocket"), env: { PAGER: "cat", diff --git a/src/integrations/terminal/__tests__/TerminalRegistry.test.ts b/src/integrations/terminal/__tests__/TerminalRegistry.test.ts index 6f535f0..cc667a8 100644 --- a/src/integrations/terminal/__tests__/TerminalRegistry.test.ts +++ b/src/integrations/terminal/__tests__/TerminalRegistry.test.ts @@ -26,7 +26,7 @@ describe("TerminalRegistry", () => { expect(mockCreateTerminal).toHaveBeenCalledWith({ cwd: "/test/path", - name: "Roo Cline", + name: "Roo Code", iconPath: expect.any(Object), env: { PAGER: "cat", diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index b13851f..6dd5dd4 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -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. const client = new Client( { - name: "Cline", + name: "Roo Code", version: this.providerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0", }, { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 10fa0c5..cab56d5 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -4,7 +4,14 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "./api" import { HistoryItem } from "./HistoryItem" import { McpServer } from "./mcp" 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 export interface ExtensionMessage { @@ -31,6 +38,8 @@ export interface ExtensionMessage { | "updatePrompt" | "systemPrompt" | "autoApprovalEnabled" + | "updateCustomMode" + | "deleteCustomMode" text?: string action?: | "chatButtonClicked" @@ -54,6 +63,8 @@ export interface ExtensionMessage { commits?: GitCommit[] listApiConfig?: ApiConfigMeta[] mode?: Mode + customMode?: ModeConfig + slug?: string } export interface ApiConfigMeta { @@ -96,6 +107,7 @@ export interface ExtensionState { enhancementApiConfigId?: string experimentalDiffStrategy?: boolean autoApprovalEnabled?: boolean + customModes: ModeConfig[] } export interface ClineMessage { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 28ae9c9..ce05976 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -1,5 +1,5 @@ import { ApiConfiguration, ApiProvider } from "./api" -import { Mode, PromptComponent } from "./modes" +import { Mode, PromptComponent, ModeConfig } from "./modes" export type PromptMode = Mode | "enhance" @@ -74,6 +74,8 @@ export interface WebviewMessage { | "enhancementApiConfigId" | "experimentalDiffStrategy" | "autoApprovalEnabled" + | "updateCustomMode" + | "deleteCustomMode" text?: string disabled?: boolean askResponse?: ClineAskResponse @@ -92,6 +94,8 @@ export interface WebviewMessage { dataUrls?: string[] values?: Record query?: string + slug?: string + modeConfig?: ModeConfig } export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse" diff --git a/src/shared/modes.ts b/src/shared/modes.ts index c8f8986..a8e0ae7 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -1,10 +1,4 @@ -// Tool options for specific tools -export type ToolOptions = { - string: readonly string[] -} - -// Tool configuration tuple type -export type ToolConfig = readonly [string] | readonly [string, ToolOptions] +import { TOOL_GROUPS, ToolGroup, ALWAYS_AVAILABLE_TOOLS } from "./tool-groups" // Mode types export type Mode = string @@ -14,7 +8,124 @@ export type ModeConfig = { slug: string name: 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() + + // 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 @@ -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):", } as const -// Main modes configuration as an ordered array -export const modes: readonly ModeConfig[] = [ - { - slug: "code", - name: "Code", - 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, +// Completely separate enhance prompt handling +export const enhancePrompt = { + default: enhance.prompt, + get: (customPrompts: Record | undefined): string => { + return customPrompts?.enhance ?? enhance.prompt }, - { - 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 +// Create the mode-specific default prompts +export const defaultPrompts: Readonly = Object.freeze( + Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])), +) + // Helper function to safely get role definition -export function getRoleDefinition(modeSlug: string): string { - const prompt = defaultPrompts[modeSlug] - if (!prompt || typeof prompt === "string") { - throw new Error(`Invalid mode slug: ${modeSlug}`) +export function getRoleDefinition(modeSlug: string, customModes?: ModeConfig[]): string { + const mode = getModeBySlug(modeSlug, customModes) + if (!mode) { + console.warn(`No mode found for slug: ${modeSlug}`) + return "" } - if (!prompt.roleDefinition) { - throw new Error(`No role definition found for mode: ${modeSlug}`) - } - return prompt.roleDefinition + return mode.roleDefinition } diff --git a/src/shared/tool-groups.ts b/src/shared/tool-groups.ts new file mode 100644 index 0000000..306db69 --- /dev/null +++ b/src/shared/tool-groups.ts @@ -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 = { + 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 = { + read: "Read Files", + edit: "Edit Files", + browser: "Use Browser", + command: "Run Commands", + mcp: "Use MCP", +} diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx index f86f871..12f7133 100644 --- a/webview-ui/src/components/chat/Announcement.tsx +++ b/webview-ui/src/components/chat/Announcement.tsx @@ -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. */ 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 (
{ style={{ position: "absolute", top: "8px", right: "8px" }}> -

- 🎉{" "}Introducing Roo Cline v{minorVersion} -

+

🎉{" "}Introducing Roo Code 4.0

-

Agent Modes Customization

- Click the new icon in - the menu bar to open the Prompts Settings and customize Agent Modes for new levels of productivity. -

    -
  • Tailor how Roo Cline behaves in different modes: Code, Architect, and Ask.
  • -
  • Preview and verify your changes using the Preview System Prompt button.
  • -
+ Our biggest update yet is here - we're officially changing our name from "Roo Cline" to "Roo Code"! + After growing beyond 50,000 installations, we're ready to chart our own course. Our heartfelt thanks to + everyone in the Cline community who helped us reach this milestone.

-

Prompt Enhancement Configuration

+

Custom Modes: Celebrating Our New Identity

- Now available for all providers! Access it directly in the chat box by clicking the{" "} - sparkle icon next to the - input field. From there, you can customize the enhancement logic and provider to best suit your - workflow. + To mark this new chapter, we're introducing the power to shape Roo Code into any role you need! Create + specialized personas and create an entire team of agents with deeply customized prompts:

    -
  • Customize how prompts are enhanced for better results in your workflow.
  • -
  • - Use the sparkle icon in the chat box to select a API configuration and provider (e.g., GPT-4) - and configure your own enhancement logic. -
  • -
  • Test your changes instantly with the Preview Prompt Enhancement tool.
  • +
  • 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
+ Just click the icon to + get started with Custom Modes!

+

Join Us for the Next Chapter

- We're very excited to see what you build with this new feature! Join us at - - reddit.com/r/roocline + We can't wait to see how you'll push Roo Code's potential even further! Share your custom modes and join + the discussion at{" "} + + reddit.com/r/RooCode - to discuss and share feedback. + .

) diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index 1eb2fc4..c317109 100644 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ b/webview-ui/src/components/chat/AutoApproveMenu.tsx @@ -209,7 +209,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { color: "var(--vscode-descriptionForeground)", 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.map((action) => ( diff --git a/webview-ui/src/components/chat/BrowserSessionRow.tsx b/webview-ui/src/components/chat/BrowserSessionRow.tsx index 0b06601..fd3d8e1 100644 --- a/webview-ui/src/components/chat/BrowserSessionRow.tsx +++ b/webview-ui/src/components/chat/BrowserSessionRow.tsx @@ -242,7 +242,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => { style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}> )} - <>Cline wants to use the browser: + <>Roo wants to use the browser:
, - Cline is having trouble..., + Roo is having trouble..., ] case "command": return [ @@ -128,9 +128,7 @@ export const ChatRowContent = ({ className="codicon codicon-terminal" style={{ color: normalColor, marginBottom: "-1.5px" }}> ), - - Cline wants to execute this command: - , + Roo wants to execute this command:, ] case "use_mcp_server": const mcpServerUse = JSON.parse(message.text || "{}") as ClineAskUseMcpServer @@ -143,8 +141,8 @@ export const ChatRowContent = ({ style={{ color: normalColor, marginBottom: "-1.5px" }}> ), - Cline wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on - the {mcpServerUse.serverName} MCP server: + Roo wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "} + {mcpServerUse.serverName} MCP server: , ] case "completion_result": @@ -208,7 +206,7 @@ export const ChatRowContent = ({ , - Cline has a question:, + Roo has a question:, ] default: return [null, null] @@ -250,7 +248,7 @@ export const ChatRowContent = ({ <>
{toolIcon(tool.tool === "appliedDiff" ? "diff" : "edit")} - Cline wants to edit this file: + Roo wants to edit this file:
{toolIcon("new-file")} - Cline wants to create a new file: + Roo wants to create a new file:
{toolIcon("file-code")} - {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:"}
{/* {message.type === "ask" - ? "Cline wants to view the top level files in this directory:" - : "Cline viewed the top level files in this directory:"} + ? "Roo wants to view the top level files in this directory:" + : "Roo viewed the top level files in this directory:"} {message.type === "ask" - ? "Cline wants to recursively view all files in this directory:" - : "Cline recursively viewed all files in this directory:"} + ? "Roo wants to recursively view all files in this directory:" + : "Roo recursively viewed all files in this directory:"} {message.type === "ask" - ? "Cline wants to view source code definition names used in this directory:" - : "Cline viewed source code definition names used in this directory:"} + ? "Roo wants to view source code definition names used in this directory:" + : "Roo viewed source code definition names used in this directory:"} {message.type === "ask" ? ( <> - Cline wants to search this directory for {tool.regex}: + Roo wants to search this directory for {tool.regex}: ) : ( <> - Cline searched this directory for {tool.regex}: + Roo searched this directory for {tool.regex}: )} @@ -428,9 +426,9 @@ export const ChatRowContent = ({ // {isInspecting ? : toolIcon("inspect")} // // {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: // )} // // @@ -663,7 +661,7 @@ export const ChatRowContent = ({
- 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 ( CMD/CTRL + Shift + P → "Update") and make sure you're using a supported shell: zsh, bash, fish, or PowerShell (CMD/CTRL + Shift + P → "Terminal: Select Default Profile").{" "} diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 0a8c338..fc4e70e 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -14,7 +14,7 @@ import ContextMenu from "./ContextMenu" import Thumbnails from "../common/Thumbnails" import { vscode } from "../../utils/vscode" 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" interface ChatTextAreaProps { @@ -50,7 +50,7 @@ const ChatTextArea = forwardRef( }, ref, ) => { - const { filePaths, currentApiConfigName, listApiConfigMeta } = useExtensionState() + const { filePaths, currentApiConfigName, listApiConfigMeta, customModes } = useExtensionState() const [gitCommits, setGitCommits] = useState([]) const [showDropdown, setShowDropdown] = useState(false) @@ -730,7 +730,7 @@ const ChatTextArea = forwardRef( minWidth: "70px", flex: "0 0 auto", }}> - {modes.map((mode) => ( + {getAllModes(customModes).map((mode) => (
) diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 607a736..49a3ac7 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -115,12 +115,12 @@ const McpView = ({ onDone }: McpViewProps) => { Model Context Protocol {" "} 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{" "} community-made servers {" "} - or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest - npm docs"). + or ask Roo to create new tools specific to your workflow (e.g., "add a tool that gets the latest npm + docs"). diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index 888d1f1..d4aef45 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -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 { 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 React, { useState, useEffect } from "react" + +// Get all available groups from GROUP_DISPLAY_NAMES +const availableGroups = Object.keys(TOOL_GROUPS) as ToolGroup[] type PromptsViewProps = { onDone: () => void } -const AGENT_MODES = modes.map((mode) => ({ - id: mode.slug, - label: mode.name, -})) - const PromptsView = ({ onDone }: PromptsViewProps) => { const { customPrompts, @@ -24,13 +37,201 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { setCustomInstructions, preferredLanguage, setPreferredLanguage, + customModes, } = useExtensionState() + + // Memoize modes to preserve array order + const modes = useMemo(() => getAllModes(customModes), [customModes]) + const [testPrompt, setTestPrompt] = useState("") const [isEnhancing, setIsEnhancing] = useState(false) - const [activeTab, setActiveTab] = useState(mode) const [isDialogOpen, setIsDialogOpen] = useState(false) const [selectedPromptContent, setSelectedPromptContent] = 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 = ( + 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(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) => { + 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(() => { const handler = (event: MessageEvent) => { @@ -53,24 +254,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { 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) => { vscode.postMessage({ type: "updateEnhancedPrompt", @@ -78,23 +261,19 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { }) } - const handleAgentPromptChange = (mode: AgentMode, e: Event | React.FormEvent) => { - 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) => { + const handleEnhancePromptChange = (e: Event | React.FormEvent): void => { const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value const trimmedValue = value.trim() - if (trimmedValue !== defaultPrompts.enhance) { - updateEnhancePrompt(trimmedValue || undefined) + if (trimmedValue !== enhancePrompt.default) { + updateEnhancePrompt(trimmedValue || enhancePrompt.default) } } - const handleAgentReset = (mode: AgentMode) => { - const existingPrompt = customPrompts?.[mode] - updateAgentPrompt(mode, { - ...(typeof existingPrompt === "object" ? existingPrompt : {}), + const handleAgentReset = (modeSlug: string) => { + // Only reset role definition for built-in modes + const existingPrompt = customPrompts?.[modeSlug] + updateAgentPrompt(modeSlug, { + ...existingPrompt, roleDefinition: undefined, }) } @@ -103,15 +282,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { 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 enhance = customPrompts?.enhance - const defaultEnhance = typeof defaultPrompts.enhance === "string" ? defaultPrompts.enhance : "" - return typeof enhance === "string" ? enhance : defaultEnhance + return enhancePrompt.get(customPrompts) } const handleTestEnhancement = () => { @@ -244,42 +416,185 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { -

Mode-Specific Prompts

+
+
+

Mode-Specific Prompts

+
+ + + + { + vscode.postMessage({ + type: "openFile", + text: "settings/cline_custom_modes.json", + }) + }}> + + +
+
-
- {AGENT_MODES.map((tab) => ( - - ))} +
+ Hit the + to create a new custom mode, or just ask Roo in chat to create one for you! +
+ +
+ {modes.map((modeConfig) => { + const isActive = mode === modeConfig.slug + return ( + + ) + })} +
-
-
+ {/* Only show name and delete for custom modes */} + {mode && findModeBySlug(mode, customModes) && ( +
+
+
Name
+
+ ) => { + 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%" }} + /> + { + vscode.postMessage({ + type: "deleteCustomMode", + slug: mode, + }) + }}> + + +
+
+
+ )} +
+
+
Role Definition
+ {!findModeBySlug(mode, customModes) && ( + { + const currentMode = getCurrentMode() + if (currentMode?.slug) { + handleAgentReset(currentMode.slug) + } + }} + title="Reset to default" + data-testid="role-definition-reset"> + + + )} +
+
+ Define Roo's expertise and personality for this mode. This description shapes how Roo + presents itself and approaches tasks. +
+ { + 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`} + /> +
+ {/* Mode settings */} + <> + {/* Show tools for all modes */} +
{ alignItems: "center", marginBottom: "4px", }}> -
Role Definition
- handleAgentReset(activeTab)} - data-testid="reset-prompt-button" - title="Revert to default"> - - -
-
- Define Cline's expertise and personality for this mode. This description shapes how - Cline presents itself and approaches tasks. +
Available Tools
+ {findModeBySlug(mode, customModes) && ( + setIsToolsEditMode(!isToolsEditMode)} + title={isToolsEditMode ? "Done editing" : "Edit tools"}> + + + )}
+ {!findModeBySlug(mode, customModes) && ( +
+ Tools for built-in modes cannot be modified +
+ )} + {isToolsEditMode && findModeBySlug(mode, customModes) ? ( +
+ {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 ( + + {GROUP_DISPLAY_NAMES[group]} + + ) + })} +
+ ) : ( +
+ {(() => { + const currentMode = getCurrentMode() + const enabledGroups = currentMode?.groups || [] + return enabledGroups.map((group) => GROUP_DISPLAY_NAMES[group]).join(", ") + })()} +
+ )}
- handleAgentPromptChange(activeTab, e)} - rows={4} - resize="vertical" - style={{ width: "100%" }} - data-testid={`${activeTab}-prompt-textarea`} - /> -
+ + + {/* Role definition for both built-in and custom modes */}
Mode-specific Custom Instructions
{ color: "var(--vscode-descriptionForeground)", marginBottom: "8px", }}> - Add behavioral guidelines specific to {activeTab} mode. These instructions enhance the base - behaviors defined above. + Add behavioral guidelines specific to {getCurrentMode()?.name || "Code"} mode.
{ - const prompt = customPrompts?.[activeTab] - return typeof prompt === "object" ? (prompt.customInstructions ?? "") : "" + const customMode = findModeBySlug(mode, customModes) + const prompt = customPrompts?.[mode] + return customMode?.customInstructions ?? prompt?.customInstructions ?? "" })()} onChange={(e) => { const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value - const existingPrompt = customPrompts?.[activeTab] - updateAgentPrompt(activeTab, { - ...(typeof existingPrompt === "object" ? existingPrompt : {}), - customInstructions: value.trim() || undefined, - }) + const customMode = findModeBySlug(mode, customModes) + if (customMode) { + // For custom modes, update the JSON file + 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} resize="vertical" style={{ width: "100%" }} - data-testid={`${activeTab}-custom-instructions-textarea`} + data-testid={`${getCurrentMode()?.slug || "code"}-custom-instructions-textarea`} />
{ color: "var(--vscode-descriptionForeground)", 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{" "} { textDecoration: "underline", }} onClick={() => { - // First create/update the file with current custom instructions - const defaultContent = `# ${activeTab} Mode Rules\n\nAdd mode-specific rules and guidelines here.` - const existingPrompt = customPrompts?.[activeTab] - const existingInstructions = - typeof existingPrompt === "object" - ? existingPrompt.customInstructions - : undefined - vscode.postMessage({ - type: "updatePrompt", - promptMode: activeTab, - customPrompt: { - ...(typeof existingPrompt === "object" ? existingPrompt : {}), - customInstructions: existingInstructions || defaultContent, - }, - }) - // Then open the file + const currentMode = getCurrentMode() + if (!currentMode) return + + // Open or create an empty file vscode.postMessage({ type: "openFile", - text: `./.clinerules-${activeTab}`, + text: `./.clinerules-${currentMode.slug}`, values: { create: true, content: "", }, }) }}> - .clinerules-{activeTab} + .clinerules-{getCurrentMode()?.slug || "code"} {" "} in your workspace.
@@ -395,10 +747,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { { - vscode.postMessage({ - type: "getSystemPrompt", - mode: activeTab, - }) + const currentMode = getCurrentMode() + if (currentMode) { + vscode.postMessage({ + type: "getSystemPrompt", + mode: currentMode.slug, + }) + } }} data-testid="preview-prompt-button"> Preview System Prompt @@ -414,8 +769,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { marginBottom: "20px", marginTop: "5px", }}> - Use prompt enhancement to get tailored suggestions or improvements for your inputs. This ensures - Cline understands your intent and provides the best possible responses. + Use prompt enhancement to get tailored suggestions or improvements for your inputs. This ensures Roo + understands your intent and provides the best possible responses.
@@ -517,6 +872,181 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
+ {isCreateModeDialogOpen && ( +
+
+
+ setIsCreateModeDialogOpen(false)} + style={{ + position: "absolute", + top: "20px", + right: "20px", + }}> + + +

Create New Mode

+
+
Name
+ ) => { + const target = + (e as CustomEvent)?.detail?.target || + ((e as any).target as HTMLInputElement) + handleNameChange(target.value) + }} + style={{ width: "100%" }} + /> +
+
+
Slug
+ ) => { + const target = + (e as CustomEvent)?.detail?.target || + ((e as any).target as HTMLInputElement) + setNewModeSlug(target.value) + }} + style={{ width: "100%" }} + /> +
+ The slug is used in URLs and file names. It should be lowercase and contain only + letters, numbers, and hyphens. +
+
+
+
Role Definition
+
+ Define Roo's expertise and personality for this mode. +
+ { + const value = + (e as CustomEvent)?.detail?.target?.value || + ((e as any).target as HTMLTextAreaElement).value + setNewModeRoleDefinition(value) + }} + rows={4} + resize="vertical" + style={{ width: "100%" }} + /> +
+
+
Available Tools
+
+ Select which tools this mode can use. +
+
+ {availableGroups.map((group) => ( + ) => { + 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]} + + ))} +
+
+
+
Custom Instructions
+
+ Add behavioral guidelines specific to this mode. +
+ { + const value = + (e as CustomEvent)?.detail?.target?.value || + ((e as any).target as HTMLTextAreaElement).value + setNewModeCustomInstructions(value) + }} + rows={4} + resize="vertical" + style={{ width: "100%" }} + /> +
+
+
+ setIsCreateModeDialogOpen(false)}>Cancel + + Create Mode + +
+
+
+ )} {isDialogOpen && (
{ expect(architectTab).toHaveAttribute("data-active", "false") }) - it("switches between tabs correctly", () => { - renderPromptsView({ mode: "code" }) + it("switches between tabs correctly", async () => { + const { rerender } = render( + + + , + ) const codeTab = screen.getByTestId("code-tab") const askTab = screen.getByTestId("ask-tab") @@ -68,16 +72,27 @@ describe("PromptsView", () => { expect(codeTab).toHaveAttribute("data-active", "true") expect(askTab).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) + rerender( + + + , + ) + expect(askTab).toHaveAttribute("data-active", "true") expect(codeTab).toHaveAttribute("data-active", "false") expect(architectTab).toHaveAttribute("data-active", "false") - // Click Architect tab + // Click Architect tab and update context fireEvent.click(architectTab) + rerender( + + + , + ) + expect(architectTab).toHaveAttribute("data-active", "true") expect(askTab).toHaveAttribute("data-active", "false") expect(codeTab).toHaveAttribute("data-active", "false") @@ -105,17 +120,47 @@ describe("PromptsView", () => { }) }) - it("resets prompt to default value", () => { - renderPromptsView() + it("resets role definition only for built-in modes", async () => { + const customMode = { + slug: "custom-mode", + name: "Custom Mode", + roleDefinition: "Custom role", + groups: [], + } - const resetButton = screen.getByTestId("reset-prompt-button") - fireEvent.click(resetButton) + // Test with built-in mode (code) + const { unmount } = render( + + + , + ) + // 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({ type: "updatePrompt", promptMode: "code", customPrompt: { roleDefinition: undefined }, }) + + // Cleanup before testing custom mode + unmount() + + // Test with custom mode + render( + + + , + ) + + // Verify reset button is not present for custom mode + expect(screen.queryByTestId("role-definition-reset")).not.toBeInTheDocument() }) it("handles API configuration selection", () => { diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 677086d..561ae5b 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -557,7 +557,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = color: "var(--vscode-descriptionForeground)", }}> - (Note: Cline uses complex prompts and works best + (Note: Roo Code uses complex prompts and works best with Claude models. Less capable models may not work as expected.)

@@ -626,7 +626,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = {" "} feature to use it with this extension.{" "} - (Note: Cline uses complex prompts and works best + (Note: Roo Code uses complex prompts and works best with Claude models. Less capable models may not work as expected.)

@@ -717,7 +717,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = fontWeight: 500, }}> 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.

@@ -780,7 +780,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = quickstart guide. - (Note: Cline uses complex prompts and works best + (Note: Roo Code uses complex prompts and works best with Claude models. Less capable models may not work as expected.)

diff --git a/webview-ui/src/components/settings/GlamaModelPicker.tsx b/webview-ui/src/components/settings/GlamaModelPicker.tsx index c4767eb..24a11ae 100644 --- a/webview-ui/src/components/settings/GlamaModelPicker.tsx +++ b/webview-ui/src/components/settings/GlamaModelPicker.tsx @@ -240,7 +240,7 @@ const GlamaModelPicker: React.FC = () => { Glama. - 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{" "} handleModelChange("anthropic/claude-3.5-sonnet")}> diff --git a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx index a19246d..ed4594a 100644 --- a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx +++ b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx @@ -240,7 +240,7 @@ const OpenRouterModelPicker: React.FC = () => { OpenRouter. - 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{" "} handleModelChange("anthropic/claude-3.5-sonnet:beta")}> diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 76b139f..e2af538 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -193,9 +193,9 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {

Auto-Approve Settings

- The following settings allow Cline to automatically perform operations without requiring - approval. Enable these settings only if you fully trust the AI and understand the associated - security risks. + The following settings allow Roo to automatically perform operations without requiring approval. + Enable these settings only if you fully trust the AI and understand the associated security + risks.

@@ -210,7 +210,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { marginTop: "5px", 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.

@@ -485,7 +485,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { marginTop: "5px", 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.

{soundEnabled && ( @@ -560,7 +560,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { marginTop: "5px", 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.

@@ -635,12 +635,12 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { }}>

If you have any questions or feedback, feel free to open an issue at{" "} - - github.com/RooVetGit/Roo-Cline + + github.com/RooVetGit/Roo-Code {" "} or join{" "} - - reddit.com/r/roocline + + reddit.com/r/RooCode

diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx index 3b6c075..3e3bbd7 100644 --- a/webview-ui/src/components/welcome/WelcomeView.tsx +++ b/webview-ui/src/components/welcome/WelcomeView.tsx @@ -22,7 +22,7 @@ const WelcomeView = () => { return (

-

Hi, I'm Cline

+

Hi, I'm Roo!

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 diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index c68be11..ea00a0c 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -14,8 +14,7 @@ import { convertTextMateToHljs } from "../utils/textMateToHljs" import { findLastIndex } from "../../../src/shared/array" import { McpServer } from "../../../src/shared/mcp" import { checkExistKey } from "../../../src/shared/checkExistApiConfig" -import { Mode } from "../../../src/core/prompts/types" -import { CustomPrompts, defaultModeSlug, defaultPrompts } from "../../../src/shared/modes" +import { Mode, CustomPrompts, defaultModeSlug, defaultPrompts, ModeConfig } from "../../../src/shared/modes" export interface ExtensionStateContextType extends ExtensionState { didHydrateState: boolean @@ -66,6 +65,8 @@ export interface ExtensionStateContextType extends ExtensionState { autoApprovalEnabled?: boolean setAutoApprovalEnabled: (value: boolean) => void handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void + customModes: ModeConfig[] + setCustomModes: (value: ModeConfig[]) => void } export const ExtensionStateContext = createContext(undefined) @@ -96,7 +97,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode enhancementApiConfigId: "", experimentalDiffStrategy: false, autoApprovalEnabled: false, + customModes: [], }) + const [didHydrateState, setDidHydrateState] = useState(false) const [showWelcome, setShowWelcome] = useState(false) const [theme, setTheme] = useState(undefined) @@ -274,6 +277,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setState((prevState) => ({ ...prevState, experimentalDiffStrategy: value })), setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })), handleInputChange, + setCustomModes: (value) => setState((prevState) => ({ ...prevState, customModes: value })), } return {children} From 45c396137bdd884fbf3c3e74e36a356e1a8d701f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Jan 2025 14:44:07 +0000 Subject: [PATCH 03/66] changeset version bump --- .changeset/pink-peaches-jump.md | 5 --- .changeset/plenty-suits-visit.md | 5 --- CHANGELOG.md | 60 +++++++++++++++++++------------- package-lock.json | 4 +-- package.json | 2 +- 5 files changed, 38 insertions(+), 38 deletions(-) delete mode 100644 .changeset/pink-peaches-jump.md delete mode 100644 .changeset/plenty-suits-visit.md diff --git a/.changeset/pink-peaches-jump.md b/.changeset/pink-peaches-jump.md deleted file mode 100644 index 6c4f416..0000000 --- a/.changeset/pink-peaches-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": minor ---- - -v3.2 diff --git a/.changeset/plenty-suits-visit.md b/.changeset/plenty-suits-visit.md deleted file mode 100644 index b5bdeb7..0000000 --- a/.changeset/plenty-suits-visit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -debug from vscode and changed output channel to Roo-Code diff --git a/CHANGELOG.md b/CHANGELOG.md index ab4ad37..130f042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,69 +1,79 @@ # Roo Code Changelog +## 3.2.0 + +### Minor Changes + +- v3.2 + +### Patch Changes + +- debug from vscode and changed output channel to Roo-Code + ## [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 " or visit the Prompts tab in the top menu to get started + - Define custom prompts + - Choose which tools each mode can access + - Create specialized assistants for any workflow + - Just type "Create a new mode for " 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] -- DeepSeek-R1 support (thanks @philipnext!) -- Experimental new unified diff algorithm can be enabled in settings (thanks @daniel-lxs!) -- More fixes to configuration profiles (thanks @samhvw8!) +- DeepSeek-R1 support (thanks @philipnext!) +- Experimental new unified diff algorithm can be enabled in settings (thanks @daniel-lxs!) +- More fixes to configuration profiles (thanks @samhvw8!) ## [3.1.6] -- Add Mistral (thanks Cline!) -- Fix bug with VSCode LM configuration profile saving (thanks @samhvw8!) +- Add Mistral (thanks Cline!) +- Fix bug with VSCode LM configuration profile saving (thanks @samhvw8!) ## [3.1.4 - 3.1.5] -- Bug fixes to the auto approve menu +- Bug fixes to the auto approve menu ## [3.1.3] -- Add auto-approve chat bar (thanks Cline!) -- Fix bug with VS Code Language Models integration +- Add auto-approve chat bar (thanks Cline!) +- Fix bug with VS Code Language Models integration ## [3.1.2] -- Experimental support for VS Code Language Models including Copilot (thanks @RaySinner / @julesmons!) -- Fix bug related to configuration profile switching (thanks @samhvw8!) -- Improvements to fuzzy search in mentions, history, and model lists (thanks @samhvw8!) -- PKCE support for Glama (thanks @punkpeye!) -- Use 'developer' message for o1 system prompt +- Experimental support for VS Code Language Models including Copilot (thanks @RaySinner / @julesmons!) +- Fix bug related to configuration profile switching (thanks @samhvw8!) +- Improvements to fuzzy search in mentions, history, and model lists (thanks @samhvw8!) +- PKCE support for Glama (thanks @punkpeye!) +- Use 'developer' message for o1 system prompt ## [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] -- 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 +- 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 ## [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] -- 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] -- 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] -- 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 +- 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] @@ -317,4 +327,4 @@ Join us at https://www.reddit.com/r/RooCode to share your custom modes and be pa ## [2.1.2] - Support for auto-approval of write operations and command execution -- Support for .clinerules custom instructions \ No newline at end of file +- Support for .clinerules custom instructions diff --git a/package-lock.json b/package-lock.json index 7518ff6..73d4eb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.1.7", + "version": "3.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.1.7", + "version": "3.2.0", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index dcb0888..57ee1e7 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Code (prev. Roo Cline)", "description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.", "publisher": "RooVeterinaryInc", - "version": "3.1.7", + "version": "3.2.0", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", From 57a9c06dddfb8dca0c1c29f98e5028d8e7927f38 Mon Sep 17 00:00:00 2001 From: R00-B0T Date: Tue, 21 Jan 2025 14:44:48 +0000 Subject: [PATCH 04/66] Updating CHANGELOG.md format --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 130f042..01799c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ # Roo Code Changelog -## 3.2.0 - -### Minor Changes +## [3.2.0] - v3.2 From 4ae6c7fae83ba32651aae921b1fdb3b0f9f765cd Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 21 Jan 2025 09:45:20 -0500 Subject: [PATCH 05/66] Update CHANGELOG.md --- CHANGELOG.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01799c9..2a06518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,6 @@ ## [3.2.0] -- v3.2 - -### Patch Changes - -- debug from vscode and changed output channel to Roo-Code - -## [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: From 96c0e0df654bfbded65c2d57afc029672abd428f Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 21 Jan 2025 09:46:01 -0500 Subject: [PATCH 06/66] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a06518..8a1516b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [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. +- **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 From 323af09269a358548d419cac300f3efe82e6d1f0 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 21 Jan 2025 09:59:50 -0500 Subject: [PATCH 07/66] Fix announcement --- .changeset/brave-dolphins-pull.md | 5 +++++ webview-ui/src/components/chat/Announcement.tsx | 10 ++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .changeset/brave-dolphins-pull.md diff --git a/.changeset/brave-dolphins-pull.md b/.changeset/brave-dolphins-pull.md new file mode 100644 index 0000000..94001bb --- /dev/null +++ b/.changeset/brave-dolphins-pull.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Fix announcement diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx index 12f7133..a15171d 100644 --- a/webview-ui/src/components/chat/Announcement.tsx +++ b/webview-ui/src/components/chat/Announcement.tsx @@ -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. */ 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 (

{ style={{ position: "absolute", top: "8px", right: "8px" }}> -

🎉{" "}Introducing Roo Code 4.0

+

+ 🎉{" "}Introducing Roo Code {minorVersion} +

- Our biggest update yet is here - we're officially changing our name from "Roo Cline" to "Roo Code"! - After growing beyond 50,000 installations, we're ready to chart our own course. Our heartfelt thanks to + Our biggest update yet is here - we're officially changing our name from Roo Cline to Roo Code! After + growing beyond 50,000 installations, we're ready to chart our own course. Our heartfelt thanks to everyone in the Cline community who helped us reach this milestone.

From 44543b2c1e58ed8abfc5701b6c7a2bfc48ebd6d7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Jan 2025 15:03:31 +0000 Subject: [PATCH 08/66] changeset version bump --- .changeset/brave-dolphins-pull.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/brave-dolphins-pull.md diff --git a/.changeset/brave-dolphins-pull.md b/.changeset/brave-dolphins-pull.md deleted file mode 100644 index 94001bb..0000000 --- a/.changeset/brave-dolphins-pull.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Fix announcement diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a1516b..a792d61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Roo Code Changelog +## 3.2.1 + +### Patch Changes + +- Fix announcement + ## [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. diff --git a/package-lock.json b/package-lock.json index 73d4eb0..ee41923 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.2.0", + "version": "3.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.2.0", + "version": "3.2.1", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index 57ee1e7..fc702d4 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Code (prev. Roo Cline)", "description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.", "publisher": "RooVeterinaryInc", - "version": "3.2.0", + "version": "3.2.1", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", From 5be63bce864aca7d73d96d84160a8da4ad765034 Mon Sep 17 00:00:00 2001 From: R00-B0T Date: Tue, 21 Jan 2025 15:04:19 +0000 Subject: [PATCH 09/66] Updating CHANGELOG.md format --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a792d61..fefd959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ # Roo Code Changelog -## 3.2.1 - -### Patch Changes +## [3.2.1] - Fix announcement From 3dddd7c1558ac6f1ec98cde551fcb3b17eb68686 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 21 Jan 2025 10:04:43 -0500 Subject: [PATCH 10/66] Update CHANGELOG.md --- CHANGELOG.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fefd959..23bed58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,6 @@ # Roo Code Changelog -## [3.2.1] - -- Fix announcement - -## [3.2.0] +## [3.2.0 - 3.2.1] - **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. From d50e075c75842d98e07f48f771d425b9f5c0404f Mon Sep 17 00:00:00 2001 From: sam hoang Date: Sun, 12 Jan 2025 19:28:25 +0700 Subject: [PATCH 11/66] feat(openai): add custom model info configuration Adds support for configuring custom OpenAI-compatible model capabilities and pricing, including: Max output tokens Context window size Image/computer use support Input/output token pricing Cache read/write pricing --- src/api/providers/openai.ts | 2 +- src/core/webview/ClineProvider.ts | 6 + src/shared/WebviewMessage.ts | 1 + src/shared/api.ts | 1 + .../src/components/settings/ApiOptions.tsx | 180 +++++++++++++++++- 5 files changed, 188 insertions(+), 2 deletions(-) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 13922a4..c2c3a89 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -108,7 +108,7 @@ export class OpenAiHandler implements ApiHandler, SingleCompletionHandler { getModel(): { id: string; info: ModelInfo } { return { id: this.options.openAiModelId ?? "", - info: openAiModelInfoSaneDefaults, + info: this.options.openAiCusModelInfo ?? openAiModelInfoSaneDefaults, } } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 72e7e27..771aae3 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -68,6 +68,7 @@ type GlobalStateKey = | "taskHistory" | "openAiBaseUrl" | "openAiModelId" + | "openAiCusModelInfo" | "ollamaModelId" | "ollamaBaseUrl" | "lmStudioModelId" @@ -1198,6 +1199,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiBaseUrl, openAiApiKey, openAiModelId, + openAiCusModelInfo, ollamaModelId, ollamaBaseUrl, lmStudioModelId, @@ -1231,6 +1233,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl) await this.storeSecret("openAiApiKey", openAiApiKey) await this.updateGlobalState("openAiModelId", openAiModelId) + await this.updateGlobalState("openAiCusModelInfo", openAiCusModelInfo) await this.updateGlobalState("ollamaModelId", ollamaModelId) await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl) await this.updateGlobalState("lmStudioModelId", lmStudioModelId) @@ -1847,6 +1850,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiBaseUrl, openAiApiKey, openAiModelId, + openAiCusModelInfo, ollamaModelId, ollamaBaseUrl, lmStudioModelId, @@ -1910,6 +1914,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("openAiBaseUrl") as Promise, this.getSecret("openAiApiKey") as Promise, this.getGlobalState("openAiModelId") as Promise, + this.getGlobalState("openAiCusModelInfo") as Promise, this.getGlobalState("ollamaModelId") as Promise, this.getGlobalState("ollamaBaseUrl") as Promise, this.getGlobalState("lmStudioModelId") as Promise, @@ -1990,6 +1995,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiBaseUrl, openAiApiKey, openAiModelId, + openAiCusModelInfo, ollamaModelId, ollamaBaseUrl, lmStudioModelId, diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index ce05976..5aaaa82 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -76,6 +76,7 @@ export interface WebviewMessage { | "autoApprovalEnabled" | "updateCustomMode" | "deleteCustomMode" + | "setOpenAiCusModelInfo" text?: string disabled?: boolean askResponse?: ClineAskResponse diff --git a/src/shared/api.ts b/src/shared/api.ts index 8f65c67..5524a1e 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -38,6 +38,7 @@ export interface ApiHandlerOptions { openAiBaseUrl?: string openAiApiKey?: string openAiModelId?: string + openAiCusModelInfo?: ModelInfo ollamaModelId?: string ollamaBaseUrl?: string lmStudioModelId?: string diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 561ae5b..9fbb6d8 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -550,6 +550,184 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = placeholder={`Default: ${azureOpenAiDefaultApiVersion}`} /> )} + + {/* Model Info Configuration */} +
+
+ Model Configuration +

+ Configure the capabilities and pricing for your custom OpenAI-compatible model +

+
+ + {/* Capabilities Section */} +
+ Capabilities +
+ { + const value = parseInt(e.target.value) + setApiConfiguration({ + ...apiConfiguration, + openAiCusModelInfo: { + ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), + maxTokens: isNaN(value) ? undefined : value + } + }) + }} + placeholder="e.g. 4096"> + Max Output Tokens + + + { + const parsed = parseInt(e.target.value) + setApiConfiguration({ + ...apiConfiguration, + openAiCusModelInfo: { + ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), + contextWindow: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.contextWindow : parsed) + } + }) + }} + placeholder="e.g. 128000"> + Context Window Size + + +
+ { + setApiConfiguration({ + ...apiConfiguration, + openAiCusModelInfo: { + ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), + supportsImages: e.target.checked + } + }) + }}> + Supports Images + + + { + setApiConfiguration({ + ...apiConfiguration, + openAiCusModelInfo: { + ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), + supportsComputerUse: e.target.checked + } + }) + }}> + Supports Computer Use + +
+
+
+ + {/* Pricing Section */} +
+ Pricing (USD per million tokens) +
+ {/* Input/Output Prices */} +
+ { + const parsed = parseFloat(e.target.value) + setApiConfiguration({ + ...apiConfiguration, + openAiCusModelInfo: { + ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), + inputPrice: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.inputPrice : parsed) + } + }) + }} + placeholder="e.g. 0.0001"> + Input Price + + + { + const parsed = parseFloat(e.target.value) + setApiConfiguration({ + ...apiConfiguration, + openAiCusModelInfo: { + ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), + outputPrice: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.outputPrice : parsed) + } + }) + }} + placeholder="e.g. 0.0002"> + Output Price + +
+ + {/* Cache Prices */} +
+ { + const parsed = parseFloat(e.target.value) + setApiConfiguration({ + ...apiConfiguration, + openAiCusModelInfo: { + ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), + cacheWritesPrice: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.cacheWritesPrice : parsed) + } + }) + }} + placeholder="e.g. 0.0001"> + Cache Write Price + + + { + const parsed = parseFloat(e.target.value) + setApiConfiguration({ + ...apiConfiguration, + openAiCusModelInfo: { + ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), + cacheReadsPrice: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.cacheReadsPrice : parsed) + } + }) + }} + placeholder="e.g. 0.00001"> + Cache Read Price + +
+
+
+
+ + { /* TODO: model info here */} + +

Date: Sun, 19 Jan 2025 00:34:05 +0700 Subject: [PATCH 12/66] fix ui and some error my has --- .../src/components/settings/ApiOptions.tsx | 223 ++++++++++-------- 1 file changed, 126 insertions(+), 97 deletions(-) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 9fbb6d8..a9ec8d5 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -166,7 +166,11 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = onChange={(checked: boolean) => { setAnthropicBaseUrlSelected(checked) if (!checked) { - setApiConfiguration({ ...apiConfiguration, anthropicBaseUrl: "" }) + handleInputChange("anthropicBaseUrl")({ + target: { + value: "", + }, + }) } }}> Use custom base URL @@ -537,7 +541,11 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = onChange={(checked: boolean) => { setAzureApiVersionSelected(checked) if (!checked) { - setApiConfiguration({ ...apiConfiguration, azureApiVersion: "" }) + handleInputChange("azureApiVersion")({ + target: { + value: "", + }, + }) } }}> Set Azure API version @@ -552,31 +560,47 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = )} {/* Model Info Configuration */} -

+
- Model Configuration -

+ Model Configuration +

Configure the capabilities and pricing for your custom OpenAI-compatible model

{/* Capabilities Section */}
- Capabilities
{ const value = parseInt(e.target.value) - setApiConfiguration({ - ...apiConfiguration, - openAiCusModelInfo: { - ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), - maxTokens: isNaN(value) ? undefined : value - } + handleInputChange("openAiCusModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCusModelInfo || + openAiModelInfoSaneDefaults), + maxTokens: isNaN(value) ? undefined : value, + }, + }, }) }} placeholder="e.g. 4096"> @@ -584,18 +608,29 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = { const parsed = parseInt(e.target.value) - setApiConfiguration({ - ...apiConfiguration, - openAiCusModelInfo: { - ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), - contextWindow: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.contextWindow : parsed) - } + handleInputChange("openAiCusModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCusModelInfo || + openAiModelInfoSaneDefaults), + contextWindow: + e.target.value === "" + ? undefined + : isNaN(parsed) + ? openAiModelInfoSaneDefaults.contextWindow + : parsed, + }, + }, }) }} placeholder="e.g. 128000"> @@ -603,58 +638,83 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
- { - setApiConfiguration({ - ...apiConfiguration, - openAiCusModelInfo: { - ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), - supportsImages: e.target.checked - } + onChange={(checked: boolean) => { + handleInputChange("openAiCusModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCusModelInfo || + openAiModelInfoSaneDefaults), + supportsImages: checked, + }, + }, }) }}> Supports Images - + - { - setApiConfiguration({ - ...apiConfiguration, - openAiCusModelInfo: { - ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), - supportsComputerUse: e.target.checked - } + onChange={(checked: boolean) => { + handleInputChange("openAiCusModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCusModelInfo || + openAiModelInfoSaneDefaults), + supportsComputerUse: checked, + }, + }, }) }}> Supports Computer Use - +
{/* Pricing Section */}
- Pricing (USD per million tokens) + + Pricing (USD per million tokens) +
{/* Input/Output Prices */}
{ const parsed = parseFloat(e.target.value) - setApiConfiguration({ - ...apiConfiguration, - openAiCusModelInfo: { - ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), - inputPrice: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.inputPrice : parsed) - } + handleInputChange("openAiCusModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCusModelInfo ?? + openAiModelInfoSaneDefaults), + inputPrice: + e.target.value === "" + ? undefined + : isNaN(parsed) + ? openAiModelInfoSaneDefaults.inputPrice + : parsed, + }, + }, }) }} placeholder="e.g. 0.0001"> @@ -662,71 +722,40 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = { const parsed = parseFloat(e.target.value) - setApiConfiguration({ - ...apiConfiguration, - openAiCusModelInfo: { - ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), - outputPrice: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.outputPrice : parsed) - } + handleInputChange("openAiCusModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCusModelInfo || + openAiModelInfoSaneDefaults), + outputPrice: + e.target.value === "" + ? undefined + : isNaN(parsed) + ? openAiModelInfoSaneDefaults.outputPrice + : parsed, + }, + }, }) }} placeholder="e.g. 0.0002"> Output Price
- - {/* Cache Prices */} -
- { - const parsed = parseFloat(e.target.value) - setApiConfiguration({ - ...apiConfiguration, - openAiCusModelInfo: { - ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), - cacheWritesPrice: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.cacheWritesPrice : parsed) - } - }) - }} - placeholder="e.g. 0.0001"> - Cache Write Price - - - { - const parsed = parseFloat(e.target.value) - setApiConfiguration({ - ...apiConfiguration, - openAiCusModelInfo: { - ...(apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults), - cacheReadsPrice: e.target.value === "" ? undefined : (isNaN(parsed) ? openAiModelInfoSaneDefaults.cacheReadsPrice : parsed) - } - }) - }} - placeholder="e.g. 0.00001"> - Cache Read Price - -
- { /* TODO: model info here */} - + {/* TODO: model info here */}

Date: Sun, 19 Jan 2025 15:19:21 +0700 Subject: [PATCH 13/66] feat(openai-compatible): tune UI UX custom model info --- .../src/components/settings/ApiOptions.tsx | 578 ++++++++++++------ 1 file changed, 400 insertions(+), 178 deletions(-) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index a9ec8d5..083c35b 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -1,4 +1,4 @@ -import { Checkbox, Dropdown } from "vscrui" +import { Checkbox, Dropdown, Pane } from "vscrui" import type { DropdownOption } from "vscrui" import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react" @@ -559,203 +559,425 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = /> )} - {/* Model Info Configuration */}

-
- Model Configuration + }} + /> + + handleInputChange("openAiCusModelInfo")({ + target: { value: openAiModelInfoSaneDefaults }, + }), + }, + ]}> +

Configure the capabilities and pricing for your custom OpenAI-compatible model

-
- {/* Capabilities Section */} -
-
- { - const value = parseInt(e.target.value) - handleInputChange("openAiCusModelInfo")({ - target: { - value: { - ...(apiConfiguration?.openAiCusModelInfo || - openAiModelInfoSaneDefaults), - maxTokens: isNaN(value) ? undefined : value, - }, - }, - }) - }} - placeholder="e.g. 4096"> - Max Output Tokens - - - { - const parsed = parseInt(e.target.value) - handleInputChange("openAiCusModelInfo")({ - target: { - value: { - ...(apiConfiguration?.openAiCusModelInfo || - openAiModelInfoSaneDefaults), - contextWindow: - e.target.value === "" - ? undefined - : isNaN(parsed) - ? openAiModelInfoSaneDefaults.contextWindow - : parsed, - }, - }, - }) - }} - placeholder="e.g. 128000"> - Context Window Size - - -
- { - handleInputChange("openAiCusModelInfo")({ - target: { - value: { - ...(apiConfiguration?.openAiCusModelInfo || - openAiModelInfoSaneDefaults), - supportsImages: checked, - }, - }, - }) - }}> - Supports Images - - - { - handleInputChange("openAiCusModelInfo")({ - target: { - value: { - ...(apiConfiguration?.openAiCusModelInfo || - openAiModelInfoSaneDefaults), - supportsComputerUse: checked, - }, - }, - }) - }}> - Supports Computer Use - -
-
-
- - {/* Pricing Section */} -
- - Pricing (USD per million tokens) - -
- {/* Input/Output Prices */} -
- { - const parsed = parseFloat(e.target.value) - handleInputChange("openAiCusModelInfo")({ - target: { - value: { - ...(apiConfiguration?.openAiCusModelInfo ?? - openAiModelInfoSaneDefaults), - inputPrice: - e.target.value === "" - ? undefined - : isNaN(parsed) - ? openAiModelInfoSaneDefaults.inputPrice - : parsed, + + Model Capabilities + +
+
+ { + const value = apiConfiguration?.openAiCusModelInfo?.maxTokens + if (!value) return "var(--vscode-input-border)" + return value > 0 + ? "var(--vscode-charts-green)" + : "var(--vscode-errorForeground)" + })(), + }} + title="Maximum number of tokens the model can generate in a single response" + onChange={(e: any) => { + const value = parseInt(e.target.value) + handleInputChange("openAiCusModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCusModelInfo || + openAiModelInfoSaneDefaults), + maxTokens: isNaN(value) ? undefined : value, + }, }, - }, - }) - }} - placeholder="e.g. 0.0001"> - Input Price - + }) + }} + placeholder="e.g. 4096"> + Max Output Tokens + +
+ + + Maximum number of tokens the model can generate in a response. Higher + values allow longer outputs but may increase costs. + +
+
- { - const parsed = parseFloat(e.target.value) - handleInputChange("openAiCusModelInfo")({ - target: { - value: { - ...(apiConfiguration?.openAiCusModelInfo || - openAiModelInfoSaneDefaults), - outputPrice: - e.target.value === "" - ? undefined - : isNaN(parsed) - ? openAiModelInfoSaneDefaults.outputPrice - : parsed, +
+ { + const value = apiConfiguration?.openAiCusModelInfo?.contextWindow + if (!value) return "var(--vscode-input-border)" + return value > 0 + ? "var(--vscode-charts-green)" + : "var(--vscode-errorForeground)" + })(), + }} + title="Total number of tokens (input + output) the model can process in a single request" + onChange={(e: any) => { + const parsed = parseInt(e.target.value) + handleInputChange("openAiCusModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCusModelInfo || + openAiModelInfoSaneDefaults), + contextWindow: + e.target.value === "" + ? undefined + : isNaN(parsed) + ? openAiModelInfoSaneDefaults.contextWindow + : parsed, + }, }, - }, - }) - }} - placeholder="e.g. 0.0002"> - Output Price - + }) + }} + placeholder="e.g. 128000"> + Context Window Size + +
+ + + Total tokens (input + output) the model can process. Larger windows + allow processing more content but may increase memory usage. + +
+
+ +
+ + Model Features + + +
+
+
+ { + handleInputChange("openAiCusModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCusModelInfo || + openAiModelInfoSaneDefaults), + supportsImages: checked, + }, + }, + }) + }}> + Image Support + + +
+

+ Allows the model to analyze and understand images, essential for + visual code assistance +

+
+ +
+
+ { + handleInputChange("openAiCusModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCusModelInfo || + openAiModelInfoSaneDefaults), + supportsComputerUse: checked, + }, + }, + }) + }}> + Computer Interaction + + +
+

+ Enables the model to execute commands and modify files for automated + assistance +

+
+
+
+
+
+ + {/* Pricing Section */} +
+
+ + Model Pricing + + + Configure token-based pricing in USD per million tokens + +
+ +
+
+ { + const value = apiConfiguration?.openAiCusModelInfo?.inputPrice + if (!value && value !== 0) return "var(--vscode-input-border)" + return value >= 0 + ? "var(--vscode-charts-green)" + : "var(--vscode-errorForeground)" + })(), + }} + onChange={(e: any) => { + const parsed = parseFloat(e.target.value) + handleInputChange("openAiCusModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCusModelInfo ?? + openAiModelInfoSaneDefaults), + inputPrice: + e.target.value === "" + ? undefined + : isNaN(parsed) + ? openAiModelInfoSaneDefaults.inputPrice + : parsed, + }, + }, + }) + }} + placeholder="e.g. 0.0001"> +
+ Input Price + +
+
+
+ +
+ { + const value = apiConfiguration?.openAiCusModelInfo?.outputPrice + if (!value && value !== 0) return "var(--vscode-input-border)" + return value >= 0 + ? "var(--vscode-charts-green)" + : "var(--vscode-errorForeground)" + })(), + }} + onChange={(e: any) => { + const parsed = parseFloat(e.target.value) + handleInputChange("openAiCusModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCusModelInfo || + openAiModelInfoSaneDefaults), + outputPrice: + e.target.value === "" + ? undefined + : isNaN(parsed) + ? openAiModelInfoSaneDefaults.outputPrice + : parsed, + }, + }, + }) + }} + placeholder="e.g. 0.0002"> +
+ Output Price + +
+
+
-
+
- {/* TODO: model info here */} + {/* end Model Info Configuration */}

Date: Tue, 21 Jan 2025 23:06:07 +0700 Subject: [PATCH 14/66] refactor: rename openAiCusModelInfo to openAiCustomModelInfo for better clarity - Rename openAiCusModelInfo to openAiCustomModelInfo across all files for better readability - Update related variable names and references to maintain consistency - Affects OpenAI provider, ClineProvider, WebviewMessage, API interfaces, and UI components --- src/api/providers/openai.ts | 2 +- src/core/webview/ClineProvider.ts | 12 ++--- src/shared/WebviewMessage.ts | 2 +- src/shared/api.ts | 2 +- .../src/components/settings/ApiOptions.tsx | 52 +++++++++---------- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index c2c3a89..d71a51f 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -108,7 +108,7 @@ export class OpenAiHandler implements ApiHandler, SingleCompletionHandler { getModel(): { id: string; info: ModelInfo } { return { id: this.options.openAiModelId ?? "", - info: this.options.openAiCusModelInfo ?? openAiModelInfoSaneDefaults, + info: this.options.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults, } } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 771aae3..23fe36e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -68,7 +68,7 @@ type GlobalStateKey = | "taskHistory" | "openAiBaseUrl" | "openAiModelId" - | "openAiCusModelInfo" + | "openAiCustomModelInfo" | "ollamaModelId" | "ollamaBaseUrl" | "lmStudioModelId" @@ -1199,7 +1199,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiBaseUrl, openAiApiKey, openAiModelId, - openAiCusModelInfo, + openAiCustomModelInfo, ollamaModelId, ollamaBaseUrl, lmStudioModelId, @@ -1233,7 +1233,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl) await this.storeSecret("openAiApiKey", openAiApiKey) await this.updateGlobalState("openAiModelId", openAiModelId) - await this.updateGlobalState("openAiCusModelInfo", openAiCusModelInfo) + await this.updateGlobalState("openAiCustomModelInfo", openAiCustomModelInfo) await this.updateGlobalState("ollamaModelId", ollamaModelId) await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl) await this.updateGlobalState("lmStudioModelId", lmStudioModelId) @@ -1850,7 +1850,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiBaseUrl, openAiApiKey, openAiModelId, - openAiCusModelInfo, + openAiCustomModelInfo, ollamaModelId, ollamaBaseUrl, lmStudioModelId, @@ -1914,7 +1914,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("openAiBaseUrl") as Promise, this.getSecret("openAiApiKey") as Promise, this.getGlobalState("openAiModelId") as Promise, - this.getGlobalState("openAiCusModelInfo") as Promise, + this.getGlobalState("openAiCustomModelInfo") as Promise, this.getGlobalState("ollamaModelId") as Promise, this.getGlobalState("ollamaBaseUrl") as Promise, this.getGlobalState("lmStudioModelId") as Promise, @@ -1995,7 +1995,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiBaseUrl, openAiApiKey, openAiModelId, - openAiCusModelInfo, + openAiCustomModelInfo, ollamaModelId, ollamaBaseUrl, lmStudioModelId, diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 5aaaa82..5705378 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -76,7 +76,7 @@ export interface WebviewMessage { | "autoApprovalEnabled" | "updateCustomMode" | "deleteCustomMode" - | "setOpenAiCusModelInfo" + | "setopenAiCustomModelInfo" text?: string disabled?: boolean askResponse?: ClineAskResponse diff --git a/src/shared/api.ts b/src/shared/api.ts index 5524a1e..8d7b919 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -38,7 +38,7 @@ export interface ApiHandlerOptions { openAiBaseUrl?: string openAiApiKey?: string openAiModelId?: string - openAiCusModelInfo?: ModelInfo + openAiCustomModelInfo?: ModelInfo ollamaModelId?: string ollamaBaseUrl?: string lmStudioModelId?: string diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 083c35b..cda5191 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -45,7 +45,7 @@ interface ApiOptionsProps { } const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) => { - const { apiConfiguration, setApiConfiguration, uriScheme, handleInputChange } = useExtensionState() + const { apiConfiguration, uriScheme, handleInputChange } = useExtensionState() const [ollamaModels, setOllamaModels] = useState([]) const [lmStudioModels, setLmStudioModels] = useState([]) const [vsCodeLmModels, setVsCodeLmModels] = useState([]) @@ -571,7 +571,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = { iconName: "refresh", onClick: () => - handleInputChange("openAiCusModelInfo")({ + handleInputChange("openAiCustomModelInfo")({ target: { value: openAiModelInfoSaneDefaults }, }), }, @@ -613,7 +613,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =

{ - const value = apiConfiguration?.openAiCusModelInfo?.maxTokens + const value = apiConfiguration?.openAiCustomModelInfo?.maxTokens if (!value) return "var(--vscode-input-border)" return value > 0 ? "var(--vscode-charts-green)" @@ -631,10 +631,10 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = title="Maximum number of tokens the model can generate in a single response" onChange={(e: any) => { const value = parseInt(e.target.value) - handleInputChange("openAiCusModelInfo")({ + handleInputChange("openAiCustomModelInfo")({ target: { value: { - ...(apiConfiguration?.openAiCusModelInfo || + ...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults), maxTokens: isNaN(value) ? undefined : value, }, @@ -664,7 +664,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
{ - const value = apiConfiguration?.openAiCusModelInfo?.contextWindow + const value = apiConfiguration?.openAiCustomModelInfo?.contextWindow if (!value) return "var(--vscode-input-border)" return value > 0 ? "var(--vscode-charts-green)" @@ -682,10 +682,10 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = title="Total number of tokens (input + output) the model can process in a single request" onChange={(e: any) => { const parsed = parseInt(e.target.value) - handleInputChange("openAiCusModelInfo")({ + handleInputChange("openAiCustomModelInfo")({ target: { value: { - ...(apiConfiguration?.openAiCusModelInfo || + ...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults), contextWindow: e.target.value === "" @@ -742,14 +742,14 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
{ - handleInputChange("openAiCusModelInfo")({ + handleInputChange("openAiCustomModelInfo")({ target: { value: { - ...(apiConfiguration?.openAiCusModelInfo || + ...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults), supportsImages: checked, }, @@ -790,14 +790,14 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
{ - handleInputChange("openAiCusModelInfo")({ + handleInputChange("openAiCustomModelInfo")({ target: { value: { - ...(apiConfiguration?.openAiCusModelInfo || + ...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults), supportsComputerUse: checked, }, @@ -874,7 +874,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
{ - const value = apiConfiguration?.openAiCusModelInfo?.inputPrice + const value = apiConfiguration?.openAiCustomModelInfo?.inputPrice if (!value && value !== 0) return "var(--vscode-input-border)" return value >= 0 ? "var(--vscode-charts-green)" @@ -891,10 +891,10 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = }} onChange={(e: any) => { const parsed = parseFloat(e.target.value) - handleInputChange("openAiCusModelInfo")({ + handleInputChange("openAiCustomModelInfo")({ target: { value: { - ...(apiConfiguration?.openAiCusModelInfo ?? + ...(apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults), inputPrice: e.target.value === "" @@ -925,7 +925,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
{ - const value = apiConfiguration?.openAiCusModelInfo?.outputPrice + const value = apiConfiguration?.openAiCustomModelInfo?.outputPrice if (!value && value !== 0) return "var(--vscode-input-border)" return value >= 0 ? "var(--vscode-charts-green)" @@ -942,10 +942,10 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = }} onChange={(e: any) => { const parsed = parseFloat(e.target.value) - handleInputChange("openAiCusModelInfo")({ + handleInputChange("openAiCustomModelInfo")({ target: { value: { - ...(apiConfiguration?.openAiCusModelInfo || + ...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults), outputPrice: e.target.value === "" @@ -1460,7 +1460,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { return { selectedProvider: provider, selectedModelId: apiConfiguration?.openAiModelId || "", - selectedModelInfo: apiConfiguration?.openAiCusModelInfo || openAiModelInfoSaneDefaults, + selectedModelInfo: apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults, } case "ollama": return { From 77fa8b1b3187f6bdbab25e121d653a4a30aab478 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 21 Jan 2025 11:04:47 -0500 Subject: [PATCH 15/66] Add diff strategy to system prompt preview --- .changeset/poor-adults-fetch.md | 5 ++ src/core/webview/ClineProvider.ts | 12 ++++- .../webview/__tests__/ClineProvider.test.ts | 53 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 .changeset/poor-adults-fetch.md diff --git a/.changeset/poor-adults-fetch.md b/.changeset/poor-adults-fetch.md new file mode 100644 index 0000000..32ac663 --- /dev/null +++ b/.changeset/poor-adults-fetch.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Fix bug where apply_diff wasn't showing up in system prompt preview diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 72e7e27..cce08e2 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -10,6 +10,7 @@ import { downloadTask } from "../../integrations/misc/export-markdown" import { openFile, openImage } from "../../integrations/misc/open-file" import { selectImages } from "../../integrations/misc/process-images" import { getTheme } from "../../integrations/theme/getTheme" +import { getDiffStrategy } from "../diff/DiffStrategy" import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker" import { McpHub } from "../../services/mcp/McpHub" import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api" @@ -962,7 +963,16 @@ export class ClineProvider implements vscode.WebviewViewProvider { preferredLanguage, browserViewportSize, mcpEnabled, + fuzzyMatchThreshold, + experimentalDiffStrategy, } = await this.getState() + + // Create diffStrategy based on current model and settings + const diffStrategy = getDiffStrategy( + apiConfiguration.apiModelId || apiConfiguration.openRouterModelId || "", + fuzzyMatchThreshold, + experimentalDiffStrategy, + ) const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || "" @@ -979,7 +989,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { cwd, apiConfiguration.openRouterModelInfo?.supportsComputerUse ?? false, mcpEnabled ? this.mcpHub : undefined, - undefined, + diffStrategy, browserViewportSize ?? "900x600", mode, { diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index ff427a9..b672110 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -70,6 +70,13 @@ jest.mock( { virtual: true }, ) +// Mock DiffStrategy +jest.mock("../../diff/DiffStrategy", () => ({ + getDiffStrategy: jest.fn().mockImplementation(() => ({ + getToolDescription: jest.fn().mockReturnValue("apply_diff tool description"), + })), +})) + // Mock dependencies jest.mock("vscode", () => ({ ExtensionContext: jest.fn(), @@ -963,6 +970,52 @@ describe("ClineProvider", () => { ) }) + test("passes diffStrategy to SYSTEM_PROMPT when previewing", async () => { + // Mock getState to return experimentalDiffStrategy and fuzzyMatchThreshold + jest.spyOn(provider, "getState").mockResolvedValue({ + apiConfiguration: { + apiProvider: "openrouter", + apiModelId: "test-model", + openRouterModelInfo: { supportsComputerUse: true }, + }, + customPrompts: {}, + mode: "code", + mcpEnabled: false, + browserViewportSize: "900x600", + experimentalDiffStrategy: true, + fuzzyMatchThreshold: 0.8, + } as any) + + // Mock SYSTEM_PROMPT to verify diffStrategy is passed + const systemPromptModule = require("../../prompts/system") + const systemPromptSpy = jest.spyOn(systemPromptModule, "SYSTEM_PROMPT") + + // Trigger getSystemPrompt + const handler = getMessageHandler() + await handler({ type: "getSystemPrompt", mode: "code" }) + + // Verify SYSTEM_PROMPT was called with correct arguments + expect(systemPromptSpy).toHaveBeenCalledWith( + expect.anything(), // context + expect.any(String), // cwd + true, // supportsComputerUse + undefined, // mcpHub (disabled) + expect.objectContaining({ + // diffStrategy + getToolDescription: expect.any(Function), + }), + "900x600", // browserViewportSize + "code", // mode + expect.any(Object), // customPrompts + expect.any(Object), // customModes + undefined, // effectiveInstructions + ) + + // Run the test again to verify it's consistent + await handler({ type: "getSystemPrompt", mode: "code" }) + expect(systemPromptSpy).toHaveBeenCalledTimes(2) + }) + test("uses correct mode-specific instructions when mode is specified", async () => { // Mock getState to return architect mode instructions jest.spyOn(provider, "getState").mockResolvedValue({ From 18f029b8e8d132b879988829861199bafbe20908 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Jan 2025 16:14:31 +0000 Subject: [PATCH 16/66] changeset version bump --- .changeset/poor-adults-fetch.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/poor-adults-fetch.md diff --git a/.changeset/poor-adults-fetch.md b/.changeset/poor-adults-fetch.md deleted file mode 100644 index 32ac663..0000000 --- a/.changeset/poor-adults-fetch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Fix bug where apply_diff wasn't showing up in system prompt preview diff --git a/CHANGELOG.md b/CHANGELOG.md index 23bed58..cb85e21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Roo Code Changelog +## 3.2.2 + +### Patch Changes + +- Fix bug where apply_diff wasn't showing up in system prompt preview + ## [3.2.0 - 3.2.1] - **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. diff --git a/package-lock.json b/package-lock.json index ee41923..34e49ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.2.1", + "version": "3.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.2.1", + "version": "3.2.2", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index fc702d4..d88163b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Code (prev. Roo Cline)", "description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.", "publisher": "RooVeterinaryInc", - "version": "3.2.1", + "version": "3.2.2", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", From 61e663a2f0b8ca5511996fef3541bbdd2d53bd6c Mon Sep 17 00:00:00 2001 From: R00-B0T Date: Tue, 21 Jan 2025 16:15:14 +0000 Subject: [PATCH 17/66] Updating CHANGELOG.md format --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb85e21..0a8d432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ # Roo Code Changelog -## 3.2.2 - -### Patch Changes +## [3.2.2] - Fix bug where apply_diff wasn't showing up in system prompt preview From 1e5f4ba856d46f2907026f9e29ad42afbecee7cf Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 21 Jan 2025 11:19:32 -0500 Subject: [PATCH 18/66] Update CHANGELOG.md --- CHANGELOG.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a8d432..9ee97ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,6 @@ # Roo Code Changelog -## [3.2.2] - -- Fix bug where apply_diff wasn't showing up in system prompt preview - -## [3.2.0 - 3.2.1] +## [3.2.0 - 3.2.2] - **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. From 5ae2c648263cc620ef7266face56fd7a8c7966dc Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 21 Jan 2025 11:20:04 -0500 Subject: [PATCH 19/66] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ee97ec..151d0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [3.2.0 - 3.2.2] -- **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. +- **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 From 95fa1e400d397418020e86ba8a80e2499772eba2 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Tue, 21 Jan 2025 23:42:31 +0700 Subject: [PATCH 20/66] update: Improve model configuration UI text and styling - Clarify model capabilities description and impact on Roo Code - Update max tokens description to mention server dependency - Rename 'Computer Interaction' to 'Computer Use' for clarity - Add spacing after model info configuration section --- .../src/components/settings/ApiOptions.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index cda5191..13e7a57 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -588,7 +588,8 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = margin: "0 0 15px 0", lineHeight: "1.4", }}> - Configure the capabilities and pricing for your custom OpenAI-compatible model + Configure the capabilities and pricing for your custom OpenAI-compatible model.
+ Be careful for the model capabilities, as they can affect how Roo Code can work.

{/* Capabilities Section */} @@ -655,8 +656,8 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = }}> - Maximum number of tokens the model can generate in a response. Higher - values allow longer outputs but may increase costs. + Maximum number of tokens the model can generate in a response.
+ (-1 is depend on server)
@@ -711,8 +712,8 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = }}> - Total tokens (input + output) the model can process. Larger windows - allow processing more content but may increase memory usage. + Total tokens (input + output) the model can process. This will help Roo + Code run correctly.
@@ -804,7 +805,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = }, }) }}> - Computer Interaction + Computer Use - Enables the model to execute commands and modify files for automated - assistance + This model feature is for computer use like sonnet 3.5 support

@@ -976,6 +976,11 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
+
{/* end Model Info Configuration */} From 8bbb64d27fca56ad22524b29e2357ca8a466389e Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 21 Jan 2025 15:39:59 -0500 Subject: [PATCH 21/66] Fix language selector --- .changeset/cyan-camels-explode.md | 5 ++++ src/core/Cline.ts | 4 ++- src/core/prompts/__tests__/sections.test.ts | 28 +++++++++++++++++++++ src/core/prompts/system.ts | 5 +++- 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 .changeset/cyan-camels-explode.md create mode 100644 src/core/prompts/__tests__/sections.test.ts diff --git a/.changeset/cyan-camels-explode.md b/.changeset/cyan-camels-explode.md new file mode 100644 index 0000000..7bc47f8 --- /dev/null +++ b/.changeset/cyan-camels-explode.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Fix bug where language selector wasn't working diff --git a/src/core/Cline.ts b/src/core/Cline.ts index f26b43b..9d82086 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -809,7 +809,8 @@ export class Cline { }) } - const { browserViewportSize, mode, customPrompts } = (await this.providerRef.deref()?.getState()) ?? {} + const { browserViewportSize, mode, customPrompts, preferredLanguage } = + (await this.providerRef.deref()?.getState()) ?? {} const { customModes } = (await this.providerRef.deref()?.getState()) ?? {} const systemPrompt = await (async () => { const provider = this.providerRef.deref() @@ -826,6 +827,7 @@ export class Cline { mode, customPrompts, customModes, + preferredLanguage, ) })() diff --git a/src/core/prompts/__tests__/sections.test.ts b/src/core/prompts/__tests__/sections.test.ts new file mode 100644 index 0000000..064639f --- /dev/null +++ b/src/core/prompts/__tests__/sections.test.ts @@ -0,0 +1,28 @@ +import { addCustomInstructions } from "../sections/custom-instructions" + +describe("addCustomInstructions", () => { + test("adds preferred language to custom instructions", async () => { + const result = await addCustomInstructions( + "mode instructions", + "global instructions", + "/test/path", + "test-mode", + { preferredLanguage: "French" }, + ) + + expect(result).toContain("Language Preference:") + expect(result).toContain("You should always speak and think in the French language") + }) + + test("works without preferred language", async () => { + const result = await addCustomInstructions( + "mode instructions", + "global instructions", + "/test/path", + "test-mode", + ) + + expect(result).not.toContain("Language Preference:") + expect(result).not.toContain("You should always speak and think in") + }) +}) diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 3d30a96..bb7797f 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -37,6 +37,7 @@ async function generatePrompt( promptComponent?: PromptComponent, customModeConfigs?: ModeConfig[], globalCustomInstructions?: string, + preferredLanguage?: string, ): Promise { if (!context) { throw new Error("Extension context is required for generating system prompt") @@ -79,7 +80,7 @@ ${getSystemInfoSection(cwd, mode, customModeConfigs)} ${getObjectiveSection()} -${await addCustomInstructions(modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, {})}` +${await addCustomInstructions(modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage })}` return basePrompt } @@ -95,6 +96,7 @@ export const SYSTEM_PROMPT = async ( customPrompts?: CustomPrompts, customModes?: ModeConfig[], globalCustomInstructions?: string, + preferredLanguage?: string, ): Promise => { if (!context) { throw new Error("Extension context is required for generating system prompt") @@ -123,5 +125,6 @@ export const SYSTEM_PROMPT = async ( promptComponent, customModes, globalCustomInstructions, + preferredLanguage, ) } From 4edd65920141323f0da6f03697bedf87a30a5ee4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Jan 2025 20:58:08 +0000 Subject: [PATCH 22/66] changeset version bump --- .changeset/cyan-camels-explode.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/cyan-camels-explode.md diff --git a/.changeset/cyan-camels-explode.md b/.changeset/cyan-camels-explode.md deleted file mode 100644 index 7bc47f8..0000000 --- a/.changeset/cyan-camels-explode.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Fix bug where language selector wasn't working diff --git a/CHANGELOG.md b/CHANGELOG.md index 151d0ea..8534492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Roo Code Changelog +## 3.2.3 + +### Patch Changes + +- Fix bug where language selector wasn't working + ## [3.2.0 - 3.2.2] - **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. diff --git a/package-lock.json b/package-lock.json index 34e49ba..7f00ae7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.2.2", + "version": "3.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.2.2", + "version": "3.2.3", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index d88163b..5bb2a8a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Code (prev. Roo Cline)", "description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.", "publisher": "RooVeterinaryInc", - "version": "3.2.2", + "version": "3.2.3", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", From f028de82ba29e60846b3156130455d148c2c6f22 Mon Sep 17 00:00:00 2001 From: R00-B0T Date: Tue, 21 Jan 2025 20:58:49 +0000 Subject: [PATCH 23/66] Updating CHANGELOG.md format --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8534492..608b528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ # Roo Code Changelog -## 3.2.3 - -### Patch Changes +## [3.2.3] - Fix bug where language selector wasn't working From b8e2dac2b9bc43ee3960ef00e989d58c40b4faf9 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 21 Jan 2025 16:10:51 -0500 Subject: [PATCH 24/66] Only allow usage of diff tool if enabled in settings --- .changeset/nervous-radios-sneeze.md | 5 + src/core/Cline.ts | 16 +- src/core/__tests__/mode-validator.test.ts | 60 ++ src/core/mode-validator.ts | 9 +- .../__snapshots__/system.test.ts.snap | 572 +++++++++++++++++- src/core/prompts/__tests__/sections.test.ts | 28 + src/core/prompts/__tests__/system.test.ts | 65 +- src/core/prompts/sections/capabilities.ts | 2 +- src/core/prompts/system.ts | 19 +- src/core/prompts/tools/index.ts | 11 +- src/core/webview/ClineProvider.ts | 30 +- .../webview/__tests__/ClineProvider.test.ts | 58 +- src/shared/ExtensionMessage.ts | 1 + src/shared/modes.ts | 14 +- 14 files changed, 850 insertions(+), 40 deletions(-) create mode 100644 .changeset/nervous-radios-sneeze.md diff --git a/.changeset/nervous-radios-sneeze.md b/.changeset/nervous-radios-sneeze.md new file mode 100644 index 0000000..e9ee6dc --- /dev/null +++ b/.changeset/nervous-radios-sneeze.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Only allow use of the diff tool if it's enabled in settings diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 9d82086..5f8af77 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1,7 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import cloneDeep from "clone-deep" import { DiffStrategy, getDiffStrategy, UnifiedDiffStrategy } from "./diff/DiffStrategy" -import { validateToolUse, isToolAllowedForMode } from "./mode-validator" +import { validateToolUse, isToolAllowedForMode, ToolName } from "./mode-validator" import delay from "delay" import fs from "fs/promises" import os from "os" @@ -827,7 +827,9 @@ export class Cline { mode, customPrompts, customModes, + this.customInstructions, preferredLanguage, + this.diffEnabled, ) })() @@ -1140,11 +1142,13 @@ export class Cline { await this.browserSession.closeBrowser() } - // Validate tool use based on current mode + // Validate tool use before execution const { mode } = (await this.providerRef.deref()?.getState()) ?? {} const { customModes } = (await this.providerRef.deref()?.getState()) ?? {} try { - validateToolUse(block.name, mode ?? defaultModeSlug, customModes) + validateToolUse(block.name as ToolName, mode ?? defaultModeSlug, customModes ?? [], { + apply_diff: this.diffEnabled, + }) } catch (error) { this.consecutiveMistakeCount++ pushToolResult(formatResponse.toolError(error.message)) @@ -2637,8 +2641,10 @@ export class Cline { // Add warning if not in code mode if ( - !isToolAllowedForMode("write_to_file", currentMode, customModes ?? []) && - !isToolAllowedForMode("apply_diff", currentMode, customModes ?? []) + !isToolAllowedForMode("write_to_file", currentMode, customModes ?? [], { + apply_diff: this.diffEnabled, + }) && + !isToolAllowedForMode("apply_diff", currentMode, customModes ?? [], { apply_diff: this.diffEnabled }) ) { const currentModeName = getModeBySlug(currentMode, customModes)?.name ?? currentMode const defaultModeName = getModeBySlug(defaultModeSlug, customModes)?.name ?? defaultModeSlug diff --git a/src/core/__tests__/mode-validator.test.ts b/src/core/__tests__/mode-validator.test.ts index 635ae77..bd6d323 100644 --- a/src/core/__tests__/mode-validator.test.ts +++ b/src/core/__tests__/mode-validator.test.ts @@ -74,6 +74,50 @@ describe("mode-validator", () => { // Should not allow tools from other groups expect(isToolAllowedForMode("write_to_file", codeMode, customModes)).toBe(false) }) + + it("respects tool requirements in custom modes", () => { + const customModes = [ + { + slug: "custom-mode", + name: "Custom Mode", + roleDefinition: "Custom role", + groups: ["edit"] as const, + }, + ] + const requirements = { apply_diff: false } + + // Should respect disabled requirement even if tool group is allowed + expect(isToolAllowedForMode("apply_diff", "custom-mode", customModes, requirements)).toBe(false) + + // Should allow other edit tools + expect(isToolAllowedForMode("write_to_file", "custom-mode", customModes, requirements)).toBe(true) + }) + }) + + describe("tool requirements", () => { + it("respects tool requirements when provided", () => { + const requirements = { apply_diff: false } + expect(isToolAllowedForMode("apply_diff", codeMode, [], requirements)).toBe(false) + + const enabledRequirements = { apply_diff: true } + expect(isToolAllowedForMode("apply_diff", codeMode, [], enabledRequirements)).toBe(true) + }) + + it("allows tools when their requirements are not specified", () => { + const requirements = { some_other_tool: true } + expect(isToolAllowedForMode("apply_diff", codeMode, [], requirements)).toBe(true) + }) + + it("handles undefined and empty requirements", () => { + expect(isToolAllowedForMode("apply_diff", codeMode, [], undefined)).toBe(true) + expect(isToolAllowedForMode("apply_diff", codeMode, [], {})).toBe(true) + }) + + it("prioritizes requirements over mode configuration", () => { + const requirements = { apply_diff: false } + // Even in code mode which allows all tools, disabled requirement should take precedence + expect(isToolAllowedForMode("apply_diff", codeMode, [], requirements)).toBe(false) + }) }) }) @@ -87,5 +131,21 @@ describe("mode-validator", () => { it("does not throw for allowed tools in architect mode", () => { expect(() => validateToolUse("read_file", "architect", [])).not.toThrow() }) + + it("throws error when tool requirement is not met", () => { + const requirements = { apply_diff: false } + expect(() => validateToolUse("apply_diff", codeMode, [], requirements)).toThrow( + 'Tool "apply_diff" is not allowed in code mode.', + ) + }) + + it("does not throw when tool requirement is met", () => { + const requirements = { apply_diff: true } + expect(() => validateToolUse("apply_diff", codeMode, [], requirements)).not.toThrow() + }) + + it("handles undefined requirements gracefully", () => { + expect(() => validateToolUse("apply_diff", codeMode, [], undefined)).not.toThrow() + }) }) }) diff --git a/src/core/mode-validator.ts b/src/core/mode-validator.ts index e2f38e2..4432997 100644 --- a/src/core/mode-validator.ts +++ b/src/core/mode-validator.ts @@ -4,8 +4,13 @@ import { ToolName } from "../shared/tool-groups" export { isToolAllowedForMode } export type { ToolName } -export function validateToolUse(toolName: ToolName, mode: Mode, customModes?: ModeConfig[]): void { - if (!isToolAllowedForMode(toolName, mode, customModes ?? [])) { +export function validateToolUse( + toolName: ToolName, + mode: Mode, + customModes?: ModeConfig[], + toolRequirements?: Record, +): void { + if (!isToolAllowedForMode(toolName, mode, customModes ?? [], toolRequirements)) { throw new Error(`Tool "${toolName}" is not allowed in ${mode} mode.`) } } diff --git a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap index ca390c8..f283080 100644 --- a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap +++ b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap @@ -1,5 +1,575 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`SYSTEM_PROMPT should exclude diff strategy tool description when diffEnabled is false 1`] = ` +"You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. + +==== + +TOOL USE + +You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use. + +# Tool Use Formatting + +Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure: + + +value1 +value2 +... + + +For example: + + +src/main.js + + +Always adhere to this format for the tool use to ensure proper parsing and execution. + +# Tools + +## read_file +Description: Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. The output includes line numbers prefixed to each line (e.g. "1 | const x = 1"), making it easier to reference specific lines when creating diffs or discussing code. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string. +Parameters: +- path: (required) The path of the file to read (relative to the current working directory /test/path) +Usage: + +File path here + + +Example: Requesting to read frontend-config.json + +frontend-config.json + + +## search_files +Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. +Parameters: +- path: (required) The path of the directory to search in (relative to the current working directory /test/path). This directory will be recursively searched. +- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax. +- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*). +Usage: + +Directory path here +Your regex pattern here +file pattern here (optional) + + +Example: Requesting to search for all .ts files in the current directory + +. +.* +*.ts + + +## list_files +Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not. +Parameters: +- path: (required) The path of the directory to list contents for (relative to the current working directory /test/path) +- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only. +Usage: + +Directory path here +true or false (optional) + + +Example: Requesting to list all files in the current directory + +. +false + + +## list_code_definition_names +Description: Request to list definition names (classes, functions, methods, etc.) used in source code files at the top level of the specified directory. This tool provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture. +Parameters: +- path: (required) The path of the directory (relative to the current working directory /test/path) to list top level source code definitions for. +Usage: + +Directory path here + + +Example: Requesting to list all top level source code definitions in the current directory + +. + + +## write_to_file +Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file. +Parameters: +- path: (required) The path of the file to write to (relative to the current working directory /test/path) +- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. +- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. +Usage: + +File path here + +Your file content here + +total number of lines in the file, including empty lines + + +Example: Requesting to write to frontend-config.json + +frontend-config.json + +{ + "apiEndpoint": "https://api.example.com", + "theme": { + "primaryColor": "#007bff", + "secondaryColor": "#6c757d", + "fontFamily": "Arial, sans-serif" + }, + "features": { + "darkMode": true, + "notifications": true, + "analytics": false + }, + "version": "1.0.0" +} + +14 + + +## execute_command +Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +Parameters: +- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. +Usage: + +Your command here + + +Example: Requesting to execute npm run dev + +npm run dev + + +## ask_followup_question +Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. +Parameters: +- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need. +Usage: + +Your question here + + +Example: Requesting to ask the user for the path to the frontend-config.json file + +What is the path to the frontend-config.json file? + + +## attempt_completion +Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. +IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. +Parameters: +- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance. +- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use \`open index.html\` to display a created html website, or \`open localhost:3000\` to display a locally running development server. But DO NOT use commands like \`echo\` or \`cat\` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. +Usage: + + +Your final result description here + +Command to demonstrate result (optional) + + +Example: Requesting to attempt completion with a result and command + + +I've updated the CSS + +open index.html + + +# Tool Use Guidelines + +1. In tags, assess what information you already have and what information you need to proceed with the task. +2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like \`ls\` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task. +3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result. +4. Formulate your tool use using the XML format specified for each tool. +5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include: + - Information about whether the tool succeeded or failed, along with any reasons for failure. + - Linter errors that may have arisen due to the changes you made, which you'll need to address. + - New terminal output in reaction to the changes, which you may need to consider or act upon. + - Any other relevant feedback or information related to the tool use. +6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user. + +It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to: +1. Confirm the success of each step before proceeding. +2. Address any issues or errors that arise immediately. +3. Adapt your approach based on new information or unexpected results. +4. Ensure that each action builds correctly on the previous ones. + +By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work. + + + +==== + +CAPABILITIES + +- You have access to tools that let you execute CLI commands on the user's computer, list files, view source code definitions, regex search, read and write files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more. +- 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. +- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring. +- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task. + - For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed. +- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance. + +==== + +MODES + +- Test modes section + +==== + +RULES + +- Your current working directory is: /test/path +- You cannot \`cd\` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path. +- Do not use the ~ character or $HOME to refer to the home directory. +- 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 '/test/path', and if so prepend with \`cd\`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run \`npm install\` in a project outside of '/test/path', 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 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 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. +- 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. +- You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. However if you can use the available tools to avoid having to ask the user questions, you should do so. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves. +- When executing commands, if you don't see the expected output, assume the terminal executed the command successfully and proceed with the task. The user's terminal may be unable to stream the output back properly. If you absolutely need to see the actual terminal output, use the ask_followup_question tool to request the user to copy and paste it back to you. +- The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it. +- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation. +- NEVER end attempt_completion result with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user. +- You are STRICTLY FORBIDDEN from starting your messages with "Great", "Certainly", "Okay", "Sure". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say "Great, I've updated the CSS" but instead something like "I've updated the CSS". It is important you be clear and technical in your messages. +- When presented with images, utilize your vision capabilities to thoroughly examine them and extract meaningful information. Incorporate these insights into your thought process as you accomplish the user's task. +- At the end of each user message, you will automatically receive environment_details. This information is not written by the user themselves, but is auto-generated to provide potentially relevant context about the project structure and environment. While this information can be valuable for understanding the project context, do not treat it as a direct part of the user's request or response. Use it to inform your actions and decisions, but don't assume the user is explicitly asking about or referring to this information unless they clearly do so in their message. When using environment_details, explain your actions clearly to ensure the user understands, as they may not be aware of these details. +- Before executing commands, check the "Actively Running Terminals" section in environment_details. If present, consider how these active processes might impact your task. For example, if a local development server is already running, you wouldn't need to start it again. If no active terminals are listed, proceed with command execution as normal. +- When using the write_to_file tool, ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code, severely impacting the user's project. +- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations. +- It is critical you wait for the user's response after each tool use, in order to confirm the success of the tool use. For example, if asked to make a todo app, you would create a file, wait for the user's response it was created successfully, then create another file if needed, wait for the user's response it was created successfully, etc. + +==== + +SYSTEM INFORMATION + +Operating System: Linux +Default Shell: /bin/bash +Home Directory: /home/user +Current Working Directory: /test/path + +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. + +==== + +OBJECTIVE + +You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically. + +1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order. +2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go. +3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided. +4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built. +5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance. + + +==== + +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. + +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules +# Rules from .clinerules: +Mock generic rules" +`; + +exports[`SYSTEM_PROMPT should exclude diff strategy tool description when diffEnabled is undefined 1`] = ` +"You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. + +==== + +TOOL USE + +You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use. + +# Tool Use Formatting + +Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure: + + +value1 +value2 +... + + +For example: + + +src/main.js + + +Always adhere to this format for the tool use to ensure proper parsing and execution. + +# Tools + +## read_file +Description: Request to read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file you do not know the contents of, for example to analyze code, review text files, or extract information from configuration files. The output includes line numbers prefixed to each line (e.g. "1 | const x = 1"), making it easier to reference specific lines when creating diffs or discussing code. Automatically extracts raw text from PDF and DOCX files. May not be suitable for other types of binary files, as it returns the raw content as a string. +Parameters: +- path: (required) The path of the file to read (relative to the current working directory /test/path) +Usage: + +File path here + + +Example: Requesting to read frontend-config.json + +frontend-config.json + + +## search_files +Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. +Parameters: +- path: (required) The path of the directory to search in (relative to the current working directory /test/path). This directory will be recursively searched. +- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax. +- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*). +Usage: + +Directory path here +Your regex pattern here +file pattern here (optional) + + +Example: Requesting to search for all .ts files in the current directory + +. +.* +*.ts + + +## list_files +Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not. +Parameters: +- path: (required) The path of the directory to list contents for (relative to the current working directory /test/path) +- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only. +Usage: + +Directory path here +true or false (optional) + + +Example: Requesting to list all files in the current directory + +. +false + + +## list_code_definition_names +Description: Request to list definition names (classes, functions, methods, etc.) used in source code files at the top level of the specified directory. This tool provides insights into the codebase structure and important constructs, encapsulating high-level concepts and relationships that are crucial for understanding the overall architecture. +Parameters: +- path: (required) The path of the directory (relative to the current working directory /test/path) to list top level source code definitions for. +Usage: + +Directory path here + + +Example: Requesting to list all top level source code definitions in the current directory + +. + + +## write_to_file +Description: Request to write full content to a file at the specified path. If the file exists, it will be overwritten with the provided content. If the file doesn't exist, it will be created. This tool will automatically create any directories needed to write the file. +Parameters: +- path: (required) The path of the file to write to (relative to the current working directory /test/path) +- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file. +- line_count: (required) The number of lines in the file. Make sure to compute this based on the actual content of the file, not the number of lines in the content you're providing. +Usage: + +File path here + +Your file content here + +total number of lines in the file, including empty lines + + +Example: Requesting to write to frontend-config.json + +frontend-config.json + +{ + "apiEndpoint": "https://api.example.com", + "theme": { + "primaryColor": "#007bff", + "secondaryColor": "#6c757d", + "fontFamily": "Arial, sans-serif" + }, + "features": { + "darkMode": true, + "notifications": true, + "analytics": false + }, + "version": "1.0.0" +} + +14 + + +## execute_command +Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +Parameters: +- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. +Usage: + +Your command here + + +Example: Requesting to execute npm run dev + +npm run dev + + +## ask_followup_question +Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. +Parameters: +- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need. +Usage: + +Your question here + + +Example: Requesting to ask the user for the path to the frontend-config.json file + +What is the path to the frontend-config.json file? + + +## attempt_completion +Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. Optionally you may provide a CLI command to showcase the result of your work. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. +IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. +Parameters: +- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance. +- command: (optional) A CLI command to execute to show a live demo of the result to the user. For example, use \`open index.html\` to display a created html website, or \`open localhost:3000\` to display a locally running development server. But DO NOT use commands like \`echo\` or \`cat\` that merely print text. This command should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. +Usage: + + +Your final result description here + +Command to demonstrate result (optional) + + +Example: Requesting to attempt completion with a result and command + + +I've updated the CSS + +open index.html + + +# Tool Use Guidelines + +1. In tags, assess what information you already have and what information you need to proceed with the task. +2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like \`ls\` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task. +3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result. +4. Formulate your tool use using the XML format specified for each tool. +5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include: + - Information about whether the tool succeeded or failed, along with any reasons for failure. + - Linter errors that may have arisen due to the changes you made, which you'll need to address. + - New terminal output in reaction to the changes, which you may need to consider or act upon. + - Any other relevant feedback or information related to the tool use. +6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user. + +It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to: +1. Confirm the success of each step before proceeding. +2. Address any issues or errors that arise immediately. +3. Adapt your approach based on new information or unexpected results. +4. Ensure that each action builds correctly on the previous ones. + +By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work. + + + +==== + +CAPABILITIES + +- You have access to tools that let you execute CLI commands on the user's computer, list files, view source code definitions, regex search, read and write files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more. +- 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. +- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring. +- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task. + - For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed. +- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance. + +==== + +MODES + +- Test modes section + +==== + +RULES + +- Your current working directory is: /test/path +- You cannot \`cd\` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path. +- Do not use the ~ character or $HOME to refer to the home directory. +- 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 '/test/path', and if so prepend with \`cd\`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run \`npm install\` in a project outside of '/test/path', 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 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 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. +- 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. +- You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. However if you can use the available tools to avoid having to ask the user questions, you should do so. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves. +- When executing commands, if you don't see the expected output, assume the terminal executed the command successfully and proceed with the task. The user's terminal may be unable to stream the output back properly. If you absolutely need to see the actual terminal output, use the ask_followup_question tool to request the user to copy and paste it back to you. +- The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it. +- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation. +- NEVER end attempt_completion result with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user. +- You are STRICTLY FORBIDDEN from starting your messages with "Great", "Certainly", "Okay", "Sure". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say "Great, I've updated the CSS" but instead something like "I've updated the CSS". It is important you be clear and technical in your messages. +- When presented with images, utilize your vision capabilities to thoroughly examine them and extract meaningful information. Incorporate these insights into your thought process as you accomplish the user's task. +- At the end of each user message, you will automatically receive environment_details. This information is not written by the user themselves, but is auto-generated to provide potentially relevant context about the project structure and environment. While this information can be valuable for understanding the project context, do not treat it as a direct part of the user's request or response. Use it to inform your actions and decisions, but don't assume the user is explicitly asking about or referring to this information unless they clearly do so in their message. When using environment_details, explain your actions clearly to ensure the user understands, as they may not be aware of these details. +- Before executing commands, check the "Actively Running Terminals" section in environment_details. If present, consider how these active processes might impact your task. For example, if a local development server is already running, you wouldn't need to start it again. If no active terminals are listed, proceed with command execution as normal. +- When using the write_to_file tool, ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code, severely impacting the user's project. +- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations. +- It is critical you wait for the user's response after each tool use, in order to confirm the success of the tool use. For example, if asked to make a todo app, you would create a file, wait for the user's response it was created successfully, then create another file if needed, wait for the user's response it was created successfully, etc. + +==== + +SYSTEM INFORMATION + +Operating System: Linux +Default Shell: /bin/bash +Home Directory: /home/user +Current Working Directory: /test/path + +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. + +==== + +OBJECTIVE + +You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically. + +1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order. +2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go. +3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis within tags. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Then, think about which of the provided tools is the most relevant tool to accomplish the user's task. Next, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided. +4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user. You may also provide a CLI command to showcase the result of your task; this can be particularly useful for web development tasks, where you can run e.g. \`open index.html\` to show the website you've built. +5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance. + + +==== + +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. + +Rules: +# Rules from .clinerules-code: +Mock mode-specific rules +# Rules from .clinerules: +Mock generic rules" +`; + exports[`SYSTEM_PROMPT should explicitly handle undefined mcpHub 1`] = ` "You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. @@ -1651,7 +2221,7 @@ Mock mode-specific rules Mock generic rules" `; -exports[`SYSTEM_PROMPT should include diff strategy tool description 1`] = ` +exports[`SYSTEM_PROMPT should include diff strategy tool description when diffEnabled is true 1`] = ` "You are Roo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. ==== diff --git a/src/core/prompts/__tests__/sections.test.ts b/src/core/prompts/__tests__/sections.test.ts index 064639f..2100016 100644 --- a/src/core/prompts/__tests__/sections.test.ts +++ b/src/core/prompts/__tests__/sections.test.ts @@ -1,4 +1,6 @@ import { addCustomInstructions } from "../sections/custom-instructions" +import { getCapabilitiesSection } from "../sections/capabilities" +import { DiffStrategy, DiffResult } from "../../diff/types" describe("addCustomInstructions", () => { test("adds preferred language to custom instructions", async () => { @@ -26,3 +28,29 @@ describe("addCustomInstructions", () => { expect(result).not.toContain("You should always speak and think in") }) }) + +describe("getCapabilitiesSection", () => { + const cwd = "/test/path" + const mcpHub = undefined + const mockDiffStrategy: DiffStrategy = { + getToolDescription: () => "apply_diff tool description", + applyDiff: async (originalContent: string, diffContent: string): Promise => { + return { success: true, content: "mock result" } + }, + } + + test("includes apply_diff in capabilities when diffStrategy is provided", () => { + const result = getCapabilitiesSection(cwd, false, mcpHub, mockDiffStrategy) + + expect(result).toContain("or apply_diff") + expect(result).toContain("then use the write_to_file or apply_diff tool") + }) + + test("excludes apply_diff from capabilities when diffStrategy is undefined", () => { + const result = getCapabilitiesSection(cwd, false, mcpHub, undefined) + + expect(result).not.toContain("or apply_diff") + expect(result).toContain("then use the write_to_file tool") + expect(result).not.toContain("write_to_file or apply_diff") + }) +}) diff --git a/src/core/prompts/__tests__/system.test.ts b/src/core/prompts/__tests__/system.test.ts index 5a87a18..d22f834 100644 --- a/src/core/prompts/__tests__/system.test.ts +++ b/src/core/prompts/__tests__/system.test.ts @@ -235,7 +235,7 @@ describe("SYSTEM_PROMPT", () => { expect(prompt).toMatchSnapshot() }) - it("should include diff strategy tool description", async () => { + it("should include diff strategy tool description when diffEnabled is true", async () => { const prompt = await SYSTEM_PROMPT( mockContext, "/test/path", @@ -246,11 +246,74 @@ describe("SYSTEM_PROMPT", () => { defaultModeSlug, // mode undefined, // customPrompts undefined, // customModes + undefined, // globalCustomInstructions + undefined, // preferredLanguage + true, // diffEnabled ) + expect(prompt).toContain("apply_diff") expect(prompt).toMatchSnapshot() }) + it("should exclude diff strategy tool description when diffEnabled is false", async () => { + const prompt = await SYSTEM_PROMPT( + mockContext, + "/test/path", + false, // supportsComputerUse + undefined, // mcpHub + new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase + undefined, // browserViewportSize + defaultModeSlug, // mode + undefined, // customPrompts + undefined, // customModes + undefined, // globalCustomInstructions + undefined, // preferredLanguage + false, // diffEnabled + ) + + expect(prompt).not.toContain("apply_diff") + expect(prompt).toMatchSnapshot() + }) + + it("should exclude diff strategy tool description when diffEnabled is undefined", async () => { + const prompt = await SYSTEM_PROMPT( + mockContext, + "/test/path", + false, // supportsComputerUse + undefined, // mcpHub + new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase + undefined, // browserViewportSize + defaultModeSlug, // mode + undefined, // customPrompts + undefined, // customModes + undefined, // globalCustomInstructions + undefined, // preferredLanguage + undefined, // diffEnabled + ) + + expect(prompt).not.toContain("apply_diff") + expect(prompt).toMatchSnapshot() + }) + + it("should include preferred language in custom instructions", async () => { + const prompt = await SYSTEM_PROMPT( + mockContext, + "/test/path", + false, // supportsComputerUse + undefined, // mcpHub + undefined, // diffStrategy + undefined, // browserViewportSize + defaultModeSlug, // mode + undefined, // customPrompts + undefined, // customModes + undefined, // globalCustomInstructions + "Spanish", // preferredLanguage + ) + + expect(prompt).toContain("Language Preference:") + expect(prompt).toContain("You should always speak and think in the Spanish language") + }) + it("should include custom mode role definition at top and instructions at bottom", async () => { const modeCustomInstructions = "Custom mode instructions" const customModes = [ diff --git a/src/core/prompts/sections/capabilities.ts b/src/core/prompts/sections/capabilities.ts index c30e38a..c292eef 100644 --- a/src/core/prompts/sections/capabilities.ts +++ b/src/core/prompts/sections/capabilities.ts @@ -17,7 +17,7 @@ CAPABILITIES - When the user initially gives you a task, a recursive list of all filepaths in the current working directory ('${cwd}') 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. - You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring. - You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task. - - For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file ${diffStrategy ? "or apply_diff " : ""}tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed. + - For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file${diffStrategy ? " or apply_diff" : ""} tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed. - You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.${ supportsComputerUse ? "\n- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.\n - For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser." diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index bb7797f..017e1e9 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -38,13 +38,17 @@ async function generatePrompt( customModeConfigs?: ModeConfig[], globalCustomInstructions?: string, preferredLanguage?: string, + diffEnabled?: boolean, ): Promise { if (!context) { throw new Error("Extension context is required for generating system prompt") } + // If diff is disabled, don't pass the diffStrategy + const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined + const [mcpServersSection, modesSection] = await Promise.all([ - getMcpServersSection(mcpHub, diffStrategy), + getMcpServersSection(mcpHub, effectiveDiffStrategy), getModesSection(context), ]) @@ -60,7 +64,7 @@ ${getToolDescriptionsForMode( mode, cwd, supportsComputerUse, - diffStrategy, + effectiveDiffStrategy, browserViewportSize, mcpHub, customModeConfigs, @@ -70,7 +74,7 @@ ${getToolUseGuidelinesSection()} ${mcpServersSection} -${getCapabilitiesSection(cwd, supportsComputerUse, mcpHub, diffStrategy)} +${getCapabilitiesSection(cwd, supportsComputerUse, mcpHub, effectiveDiffStrategy)} ${modesSection} @@ -80,7 +84,7 @@ ${getSystemInfoSection(cwd, mode, customModeConfigs)} ${getObjectiveSection()} -${await addCustomInstructions(modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage })}` +${await addCustomInstructions(promptComponent?.customInstructions || modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage })}` return basePrompt } @@ -97,6 +101,7 @@ export const SYSTEM_PROMPT = async ( customModes?: ModeConfig[], globalCustomInstructions?: string, preferredLanguage?: string, + diffEnabled?: boolean, ): Promise => { if (!context) { throw new Error("Extension context is required for generating system prompt") @@ -114,17 +119,21 @@ export const SYSTEM_PROMPT = async ( // 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] + // If diff is disabled, don't pass the diffStrategy + const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined + return generatePrompt( context, cwd, supportsComputerUse, currentMode.slug, mcpHub, - diffStrategy, + effectiveDiffStrategy, browserViewportSize, promptComponent, customModes, globalCustomInstructions, preferredLanguage, + diffEnabled, ) } diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index 001942c..5fb7662 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -50,21 +50,24 @@ export function getToolDescriptionsForMode( mcpHub, } - // Get all tools from the mode's groups and always available tools const tools = new Set() // Add tools from mode's groups config.groups.forEach((group) => { - TOOL_GROUPS[group].forEach((tool) => tools.add(tool)) + TOOL_GROUPS[group].forEach((tool) => { + if (isToolAllowedForMode(tool as ToolName, mode, customModes ?? [])) { + tools.add(tool) + } + }) }) // Add always available tools ALWAYS_AVAILABLE_TOOLS.forEach((tool) => tools.add(tool)) - // Map tool descriptions for all allowed tools + // Map tool descriptions for allowed tools const descriptions = Array.from(tools).map((toolName) => { const descriptionFn = toolDescriptionMap[toolName] - if (!descriptionFn || !isToolAllowedForMode(toolName as ToolName, mode, customModes ?? [])) { + if (!descriptionFn) { return undefined } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 0fd2dd5..fada031 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -18,7 +18,16 @@ import { findLast } from "../../shared/array" import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage" import { HistoryItem } from "../../shared/HistoryItem" import { WebviewMessage } from "../../shared/WebviewMessage" -import { defaultModeSlug } from "../../shared/modes" +import { + Mode, + modes, + CustomPrompts, + PromptComponent, + enhance, + ModeConfig, + defaultModeSlug, + getModeBySlug, +} from "../../shared/modes" import { SYSTEM_PROMPT } from "../prompts/system" import { fileExistsAtPath } from "../../utils/fs" import { Cline } from "../Cline" @@ -31,7 +40,6 @@ import { enhancePrompt } from "../../utils/enhance-prompt" import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git" import { ConfigManager } from "../config/ConfigManager" 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 @@ -963,6 +971,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { customInstructions, preferredLanguage, browserViewportSize, + diffEnabled, mcpEnabled, fuzzyMatchThreshold, experimentalDiffStrategy, @@ -980,11 +989,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { const mode = message.mode ?? defaultModeSlug const customModes = await this.customModesManager.getCustomModes() - const modePrompt = customPrompts?.[mode] - const effectiveInstructions = [customInstructions, modePrompt?.customInstructions] - .filter(Boolean) - .join("\n\n") - const systemPrompt = await SYSTEM_PROMPT( this.context, cwd, @@ -993,15 +997,11 @@ export class ClineProvider implements vscode.WebviewViewProvider { diffStrategy, browserViewportSize ?? "900x600", mode, - { - ...customPrompts, - [mode]: { - ...(modePrompt ?? {}), - customInstructions: undefined, // Prevent double-inclusion - }, - }, + customPrompts, customModes, - effectiveInstructions || undefined, + customInstructions, + preferredLanguage, + diffEnabled, ) await this.postMessageToWebview({ diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index b672110..63dc2d5 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -970,8 +970,8 @@ describe("ClineProvider", () => { ) }) - test("passes diffStrategy to SYSTEM_PROMPT when previewing", async () => { - // Mock getState to return experimentalDiffStrategy and fuzzyMatchThreshold + test("passes diffStrategy and diffEnabled to SYSTEM_PROMPT when previewing", async () => { + // Mock getState to return experimentalDiffStrategy, diffEnabled and fuzzyMatchThreshold jest.spyOn(provider, "getState").mockResolvedValue({ apiConfiguration: { apiProvider: "openrouter", @@ -983,10 +983,11 @@ describe("ClineProvider", () => { mcpEnabled: false, browserViewportSize: "900x600", experimentalDiffStrategy: true, + diffEnabled: true, fuzzyMatchThreshold: 0.8, } as any) - // Mock SYSTEM_PROMPT to verify diffStrategy is passed + // Mock SYSTEM_PROMPT to verify diffStrategy and diffEnabled are passed const systemPromptModule = require("../../prompts/system") const systemPromptSpy = jest.spyOn(systemPromptModule, "SYSTEM_PROMPT") @@ -1006,9 +1007,11 @@ describe("ClineProvider", () => { }), "900x600", // browserViewportSize "code", // mode - expect.any(Object), // customPrompts - expect.any(Object), // customModes + {}, // customPrompts + {}, // customModes undefined, // effectiveInstructions + undefined, // preferredLanguage + true, // diffEnabled ) // Run the test again to verify it's consistent @@ -1016,6 +1019,51 @@ describe("ClineProvider", () => { expect(systemPromptSpy).toHaveBeenCalledTimes(2) }) + test("passes diffEnabled: false to SYSTEM_PROMPT when diff is disabled", async () => { + // Mock getState to return diffEnabled: false + jest.spyOn(provider, "getState").mockResolvedValue({ + apiConfiguration: { + apiProvider: "openrouter", + apiModelId: "test-model", + openRouterModelInfo: { supportsComputerUse: true }, + }, + customPrompts: {}, + mode: "code", + mcpEnabled: false, + browserViewportSize: "900x600", + experimentalDiffStrategy: true, + diffEnabled: false, + fuzzyMatchThreshold: 0.8, + } as any) + + // Mock SYSTEM_PROMPT to verify diffEnabled is passed as false + const systemPromptModule = require("../../prompts/system") + const systemPromptSpy = jest.spyOn(systemPromptModule, "SYSTEM_PROMPT") + + // Trigger getSystemPrompt + const handler = getMessageHandler() + await handler({ type: "getSystemPrompt", mode: "code" }) + + // Verify SYSTEM_PROMPT was called with diffEnabled: false + expect(systemPromptSpy).toHaveBeenCalledWith( + expect.anything(), // context + expect.any(String), // cwd + true, // supportsComputerUse + undefined, // mcpHub (disabled) + expect.objectContaining({ + // diffStrategy + getToolDescription: expect.any(Function), + }), + "900x600", // browserViewportSize + "code", // mode + {}, // customPrompts + {}, // customModes + undefined, // effectiveInstructions + undefined, // preferredLanguage + false, // diffEnabled + ) + }) + test("uses correct mode-specific instructions when mode is specified", async () => { // Mock getState to return architect mode instructions jest.spyOn(provider, "getState").mockResolvedValue({ diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index cab56d5..3bcd8a0 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -108,6 +108,7 @@ export interface ExtensionState { experimentalDiffStrategy?: boolean autoApprovalEnabled?: boolean customModes: ModeConfig[] + toolRequirements?: Record // Map of tool names to their requirements (e.g. {"apply_diff": true} if diffEnabled) } export interface ClineMessage { diff --git a/src/shared/modes.ts b/src/shared/modes.ts index a8e0ae7..c6ea89a 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -103,12 +103,24 @@ 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 { +export function isToolAllowedForMode( + tool: string, + modeSlug: string, + customModes: ModeConfig[], + toolRequirements?: Record, +): boolean { // Always allow these tools if (ALWAYS_AVAILABLE_TOOLS.includes(tool as any)) { return true } + // Check tool requirements if any exist + if (toolRequirements && tool in toolRequirements) { + if (!toolRequirements[tool]) { + return false + } + } + const mode = getModeBySlug(modeSlug, customModes) if (!mode) { return false From 309fc7def3ba7045a801ca8e9719476cf6cb9eec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Jan 2025 23:38:22 +0000 Subject: [PATCH 25/66] changeset version bump --- .changeset/nervous-radios-sneeze.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/nervous-radios-sneeze.md diff --git a/.changeset/nervous-radios-sneeze.md b/.changeset/nervous-radios-sneeze.md deleted file mode 100644 index e9ee6dc..0000000 --- a/.changeset/nervous-radios-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Only allow use of the diff tool if it's enabled in settings diff --git a/CHANGELOG.md b/CHANGELOG.md index 608b528..9195e7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Roo Code Changelog +## 3.2.4 + +### Patch Changes + +- Only allow use of the diff tool if it's enabled in settings + ## [3.2.3] - Fix bug where language selector wasn't working diff --git a/package-lock.json b/package-lock.json index 7f00ae7..a6eb8cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.2.3", + "version": "3.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.2.3", + "version": "3.2.4", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index 5bb2a8a..026371a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Code (prev. Roo Cline)", "description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.", "publisher": "RooVeterinaryInc", - "version": "3.2.3", + "version": "3.2.4", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", From 00f0945c270216727fe58de049e11b548adfc158 Mon Sep 17 00:00:00 2001 From: R00-B0T Date: Tue, 21 Jan 2025 23:39:05 +0000 Subject: [PATCH 26/66] Updating CHANGELOG.md format --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9195e7f..a84b14c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ # Roo Code Changelog -## 3.2.4 - -### Patch Changes +## [3.2.4] - Only allow use of the diff tool if it's enabled in settings From 53af972579fad53e7c288f6e96125c2804749b71 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 21 Jan 2025 19:23:27 -0500 Subject: [PATCH 27/66] Update openrouter.ts --- src/api/providers/openrouter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 3f0b88b..f28d74b 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -30,7 +30,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { apiKey: this.options.openRouterApiKey, defaultHeaders: { "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline", - "X-Title": "Roo-Code", + "X-Title": "Roo Code", }, }) } From 2a11bcabfd8f30e335e89fa92cc910b8e62f5c44 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 21 Jan 2025 19:32:59 -0500 Subject: [PATCH 28/66] Update openrouter.test.ts --- src/api/providers/__tests__/openrouter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/providers/__tests__/openrouter.test.ts b/src/api/providers/__tests__/openrouter.test.ts index 039bf97..18f81ce 100644 --- a/src/api/providers/__tests__/openrouter.test.ts +++ b/src/api/providers/__tests__/openrouter.test.ts @@ -36,7 +36,7 @@ describe("OpenRouterHandler", () => { apiKey: mockOptions.openRouterApiKey, defaultHeaders: { "HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline", - "X-Title": "Roo-Code", + "X-Title": "Roo Code", }, }) }) From 5a06dea7d32869a79f6427a696d9b7dc1eb06d5c Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 21 Jan 2025 21:13:50 -0700 Subject: [PATCH 29/66] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ebbd66..b17c1ac 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Roo Code +# Roo Code (prev. Roo Cline) **Roo Code** is an AI-powered **autonomous coding agent** that lives in your editor. It can: From 8d0acfa9871ac0848746fcd4600d9cf4311f99fc Mon Sep 17 00:00:00 2001 From: Tom X Nguyen Date: Wed, 22 Jan 2025 15:47:37 +0700 Subject: [PATCH 30/66] style: Align text and button in user input box --- webview-ui/src/components/chat/ChatRow.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 5efc698..871e6e2 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -567,7 +567,7 @@ export const ChatRowContent = ({ style={{ display: "flex", justifyContent: "space-between", - alignItems: "flex-start", + alignItems: "center", gap: "10px", }}> {highlightMentions(message.text)} @@ -577,7 +577,8 @@ export const ChatRowContent = ({ padding: "3px", flexShrink: 0, height: "24px", - marginTop: "-6px", + marginTop: "-3px", + marginBottom: "-3px", marginRight: "-6px", }} disabled={isStreaming} From 5d5e69fe21d17b3b6763c0416d4c1b48cf035cc9 Mon Sep 17 00:00:00 2001 From: Tom X Nguyen Date: Wed, 22 Jan 2025 19:14:19 +0700 Subject: [PATCH 31/66] chore: add gemini flash thinking 01-21 --- src/shared/api.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shared/api.ts b/src/shared/api.ts index 8d7b919..01dce34 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -414,6 +414,14 @@ export const openAiModelInfoSaneDefaults: ModelInfo = { export type GeminiModelId = keyof typeof geminiModels export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-thinking-exp-1219" export const geminiModels = { + "gemini-2.0-flash-thinking-exp-01-21": { + maxTokens: 8192, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, "gemini-2.0-flash-thinking-exp-1219": { maxTokens: 8192, contextWindow: 32_767, From 8ad904a13cdbf49ed5071703b9c850e58874fd00 Mon Sep 17 00:00:00 2001 From: Tom X Nguyen Date: Wed, 22 Jan 2025 20:24:03 +0700 Subject: [PATCH 32/66] fix: avoid deleting configs if the currentApiConfigName hasn't been changed --- webview-ui/src/components/settings/ApiConfigManager.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx index 0a60f60..87c779b 100644 --- a/webview-ui/src/components/settings/ApiConfigManager.tsx +++ b/webview-ui/src/components/settings/ApiConfigManager.tsx @@ -58,7 +58,9 @@ const ApiConfigManager = ({ if (editState === "new") { onUpsertConfig(trimmedValue) } else if (editState === "rename" && currentApiConfigName) { - onRenameConfig(currentApiConfigName, trimmedValue) + if (currentApiConfigName !== trimmedValue) { + onRenameConfig(currentApiConfigName, trimmedValue) + } } setEditState(null) From 93a571038f3335d92e918418a004dfe552ea357f Mon Sep 17 00:00:00 2001 From: Tom X Nguyen Date: Wed, 22 Jan 2025 22:35:15 +0700 Subject: [PATCH 33/66] fix: update output length to 65536 --- src/shared/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/api.ts b/src/shared/api.ts index 01dce34..cdbe5b7 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -415,7 +415,7 @@ export type GeminiModelId = keyof typeof geminiModels export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-thinking-exp-1219" export const geminiModels = { "gemini-2.0-flash-thinking-exp-01-21": { - maxTokens: 8192, + maxTokens: 65_536, contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: false, From 4b24564087c57fc51ce76542d952f753208bd26f Mon Sep 17 00:00:00 2001 From: Tom X Nguyen Date: Wed, 22 Jan 2025 23:01:52 +0700 Subject: [PATCH 34/66] style: remove double scroll bar through unsetting overflow --- webview-ui/src/index.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index ad2d8b5..7995b7d 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -119,6 +119,11 @@ https://github.com/microsoft/vscode-webview-ui-toolkit/tree/main/src/dropdown#wi margin-bottom: 2px; } +/* Fix dropdown double scrollbar overflow */ +#api-provider > div > ul { + overflow: unset; +} + /* Fix scrollbar in dropdown */ vscode-dropdown::part(listbox) { From ece63103c7b8d8041c1502ed820786c05e788439 Mon Sep 17 00:00:00 2001 From: Tom X Nguyen Date: Wed, 22 Jan 2025 23:02:32 +0700 Subject: [PATCH 35/66] style: align chatrow through padding and keep flex-start for the trash --- webview-ui/src/components/chat/ChatRow.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 871e6e2..74a6772 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -567,10 +567,12 @@ export const ChatRowContent = ({ style={{ display: "flex", justifyContent: "space-between", - alignItems: "center", + alignItems: "flex-start", gap: "10px", }}> - {highlightMentions(message.text)} + + {highlightMentions(message.text)} + Date: Wed, 22 Jan 2025 08:09:11 -0800 Subject: [PATCH 36/66] v3.2.5 --- .changeset/tender-tables-remain.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tender-tables-remain.md diff --git a/.changeset/tender-tables-remain.md b/.changeset/tender-tables-remain.md new file mode 100644 index 0000000..cbdf6d2 --- /dev/null +++ b/.changeset/tender-tables-remain.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Added gemini flash thinking 01-21 model and a few visual fixes (thanks @monotykamary!) From 82a6d13c207d7df37ac7c6c0f0115b2d5b751427 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Wed, 22 Jan 2025 08:10:12 -0800 Subject: [PATCH 37/66] Change default --- src/api/providers/__tests__/gemini.test.ts | 2 +- src/shared/api.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/providers/__tests__/gemini.test.ts b/src/api/providers/__tests__/gemini.test.ts index a8a4eec..e8a5594 100644 --- a/src/api/providers/__tests__/gemini.test.ts +++ b/src/api/providers/__tests__/gemini.test.ts @@ -204,7 +204,7 @@ describe("GeminiHandler", () => { geminiApiKey: "test-key", }) const modelInfo = invalidHandler.getModel() - expect(modelInfo.id).toBe("gemini-2.0-flash-thinking-exp-1219") // Default model + expect(modelInfo.id).toBe("gemini-2.0-flash-thinking-exp-01-21") // Default model }) }) }) diff --git a/src/shared/api.ts b/src/shared/api.ts index cdbe5b7..860eb36 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -412,7 +412,7 @@ export const openAiModelInfoSaneDefaults: ModelInfo = { // Gemini // https://ai.google.dev/gemini-api/docs/models/gemini export type GeminiModelId = keyof typeof geminiModels -export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-thinking-exp-1219" +export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-thinking-exp-01-21" export const geminiModels = { "gemini-2.0-flash-thinking-exp-01-21": { maxTokens: 65_536, From 212f60970981dfd5b70fd5420e78348319f9bb9f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Jan 2025 16:22:09 +0000 Subject: [PATCH 38/66] changeset version bump --- .changeset/tender-tables-remain.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/tender-tables-remain.md diff --git a/.changeset/tender-tables-remain.md b/.changeset/tender-tables-remain.md deleted file mode 100644 index cbdf6d2..0000000 --- a/.changeset/tender-tables-remain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Added gemini flash thinking 01-21 model and a few visual fixes (thanks @monotykamary!) diff --git a/CHANGELOG.md b/CHANGELOG.md index a84b14c..e21ad20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Roo Code Changelog +## 3.2.5 + +### Patch Changes + +- Added gemini flash thinking 01-21 model and a few visual fixes (thanks @monotykamary!) + ## [3.2.4] - Only allow use of the diff tool if it's enabled in settings diff --git a/package-lock.json b/package-lock.json index a6eb8cd..bbc4754 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.2.4", + "version": "3.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.2.4", + "version": "3.2.5", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index 026371a..fbb532a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Code (prev. Roo Cline)", "description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.", "publisher": "RooVeterinaryInc", - "version": "3.2.4", + "version": "3.2.5", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", From 6e17442ed8edb9429975b02e923072c044d1106b Mon Sep 17 00:00:00 2001 From: R00-B0T Date: Wed, 22 Jan 2025 16:22:53 +0000 Subject: [PATCH 39/66] Updating CHANGELOG.md format --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e21ad20..6d32a0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ # Roo Code Changelog -## 3.2.5 - -### Patch Changes +## [3.2.5] - Added gemini flash thinking 01-21 model and a few visual fixes (thanks @monotykamary!) From 8e48b734a5e260158be3e45a1cb5b2ee4dc3dc8b Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Wed, 22 Jan 2025 08:42:01 -0800 Subject: [PATCH 40/66] Fix bug with role definition overrides for built-in modes --- .changeset/purple-grapes-destroy.md | 5 +++ src/core/prompts/__tests__/system.test.ts | 50 +++++++++++++++++++++++ src/core/prompts/system.ts | 2 +- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .changeset/purple-grapes-destroy.md diff --git a/.changeset/purple-grapes-destroy.md b/.changeset/purple-grapes-destroy.md new file mode 100644 index 0000000..ace0241 --- /dev/null +++ b/.changeset/purple-grapes-destroy.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Fix bug with role definition overrides for built-in modes diff --git a/src/core/prompts/__tests__/system.test.ts b/src/core/prompts/__tests__/system.test.ts index d22f834..6ecf7ef 100644 --- a/src/core/prompts/__tests__/system.test.ts +++ b/src/core/prompts/__tests__/system.test.ts @@ -350,6 +350,56 @@ describe("SYSTEM_PROMPT", () => { expect(customInstructionsIndex).toBeGreaterThan(userInstructionsHeader) }) + it("should use promptComponent roleDefinition when available", async () => { + const customPrompts = { + [defaultModeSlug]: { + roleDefinition: "Custom prompt role definition", + customInstructions: "Custom prompt instructions", + }, + } + + const prompt = await SYSTEM_PROMPT( + mockContext, + "/test/path", + false, + undefined, + undefined, + undefined, + defaultModeSlug, + customPrompts, + undefined, + ) + + // Role definition from promptComponent should be at the top + expect(prompt.indexOf("Custom prompt role definition")).toBeLessThan(prompt.indexOf("TOOL USE")) + // Should not contain the default mode's role definition + expect(prompt).not.toContain(modes[0].roleDefinition) + }) + + it("should fallback to modeConfig roleDefinition when promptComponent has no roleDefinition", async () => { + const customPrompts = { + [defaultModeSlug]: { + customInstructions: "Custom prompt instructions", + // No roleDefinition provided + }, + } + + const prompt = await SYSTEM_PROMPT( + mockContext, + "/test/path", + false, + undefined, + undefined, + undefined, + defaultModeSlug, + customPrompts, + undefined, + ) + + // Should use the default mode's role definition + expect(prompt.indexOf(modes[0].roleDefinition)).toBeLessThan(prompt.indexOf("TOOL USE")) + }) + afterAll(() => { jest.restoreAllMocks() }) diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 017e1e9..2546adc 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -54,7 +54,7 @@ async function generatePrompt( // 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 roleDefinition = promptComponent?.roleDefinition || modeConfig.roleDefinition const basePrompt = `${roleDefinition} From 28df0ab0040e22c48f1d8a825ba2fc3e949195b5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Jan 2025 16:48:01 +0000 Subject: [PATCH 41/66] changeset version bump --- .changeset/purple-grapes-destroy.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/purple-grapes-destroy.md diff --git a/.changeset/purple-grapes-destroy.md b/.changeset/purple-grapes-destroy.md deleted file mode 100644 index ace0241..0000000 --- a/.changeset/purple-grapes-destroy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Fix bug with role definition overrides for built-in modes diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d32a0a..9a72556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Roo Code Changelog +## 3.2.6 + +### Patch Changes + +- Fix bug with role definition overrides for built-in modes + ## [3.2.5] - Added gemini flash thinking 01-21 model and a few visual fixes (thanks @monotykamary!) diff --git a/package-lock.json b/package-lock.json index bbc4754..565bdac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.2.5", + "version": "3.2.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.2.5", + "version": "3.2.6", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index fbb532a..88ad20b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Code (prev. Roo Cline)", "description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.", "publisher": "RooVeterinaryInc", - "version": "3.2.5", + "version": "3.2.6", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", From 447400fecc08a3fa4698a71cfe0dd326a6ac1d90 Mon Sep 17 00:00:00 2001 From: R00-B0T Date: Wed, 22 Jan 2025 16:48:43 +0000 Subject: [PATCH 42/66] Updating CHANGELOG.md format --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a72556..f359c41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ # Roo Code Changelog -## 3.2.6 - -### Patch Changes +## [3.2.6] - Fix bug with role definition overrides for built-in modes From fc03237a4ff16a2b5d3d988b022e1536b467f351 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Wed, 22 Jan 2025 12:56:54 -0800 Subject: [PATCH 43/66] Revert "fix: avoid deleting configs if the currentApiConfigName hasn't been changed" This reverts commit 8ad904a13cdbf49ed5071703b9c850e58874fd00. --- webview-ui/src/components/settings/ApiConfigManager.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx index 87c779b..0a60f60 100644 --- a/webview-ui/src/components/settings/ApiConfigManager.tsx +++ b/webview-ui/src/components/settings/ApiConfigManager.tsx @@ -58,9 +58,7 @@ const ApiConfigManager = ({ if (editState === "new") { onUpsertConfig(trimmedValue) } else if (editState === "rename" && currentApiConfigName) { - if (currentApiConfigName !== trimmedValue) { - onRenameConfig(currentApiConfigName, trimmedValue) - } + onRenameConfig(currentApiConfigName, trimmedValue) } setEditState(null) From 8e6b38870d02e627f578f80371cd27de03046e5c Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Wed, 22 Jan 2025 12:58:55 -0800 Subject: [PATCH 44/66] Release --- .changeset/dull-numbers-run.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dull-numbers-run.md diff --git a/.changeset/dull-numbers-run.md b/.changeset/dull-numbers-run.md new file mode 100644 index 0000000..c3b76e5 --- /dev/null +++ b/.changeset/dull-numbers-run.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Fix bug creating new configuration profiles From 0cfb388382bbb40fcee224acdd9db24678bc385b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Jan 2025 21:03:04 +0000 Subject: [PATCH 45/66] changeset version bump --- .changeset/dull-numbers-run.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/dull-numbers-run.md diff --git a/.changeset/dull-numbers-run.md b/.changeset/dull-numbers-run.md deleted file mode 100644 index c3b76e5..0000000 --- a/.changeset/dull-numbers-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Fix bug creating new configuration profiles diff --git a/CHANGELOG.md b/CHANGELOG.md index f359c41..12011b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Roo Code Changelog +## 3.2.7 + +### Patch Changes + +- Fix bug creating new configuration profiles + ## [3.2.6] - Fix bug with role definition overrides for built-in modes diff --git a/package-lock.json b/package-lock.json index 565bdac..ee1cbf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.2.6", + "version": "3.2.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.2.6", + "version": "3.2.7", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index 88ad20b..6ae6c86 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Code (prev. Roo Cline)", "description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.", "publisher": "RooVeterinaryInc", - "version": "3.2.6", + "version": "3.2.7", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", From 4a39eaf40190d0e00407a750c48262571a59e0ca Mon Sep 17 00:00:00 2001 From: R00-B0T Date: Wed, 22 Jan 2025 21:03:46 +0000 Subject: [PATCH 46/66] Updating CHANGELOG.md format --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12011b7..2643148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ # Roo Code Changelog -## 3.2.7 - -### Patch Changes +## [3.2.7] - Fix bug creating new configuration profiles From 576c92ff4c14ac0c12c64a893b9f28c6d39711e6 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 22 Jan 2025 16:17:31 -0600 Subject: [PATCH 47/66] feat: poll usage --- src/api/providers/glama.ts | 44 ++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/api/providers/glama.ts b/src/api/providers/glama.ts index c27161a..a2025ea 100644 --- a/src/api/providers/glama.ts +++ b/src/api/providers/glama.ts @@ -110,26 +110,38 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler { } try { - const response = await axios.get( - `https://glama.ai/api/gateway/v1/completion-requests/${completionRequestId}`, - { - headers: { - Authorization: `Bearer ${this.options.glamaApiKey}`, + let attempt = 0 + + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + + while (attempt++ < 10) { + // In case of an interrupted request, we need to wait for the upstream API to finish processing the request + // before we can fetch information about the token usage and cost. + const response = await axios.get( + `https://glama.ai/api/gateway/v1/completion-requests/${completionRequestId}`, + { + headers: { + Authorization: `Bearer ${this.options.glamaApiKey}`, + }, }, - }, - ) + ) - const completionRequest = response.data + const completionRequest = response.data - if (completionRequest.tokenUsage) { - yield { - type: "usage", - cacheWriteTokens: completionRequest.tokenUsage.cacheCreationInputTokens, - cacheReadTokens: completionRequest.tokenUsage.cacheReadInputTokens, - inputTokens: completionRequest.tokenUsage.promptTokens, - outputTokens: completionRequest.tokenUsage.completionTokens, - totalCost: parseFloat(completionRequest.totalCostUsd), + if (completionRequest.tokenUsage && completionRequest.totalCostUsd) { + yield { + type: "usage", + cacheWriteTokens: completionRequest.tokenUsage.cacheCreationInputTokens, + cacheReadTokens: completionRequest.tokenUsage.cacheReadInputTokens, + inputTokens: completionRequest.tokenUsage.promptTokens, + outputTokens: completionRequest.tokenUsage.completionTokens, + totalCost: parseFloat(completionRequest.totalCostUsd), + } + + break } + + await delay(200) } } catch (error) { console.error("Error fetching Glama completion details", error) From 2b059c6d15711a27a2bdc258b57444fe1d329a41 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 23 Jan 2025 08:14:15 -0800 Subject: [PATCH 48/66] Fix button to open custom modes settings --- .changeset/happy-camels-rhyme.md | 5 +++++ src/core/webview/ClineProvider.ts | 7 +++++++ src/shared/WebviewMessage.ts | 1 + webview-ui/src/components/prompts/PromptsView.tsx | 3 +-- 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 .changeset/happy-camels-rhyme.md diff --git a/.changeset/happy-camels-rhyme.md b/.changeset/happy-camels-rhyme.md new file mode 100644 index 0000000..ddd3763 --- /dev/null +++ b/.changeset/happy-camels-rhyme.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Fix button to open custom modes settings diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index fada031..af4bc71 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -655,6 +655,13 @@ export class ClineProvider implements vscode.WebviewViewProvider { } break } + case "openCustomModesSettings": { + const customModesFilePath = await this.customModesManager.getCustomModesFilePath() + if (customModesFilePath) { + openFile(customModesFilePath) + } + break + } case "restartMcpServer": { try { await this.mcpHub?.restartConnection(message.text!) diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 5705378..b63b898 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -77,6 +77,7 @@ export interface WebviewMessage { | "updateCustomMode" | "deleteCustomMode" | "setopenAiCustomModelInfo" + | "openCustomModesSettings" text?: string disabled?: boolean askResponse?: ClineAskResponse diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index d4aef45..ebd4d0f 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -434,8 +434,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { title="Edit modes configuration" onClick={() => { vscode.postMessage({ - type: "openFile", - text: "settings/cline_custom_modes.json", + type: "openCustomModesSettings", }) }}> From f745f080f48caeca9eddbdd8debc94c235c9f76c Mon Sep 17 00:00:00 2001 From: sam hoang Date: Fri, 24 Jan 2025 00:14:55 +0700 Subject: [PATCH 49/66] feat: add explicit Azure OpenAI flag and setup memory bank docs - Add openAiUseAzure flag to force Azure OpenAI client initialization - Add "Use Azure" checkbox in API settings UI This change improves Azure OpenAI configuration flexibility by allowing users to explicitly opt-in to Azure client, regardless of the base URL pattern. --- src/api/providers/openai.ts | 2 +- src/core/webview/ClineProvider.ts | 6 ++++++ src/shared/api.ts | 1 + webview-ui/src/components/settings/ApiOptions.tsx | 9 +++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index d71a51f..c5e3ae9 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -18,7 +18,7 @@ export class OpenAiHandler implements ApiHandler, SingleCompletionHandler { this.options = options // Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai const urlHost = new URL(this.options.openAiBaseUrl ?? "").host - if (urlHost === "azure.com" || urlHost.endsWith(".azure.com")) { + if (urlHost === "azure.com" || urlHost.endsWith(".azure.com") || options.openAiUseAzure) { this.client = new AzureOpenAI({ baseURL: this.options.openAiBaseUrl, apiKey: this.options.openAiApiKey, diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index fada031..473df3b 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -78,6 +78,7 @@ type GlobalStateKey = | "openAiBaseUrl" | "openAiModelId" | "openAiCustomModelInfo" + | "openAiUseAzure" | "ollamaModelId" | "ollamaBaseUrl" | "lmStudioModelId" @@ -1210,6 +1211,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiApiKey, openAiModelId, openAiCustomModelInfo, + openAiUseAzure, ollamaModelId, ollamaBaseUrl, lmStudioModelId, @@ -1244,6 +1246,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.storeSecret("openAiApiKey", openAiApiKey) await this.updateGlobalState("openAiModelId", openAiModelId) await this.updateGlobalState("openAiCustomModelInfo", openAiCustomModelInfo) + await this.updateGlobalState("openAiUseAzure", openAiUseAzure) await this.updateGlobalState("ollamaModelId", ollamaModelId) await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl) await this.updateGlobalState("lmStudioModelId", lmStudioModelId) @@ -1861,6 +1864,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiApiKey, openAiModelId, openAiCustomModelInfo, + openAiUseAzure, ollamaModelId, ollamaBaseUrl, lmStudioModelId, @@ -1925,6 +1929,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getSecret("openAiApiKey") as Promise, this.getGlobalState("openAiModelId") as Promise, this.getGlobalState("openAiCustomModelInfo") as Promise, + this.getGlobalState("openAiUseAzure") as Promise, this.getGlobalState("ollamaModelId") as Promise, this.getGlobalState("ollamaBaseUrl") as Promise, this.getGlobalState("lmStudioModelId") as Promise, @@ -2006,6 +2011,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiApiKey, openAiModelId, openAiCustomModelInfo, + openAiUseAzure, ollamaModelId, ollamaBaseUrl, lmStudioModelId, diff --git a/src/shared/api.ts b/src/shared/api.ts index 860eb36..72d6f17 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -39,6 +39,7 @@ export interface ApiHandlerOptions { openAiApiKey?: string openAiModelId?: string openAiCustomModelInfo?: ModelInfo + openAiUseAzure?: boolean ollamaModelId?: string ollamaBaseUrl?: string lmStudioModelId?: string diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 13e7a57..9a84b07 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -536,6 +536,15 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = Enable streaming
+ { + handleInputChange("openAiUseAzure")({ + target: { value: checked }, + }) + }}> + Use Azure + { From 0c81f427cb24dc6534d1ac0a3c620a9881112c2d Mon Sep 17 00:00:00 2001 From: sam hoang Date: Fri, 24 Jan 2025 00:40:53 +0700 Subject: [PATCH 50/66] Revert onChange back to onInput --- .../src/components/settings/ApiOptions.tsx | 38 +++++++++---------- .../components/settings/GlamaModelPicker.tsx | 11 +----- .../components/settings/OpenAiModelPicker.tsx | 11 +----- .../settings/OpenRouterModelPicker.tsx | 11 +----- 4 files changed, 22 insertions(+), 49 deletions(-) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 9a84b07..4557ffd 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -156,7 +156,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.apiKey || ""} style={{ width: "100%" }} type="password" - onChange={handleInputChange("apiKey")} + onInput={handleInputChange("apiKey")} placeholder="Enter API Key..."> Anthropic API Key @@ -181,7 +181,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.anthropicBaseUrl || ""} style={{ width: "100%", marginTop: 3 }} type="url" - onChange={handleInputChange("anthropicBaseUrl")} + onInput={handleInputChange("anthropicBaseUrl")} placeholder="Default: https://api.anthropic.com" /> )} @@ -210,7 +210,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.glamaApiKey || ""} style={{ width: "100%" }} type="password" - onChange={handleInputChange("glamaApiKey")} + onInput={handleInputChange("glamaApiKey")} placeholder="Enter API Key..."> Glama API Key @@ -239,7 +239,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.openAiNativeApiKey || ""} style={{ width: "100%" }} type="password" - onChange={handleInputChange("openAiNativeApiKey")} + onInput={handleInputChange("openAiNativeApiKey")} placeholder="Enter API Key..."> OpenAI API Key @@ -267,7 +267,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.mistralApiKey || ""} style={{ width: "100%" }} type="password" - onChange={handleInputChange("mistralApiKey")} + onInput={handleInputChange("mistralApiKey")} placeholder="Enter API Key..."> Mistral API Key @@ -298,7 +298,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.openRouterApiKey || ""} style={{ width: "100%" }} type="password" - onChange={handleInputChange("openRouterApiKey")} + onInput={handleInputChange("openRouterApiKey")} placeholder="Enter API Key..."> OpenRouter API Key @@ -344,7 +344,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.awsAccessKey || ""} style={{ width: "100%" }} type="password" - onChange={handleInputChange("awsAccessKey")} + onInput={handleInputChange("awsAccessKey")} placeholder="Enter Access Key..."> AWS Access Key @@ -352,7 +352,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.awsSecretKey || ""} style={{ width: "100%" }} type="password" - onChange={handleInputChange("awsSecretKey")} + onInput={handleInputChange("awsSecretKey")} placeholder="Enter Secret Key..."> AWS Secret Key @@ -360,7 +360,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.awsSessionToken || ""} style={{ width: "100%" }} type="password" - onChange={handleInputChange("awsSessionToken")} + onInput={handleInputChange("awsSessionToken")} placeholder="Enter Session Token..."> AWS Session Token @@ -426,7 +426,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = Google Cloud Project ID @@ -484,7 +484,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.geminiApiKey || ""} style={{ width: "100%" }} type="password" - onChange={handleInputChange("geminiApiKey")} + onInput={handleInputChange("geminiApiKey")} placeholder="Enter API Key..."> Gemini API Key @@ -512,7 +512,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.openAiBaseUrl || ""} style={{ width: "100%" }} type="url" - onChange={handleInputChange("openAiBaseUrl")} + onInput={handleInputChange("openAiBaseUrl")} placeholder={"Enter base URL..."}> Base URL @@ -520,7 +520,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.openAiApiKey || ""} style={{ width: "100%" }} type="password" - onChange={handleInputChange("openAiApiKey")} + onInput={handleInputChange("openAiApiKey")} placeholder="Enter API Key..."> API Key @@ -563,7 +563,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = )} @@ -1013,14 +1013,14 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.lmStudioBaseUrl || ""} style={{ width: "100%" }} type="url" - onChange={handleInputChange("lmStudioBaseUrl")} + onInput={handleInputChange("lmStudioBaseUrl")} placeholder={"Default: http://localhost:1234"}> Base URL (optional) Model ID @@ -1082,7 +1082,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.deepSeekApiKey || ""} style={{ width: "100%" }} type="password" - onChange={handleInputChange("deepSeekApiKey")} + onInput={handleInputChange("deepSeekApiKey")} placeholder="Enter API Key..."> DeepSeek API Key @@ -1172,14 +1172,14 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = value={apiConfiguration?.ollamaBaseUrl || ""} style={{ width: "100%" }} type="url" - onChange={handleInputChange("ollamaBaseUrl")} + onInput={handleInputChange("ollamaBaseUrl")} placeholder={"Default: http://localhost:11434"}> Base URL (optional) Model ID diff --git a/webview-ui/src/components/settings/GlamaModelPicker.tsx b/webview-ui/src/components/settings/GlamaModelPicker.tsx index 24a11ae..07d75be 100644 --- a/webview-ui/src/components/settings/GlamaModelPicker.tsx +++ b/webview-ui/src/components/settings/GlamaModelPicker.tsx @@ -167,17 +167,8 @@ const GlamaModelPicker: React.FC = () => { placeholder="Search and select a model..." value={searchTerm} onInput={(e) => { - const newModelId = (e.target as HTMLInputElement)?.value?.toLowerCase() - const apiConfig = { - ...apiConfiguration, - openAiModelId: newModelId, - } - setApiConfiguration(apiConfig) - setSearchTerm(newModelId) - setIsDropdownVisible(true) - }} - onChange={(e) => { handleModelChange((e.target as HTMLInputElement)?.value?.toLowerCase()) + setIsDropdownVisible(true) }} onFocus={() => setIsDropdownVisible(true)} onKeyDown={handleKeyDown} diff --git a/webview-ui/src/components/settings/OpenAiModelPicker.tsx b/webview-ui/src/components/settings/OpenAiModelPicker.tsx index 9455f2a..05cd000 100644 --- a/webview-ui/src/components/settings/OpenAiModelPicker.tsx +++ b/webview-ui/src/components/settings/OpenAiModelPicker.tsx @@ -159,17 +159,8 @@ const OpenAiModelPicker: React.FC = () => { placeholder="Search and select a model..." value={searchTerm} onInput={(e) => { - const newModelId = (e.target as HTMLInputElement)?.value?.toLowerCase() - const apiConfig = { - ...apiConfiguration, - openAiModelId: newModelId, - } - setApiConfiguration(apiConfig) - setSearchTerm(newModelId) - setIsDropdownVisible(true) - }} - onChange={(e) => { handleModelChange((e.target as HTMLInputElement)?.value?.toLowerCase()) + setIsDropdownVisible(true) }} onFocus={() => setIsDropdownVisible(true)} onKeyDown={handleKeyDown} diff --git a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx index ed4594a..a1761cd 100644 --- a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx +++ b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx @@ -167,17 +167,8 @@ const OpenRouterModelPicker: React.FC = () => { placeholder="Search and select a model..." value={searchTerm} onInput={(e) => { - const newModelId = (e.target as HTMLInputElement)?.value?.toLowerCase() - const apiConfig = { - ...apiConfiguration, - openAiModelId: newModelId, - } - setApiConfiguration(apiConfig) - setSearchTerm(newModelId) - setIsDropdownVisible(true) - }} - onChange={(e) => { handleModelChange((e.target as HTMLInputElement)?.value?.toLowerCase()) + setIsDropdownVisible(true) }} onFocus={() => setIsDropdownVisible(true)} onKeyDown={handleKeyDown} From 7c875f1fea6e0e4406aefd75aa4c569cccbdf316 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Sun, 12 Jan 2025 17:51:00 +0700 Subject: [PATCH 51/66] feat: add code action prompt handlers for explain, fix and improve code --- .../prompts/__tests__/code-actions.test.ts | 63 +++++++++++++++++++ src/core/prompts/code-actions.ts | 50 +++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 src/core/prompts/__tests__/code-actions.test.ts create mode 100644 src/core/prompts/code-actions.ts diff --git a/src/core/prompts/__tests__/code-actions.test.ts b/src/core/prompts/__tests__/code-actions.test.ts new file mode 100644 index 0000000..ad4c281 --- /dev/null +++ b/src/core/prompts/__tests__/code-actions.test.ts @@ -0,0 +1,63 @@ +import { explainCodePrompt, fixCodePrompt, improveCodePrompt } from '../code-actions'; + +describe('Code Action Prompts', () => { + const testFilePath = 'test/file.ts'; + const testCode = 'function test() { return true; }'; + + describe('explainCodePrompt', () => { + it('should format explain prompt correctly', () => { + const prompt = explainCodePrompt(testFilePath, testCode); + + expect(prompt).toContain(`@/${testFilePath}`); + expect(prompt).toContain(testCode); + expect(prompt).toContain('purpose and functionality'); + expect(prompt).toContain('Key components'); + expect(prompt).toContain('Important patterns'); + }); + }); + + describe('fixCodePrompt', () => { + it('should format fix prompt without diagnostics', () => { + const prompt = fixCodePrompt(testFilePath, testCode); + + expect(prompt).toContain(`@/${testFilePath}`); + expect(prompt).toContain(testCode); + expect(prompt).toContain('Address all detected problems'); + expect(prompt).not.toContain('Current problems detected'); + }); + + it('should format fix prompt with diagnostics', () => { + const diagnostics = [ + { + source: 'eslint', + message: 'Missing semicolon', + code: 'semi' + }, + { + message: 'Unused variable', + severity: 1 + } + ]; + + const prompt = fixCodePrompt(testFilePath, testCode, diagnostics); + + expect(prompt).toContain('Current problems detected:'); + expect(prompt).toContain('[eslint] Missing semicolon (semi)'); + expect(prompt).toContain('[Error] Unused variable'); + expect(prompt).toContain(testCode); + }); + }); + + describe('improveCodePrompt', () => { + it('should format improve prompt correctly', () => { + const prompt = improveCodePrompt(testFilePath, testCode); + + expect(prompt).toContain(`@/${testFilePath}`); + expect(prompt).toContain(testCode); + expect(prompt).toContain('Code readability'); + expect(prompt).toContain('Performance optimization'); + expect(prompt).toContain('Best practices'); + expect(prompt).toContain('Error handling'); + }); + }); +}); \ No newline at end of file diff --git a/src/core/prompts/code-actions.ts b/src/core/prompts/code-actions.ts new file mode 100644 index 0000000..3fd03ae --- /dev/null +++ b/src/core/prompts/code-actions.ts @@ -0,0 +1,50 @@ +export const explainCodePrompt = (filePath: string, selectedText: string) => ` +Explain the following code from file path @/${filePath}: + +\`\`\` +${selectedText} +\`\`\` + +Please provide a clear and concise explanation of what this code does, including: +1. The purpose and functionality +2. Key components and their interactions +3. Important patterns or techniques used +`; + +export const fixCodePrompt = (filePath: string, selectedText: string, diagnostics?: any[]) => { + const diagnosticText = diagnostics && diagnostics.length > 0 + ? `\nCurrent problems detected: +${diagnostics.map(d => `- [${d.source || 'Error'}] ${d.message}${d.code ? ` (${d.code})` : ''}`).join('\n')}` + : ''; + + return ` +Fix any issues in the following code from file path @/${filePath} +${diagnosticText} + +\`\`\` +${selectedText} +\`\`\` + +Please: +1. Address all detected problems listed above (if any) +2. Identify any other potential bugs or issues +3. Provide corrected code +4. Explain what was fixed and why +`; +}; + +export const improveCodePrompt = (filePath: string, selectedText: string) => ` +Improve the following code from file path @/${filePath}: + +\`\`\` +${selectedText} +\`\`\` + +Please suggest improvements for: +1. Code readability and maintainability +2. Performance optimization +3. Best practices and patterns +4. Error handling and edge cases + +Provide the improved code along with explanations for each enhancement. +`; \ No newline at end of file From 86b051df3509f906e9bbf51f45da061e2c0cbeb2 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Sun, 12 Jan 2025 17:51:15 +0700 Subject: [PATCH 52/66] feat: implement code action provider for VS Code integration --- src/core/CodeActionProvider.ts | 181 ++++++++++++++++++ src/core/__tests__/CodeActionProvider.test.ts | 145 ++++++++++++++ 2 files changed, 326 insertions(+) create mode 100644 src/core/CodeActionProvider.ts create mode 100644 src/core/__tests__/CodeActionProvider.test.ts diff --git a/src/core/CodeActionProvider.ts b/src/core/CodeActionProvider.ts new file mode 100644 index 0000000..11bafdb --- /dev/null +++ b/src/core/CodeActionProvider.ts @@ -0,0 +1,181 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; + +const ACTION_NAMES = { + EXPLAIN: 'Roo Cline: Explain Code', + FIX: 'Roo Cline: Fix Code', + IMPROVE: 'Roo Cline: Improve Code' +} as const; + +const COMMAND_IDS = { + EXPLAIN: 'roo-cline.explainCode', + FIX: 'roo-cline.fixCode', + IMPROVE: 'roo-cline.improveCode' +} as const; + +interface DiagnosticData { + message: string; + severity: vscode.DiagnosticSeverity; + code?: string | number | { value: string | number; target: vscode.Uri }; + source?: string; + range: vscode.Range; +} + +interface EffectiveRange { + range: vscode.Range; + text: string; +} + +export class CodeActionProvider implements vscode.CodeActionProvider { + public static readonly providedCodeActionKinds = [ + vscode.CodeActionKind.QuickFix, + vscode.CodeActionKind.RefactorRewrite, + ]; + + // Cache file paths for performance + private readonly filePathCache = new WeakMap(); + + private getEffectiveRange( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection + ): EffectiveRange | null { + try { + const selectedText = document.getText(range); + if (selectedText) { + return { range, text: selectedText }; + } + + const currentLine = document.lineAt(range.start.line); + if (!currentLine.text.trim()) { + return null; + } + + // Optimize range creation by checking bounds first + const startLine = Math.max(0, currentLine.lineNumber - 1); + const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1); + + // Only create new positions if needed + const effectiveRange = new vscode.Range( + startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0), + endLine === currentLine.lineNumber ? range.end : new vscode.Position(endLine, document.lineAt(endLine).text.length) + ); + + return { + range: effectiveRange, + text: document.getText(effectiveRange) + }; + } catch (error) { + console.error('Error getting effective range:', error); + return null; + } + } + + private getFilePath(document: vscode.TextDocument): string { + // Check cache first + let filePath = this.filePathCache.get(document); + if (filePath) { + return filePath; + } + + try { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); + if (!workspaceFolder) { + filePath = document.uri.fsPath; + } else { + const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath); + filePath = (!relativePath || relativePath.startsWith('..')) ? document.uri.fsPath : relativePath; + } + + // Cache the result + this.filePathCache.set(document, filePath); + return filePath; + } catch (error) { + console.error('Error getting file path:', error); + return document.uri.fsPath; + } + } + + private createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData { + return { + message: diagnostic.message, + severity: diagnostic.severity, + code: diagnostic.code, + source: diagnostic.source, + range: diagnostic.range // Reuse the range object + }; + } + + private createAction( + title: string, + kind: vscode.CodeActionKind, + command: string, + args: any[] + ): vscode.CodeAction { + const action = new vscode.CodeAction(title, kind); + action.command = { command, title, arguments: args }; + return action; + } + + private hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean { + // Optimize range intersection check + return !( + range2.end.line < range1.start.line || + range2.start.line > range1.end.line || + (range2.end.line === range1.start.line && range2.end.character < range1.start.character) || + (range2.start.line === range1.end.line && range2.start.character > range1.end.character) + ); + } + + public provideCodeActions( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext + ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { + try { + const effectiveRange = this.getEffectiveRange(document, range); + if (!effectiveRange) { + return []; + } + + const filePath = this.getFilePath(document); + const actions: vscode.CodeAction[] = []; + + // Create actions using helper method + actions.push(this.createAction( + ACTION_NAMES.EXPLAIN, + vscode.CodeActionKind.QuickFix, + COMMAND_IDS.EXPLAIN, + [filePath, effectiveRange.text] + )); + + // Only process diagnostics if they exist + if (context.diagnostics.length > 0) { + const relevantDiagnostics = context.diagnostics.filter(d => + this.hasIntersectingRange(effectiveRange.range, d.range) + ); + + if (relevantDiagnostics.length > 0) { + const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData); + actions.push(this.createAction( + ACTION_NAMES.FIX, + vscode.CodeActionKind.QuickFix, + COMMAND_IDS.FIX, + [filePath, effectiveRange.text, diagnosticMessages] + )); + } + } + + actions.push(this.createAction( + ACTION_NAMES.IMPROVE, + vscode.CodeActionKind.RefactorRewrite, + COMMAND_IDS.IMPROVE, + [filePath, effectiveRange.text] + )); + + return actions; + } catch (error) { + console.error('Error providing code actions:', error); + return []; + } + } +} \ No newline at end of file diff --git a/src/core/__tests__/CodeActionProvider.test.ts b/src/core/__tests__/CodeActionProvider.test.ts new file mode 100644 index 0000000..cdc1acf --- /dev/null +++ b/src/core/__tests__/CodeActionProvider.test.ts @@ -0,0 +1,145 @@ +import * as vscode from 'vscode'; +import { CodeActionProvider } from '../CodeActionProvider'; + +// Mock VSCode API +jest.mock('vscode', () => ({ + CodeAction: jest.fn().mockImplementation((title, kind) => ({ + title, + kind, + command: undefined + })), + CodeActionKind: { + QuickFix: { value: 'quickfix' }, + RefactorRewrite: { value: 'refactor.rewrite' } + }, + Range: jest.fn().mockImplementation((startLine, startChar, endLine, endChar) => ({ + start: { line: startLine, character: startChar }, + end: { line: endLine, character: endChar } + })), + Position: jest.fn().mockImplementation((line, character) => ({ + line, + character + })), + workspace: { + getWorkspaceFolder: jest.fn() + }, + DiagnosticSeverity: { + Error: 0, + Warning: 1, + Information: 2, + Hint: 3 + } +})); + +describe('CodeActionProvider', () => { + let provider: CodeActionProvider; + let mockDocument: any; + let mockRange: any; + let mockContext: any; + + beforeEach(() => { + provider = new CodeActionProvider(); + + // Mock document + mockDocument = { + getText: jest.fn(), + lineAt: jest.fn(), + lineCount: 10, + uri: { fsPath: '/test/file.ts' } + }; + + // Mock range + mockRange = new vscode.Range(0, 0, 0, 10); + + // Mock context + mockContext = { + diagnostics: [] + }; + }); + + describe('getEffectiveRange', () => { + it('should return selected text when available', () => { + mockDocument.getText.mockReturnValue('selected text'); + + const result = (provider as any).getEffectiveRange(mockDocument, mockRange); + + expect(result).toEqual({ + range: mockRange, + text: 'selected text' + }); + }); + + it('should return null for empty line', () => { + mockDocument.getText.mockReturnValue(''); + mockDocument.lineAt.mockReturnValue({ text: '', lineNumber: 0 }); + + const result = (provider as any).getEffectiveRange(mockDocument, mockRange); + + expect(result).toBeNull(); + }); + }); + + describe('getFilePath', () => { + it('should return relative path when in workspace', () => { + const mockWorkspaceFolder = { + uri: { fsPath: '/test' } + }; + (vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder); + + const result = (provider as any).getFilePath(mockDocument); + + expect(result).toBe('file.ts'); + }); + + it('should return absolute path when not in workspace', () => { + (vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null); + + const result = (provider as any).getFilePath(mockDocument); + + expect(result).toBe('/test/file.ts'); + }); + }); + + describe('provideCodeActions', () => { + beforeEach(() => { + mockDocument.getText.mockReturnValue('test code'); + mockDocument.lineAt.mockReturnValue({ text: 'test code', lineNumber: 0 }); + }); + + it('should provide explain and improve actions by default', () => { + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext); + + expect(actions).toHaveLength(2); + expect((actions as any)[0].title).toBe('Roo Cline: Explain Code'); + expect((actions as any)[1].title).toBe('Roo Cline: Improve Code'); + }); + + it('should provide fix action when diagnostics exist', () => { + mockContext.diagnostics = [{ + message: 'test error', + severity: vscode.DiagnosticSeverity.Error, + range: mockRange + }]; + + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext); + + expect(actions).toHaveLength(3); + expect((actions as any).some((a: any) => a.title === 'Roo Cline: Fix Code')).toBe(true); + }); + + it('should handle errors gracefully', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + mockDocument.getText.mockImplementation(() => { + throw new Error('Test error'); + }); + mockDocument.lineAt.mockReturnValue({ text: 'test', lineNumber: 0 }); + + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext); + + expect(actions).toEqual([]); + expect(consoleErrorSpy).toHaveBeenCalledWith('Error getting effective range:', expect.any(Error)); + + consoleErrorSpy.mockRestore(); + }); + }); +}); \ No newline at end of file From 273bfc410bb4b6fcc009cb4f5b5cd3c090b33496 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Sun, 12 Jan 2025 17:51:31 +0700 Subject: [PATCH 53/66] feat: register new code action commands in package manifest --- package.json | 156 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 94 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index 6ae6c86..7715001 100644 --- a/package.json +++ b/package.json @@ -72,70 +72,102 @@ "title": "New Task", "icon": "$(add)" }, - { - "command": "roo-cline.mcpButtonClicked", - "title": "MCP Servers", - "icon": "$(server)" - }, - { - "command": "roo-cline.promptsButtonClicked", - "title": "Prompts", - "icon": "$(notebook)" - }, - { - "command": "roo-cline.historyButtonClicked", - "title": "History", - "icon": "$(history)" - }, - { - "command": "roo-cline.popoutButtonClicked", - "title": "Open in Editor", - "icon": "$(link-external)" - }, - { - "command": "roo-cline.settingsButtonClicked", - "title": "Settings", - "icon": "$(settings-gear)" - }, - { - "command": "roo-cline.openInNewTab", - "title": "Open In New Tab", - "category": "Roo Code" - } + { + "command": "roo-cline.mcpButtonClicked", + "title": "MCP Servers", + "icon": "$(server)" + }, + { + "command": "roo-cline.promptsButtonClicked", + "title": "Prompts", + "icon": "$(notebook)" + }, + { + "command": "roo-cline.historyButtonClicked", + "title": "History", + "icon": "$(history)" + }, + { + "command": "roo-cline.popoutButtonClicked", + "title": "Open in Editor", + "icon": "$(link-external)" + }, + { + "command": "roo-cline.settingsButtonClicked", + "title": "Settings", + "icon": "$(settings-gear)" + }, + { + "command": "roo-cline.openInNewTab", + "title": "Open In New Tab", + "category": "Roo Code" + }, + { + "command": "roo-cline.explainCode", + "title": "Explain Code", + "category": "Roo Cline" + }, + { + "command": "roo-cline.fixCode", + "title": "Fix Code", + "category": "Roo Cline" + }, + { + "command": "roo-cline.improveCode", + "title": "Improve Code", + "category": "Roo Cline" + } ], "menus": { - "view/title": [ - { - "command": "roo-cline.plusButtonClicked", - "group": "navigation@1", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.promptsButtonClicked", - "group": "navigation@2", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.mcpButtonClicked", - "group": "navigation@3", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.historyButtonClicked", - "group": "navigation@4", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.popoutButtonClicked", - "group": "navigation@5", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.settingsButtonClicked", - "group": "navigation@6", - "when": "view == roo-cline.SidebarProvider" - } - ] + "editor/context": [ + { + "command": "roo-cline.explainCode", + "when": "editorHasSelection", + "group": "Roo Cline@1" + }, + { + "command": "roo-cline.fixCode", + "when": "editorHasSelection", + "group": "Roo Cline@2" + }, + { + "command": "roo-cline.improveCode", + "when": "editorHasSelection", + "group": "Roo Cline@3" + } + ], + "view/title": [ + { + "command": "roo-cline.plusButtonClicked", + "group": "navigation@1", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.promptsButtonClicked", + "group": "navigation@2", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.mcpButtonClicked", + "group": "navigation@3", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.historyButtonClicked", + "group": "navigation@4", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.popoutButtonClicked", + "group": "navigation@5", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.settingsButtonClicked", + "group": "navigation@6", + "when": "view == roo-cline.SidebarProvider" + } + ] }, "configuration": { "title": "Roo Code", From 02a8eb96f12359bdc32f471bfb96ada489d99cd3 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Sun, 12 Jan 2025 17:51:46 +0700 Subject: [PATCH 54/66] feat: integrate code actions into extension activation --- src/extension.ts | 47 ++++++++++++++++++++++++++++++++++++++ src/test/extension.test.ts | 17 ++++++++------ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 0cf3053..e6cfde7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,6 +5,8 @@ import * as vscode from "vscode" import { ClineProvider } from "./core/webview/ClineProvider" import { createClineAPI } from "./exports" import "./utils/path" // necessary to have access to String.prototype.toPosix +import { CodeActionProvider } from "./core/CodeActionProvider" +import { explainCodePrompt, fixCodePrompt, improveCodePrompt } from "./core/prompts/code-actions" import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" /* @@ -158,6 +160,51 @@ export function activate(context: vscode.ExtensionContext) { } context.subscriptions.push(vscode.window.registerUriHandler({ handleUri })) + // Register code actions provider + context.subscriptions.push( + vscode.languages.registerCodeActionsProvider( + { pattern: "**/*" }, + new CodeActionProvider(), + { + providedCodeActionKinds: CodeActionProvider.providedCodeActionKinds + } + ) + ); + + // Register code action commands + context.subscriptions.push( + vscode.commands.registerCommand("roo-cline.explainCode", async (filePath: string, selectedText: string) => { + const visibleProvider = ClineProvider.getVisibleInstance() + if (!visibleProvider) { + return + } + const prompt = explainCodePrompt(filePath, selectedText) + await visibleProvider.initClineWithTask(prompt) + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand("roo-cline.fixCode", async (filePath: string, selectedText: string, diagnostics?: any[]) => { + const visibleProvider = ClineProvider.getVisibleInstance() + if (!visibleProvider) { + return + } + const prompt = fixCodePrompt(filePath, selectedText, diagnostics) + await visibleProvider.initClineWithTask(prompt) + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand("roo-cline.improveCode", async (filePath: string, selectedText: string) => { + const visibleProvider = ClineProvider.getVisibleInstance() + if (!visibleProvider) { + return + } + const prompt = improveCodePrompt(filePath, selectedText) + await visibleProvider.initClineWithTask(prompt) + }) + ); + return createClineAPI(outputChannel, sidebarProvider) } diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index c67b3db..a3237e2 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -117,13 +117,16 @@ suite("Roo Cline Extension Test Suite", () => { // Test core commands are registered const expectedCommands = [ - "roo-cline.plusButtonClicked", - "roo-cline.mcpButtonClicked", - "roo-cline.historyButtonClicked", - "roo-cline.popoutButtonClicked", - "roo-cline.settingsButtonClicked", - "roo-cline.openInNewTab", - ] + 'roo-cline.plusButtonClicked', + 'roo-cline.mcpButtonClicked', + 'roo-cline.historyButtonClicked', + 'roo-cline.popoutButtonClicked', + 'roo-cline.settingsButtonClicked', + 'roo-cline.openInNewTab', + 'roo-cline.explainCode', + 'roo-cline.fixCode', + 'roo-cline.improveCode' + ]; for (const cmd of expectedCommands) { assert.strictEqual(commands.includes(cmd), true, `Command ${cmd} should be registered`) From 1b26f91ea7378d11062aa5299d48686ea197b4bc Mon Sep 17 00:00:00 2001 From: sam hoang Date: Sun, 12 Jan 2025 21:20:02 +0700 Subject: [PATCH 55/66] refactor(code-actions): implement parameter object pattern for prompt generation - Extract prompt templates into constants - Add createPrompt utility for template string handling - Consolidate code action handling in ClineProvider - Update tests to use new parameter object pattern --- .../prompts/__tests__/code-actions.test.ts | 21 +++++-- src/core/prompts/code-actions.ts | 59 +++++++++++++------ src/core/webview/ClineProvider.ts | 14 +++++ src/extension.ts | 25 +++----- 4 files changed, 79 insertions(+), 40 deletions(-) diff --git a/src/core/prompts/__tests__/code-actions.test.ts b/src/core/prompts/__tests__/code-actions.test.ts index ad4c281..0ddb6bd 100644 --- a/src/core/prompts/__tests__/code-actions.test.ts +++ b/src/core/prompts/__tests__/code-actions.test.ts @@ -6,7 +6,10 @@ describe('Code Action Prompts', () => { describe('explainCodePrompt', () => { it('should format explain prompt correctly', () => { - const prompt = explainCodePrompt(testFilePath, testCode); + const prompt = explainCodePrompt({ + filePath: testFilePath, + selectedText: testCode + }); expect(prompt).toContain(`@/${testFilePath}`); expect(prompt).toContain(testCode); @@ -18,7 +21,10 @@ describe('Code Action Prompts', () => { describe('fixCodePrompt', () => { it('should format fix prompt without diagnostics', () => { - const prompt = fixCodePrompt(testFilePath, testCode); + const prompt = fixCodePrompt({ + filePath: testFilePath, + selectedText: testCode + }); expect(prompt).toContain(`@/${testFilePath}`); expect(prompt).toContain(testCode); @@ -39,7 +45,11 @@ describe('Code Action Prompts', () => { } ]; - const prompt = fixCodePrompt(testFilePath, testCode, diagnostics); + const prompt = fixCodePrompt({ + filePath: testFilePath, + selectedText: testCode, + diagnostics + }); expect(prompt).toContain('Current problems detected:'); expect(prompt).toContain('[eslint] Missing semicolon (semi)'); @@ -50,7 +60,10 @@ describe('Code Action Prompts', () => { describe('improveCodePrompt', () => { it('should format improve prompt correctly', () => { - const prompt = improveCodePrompt(testFilePath, testCode); + const prompt = improveCodePrompt({ + filePath: testFilePath, + selectedText: testCode + }); expect(prompt).toContain(`@/${testFilePath}`); expect(prompt).toContain(testCode); diff --git a/src/core/prompts/code-actions.ts b/src/core/prompts/code-actions.ts index 3fd03ae..2b9445d 100644 --- a/src/core/prompts/code-actions.ts +++ b/src/core/prompts/code-actions.ts @@ -1,8 +1,29 @@ -export const explainCodePrompt = (filePath: string, selectedText: string) => ` -Explain the following code from file path @/${filePath}: +type PromptParams = Record; + +const generateDiagnosticText = (diagnostics?: any[]) => { + if (!diagnostics?.length) return ''; + return `\nCurrent problems detected:\n${diagnostics.map(d => + `- [${d.source || 'Error'}] ${d.message}${d.code ? ` (${d.code})` : ''}` + ).join('\n')}`; +}; + +const createPrompt = (template: string, params: PromptParams): string => { + let result = template; + for (const [key, value] of Object.entries(params)) { + if (key === 'diagnostics') { + result = result.replaceAll('${diagnosticText}', generateDiagnosticText(value as any[])); + } else { + result = result.replaceAll(`\${${key}}`, value as string); + } + } + return result; +}; + +const EXPLAIN_TEMPLATE = ` +Explain the following code from file path @/\${filePath}: \`\`\` -${selectedText} +\${selectedText} \`\`\` Please provide a clear and concise explanation of what this code does, including: @@ -11,18 +32,12 @@ Please provide a clear and concise explanation of what this code does, including 3. Important patterns or techniques used `; -export const fixCodePrompt = (filePath: string, selectedText: string, diagnostics?: any[]) => { - const diagnosticText = diagnostics && diagnostics.length > 0 - ? `\nCurrent problems detected: -${diagnostics.map(d => `- [${d.source || 'Error'}] ${d.message}${d.code ? ` (${d.code})` : ''}`).join('\n')}` - : ''; - - return ` -Fix any issues in the following code from file path @/${filePath} -${diagnosticText} +const FIX_TEMPLATE = ` +Fix any issues in the following code from file path @/\${filePath} +\${diagnosticText} \`\`\` -${selectedText} +\${selectedText} \`\`\` Please: @@ -31,13 +46,12 @@ Please: 3. Provide corrected code 4. Explain what was fixed and why `; -}; -export const improveCodePrompt = (filePath: string, selectedText: string) => ` -Improve the following code from file path @/${filePath}: +const IMPROVE_TEMPLATE = ` +Improve the following code from file path @/\${filePath}: \`\`\` -${selectedText} +\${selectedText} \`\`\` Please suggest improvements for: @@ -47,4 +61,13 @@ Please suggest improvements for: 4. Error handling and edge cases Provide the improved code along with explanations for each enhancement. -`; \ No newline at end of file +`; + +export const explainCodePrompt = (params: PromptParams) => + createPrompt(EXPLAIN_TEMPLATE, params); + +export const fixCodePrompt = (params: PromptParams) => + createPrompt(FIX_TEMPLATE, params); + +export const improveCodePrompt = (params: PromptParams) => + createPrompt(IMPROVE_TEMPLATE, params); \ No newline at end of file diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b3e5235..17289f5 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -181,6 +181,20 @@ export class ClineProvider implements vscode.WebviewViewProvider { return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true) } + public static async handleCodeAction( + promptGenerator: (params: Record) => string, + params: Record + ): Promise { + const visibleProvider = ClineProvider.getVisibleInstance() + if (!visibleProvider) { + return + } + + const prompt = promptGenerator(params) + + await visibleProvider.initClineWithTask(prompt) + } + resolveWebviewView( webviewView: vscode.WebviewView | vscode.WebviewPanel, //context: vscode.WebviewViewResolveContext, used to recreate a deallocated webview, but we don't need this since we use retainContextWhenHidden diff --git a/src/extension.ts b/src/extension.ts index e6cfde7..6c8c9e7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -174,34 +174,23 @@ export function activate(context: vscode.ExtensionContext) { // Register code action commands context.subscriptions.push( vscode.commands.registerCommand("roo-cline.explainCode", async (filePath: string, selectedText: string) => { - const visibleProvider = ClineProvider.getVisibleInstance() - if (!visibleProvider) { - return - } - const prompt = explainCodePrompt(filePath, selectedText) - await visibleProvider.initClineWithTask(prompt) + await ClineProvider.handleCodeAction(explainCodePrompt, { filePath, selectedText }) }) ); context.subscriptions.push( vscode.commands.registerCommand("roo-cline.fixCode", async (filePath: string, selectedText: string, diagnostics?: any[]) => { - const visibleProvider = ClineProvider.getVisibleInstance() - if (!visibleProvider) { - return - } - const prompt = fixCodePrompt(filePath, selectedText, diagnostics) - await visibleProvider.initClineWithTask(prompt) + await ClineProvider.handleCodeAction(fixCodePrompt, { + filePath, + selectedText, + ...(diagnostics ? { diagnostics } : {}) + }) }) ); context.subscriptions.push( vscode.commands.registerCommand("roo-cline.improveCode", async (filePath: string, selectedText: string) => { - const visibleProvider = ClineProvider.getVisibleInstance() - if (!visibleProvider) { - return - } - const prompt = improveCodePrompt(filePath, selectedText) - await visibleProvider.initClineWithTask(prompt) + await ClineProvider.handleCodeAction(improveCodePrompt, { filePath, selectedText }) }) ); From 78457917207a90616a6f24b9eb4fbbe1dbe104a4 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Mon, 13 Jan 2025 02:48:52 +0700 Subject: [PATCH 56/66] feat(code-actions): add user input and customizable templates Add ability to provide custom input when using code actions Make code action templates customizable and resettable Refactor code action handling for better maintainability Add state management for utility prompts --- src/core/CodeActionProvider.ts | 2 +- src/core/prompts/code-actions.ts | 32 ++++++++++----- src/core/webview/ClineProvider.ts | 12 +++++- src/extension.ts | 65 ++++++++++++++++++++++--------- 4 files changed, 81 insertions(+), 30 deletions(-) diff --git a/src/core/CodeActionProvider.ts b/src/core/CodeActionProvider.ts index 11bafdb..cac2bdc 100644 --- a/src/core/CodeActionProvider.ts +++ b/src/core/CodeActionProvider.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; -const ACTION_NAMES = { +export const ACTION_NAMES = { EXPLAIN: 'Roo Cline: Explain Code', FIX: 'Roo Cline: Fix Code', IMPROVE: 'Roo Cline: Improve Code' diff --git a/src/core/prompts/code-actions.ts b/src/core/prompts/code-actions.ts index 2b9445d..0811c48 100644 --- a/src/core/prompts/code-actions.ts +++ b/src/core/prompts/code-actions.ts @@ -2,12 +2,12 @@ type PromptParams = Record; const generateDiagnosticText = (diagnostics?: any[]) => { if (!diagnostics?.length) return ''; - return `\nCurrent problems detected:\n${diagnostics.map(d => + return `\nCurrent problems detected:\n${diagnostics.map(d => `- [${d.source || 'Error'}] ${d.message}${d.code ? ` (${d.code})` : ''}` ).join('\n')}`; }; -const createPrompt = (template: string, params: PromptParams): string => { +export const createPrompt = (template: string, params: PromptParams): string => { let result = template; for (const [key, value] of Object.entries(params)) { if (key === 'diagnostics') { @@ -16,11 +16,16 @@ const createPrompt = (template: string, params: PromptParams): string => { result = result.replaceAll(`\${${key}}`, value as string); } } + + // Replace any remaining user_input placeholders with empty string + result = result.replaceAll('${userInput}', ''); + return result; }; -const EXPLAIN_TEMPLATE = ` +export const EXPLAIN_TEMPLATE = ` Explain the following code from file path @/\${filePath}: +\${userInput} \`\`\` \${selectedText} @@ -32,9 +37,10 @@ Please provide a clear and concise explanation of what this code does, including 3. Important patterns or techniques used `; -const FIX_TEMPLATE = ` +export const FIX_TEMPLATE = ` Fix any issues in the following code from file path @/\${filePath} \${diagnosticText} +\${userInput} \`\`\` \${selectedText} @@ -47,8 +53,9 @@ Please: 4. Explain what was fixed and why `; -const IMPROVE_TEMPLATE = ` +export const IMPROVE_TEMPLATE = ` Improve the following code from file path @/\${filePath}: +\${userInput} \`\`\` \${selectedText} @@ -63,11 +70,18 @@ Please suggest improvements for: Provide the improved code along with explanations for each enhancement. `; -export const explainCodePrompt = (params: PromptParams) => +export const explainCodePrompt = (params: PromptParams) => createPrompt(EXPLAIN_TEMPLATE, params); -export const fixCodePrompt = (params: PromptParams) => +export const fixCodePrompt = (params: PromptParams) => createPrompt(FIX_TEMPLATE, params); -export const improveCodePrompt = (params: PromptParams) => - createPrompt(IMPROVE_TEMPLATE, params); \ No newline at end of file +export const improveCodePrompt = (params: PromptParams) => + createPrompt(IMPROVE_TEMPLATE, params); + +// Get template based on prompt type +export const defaultTemplates = { + 'EXPLAIN': EXPLAIN_TEMPLATE, + 'FIX': FIX_TEMPLATE, + 'IMPROVE': IMPROVE_TEMPLATE +} \ No newline at end of file diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 17289f5..d897838 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -40,6 +40,12 @@ import { enhancePrompt } from "../../utils/enhance-prompt" import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git" import { ConfigManager } from "../config/ConfigManager" import { CustomModesManager } from "../config/CustomModesManager" +import { + defaultTemplates, + createPrompt +} from "../prompts/code-actions" + +import { ACTION_NAMES } from "../CodeActionProvider" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -182,7 +188,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { } public static async handleCodeAction( - promptGenerator: (params: Record) => string, + promptType: keyof typeof ACTION_NAMES, params: Record ): Promise { const visibleProvider = ClineProvider.getVisibleInstance() @@ -190,8 +196,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { return } - const prompt = promptGenerator(params) + const { utilPrompt } = await visibleProvider.getState() + const template = utilPrompt?.[promptType] ?? defaultTemplates[promptType] + const prompt = createPrompt(template, params) await visibleProvider.initClineWithTask(prompt) } diff --git a/src/extension.ts b/src/extension.ts index 6c8c9e7..cf910fc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,7 +5,7 @@ import * as vscode from "vscode" import { ClineProvider } from "./core/webview/ClineProvider" import { createClineAPI } from "./exports" import "./utils/path" // necessary to have access to String.prototype.toPosix -import { CodeActionProvider } from "./core/CodeActionProvider" +import { ACTION_NAMES, CodeActionProvider } from "./core/CodeActionProvider" import { explainCodePrompt, fixCodePrompt, improveCodePrompt } from "./core/prompts/code-actions" import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" @@ -171,27 +171,56 @@ export function activate(context: vscode.ExtensionContext) { ) ); - // Register code action commands - context.subscriptions.push( - vscode.commands.registerCommand("roo-cline.explainCode", async (filePath: string, selectedText: string) => { - await ClineProvider.handleCodeAction(explainCodePrompt, { filePath, selectedText }) - }) - ); + // Helper function to handle code actions + const registerCodeAction = ( + context: vscode.ExtensionContext, + command: string, + promptType: keyof typeof ACTION_NAMES, + inputPrompt: string, + inputPlaceholder: string + ) => { + context.subscriptions.push( + vscode.commands.registerCommand(command, async (filePath: string, selectedText: string, diagnostics?: any[]) => { + const userInput = await vscode.window.showInputBox({ + prompt: inputPrompt, + placeHolder: inputPlaceholder + }); - context.subscriptions.push( - vscode.commands.registerCommand("roo-cline.fixCode", async (filePath: string, selectedText: string, diagnostics?: any[]) => { - await ClineProvider.handleCodeAction(fixCodePrompt, { - filePath, - selectedText, - ...(diagnostics ? { diagnostics } : {}) + const params = { + filePath, + selectedText, + ...(diagnostics ? { diagnostics } : {}), + ...(userInput ? { userInput } : {}) + }; + + await ClineProvider.handleCodeAction(promptType, params); }) - }) + ); + }; + + // Register code action commands + registerCodeAction( + context, + "roo-cline.explainCode", + 'EXPLAIN', + "Any specific questions about this code?", + "E.g. How does the error handling work?" ); - context.subscriptions.push( - vscode.commands.registerCommand("roo-cline.improveCode", async (filePath: string, selectedText: string) => { - await ClineProvider.handleCodeAction(improveCodePrompt, { filePath, selectedText }) - }) + registerCodeAction( + context, + "roo-cline.fixCode", + 'FIX', + "Any specific concerns about fixing this code?", + "E.g. Maintain backward compatibility" + ); + + registerCodeAction( + context, + "roo-cline.improveCode", + 'IMPROVE', + "Any specific aspects you want to improve?", + "E.g. Focus on performance optimization" ); return createClineAPI(outputChannel, sidebarProvider) From 22907a05787b089e83b48c27a2ac987b4e82ea88 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Thu, 23 Jan 2025 00:31:43 +0700 Subject: [PATCH 57/66] refactor: consolidate prompt functionality into support-prompt module - Move code action prompts from core/prompts to shared/support-prompt - Migrate enhance prompt functionality from modes to support-prompt - Add UI for managing code action prompts in PromptsView - Update types and interfaces for better prompt management --- .../prompts/__tests__/code-actions.test.ts | 76 ---------- src/core/prompts/code-actions.ts | 87 ----------- src/core/webview/ClineProvider.ts | 85 +++++++---- src/extension.ts | 66 ++++----- src/shared/WebviewMessage.ts | 3 +- src/shared/__tests__/support-prompts.test.ts | 138 ++++++++++++++++++ src/shared/modes.ts | 37 ++--- src/shared/support-prompt.ts | 118 +++++++++++++++ .../src/components/prompts/PromptsView.tsx | 125 ++++++++++++++-- 9 files changed, 467 insertions(+), 268 deletions(-) delete mode 100644 src/core/prompts/__tests__/code-actions.test.ts delete mode 100644 src/core/prompts/code-actions.ts create mode 100644 src/shared/__tests__/support-prompts.test.ts create mode 100644 src/shared/support-prompt.ts diff --git a/src/core/prompts/__tests__/code-actions.test.ts b/src/core/prompts/__tests__/code-actions.test.ts deleted file mode 100644 index 0ddb6bd..0000000 --- a/src/core/prompts/__tests__/code-actions.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { explainCodePrompt, fixCodePrompt, improveCodePrompt } from '../code-actions'; - -describe('Code Action Prompts', () => { - const testFilePath = 'test/file.ts'; - const testCode = 'function test() { return true; }'; - - describe('explainCodePrompt', () => { - it('should format explain prompt correctly', () => { - const prompt = explainCodePrompt({ - filePath: testFilePath, - selectedText: testCode - }); - - expect(prompt).toContain(`@/${testFilePath}`); - expect(prompt).toContain(testCode); - expect(prompt).toContain('purpose and functionality'); - expect(prompt).toContain('Key components'); - expect(prompt).toContain('Important patterns'); - }); - }); - - describe('fixCodePrompt', () => { - it('should format fix prompt without diagnostics', () => { - const prompt = fixCodePrompt({ - filePath: testFilePath, - selectedText: testCode - }); - - expect(prompt).toContain(`@/${testFilePath}`); - expect(prompt).toContain(testCode); - expect(prompt).toContain('Address all detected problems'); - expect(prompt).not.toContain('Current problems detected'); - }); - - it('should format fix prompt with diagnostics', () => { - const diagnostics = [ - { - source: 'eslint', - message: 'Missing semicolon', - code: 'semi' - }, - { - message: 'Unused variable', - severity: 1 - } - ]; - - const prompt = fixCodePrompt({ - filePath: testFilePath, - selectedText: testCode, - diagnostics - }); - - expect(prompt).toContain('Current problems detected:'); - expect(prompt).toContain('[eslint] Missing semicolon (semi)'); - expect(prompt).toContain('[Error] Unused variable'); - expect(prompt).toContain(testCode); - }); - }); - - describe('improveCodePrompt', () => { - it('should format improve prompt correctly', () => { - const prompt = improveCodePrompt({ - filePath: testFilePath, - selectedText: testCode - }); - - expect(prompt).toContain(`@/${testFilePath}`); - expect(prompt).toContain(testCode); - expect(prompt).toContain('Code readability'); - expect(prompt).toContain('Performance optimization'); - expect(prompt).toContain('Best practices'); - expect(prompt).toContain('Error handling'); - }); - }); -}); \ No newline at end of file diff --git a/src/core/prompts/code-actions.ts b/src/core/prompts/code-actions.ts deleted file mode 100644 index 0811c48..0000000 --- a/src/core/prompts/code-actions.ts +++ /dev/null @@ -1,87 +0,0 @@ -type PromptParams = Record; - -const generateDiagnosticText = (diagnostics?: any[]) => { - if (!diagnostics?.length) return ''; - return `\nCurrent problems detected:\n${diagnostics.map(d => - `- [${d.source || 'Error'}] ${d.message}${d.code ? ` (${d.code})` : ''}` - ).join('\n')}`; -}; - -export const createPrompt = (template: string, params: PromptParams): string => { - let result = template; - for (const [key, value] of Object.entries(params)) { - if (key === 'diagnostics') { - result = result.replaceAll('${diagnosticText}', generateDiagnosticText(value as any[])); - } else { - result = result.replaceAll(`\${${key}}`, value as string); - } - } - - // Replace any remaining user_input placeholders with empty string - result = result.replaceAll('${userInput}', ''); - - return result; -}; - -export const EXPLAIN_TEMPLATE = ` -Explain the following code from file path @/\${filePath}: -\${userInput} - -\`\`\` -\${selectedText} -\`\`\` - -Please provide a clear and concise explanation of what this code does, including: -1. The purpose and functionality -2. Key components and their interactions -3. Important patterns or techniques used -`; - -export const FIX_TEMPLATE = ` -Fix any issues in the following code from file path @/\${filePath} -\${diagnosticText} -\${userInput} - -\`\`\` -\${selectedText} -\`\`\` - -Please: -1. Address all detected problems listed above (if any) -2. Identify any other potential bugs or issues -3. Provide corrected code -4. Explain what was fixed and why -`; - -export const IMPROVE_TEMPLATE = ` -Improve the following code from file path @/\${filePath}: -\${userInput} - -\`\`\` -\${selectedText} -\`\`\` - -Please suggest improvements for: -1. Code readability and maintainability -2. Performance optimization -3. Best practices and patterns -4. Error handling and edge cases - -Provide the improved code along with explanations for each enhancement. -`; - -export const explainCodePrompt = (params: PromptParams) => - createPrompt(EXPLAIN_TEMPLATE, params); - -export const fixCodePrompt = (params: PromptParams) => - createPrompt(FIX_TEMPLATE, params); - -export const improveCodePrompt = (params: PromptParams) => - createPrompt(IMPROVE_TEMPLATE, params); - -// Get template based on prompt type -export const defaultTemplates = { - 'EXPLAIN': EXPLAIN_TEMPLATE, - 'FIX': FIX_TEMPLATE, - 'IMPROVE': IMPROVE_TEMPLATE -} \ No newline at end of file diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d897838..f919df6 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1,4 +1,5 @@ import { Anthropic } from "@anthropic-ai/sdk" +import delay from "delay" import axios from "axios" import fs from "fs/promises" import os from "os" @@ -23,7 +24,6 @@ import { modes, CustomPrompts, PromptComponent, - enhance, ModeConfig, defaultModeSlug, getModeBySlug, @@ -40,10 +40,7 @@ import { enhancePrompt } from "../../utils/enhance-prompt" import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git" import { ConfigManager } from "../config/ConfigManager" import { CustomModesManager } from "../config/CustomModesManager" -import { - defaultTemplates, - createPrompt -} from "../prompts/code-actions" +import { enhance, codeActionPrompt } from "../../shared/support-prompt" import { ACTION_NAMES } from "../CodeActionProvider" @@ -189,17 +186,27 @@ export class ClineProvider implements vscode.WebviewViewProvider { public static async handleCodeAction( promptType: keyof typeof ACTION_NAMES, - params: Record + params: Record, ): Promise { - const visibleProvider = ClineProvider.getVisibleInstance() + let visibleProvider = ClineProvider.getVisibleInstance() + + // If no visible provider, try to show the sidebar view + if (!visibleProvider) { + await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus") + // Wait briefly for the view to become visible + await delay(100) + visibleProvider = ClineProvider.getVisibleInstance() + } + + // If still no visible provider, return if (!visibleProvider) { return } - const { utilPrompt } = await visibleProvider.getState() + const { customPrompts } = await visibleProvider.getState() + + const prompt = codeActionPrompt.create(promptType, params, customPrompts) - const template = utilPrompt?.[promptType] ?? defaultTemplates[promptType] - const prompt = createPrompt(template, params) await visibleProvider.initClineWithTask(prompt) } @@ -297,7 +304,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { experimentalDiffStrategy, } = await this.getState() - const modePrompt = customPrompts?.[mode] + const modePrompt = customPrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") this.cline = new Cline( @@ -325,7 +332,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { experimentalDiffStrategy, } = await this.getState() - const modePrompt = customPrompts?.[mode] + const modePrompt = customPrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") this.cline = new Cline( @@ -804,29 +811,49 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.postStateToWebview() break - case "updateEnhancedPrompt": - const existingPrompts = (await this.getGlobalState("customPrompts")) || {} + case "updateSupportPrompt": + try { + if (Object.keys(message?.values ?? {}).length === 0) { + return + } - const updatedPrompts = { - ...existingPrompts, - enhance: message.text, + const existingPrompts = (await this.getGlobalState("customPrompts")) || {} + + const updatedPrompts = { + ...existingPrompts, + ...message.values, + } + + await this.updateGlobalState("customPrompts", updatedPrompts) + await this.postStateToWebview() + } catch (error) { + console.error("Error update support prompt:", error) + vscode.window.showErrorMessage("Failed to update support prompt") } + break + case "resetSupportPrompt": + try { + if (!message?.text) { + return + } - await this.updateGlobalState("customPrompts", updatedPrompts) + const existingPrompts = ((await this.getGlobalState("customPrompts")) || {}) as Record< + string, + any + > - // Get current state and explicitly include customPrompts - const currentState = await this.getState() + const updatedPrompts = { + ...existingPrompts, + } - const stateWithPrompts = { - ...currentState, - customPrompts: updatedPrompts, + updatedPrompts[message.text] = undefined + + await this.updateGlobalState("customPrompts", updatedPrompts) + await this.postStateToWebview() + } catch (error) { + console.error("Error reset support prompt:", error) + vscode.window.showErrorMessage("Failed to reset support prompt") } - - // Post state with prompts - this.view?.webview.postMessage({ - type: "state", - state: stateWithPrompts, - }) break case "updatePrompt": if (message.promptMode && message.customPrompt !== undefined) { diff --git a/src/extension.ts b/src/extension.ts index cf910fc..db011e0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,7 +6,6 @@ import { ClineProvider } from "./core/webview/ClineProvider" import { createClineAPI } from "./exports" import "./utils/path" // necessary to have access to String.prototype.toPosix import { ACTION_NAMES, CodeActionProvider } from "./core/CodeActionProvider" -import { explainCodePrompt, fixCodePrompt, improveCodePrompt } from "./core/prompts/code-actions" import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" /* @@ -162,14 +161,10 @@ export function activate(context: vscode.ExtensionContext) { // Register code actions provider context.subscriptions.push( - vscode.languages.registerCodeActionsProvider( - { pattern: "**/*" }, - new CodeActionProvider(), - { - providedCodeActionKinds: CodeActionProvider.providedCodeActionKinds - } - ) - ); + vscode.languages.registerCodeActionsProvider({ pattern: "**/*" }, new CodeActionProvider(), { + providedCodeActionKinds: CodeActionProvider.providedCodeActionKinds, + }), + ) // Helper function to handle code actions const registerCodeAction = ( @@ -177,51 +172,54 @@ export function activate(context: vscode.ExtensionContext) { command: string, promptType: keyof typeof ACTION_NAMES, inputPrompt: string, - inputPlaceholder: string + inputPlaceholder: string, ) => { context.subscriptions.push( - vscode.commands.registerCommand(command, async (filePath: string, selectedText: string, diagnostics?: any[]) => { - const userInput = await vscode.window.showInputBox({ - prompt: inputPrompt, - placeHolder: inputPlaceholder - }); + vscode.commands.registerCommand( + command, + async (filePath: string, selectedText: string, diagnostics?: any[]) => { + const userInput = await vscode.window.showInputBox({ + prompt: inputPrompt, + placeHolder: inputPlaceholder, + }) - const params = { - filePath, - selectedText, - ...(diagnostics ? { diagnostics } : {}), - ...(userInput ? { userInput } : {}) - }; + const params = { + filePath, + selectedText, + ...(diagnostics ? { diagnostics } : {}), + ...(userInput ? { userInput } : {}), + } - await ClineProvider.handleCodeAction(promptType, params); - }) - ); - }; + await ClineProvider.handleCodeAction(promptType, params) + }, + ), + ) + } // Register code action commands registerCodeAction( context, "roo-cline.explainCode", - 'EXPLAIN', + "EXPLAIN", "Any specific questions about this code?", - "E.g. How does the error handling work?" - ); + "E.g. How does the error handling work?", + ) registerCodeAction( context, "roo-cline.fixCode", - 'FIX', + "FIX", "Any specific concerns about fixing this code?", - "E.g. Maintain backward compatibility" - ); + "E.g. Maintain backward compatibility", + ) registerCodeAction( context, "roo-cline.improveCode", - 'IMPROVE', + "IMPROVE", "Any specific aspects you want to improve?", - "E.g. Focus on performance optimization" - ); + "E.g. Focus on performance optimization", + ) return createClineAPI(outputChannel, sidebarProvider) } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index b63b898..e2830bf 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -68,7 +68,8 @@ export interface WebviewMessage { | "requestVsCodeLmModels" | "mode" | "updatePrompt" - | "updateEnhancedPrompt" + | "updateSupportPrompt" + | "resetSupportPrompt" | "getSystemPrompt" | "systemPrompt" | "enhancementApiConfigId" diff --git a/src/shared/__tests__/support-prompts.test.ts b/src/shared/__tests__/support-prompts.test.ts new file mode 100644 index 0000000..92d2342 --- /dev/null +++ b/src/shared/__tests__/support-prompts.test.ts @@ -0,0 +1,138 @@ +import { codeActionPrompt, type CodeActionType } from "../support-prompt" + +describe("Code Action Prompts", () => { + const testFilePath = "test/file.ts" + const testCode = "function test() { return true; }" + + describe("EXPLAIN action", () => { + it("should format explain prompt correctly", () => { + const prompt = codeActionPrompt.create("EXPLAIN", { + filePath: testFilePath, + selectedText: testCode, + }) + + expect(prompt).toContain(`@/${testFilePath}`) + expect(prompt).toContain(testCode) + expect(prompt).toContain("purpose and functionality") + expect(prompt).toContain("Key components") + expect(prompt).toContain("Important patterns") + }) + }) + + describe("FIX action", () => { + it("should format fix prompt without diagnostics", () => { + const prompt = codeActionPrompt.create("FIX", { + filePath: testFilePath, + selectedText: testCode, + }) + + expect(prompt).toContain(`@/${testFilePath}`) + expect(prompt).toContain(testCode) + expect(prompt).toContain("Address all detected problems") + expect(prompt).not.toContain("Current problems detected") + }) + + it("should format fix prompt with diagnostics", () => { + const diagnostics = [ + { + source: "eslint", + message: "Missing semicolon", + code: "semi", + }, + { + message: "Unused variable", + severity: 1, + }, + ] + + const prompt = codeActionPrompt.create("FIX", { + filePath: testFilePath, + selectedText: testCode, + diagnostics, + }) + + expect(prompt).toContain("Current problems detected:") + expect(prompt).toContain("[eslint] Missing semicolon (semi)") + expect(prompt).toContain("[Error] Unused variable") + expect(prompt).toContain(testCode) + }) + }) + + describe("IMPROVE action", () => { + it("should format improve prompt correctly", () => { + const prompt = codeActionPrompt.create("IMPROVE", { + filePath: testFilePath, + selectedText: testCode, + }) + + expect(prompt).toContain(`@/${testFilePath}`) + expect(prompt).toContain(testCode) + expect(prompt).toContain("Code readability") + expect(prompt).toContain("Performance optimization") + expect(prompt).toContain("Best practices") + expect(prompt).toContain("Error handling") + }) + }) + + describe("get template", () => { + it("should return default template when no custom prompts provided", () => { + const template = codeActionPrompt.get(undefined, "EXPLAIN") + expect(template).toBe(codeActionPrompt.default.EXPLAIN) + }) + + it("should return custom template when provided", () => { + const customTemplate = "Custom template for explaining code" + const customPrompts = { + EXPLAIN: customTemplate, + } + const template = codeActionPrompt.get(customPrompts, "EXPLAIN") + expect(template).toBe(customTemplate) + }) + + it("should return default template when custom prompts does not include type", () => { + const customPrompts = { + SOMETHING_ELSE: "Other template", + } + const template = codeActionPrompt.get(customPrompts, "EXPLAIN") + expect(template).toBe(codeActionPrompt.default.EXPLAIN) + }) + }) + + describe("create with custom prompts", () => { + it("should use custom template when provided", () => { + const customTemplate = "Custom template for ${filePath}" + const customPrompts = { + EXPLAIN: customTemplate, + } + + const prompt = codeActionPrompt.create( + "EXPLAIN", + { + filePath: testFilePath, + selectedText: testCode, + }, + customPrompts, + ) + + expect(prompt).toContain(`Custom template for ${testFilePath}`) + expect(prompt).not.toContain("purpose and functionality") + }) + + it("should use default template when custom prompts does not include type", () => { + const customPrompts = { + EXPLAIN: "Other template", + } + + const prompt = codeActionPrompt.create( + "EXPLAIN", + { + filePath: testFilePath, + selectedText: testCode, + }, + customPrompts, + ) + + expect(prompt).toContain("Other template") + }) + }) +}) diff --git a/src/shared/modes.ts b/src/shared/modes.ts index c6ea89a..451e661 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -12,6 +12,16 @@ export type ModeConfig = { groups: readonly ToolGroup[] // Now uses groups instead of tools array } +// Mode-specific prompts only +export type PromptComponent = { + roleDefinition?: string + customInstructions?: string +} + +export type CustomPrompts = { + [key: string]: PromptComponent | undefined | string +} + // Helper to get all tools for a mode export function getToolsForMode(groups: readonly ToolGroup[]): string[] { const tools = new Set() @@ -130,33 +140,6 @@ export function isToolAllowedForMode( 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 -export type EnhanceConfig = { - prompt: string -} - -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):", -} as const - -// Completely separate enhance prompt handling -export const enhancePrompt = { - default: enhance.prompt, - get: (customPrompts: Record | undefined): string => { - return customPrompts?.enhance ?? enhance.prompt - }, -} as const - // Create the mode-specific default prompts export const defaultPrompts: Readonly = Object.freeze( Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])), diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts new file mode 100644 index 0000000..4ce9f93 --- /dev/null +++ b/src/shared/support-prompt.ts @@ -0,0 +1,118 @@ +// Separate enhance prompt type and definition +export type EnhanceConfig = { + prompt: string +} + +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):", +} as const + +// Completely separate enhance prompt handling +export const enhancePrompt = { + default: enhance.prompt, + get: (customPrompts: Record | undefined): string => { + return customPrompts?.enhance ?? enhance.prompt + }, +} as const + +// Code action prompts +type PromptParams = Record + +const generateDiagnosticText = (diagnostics?: any[]) => { + if (!diagnostics?.length) return "" + return `\nCurrent problems detected:\n${diagnostics + .map((d) => `- [${d.source || "Error"}] ${d.message}${d.code ? ` (${d.code})` : ""}`) + .join("\n")}` +} + +export const createPrompt = (template: string, params: PromptParams): string => { + let result = template + for (const [key, value] of Object.entries(params)) { + if (key === "diagnostics") { + result = result.replaceAll("${diagnosticText}", generateDiagnosticText(value as any[])) + } else { + result = result.replaceAll(`\${${key}}`, value as string) + } + } + + // Replace any remaining user_input placeholders with empty string + result = result.replaceAll("${userInput}", "") + + return result +} + +const EXPLAIN_TEMPLATE = ` +Explain the following code from file path @/\${filePath}: +\${userInput} + +\`\`\` +\${selectedText} +\`\`\` + +Please provide a clear and concise explanation of what this code does, including: +1. The purpose and functionality +2. Key components and their interactions +3. Important patterns or techniques used +` + +const FIX_TEMPLATE = ` +Fix any issues in the following code from file path @/\${filePath} +\${diagnosticText} +\${userInput} + +\`\`\` +\${selectedText} +\`\`\` + +Please: +1. Address all detected problems listed above (if any) +2. Identify any other potential bugs or issues +3. Provide corrected code +4. Explain what was fixed and why +` + +const IMPROVE_TEMPLATE = ` +Improve the following code from file path @/\${filePath}: +\${userInput} + +\`\`\` +\${selectedText} +\`\`\` + +Please suggest improvements for: +1. Code readability and maintainability +2. Performance optimization +3. Best practices and patterns +4. Error handling and edge cases + +Provide the improved code along with explanations for each enhancement. +` + +// Get template based on prompt type +const defaultTemplates = { + EXPLAIN: EXPLAIN_TEMPLATE, + FIX: FIX_TEMPLATE, + IMPROVE: IMPROVE_TEMPLATE, +} as const + +type CodeActionType = keyof typeof defaultTemplates + +export const codeActionPrompt = { + default: defaultTemplates, + get: (customPrompts: Record | undefined, type: CodeActionType): string => { + return customPrompts?.[type] ?? defaultTemplates[type] + }, + create: (type: CodeActionType, params: PromptParams, customPrompts?: Record): string => { + const template = codeActionPrompt.get(customPrompts, type) + return createPrompt(template, params) + }, +} as const + +export type { CodeActionType } + +// User-friendly labels for code action types +export const codeActionLabels: Record = { + FIX: "Fix Issues", + EXPLAIN: "Explain Code", + IMPROVE: "Improve Code", +} as const diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index ebd4d0f..a767fd4 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -8,14 +8,14 @@ import { VSCodeCheckbox, } from "@vscode/webview-ui-toolkit/react" import { useExtensionState } from "../../context/ExtensionStateContext" +import { Mode, PromptComponent, getRoleDefinition, getAllModes, ModeConfig } from "../../../../src/shared/modes" import { - Mode, - PromptComponent, - getRoleDefinition, - getAllModes, - ModeConfig, enhancePrompt, -} from "../../../../src/shared/modes" + codeActionPrompt, + CodeActionType, + codeActionLabels, +} from "../../../../src/shared/support-prompt" + import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups" import { vscode } from "../../utils/vscode" @@ -50,11 +50,12 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const [selectedPromptTitle, setSelectedPromptTitle] = useState("") const [isToolsEditMode, setIsToolsEditMode] = useState(false) const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false) + const [activeCodeActionTab, setActiveCodeActionTab] = useState("FIX") // Direct update functions const updateAgentPrompt = useCallback( (mode: Mode, promptData: PromptComponent) => { - const existingPrompt = customPrompts?.[mode] + const existingPrompt = customPrompts?.[mode] as PromptComponent const updatedPrompt = { ...existingPrompt, ...promptData } // Only include properties that differ from defaults @@ -256,8 +257,19 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const updateEnhancePrompt = (value: string | undefined) => { vscode.postMessage({ - type: "updateEnhancedPrompt", - text: value, + type: "updateSupportPrompt", + values: { + enhance: value, + }, + }) + } + + const updateCodeActionPrompt = (type: CodeActionType, value: string | undefined) => { + vscode.postMessage({ + type: "updateSupportPrompt", + values: { + [type]: value, + }, }) } @@ -271,7 +283,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const handleAgentReset = (modeSlug: string) => { // Only reset role definition for built-in modes - const existingPrompt = customPrompts?.[modeSlug] + const existingPrompt = customPrompts?.[modeSlug] as PromptComponent updateAgentPrompt(modeSlug, { ...existingPrompt, roleDefinition: undefined, @@ -279,13 +291,27 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { } const handleEnhanceReset = () => { - updateEnhancePrompt(undefined) + vscode.postMessage({ + type: "resetSupportPrompt", + text: "enhance", + }) + } + + const handleCodeActionReset = (type: CodeActionType) => { + vscode.postMessage({ + type: "resetSupportPrompt", + text: type, + }) } const getEnhancePromptValue = (): string => { return enhancePrompt.get(customPrompts) } + const getCodeActionPromptValue = (type: CodeActionType): string => { + return codeActionPrompt.get(customPrompts, type) + } + const handleTestEnhancement = () => { if (!testPrompt.trim()) return @@ -563,7 +589,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { { const customMode = findModeBySlug(mode, customModes) - const prompt = customPrompts?.[mode] + const prompt = customPrompts?.[mode] as PromptComponent return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode) })()} onChange={(e) => { @@ -680,7 +706,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { { const customMode = findModeBySlug(mode, customModes) - const prompt = customPrompts?.[mode] + const prompt = customPrompts?.[mode] as PromptComponent return customMode?.customInstructions ?? prompt?.customInstructions ?? "" })()} onChange={(e) => { @@ -696,7 +722,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { }) } else { // For built-in modes, update the prompts - const existingPrompt = customPrompts?.[mode] + const existingPrompt = customPrompts?.[mode] as PromptComponent updateAgentPrompt(mode, { ...existingPrompt, customInstructions: value.trim() || undefined, @@ -759,6 +785,77 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
+
+
Code Action Prompts
+
+ {Object.keys(codeActionPrompt.default).map((type) => ( + + ))} +
+ + {/* Show active tab content */} +
+
+
{activeCodeActionTab} Prompt
+ handleCodeActionReset(activeCodeActionTab)} + title={`Reset ${activeCodeActionTab} prompt to default`}> + + +
+ { + const value = + (e as CustomEvent)?.detail?.target?.value || + ((e as any).target as HTMLTextAreaElement).value + const trimmedValue = value.trim() + updateCodeActionPrompt(activeCodeActionTab, trimmedValue || undefined) + }} + rows={4} + resize="vertical" + style={{ width: "100%" }} + /> +
+
+

Prompt Enhancement

Date: Thu, 23 Jan 2025 10:46:04 +0700 Subject: [PATCH 58/66] refactor: consolidate code action and enhance prompts into unified support prompts system - Rename codeActionPrompt to supportPrompt for better clarity - Move enhance prompt functionality into support prompts system - Add ENHANCE tab alongside other support prompt types - Update UI to show enhancement configuration in ENHANCE tab - Update tests to reflect new unified structure This change simplifies the prompt system by treating enhancement as another type of support prompt rather than a separate system. --- src/core/webview/ClineProvider.ts | 12 +- src/shared/__tests__/support-prompts.test.ts | 40 ++- src/shared/support-prompt.ts | 49 ++-- .../src/components/prompts/PromptsView.tsx | 248 +++++++----------- .../prompts/__tests__/PromptsView.test.tsx | 4 + 5 files changed, 140 insertions(+), 213 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index f919df6..b28bad6 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -40,7 +40,7 @@ import { enhancePrompt } from "../../utils/enhance-prompt" import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git" import { ConfigManager } from "../config/ConfigManager" import { CustomModesManager } from "../config/CustomModesManager" -import { enhance, codeActionPrompt } from "../../shared/support-prompt" +import { supportPrompt } from "../../shared/support-prompt" import { ACTION_NAMES } from "../CodeActionProvider" @@ -205,7 +205,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { const { customPrompts } = await visibleProvider.getState() - const prompt = codeActionPrompt.create(promptType, params, customPrompts) + const prompt = supportPrompt.create(promptType, params, customPrompts) await visibleProvider.initClineWithTask(prompt) } @@ -996,16 +996,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { } } - const getEnhancePrompt = (value: string | PromptComponent | undefined): string => { - if (typeof value === "string") { - return value - } - return enhance.prompt // Use the constant from modes.ts which we know is a string - } const enhancedPrompt = await enhancePrompt( configToUse, message.text, - getEnhancePrompt(customPrompts?.enhance), + supportPrompt.get(customPrompts, "ENHANCE"), ) await this.postMessageToWebview({ type: "enhancedPrompt", diff --git a/src/shared/__tests__/support-prompts.test.ts b/src/shared/__tests__/support-prompts.test.ts index 92d2342..ee7253e 100644 --- a/src/shared/__tests__/support-prompts.test.ts +++ b/src/shared/__tests__/support-prompts.test.ts @@ -1,4 +1,4 @@ -import { codeActionPrompt, type CodeActionType } from "../support-prompt" +import { supportPrompt } from "../support-prompt" describe("Code Action Prompts", () => { const testFilePath = "test/file.ts" @@ -6,7 +6,7 @@ describe("Code Action Prompts", () => { describe("EXPLAIN action", () => { it("should format explain prompt correctly", () => { - const prompt = codeActionPrompt.create("EXPLAIN", { + const prompt = supportPrompt.create("EXPLAIN", { filePath: testFilePath, selectedText: testCode, }) @@ -21,7 +21,7 @@ describe("Code Action Prompts", () => { describe("FIX action", () => { it("should format fix prompt without diagnostics", () => { - const prompt = codeActionPrompt.create("FIX", { + const prompt = supportPrompt.create("FIX", { filePath: testFilePath, selectedText: testCode, }) @@ -45,7 +45,7 @@ describe("Code Action Prompts", () => { }, ] - const prompt = codeActionPrompt.create("FIX", { + const prompt = supportPrompt.create("FIX", { filePath: testFilePath, selectedText: testCode, diagnostics, @@ -60,7 +60,7 @@ describe("Code Action Prompts", () => { describe("IMPROVE action", () => { it("should format improve prompt correctly", () => { - const prompt = codeActionPrompt.create("IMPROVE", { + const prompt = supportPrompt.create("IMPROVE", { filePath: testFilePath, selectedText: testCode, }) @@ -74,10 +74,26 @@ describe("Code Action Prompts", () => { }) }) + describe("ENHANCE action", () => { + it("should format enhance prompt correctly", () => { + const prompt = supportPrompt.create("ENHANCE", { + filePath: testFilePath, + selectedText: testCode, + }) + + expect(prompt).toBe( + "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):", + ) + // Verify it ignores parameters since ENHANCE template doesn't use any + expect(prompt).not.toContain(testFilePath) + expect(prompt).not.toContain(testCode) + }) + }) + describe("get template", () => { it("should return default template when no custom prompts provided", () => { - const template = codeActionPrompt.get(undefined, "EXPLAIN") - expect(template).toBe(codeActionPrompt.default.EXPLAIN) + const template = supportPrompt.get(undefined, "EXPLAIN") + expect(template).toBe(supportPrompt.default.EXPLAIN) }) it("should return custom template when provided", () => { @@ -85,7 +101,7 @@ describe("Code Action Prompts", () => { const customPrompts = { EXPLAIN: customTemplate, } - const template = codeActionPrompt.get(customPrompts, "EXPLAIN") + const template = supportPrompt.get(customPrompts, "EXPLAIN") expect(template).toBe(customTemplate) }) @@ -93,8 +109,8 @@ describe("Code Action Prompts", () => { const customPrompts = { SOMETHING_ELSE: "Other template", } - const template = codeActionPrompt.get(customPrompts, "EXPLAIN") - expect(template).toBe(codeActionPrompt.default.EXPLAIN) + const template = supportPrompt.get(customPrompts, "EXPLAIN") + expect(template).toBe(supportPrompt.default.EXPLAIN) }) }) @@ -105,7 +121,7 @@ describe("Code Action Prompts", () => { EXPLAIN: customTemplate, } - const prompt = codeActionPrompt.create( + const prompt = supportPrompt.create( "EXPLAIN", { filePath: testFilePath, @@ -123,7 +139,7 @@ describe("Code Action Prompts", () => { EXPLAIN: "Other template", } - const prompt = codeActionPrompt.create( + const prompt = supportPrompt.create( "EXPLAIN", { filePath: testFilePath, diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 4ce9f93..406e981 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -1,21 +1,4 @@ -// Separate enhance prompt type and definition -export type EnhanceConfig = { - prompt: string -} - -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):", -} as const - -// Completely separate enhance prompt handling -export const enhancePrompt = { - default: enhance.prompt, - get: (customPrompts: Record | undefined): string => { - return customPrompts?.enhance ?? enhance.prompt - }, -} as const - -// Code action prompts +// Support prompts type PromptParams = Record const generateDiagnosticText = (diagnostics?: any[]) => { @@ -41,8 +24,7 @@ export const createPrompt = (template: string, params: PromptParams): string => return result } -const EXPLAIN_TEMPLATE = ` -Explain the following code from file path @/\${filePath}: +const EXPLAIN_TEMPLATE = `Explain the following code from file path @/\${filePath}: \${userInput} \`\`\` @@ -55,8 +37,7 @@ Please provide a clear and concise explanation of what this code does, including 3. Important patterns or techniques used ` -const FIX_TEMPLATE = ` -Fix any issues in the following code from file path @/\${filePath} +const FIX_TEMPLATE = `Fix any issues in the following code from file path @/\${filePath} \${diagnosticText} \${userInput} @@ -71,8 +52,7 @@ Please: 4. Explain what was fixed and why ` -const IMPROVE_TEMPLATE = ` -Improve the following code from file path @/\${filePath}: +const IMPROVE_TEMPLATE = `Improve the following code from file path @/\${filePath}: \${userInput} \`\`\` @@ -88,31 +68,36 @@ Please suggest improvements for: Provide the improved code along with explanations for each enhancement. ` +const ENHANCE_TEMPLATE = + "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):" + // Get template based on prompt type const defaultTemplates = { EXPLAIN: EXPLAIN_TEMPLATE, FIX: FIX_TEMPLATE, IMPROVE: IMPROVE_TEMPLATE, + ENHANCE: ENHANCE_TEMPLATE, } as const -type CodeActionType = keyof typeof defaultTemplates +type SupportPromptType = keyof typeof defaultTemplates -export const codeActionPrompt = { +export const supportPrompt = { default: defaultTemplates, - get: (customPrompts: Record | undefined, type: CodeActionType): string => { + get: (customPrompts: Record | undefined, type: SupportPromptType): string => { return customPrompts?.[type] ?? defaultTemplates[type] }, - create: (type: CodeActionType, params: PromptParams, customPrompts?: Record): string => { - const template = codeActionPrompt.get(customPrompts, type) + create: (type: SupportPromptType, params: PromptParams, customPrompts?: Record): string => { + const template = supportPrompt.get(customPrompts, type) return createPrompt(template, params) }, } as const -export type { CodeActionType } +export type { SupportPromptType } -// User-friendly labels for code action types -export const codeActionLabels: Record = { +// User-friendly labels for support prompt types +export const supportPromptLabels: Record = { FIX: "Fix Issues", EXPLAIN: "Explain Code", IMPROVE: "Improve Code", + ENHANCE: "Enhance Prompt", } as const diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index a767fd4..6f5085a 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -9,12 +9,7 @@ import { } from "@vscode/webview-ui-toolkit/react" import { useExtensionState } from "../../context/ExtensionStateContext" import { Mode, PromptComponent, getRoleDefinition, getAllModes, ModeConfig } from "../../../../src/shared/modes" -import { - enhancePrompt, - codeActionPrompt, - CodeActionType, - codeActionLabels, -} from "../../../../src/shared/support-prompt" +import { supportPrompt, SupportPromptType, supportPromptLabels } from "../../../../src/shared/support-prompt" import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups" import { vscode } from "../../utils/vscode" @@ -50,7 +45,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const [selectedPromptTitle, setSelectedPromptTitle] = useState("") const [isToolsEditMode, setIsToolsEditMode] = useState(false) const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false) - const [activeCodeActionTab, setActiveCodeActionTab] = useState("FIX") + const [activeSupportTab, setActiveSupportTab] = useState("EXPLAIN") // Direct update functions const updateAgentPrompt = useCallback( @@ -255,16 +250,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { return () => window.removeEventListener("message", handler) }, []) - const updateEnhancePrompt = (value: string | undefined) => { - vscode.postMessage({ - type: "updateSupportPrompt", - values: { - enhance: value, - }, - }) - } - - const updateCodeActionPrompt = (type: CodeActionType, value: string | undefined) => { + const updateSupportPrompt = (type: SupportPromptType, value: string | undefined) => { vscode.postMessage({ type: "updateSupportPrompt", values: { @@ -273,14 +259,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { }) } - const handleEnhancePromptChange = (e: Event | React.FormEvent): void => { - const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value - const trimmedValue = value.trim() - if (trimmedValue !== enhancePrompt.default) { - updateEnhancePrompt(trimmedValue || enhancePrompt.default) - } - } - const handleAgentReset = (modeSlug: string) => { // Only reset role definition for built-in modes const existingPrompt = customPrompts?.[modeSlug] as PromptComponent @@ -290,26 +268,15 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { }) } - const handleEnhanceReset = () => { - vscode.postMessage({ - type: "resetSupportPrompt", - text: "enhance", - }) - } - - const handleCodeActionReset = (type: CodeActionType) => { + const handleSupportReset = (type: SupportPromptType) => { vscode.postMessage({ type: "resetSupportPrompt", text: type, }) } - const getEnhancePromptValue = (): string => { - return enhancePrompt.get(customPrompts) - } - - const getCodeActionPromptValue = (type: CodeActionType): string => { - return codeActionPrompt.get(customPrompts, type) + const getSupportPromptValue = (type: SupportPromptType): string => { + return supportPrompt.get(customPrompts, type) } const handleTestEnhancement = () => { @@ -786,7 +753,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
-
Code Action Prompts
+
Support Prompts
{ paddingBottom: "4px", paddingRight: "20px", }}> - {Object.keys(codeActionPrompt.default).map((type) => ( + {Object.keys(supportPrompt.default).map((type) => ( ))}
{/* Show active tab content */} -
+
{ alignItems: "center", marginBottom: "4px", }}> -
{activeCodeActionTab} Prompt
+
{activeSupportTab} Prompt
handleCodeActionReset(activeCodeActionTab)} - title={`Reset ${activeCodeActionTab} prompt to default`}> + onClick={() => handleSupportReset(activeSupportTab)} + title={`Reset ${activeSupportTab} prompt to default`}>
+ + {activeSupportTab === "ENHANCE" && ( +
+
+ Use prompt enhancement to get tailored suggestions or improvements for your inputs. + This ensures Roo understands your intent and provides the best possible responses. +
+
+
+
API Configuration
+
+ You can select an API configuration to always use for enhancing prompts, or + just use whatever is currently selected +
+
+ { + const value = e.detail?.target?.value || e.target?.value + setEnhancementApiConfigId(value) + vscode.postMessage({ + type: "enhancementApiConfigId", + text: value, + }) + }} + style={{ width: "300px" }}> + Use currently selected API configuration + {(listApiConfigMeta || []).map((config) => ( + + {config.name} + + ))} + +
+
+ )} + { const value = (e as CustomEvent)?.detail?.target?.value || ((e as any).target as HTMLTextAreaElement).value const trimmedValue = value.trim() - updateCodeActionPrompt(activeCodeActionTab, trimmedValue || undefined) + updateSupportPrompt(activeSupportTab, trimmedValue || undefined) }} rows={4} resize="vertical" style={{ width: "100%" }} /> -
-
-

Prompt Enhancement

- -
- Use prompt enhancement to get tailored suggestions or improvements for your inputs. This ensures Roo - understands your intent and provides the best possible responses. -
- -
-
-
-
-
API Configuration
-
- You can select an API configuration to always use for enhancing prompts, or just use - whatever is currently selected -
-
- { - const value = e.detail?.target?.value || e.target?.value - setEnhancementApiConfigId(value) - vscode.postMessage({ - type: "enhancementApiConfigId", - text: value, - }) - }} - style={{ width: "300px" }}> - Use currently selected API configuration - {(listApiConfigMeta || []).map((config) => ( - - {config.name} - - ))} - -
- -
-
-
Enhancement Prompt
-
+ {activeSupportTab === "ENHANCE" && ( +
+ setTestPrompt((e.target as HTMLTextAreaElement).value)} + placeholder="Enter a prompt to test the enhancement" + rows={3} + resize="vertical" + style={{ width: "100%" }} + data-testid="test-prompt-textarea" + /> +
- + onClick={handleTestEnhancement} + disabled={isEnhancing} + appearance="primary"> + Preview Prompt Enhancement
-
- This prompt will be used to refine your input when you hit the sparkle icon in chat. -
-
- - -
- setTestPrompt((e.target as HTMLTextAreaElement).value)} - placeholder="Enter a prompt to test the enhancement" - rows={3} - resize="vertical" - style={{ width: "100%" }} - data-testid="test-prompt-textarea" - /> -
- - Preview Prompt Enhancement - -
-
+ )}
- - {/* Bottom padding */} -
{isCreateModeDialogOpen && ( diff --git a/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx b/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx index 5619d26..93e8698 100644 --- a/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx +++ b/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx @@ -166,6 +166,10 @@ describe("PromptsView", () => { it("handles API configuration selection", () => { renderPromptsView() + // Click the ENHANCE tab first to show the API config dropdown + const enhanceTab = screen.getByTestId("ENHANCE-tab") + fireEvent.click(enhanceTab) + const dropdown = screen.getByTestId("api-config-dropdown") fireEvent( dropdown, From 149e86ed0a6464cbb76d2562239faa3c1e2a4b87 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Fri, 24 Jan 2025 00:51:35 +0700 Subject: [PATCH 59/66] fix comment on pr --- package.json | 188 +++++------ src/core/CodeActionProvider.ts | 300 +++++++++--------- src/core/__tests__/CodeActionProvider.test.ts | 264 +++++++-------- src/extension.ts | 6 +- src/test/extension.test.ts | 43 ++- 5 files changed, 398 insertions(+), 403 deletions(-) diff --git a/package.json b/package.json index 7715001..e5cf882 100644 --- a/package.json +++ b/package.json @@ -72,102 +72,102 @@ "title": "New Task", "icon": "$(add)" }, - { - "command": "roo-cline.mcpButtonClicked", - "title": "MCP Servers", - "icon": "$(server)" - }, - { - "command": "roo-cline.promptsButtonClicked", - "title": "Prompts", - "icon": "$(notebook)" - }, - { - "command": "roo-cline.historyButtonClicked", - "title": "History", - "icon": "$(history)" - }, - { - "command": "roo-cline.popoutButtonClicked", - "title": "Open in Editor", - "icon": "$(link-external)" - }, - { - "command": "roo-cline.settingsButtonClicked", - "title": "Settings", - "icon": "$(settings-gear)" - }, - { - "command": "roo-cline.openInNewTab", - "title": "Open In New Tab", - "category": "Roo Code" - }, - { - "command": "roo-cline.explainCode", - "title": "Explain Code", - "category": "Roo Cline" - }, - { - "command": "roo-cline.fixCode", - "title": "Fix Code", - "category": "Roo Cline" - }, - { - "command": "roo-cline.improveCode", - "title": "Improve Code", - "category": "Roo Cline" - } + { + "command": "roo-cline.mcpButtonClicked", + "title": "MCP Servers", + "icon": "$(server)" + }, + { + "command": "roo-cline.promptsButtonClicked", + "title": "Prompts", + "icon": "$(notebook)" + }, + { + "command": "roo-cline.historyButtonClicked", + "title": "History", + "icon": "$(history)" + }, + { + "command": "roo-cline.popoutButtonClicked", + "title": "Open in Editor", + "icon": "$(link-external)" + }, + { + "command": "roo-cline.settingsButtonClicked", + "title": "Settings", + "icon": "$(settings-gear)" + }, + { + "command": "roo-cline.openInNewTab", + "title": "Open In New Tab", + "category": "Roo Code" + }, + { + "command": "roo-cline.explainCode", + "title": "Explain Code", + "category": "Roo Code" + }, + { + "command": "roo-cline.fixCode", + "title": "Fix Code", + "category": "Roo Code" + }, + { + "command": "roo-cline.improveCode", + "title": "Improve Code", + "category": "Roo Code" + } ], "menus": { - "editor/context": [ - { - "command": "roo-cline.explainCode", - "when": "editorHasSelection", - "group": "Roo Cline@1" - }, - { - "command": "roo-cline.fixCode", - "when": "editorHasSelection", - "group": "Roo Cline@2" - }, - { - "command": "roo-cline.improveCode", - "when": "editorHasSelection", - "group": "Roo Cline@3" - } - ], - "view/title": [ - { - "command": "roo-cline.plusButtonClicked", - "group": "navigation@1", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.promptsButtonClicked", - "group": "navigation@2", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.mcpButtonClicked", - "group": "navigation@3", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.historyButtonClicked", - "group": "navigation@4", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.popoutButtonClicked", - "group": "navigation@5", - "when": "view == roo-cline.SidebarProvider" - }, - { - "command": "roo-cline.settingsButtonClicked", - "group": "navigation@6", - "when": "view == roo-cline.SidebarProvider" - } - ] + "editor/context": [ + { + "command": "roo-cline.explainCode", + "when": "editorHasSelection", + "group": "Roo Code@1" + }, + { + "command": "roo-cline.fixCode", + "when": "editorHasSelection", + "group": "Roo Code@2" + }, + { + "command": "roo-cline.improveCode", + "when": "editorHasSelection", + "group": "Roo Code@3" + } + ], + "view/title": [ + { + "command": "roo-cline.plusButtonClicked", + "group": "navigation@1", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.promptsButtonClicked", + "group": "navigation@2", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.mcpButtonClicked", + "group": "navigation@3", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.historyButtonClicked", + "group": "navigation@4", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.popoutButtonClicked", + "group": "navigation@5", + "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.settingsButtonClicked", + "group": "navigation@6", + "when": "view == roo-cline.SidebarProvider" + } + ] }, "configuration": { "title": "Roo Code", diff --git a/src/core/CodeActionProvider.ts b/src/core/CodeActionProvider.ts index cac2bdc..d3b980a 100644 --- a/src/core/CodeActionProvider.ts +++ b/src/core/CodeActionProvider.ts @@ -1,181 +1,179 @@ -import * as vscode from 'vscode'; -import * as path from 'path'; +import * as vscode from "vscode" +import * as path from "path" export const ACTION_NAMES = { - EXPLAIN: 'Roo Cline: Explain Code', - FIX: 'Roo Cline: Fix Code', - IMPROVE: 'Roo Cline: Improve Code' -} as const; + EXPLAIN: "Roo Code: Explain Code", + FIX: "Roo Code: Fix Code", + IMPROVE: "Roo Code: Improve Code", +} as const const COMMAND_IDS = { - EXPLAIN: 'roo-cline.explainCode', - FIX: 'roo-cline.fixCode', - IMPROVE: 'roo-cline.improveCode' -} as const; + EXPLAIN: "roo-cline.explainCode", + FIX: "roo-cline.fixCode", + IMPROVE: "roo-cline.improveCode", +} as const interface DiagnosticData { - message: string; - severity: vscode.DiagnosticSeverity; - code?: string | number | { value: string | number; target: vscode.Uri }; - source?: string; - range: vscode.Range; + message: string + severity: vscode.DiagnosticSeverity + code?: string | number | { value: string | number; target: vscode.Uri } + source?: string + range: vscode.Range } interface EffectiveRange { - range: vscode.Range; - text: string; + range: vscode.Range + text: string } export class CodeActionProvider implements vscode.CodeActionProvider { - public static readonly providedCodeActionKinds = [ - vscode.CodeActionKind.QuickFix, - vscode.CodeActionKind.RefactorRewrite, - ]; + public static readonly providedCodeActionKinds = [ + vscode.CodeActionKind.QuickFix, + vscode.CodeActionKind.RefactorRewrite, + ] - // Cache file paths for performance - private readonly filePathCache = new WeakMap(); + // Cache file paths for performance + private readonly filePathCache = new WeakMap() - private getEffectiveRange( - document: vscode.TextDocument, - range: vscode.Range | vscode.Selection - ): EffectiveRange | null { - try { - const selectedText = document.getText(range); - if (selectedText) { - return { range, text: selectedText }; - } + private getEffectiveRange( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + ): EffectiveRange | null { + try { + const selectedText = document.getText(range) + if (selectedText) { + return { range, text: selectedText } + } - const currentLine = document.lineAt(range.start.line); - if (!currentLine.text.trim()) { - return null; - } + const currentLine = document.lineAt(range.start.line) + if (!currentLine.text.trim()) { + return null + } - // Optimize range creation by checking bounds first - const startLine = Math.max(0, currentLine.lineNumber - 1); - const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1); - - // Only create new positions if needed - const effectiveRange = new vscode.Range( - startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0), - endLine === currentLine.lineNumber ? range.end : new vscode.Position(endLine, document.lineAt(endLine).text.length) - ); + // Optimize range creation by checking bounds first + const startLine = Math.max(0, currentLine.lineNumber - 1) + const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1) - return { - range: effectiveRange, - text: document.getText(effectiveRange) - }; - } catch (error) { - console.error('Error getting effective range:', error); - return null; - } - } + // Only create new positions if needed + const effectiveRange = new vscode.Range( + startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0), + endLine === currentLine.lineNumber + ? range.end + : new vscode.Position(endLine, document.lineAt(endLine).text.length), + ) - private getFilePath(document: vscode.TextDocument): string { - // Check cache first - let filePath = this.filePathCache.get(document); - if (filePath) { - return filePath; - } + return { + range: effectiveRange, + text: document.getText(effectiveRange), + } + } catch (error) { + console.error("Error getting effective range:", error) + return null + } + } - try { - const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); - if (!workspaceFolder) { - filePath = document.uri.fsPath; - } else { - const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath); - filePath = (!relativePath || relativePath.startsWith('..')) ? document.uri.fsPath : relativePath; - } + private getFilePath(document: vscode.TextDocument): string { + // Check cache first + let filePath = this.filePathCache.get(document) + if (filePath) { + return filePath + } - // Cache the result - this.filePathCache.set(document, filePath); - return filePath; - } catch (error) { - console.error('Error getting file path:', error); - return document.uri.fsPath; - } - } + try { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri) + if (!workspaceFolder) { + filePath = document.uri.fsPath + } else { + const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath) + filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath + } - private createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData { - return { - message: diagnostic.message, - severity: diagnostic.severity, - code: diagnostic.code, - source: diagnostic.source, - range: diagnostic.range // Reuse the range object - }; - } + // Cache the result + this.filePathCache.set(document, filePath) + return filePath + } catch (error) { + console.error("Error getting file path:", error) + return document.uri.fsPath + } + } - private createAction( - title: string, - kind: vscode.CodeActionKind, - command: string, - args: any[] - ): vscode.CodeAction { - const action = new vscode.CodeAction(title, kind); - action.command = { command, title, arguments: args }; - return action; - } + private createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData { + return { + message: diagnostic.message, + severity: diagnostic.severity, + code: diagnostic.code, + source: diagnostic.source, + range: diagnostic.range, // Reuse the range object + } + } - private hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean { - // Optimize range intersection check - return !( - range2.end.line < range1.start.line || - range2.start.line > range1.end.line || - (range2.end.line === range1.start.line && range2.end.character < range1.start.character) || - (range2.start.line === range1.end.line && range2.start.character > range1.end.character) - ); - } + private createAction(title: string, kind: vscode.CodeActionKind, command: string, args: any[]): vscode.CodeAction { + const action = new vscode.CodeAction(title, kind) + action.command = { command, title, arguments: args } + return action + } - public provideCodeActions( - document: vscode.TextDocument, - range: vscode.Range | vscode.Selection, - context: vscode.CodeActionContext - ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { - try { - const effectiveRange = this.getEffectiveRange(document, range); - if (!effectiveRange) { - return []; - } + private hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean { + // Optimize range intersection check + return !( + range2.end.line < range1.start.line || + range2.start.line > range1.end.line || + (range2.end.line === range1.start.line && range2.end.character < range1.start.character) || + (range2.start.line === range1.end.line && range2.start.character > range1.end.character) + ) + } - const filePath = this.getFilePath(document); - const actions: vscode.CodeAction[] = []; + public provideCodeActions( + document: vscode.TextDocument, + range: vscode.Range | vscode.Selection, + context: vscode.CodeActionContext, + ): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { + try { + const effectiveRange = this.getEffectiveRange(document, range) + if (!effectiveRange) { + return [] + } - // Create actions using helper method - actions.push(this.createAction( - ACTION_NAMES.EXPLAIN, - vscode.CodeActionKind.QuickFix, - COMMAND_IDS.EXPLAIN, - [filePath, effectiveRange.text] - )); + const filePath = this.getFilePath(document) + const actions: vscode.CodeAction[] = [] - // Only process diagnostics if they exist - if (context.diagnostics.length > 0) { - const relevantDiagnostics = context.diagnostics.filter(d => - this.hasIntersectingRange(effectiveRange.range, d.range) - ); + // Create actions using helper method + actions.push( + this.createAction(ACTION_NAMES.EXPLAIN, vscode.CodeActionKind.QuickFix, COMMAND_IDS.EXPLAIN, [ + filePath, + effectiveRange.text, + ]), + ) - if (relevantDiagnostics.length > 0) { - const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData); - actions.push(this.createAction( - ACTION_NAMES.FIX, - vscode.CodeActionKind.QuickFix, - COMMAND_IDS.FIX, - [filePath, effectiveRange.text, diagnosticMessages] - )); - } - } + // Only process diagnostics if they exist + if (context.diagnostics.length > 0) { + const relevantDiagnostics = context.diagnostics.filter((d) => + this.hasIntersectingRange(effectiveRange.range, d.range), + ) - actions.push(this.createAction( - ACTION_NAMES.IMPROVE, - vscode.CodeActionKind.RefactorRewrite, - COMMAND_IDS.IMPROVE, - [filePath, effectiveRange.text] - )); + if (relevantDiagnostics.length > 0) { + const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData) + actions.push( + this.createAction(ACTION_NAMES.FIX, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [ + filePath, + effectiveRange.text, + diagnosticMessages, + ]), + ) + } + } - return actions; - } catch (error) { - console.error('Error providing code actions:', error); - return []; - } - } -} \ No newline at end of file + actions.push( + this.createAction(ACTION_NAMES.IMPROVE, vscode.CodeActionKind.RefactorRewrite, COMMAND_IDS.IMPROVE, [ + filePath, + effectiveRange.text, + ]), + ) + + return actions + } catch (error) { + console.error("Error providing code actions:", error) + return [] + } + } +} diff --git a/src/core/__tests__/CodeActionProvider.test.ts b/src/core/__tests__/CodeActionProvider.test.ts index cdc1acf..d0bfc8e 100644 --- a/src/core/__tests__/CodeActionProvider.test.ts +++ b/src/core/__tests__/CodeActionProvider.test.ts @@ -1,145 +1,147 @@ -import * as vscode from 'vscode'; -import { CodeActionProvider } from '../CodeActionProvider'; +import * as vscode from "vscode" +import { CodeActionProvider } from "../CodeActionProvider" // Mock VSCode API -jest.mock('vscode', () => ({ - CodeAction: jest.fn().mockImplementation((title, kind) => ({ - title, - kind, - command: undefined - })), - CodeActionKind: { - QuickFix: { value: 'quickfix' }, - RefactorRewrite: { value: 'refactor.rewrite' } - }, - Range: jest.fn().mockImplementation((startLine, startChar, endLine, endChar) => ({ - start: { line: startLine, character: startChar }, - end: { line: endLine, character: endChar } - })), - Position: jest.fn().mockImplementation((line, character) => ({ - line, - character - })), - workspace: { - getWorkspaceFolder: jest.fn() - }, - DiagnosticSeverity: { - Error: 0, - Warning: 1, - Information: 2, - Hint: 3 - } -})); +jest.mock("vscode", () => ({ + CodeAction: jest.fn().mockImplementation((title, kind) => ({ + title, + kind, + command: undefined, + })), + CodeActionKind: { + QuickFix: { value: "quickfix" }, + RefactorRewrite: { value: "refactor.rewrite" }, + }, + Range: jest.fn().mockImplementation((startLine, startChar, endLine, endChar) => ({ + start: { line: startLine, character: startChar }, + end: { line: endLine, character: endChar }, + })), + Position: jest.fn().mockImplementation((line, character) => ({ + line, + character, + })), + workspace: { + getWorkspaceFolder: jest.fn(), + }, + DiagnosticSeverity: { + Error: 0, + Warning: 1, + Information: 2, + Hint: 3, + }, +})) -describe('CodeActionProvider', () => { - let provider: CodeActionProvider; - let mockDocument: any; - let mockRange: any; - let mockContext: any; +describe("CodeActionProvider", () => { + let provider: CodeActionProvider + let mockDocument: any + let mockRange: any + let mockContext: any - beforeEach(() => { - provider = new CodeActionProvider(); - - // Mock document - mockDocument = { - getText: jest.fn(), - lineAt: jest.fn(), - lineCount: 10, - uri: { fsPath: '/test/file.ts' } - }; + beforeEach(() => { + provider = new CodeActionProvider() - // Mock range - mockRange = new vscode.Range(0, 0, 0, 10); + // Mock document + mockDocument = { + getText: jest.fn(), + lineAt: jest.fn(), + lineCount: 10, + uri: { fsPath: "/test/file.ts" }, + } - // Mock context - mockContext = { - diagnostics: [] - }; - }); + // Mock range + mockRange = new vscode.Range(0, 0, 0, 10) - describe('getEffectiveRange', () => { - it('should return selected text when available', () => { - mockDocument.getText.mockReturnValue('selected text'); - - const result = (provider as any).getEffectiveRange(mockDocument, mockRange); - - expect(result).toEqual({ - range: mockRange, - text: 'selected text' - }); - }); + // Mock context + mockContext = { + diagnostics: [], + } + }) - it('should return null for empty line', () => { - mockDocument.getText.mockReturnValue(''); - mockDocument.lineAt.mockReturnValue({ text: '', lineNumber: 0 }); - - const result = (provider as any).getEffectiveRange(mockDocument, mockRange); - - expect(result).toBeNull(); - }); - }); + describe("getEffectiveRange", () => { + it("should return selected text when available", () => { + mockDocument.getText.mockReturnValue("selected text") - describe('getFilePath', () => { - it('should return relative path when in workspace', () => { - const mockWorkspaceFolder = { - uri: { fsPath: '/test' } - }; - (vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder); - - const result = (provider as any).getFilePath(mockDocument); - - expect(result).toBe('file.ts'); - }); + const result = (provider as any).getEffectiveRange(mockDocument, mockRange) - it('should return absolute path when not in workspace', () => { - (vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null); - - const result = (provider as any).getFilePath(mockDocument); - - expect(result).toBe('/test/file.ts'); - }); - }); + expect(result).toEqual({ + range: mockRange, + text: "selected text", + }) + }) - describe('provideCodeActions', () => { - beforeEach(() => { - mockDocument.getText.mockReturnValue('test code'); - mockDocument.lineAt.mockReturnValue({ text: 'test code', lineNumber: 0 }); - }); + it("should return null for empty line", () => { + mockDocument.getText.mockReturnValue("") + mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 }) - it('should provide explain and improve actions by default', () => { - const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext); - - expect(actions).toHaveLength(2); - expect((actions as any)[0].title).toBe('Roo Cline: Explain Code'); - expect((actions as any)[1].title).toBe('Roo Cline: Improve Code'); - }); + const result = (provider as any).getEffectiveRange(mockDocument, mockRange) - it('should provide fix action when diagnostics exist', () => { - mockContext.diagnostics = [{ - message: 'test error', - severity: vscode.DiagnosticSeverity.Error, - range: mockRange - }]; - - const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext); - - expect(actions).toHaveLength(3); - expect((actions as any).some((a: any) => a.title === 'Roo Cline: Fix Code')).toBe(true); - }); + expect(result).toBeNull() + }) + }) - it('should handle errors gracefully', () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - mockDocument.getText.mockImplementation(() => { - throw new Error('Test error'); - }); - mockDocument.lineAt.mockReturnValue({ text: 'test', lineNumber: 0 }); - - const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext); - - expect(actions).toEqual([]); - expect(consoleErrorSpy).toHaveBeenCalledWith('Error getting effective range:', expect.any(Error)); - - consoleErrorSpy.mockRestore(); - }); - }); -}); \ No newline at end of file + describe("getFilePath", () => { + it("should return relative path when in workspace", () => { + const mockWorkspaceFolder = { + uri: { fsPath: "/test" }, + } + ;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder) + + const result = (provider as any).getFilePath(mockDocument) + + expect(result).toBe("file.ts") + }) + + it("should return absolute path when not in workspace", () => { + ;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null) + + const result = (provider as any).getFilePath(mockDocument) + + expect(result).toBe("/test/file.ts") + }) + }) + + describe("provideCodeActions", () => { + beforeEach(() => { + mockDocument.getText.mockReturnValue("test code") + mockDocument.lineAt.mockReturnValue({ text: "test code", lineNumber: 0 }) + }) + + it("should provide explain and improve actions by default", () => { + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + + expect(actions).toHaveLength(2) + expect((actions as any)[0].title).toBe("Roo Code: Explain Code") + expect((actions as any)[1].title).toBe("Roo Code: Improve Code") + }) + + it("should provide fix action when diagnostics exist", () => { + mockContext.diagnostics = [ + { + message: "test error", + severity: vscode.DiagnosticSeverity.Error, + range: mockRange, + }, + ] + + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + + expect(actions).toHaveLength(3) + expect((actions as any).some((a: any) => a.title === "Roo Code: Fix Code")).toBe(true) + }) + + it("should handle errors gracefully", () => { + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}) + mockDocument.getText.mockImplementation(() => { + throw new Error("Test error") + }) + mockDocument.lineAt.mockReturnValue({ text: "test", lineNumber: 0 }) + + const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext) + + expect(actions).toEqual([]) + expect(consoleErrorSpy).toHaveBeenCalledWith("Error getting effective range:", expect.any(Error)) + + consoleErrorSpy.mockRestore() + }) + }) +}) diff --git a/src/extension.ts b/src/extension.ts index db011e0..b03e9e5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -201,7 +201,7 @@ export function activate(context: vscode.ExtensionContext) { context, "roo-cline.explainCode", "EXPLAIN", - "Any specific questions about this code?", + "What would you like Roo to explain?", "E.g. How does the error handling work?", ) @@ -209,7 +209,7 @@ export function activate(context: vscode.ExtensionContext) { context, "roo-cline.fixCode", "FIX", - "Any specific concerns about fixing this code?", + "What would you like Roo to fix?", "E.g. Maintain backward compatibility", ) @@ -217,7 +217,7 @@ export function activate(context: vscode.ExtensionContext) { context, "roo-cline.improveCode", "IMPROVE", - "Any specific aspects you want to improve?", + "What would you like Roo to improve?", "E.g. Focus on performance optimization", ) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index a3237e2..aa6b780 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -8,8 +8,8 @@ const dotenv = require("dotenv") const testEnvPath = path.join(__dirname, ".test_env") dotenv.config({ path: testEnvPath }) -suite("Roo Cline Extension Test Suite", () => { - vscode.window.showInformationMessage("Starting Roo Cline extension tests.") +suite("Roo Code Extension Test Suite", () => { + vscode.window.showInformationMessage("Starting Roo Code extension tests.") test("Extension should be present", () => { const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline") @@ -117,16 +117,16 @@ suite("Roo Cline Extension Test Suite", () => { // Test core commands are registered const expectedCommands = [ - 'roo-cline.plusButtonClicked', - 'roo-cline.mcpButtonClicked', - 'roo-cline.historyButtonClicked', - 'roo-cline.popoutButtonClicked', - 'roo-cline.settingsButtonClicked', - 'roo-cline.openInNewTab', - 'roo-cline.explainCode', - 'roo-cline.fixCode', - 'roo-cline.improveCode' - ]; + "roo-cline.plusButtonClicked", + "roo-cline.mcpButtonClicked", + "roo-cline.historyButtonClicked", + "roo-cline.popoutButtonClicked", + "roo-cline.settingsButtonClicked", + "roo-cline.openInNewTab", + "roo-cline.explainCode", + "roo-cline.fixCode", + "roo-cline.improveCode", + ] for (const cmd of expectedCommands) { assert.strictEqual(commands.includes(cmd), true, `Command ${cmd} should be registered`) @@ -136,7 +136,7 @@ suite("Roo Cline Extension Test Suite", () => { test("Views should be registered", () => { const view = vscode.window.createWebviewPanel( "roo-cline.SidebarProvider", - "Roo Cline", + "Roo Code", vscode.ViewColumn.One, {}, ) @@ -184,17 +184,12 @@ suite("Roo Cline Extension Test Suite", () => { // Create webview panel with development options const extensionUri = extension.extensionUri - const panel = vscode.window.createWebviewPanel( - "roo-cline.SidebarProvider", - "Roo Cline", - vscode.ViewColumn.One, - { - enableScripts: true, - enableCommandUris: true, - retainContextWhenHidden: true, - localResourceRoots: [extensionUri], - }, - ) + const panel = vscode.window.createWebviewPanel("roo-cline.SidebarProvider", "Roo Code", vscode.ViewColumn.One, { + enableScripts: true, + enableCommandUris: true, + retainContextWhenHidden: true, + localResourceRoots: [extensionUri], + }) try { // Initialize webview with development context From 085d42873c8c8b6e04da2023198328dfc3b64781 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Fri, 24 Jan 2025 01:14:48 +0700 Subject: [PATCH 60/66] refactor: generalize prompt enhancement into single completion handler - Rename enhance-prompt.ts to single-completion-handler.ts for better clarity - Refactor enhancement logic to be more generic and reusable - Update prompt template handling to use template literals - Adjust tests and imports accordingly --- src/core/webview/ClineProvider.ts | 14 +++++--- src/shared/__tests__/support-prompts.test.ts | 5 ++- src/shared/support-prompt.ts | 5 +-- src/utils/__tests__/enhance-prompt.test.ts | 32 +++++++++++++------ ...prompt.ts => single-completion-handler.ts} | 11 ++----- 5 files changed, 39 insertions(+), 28 deletions(-) rename src/utils/{enhance-prompt.ts => single-completion-handler.ts} (71%) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b28bad6..797def3 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -36,7 +36,7 @@ import { getNonce } from "./getNonce" import { getUri } from "./getUri" import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound" import { checkExistKey } from "../../shared/checkExistApiConfig" -import { enhancePrompt } from "../../utils/enhance-prompt" +import { singleCompletionHandler } from "../../utils/single-completion-handler" import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git" import { ConfigManager } from "../config/ConfigManager" import { CustomModesManager } from "../config/CustomModesManager" @@ -996,11 +996,17 @@ export class ClineProvider implements vscode.WebviewViewProvider { } } - const enhancedPrompt = await enhancePrompt( + const enhancedPrompt = await singleCompletionHandler( configToUse, - message.text, - supportPrompt.get(customPrompts, "ENHANCE"), + supportPrompt.create( + "ENHANCE", + { + userInput: message.text, + }, + customPrompts, + ), ) + await this.postMessageToWebview({ type: "enhancedPrompt", text: enhancedPrompt, diff --git a/src/shared/__tests__/support-prompts.test.ts b/src/shared/__tests__/support-prompts.test.ts index ee7253e..cd27a38 100644 --- a/src/shared/__tests__/support-prompts.test.ts +++ b/src/shared/__tests__/support-prompts.test.ts @@ -77,12 +77,11 @@ describe("Code Action Prompts", () => { describe("ENHANCE action", () => { it("should format enhance prompt correctly", () => { const prompt = supportPrompt.create("ENHANCE", { - filePath: testFilePath, - selectedText: testCode, + userInput: "test", }) expect(prompt).toBe( - "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):", + "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):\n\ntest", ) // Verify it ignores parameters since ENHANCE template doesn't use any expect(prompt).not.toContain(testFilePath) diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 406e981..715d8c6 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -68,8 +68,9 @@ Please suggest improvements for: Provide the improved code along with explanations for each enhancement. ` -const ENHANCE_TEMPLATE = - "Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):" +const ENHANCE_TEMPLATE = `Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes): + +\${userInput}` // Get template based on prompt type const defaultTemplates = { diff --git a/src/utils/__tests__/enhance-prompt.test.ts b/src/utils/__tests__/enhance-prompt.test.ts index 69fdd04..d3cca04 100644 --- a/src/utils/__tests__/enhance-prompt.test.ts +++ b/src/utils/__tests__/enhance-prompt.test.ts @@ -1,7 +1,7 @@ -import { enhancePrompt } from "../enhance-prompt" +import { singleCompletionHandler } from "../single-completion-handler" import { ApiConfiguration } from "../../shared/api" import { buildApiHandler, SingleCompletionHandler } from "../../api" -import { defaultPrompts } from "../../shared/modes" +import { supportPrompt } from "../../shared/support-prompt" // Mock the API handler jest.mock("../../api", () => ({ @@ -34,17 +34,29 @@ describe("enhancePrompt", () => { }) it("enhances prompt using default enhancement prompt when no custom prompt provided", async () => { - const result = await enhancePrompt(mockApiConfig, "Test prompt") + const result = await singleCompletionHandler(mockApiConfig, "Test prompt") expect(result).toBe("Enhanced prompt") const handler = buildApiHandler(mockApiConfig) - expect((handler as any).completePrompt).toHaveBeenCalledWith(`${defaultPrompts.enhance}\n\nTest prompt`) + expect((handler as any).completePrompt).toHaveBeenCalledWith(`Test prompt`) }) it("enhances prompt using custom enhancement prompt when provided", async () => { const customEnhancePrompt = "You are a custom prompt enhancer" + const customEnhancePromptWithTemplate = customEnhancePrompt + "\n\n${userInput}" - const result = await enhancePrompt(mockApiConfig, "Test prompt", customEnhancePrompt) + const result = await singleCompletionHandler( + mockApiConfig, + supportPrompt.create( + "ENHANCE", + { + userInput: "Test prompt", + }, + { + ENHANCE: customEnhancePromptWithTemplate, + }, + ), + ) expect(result).toBe("Enhanced prompt") const handler = buildApiHandler(mockApiConfig) @@ -52,11 +64,11 @@ describe("enhancePrompt", () => { }) it("throws error for empty prompt input", async () => { - await expect(enhancePrompt(mockApiConfig, "")).rejects.toThrow("No prompt text provided") + await expect(singleCompletionHandler(mockApiConfig, "")).rejects.toThrow("No prompt text provided") }) it("throws error for missing API configuration", async () => { - await expect(enhancePrompt({} as ApiConfiguration, "Test prompt")).rejects.toThrow( + await expect(singleCompletionHandler({} as ApiConfiguration, "Test prompt")).rejects.toThrow( "No valid API configuration provided", ) }) @@ -75,7 +87,7 @@ describe("enhancePrompt", () => { }), }) - await expect(enhancePrompt(mockApiConfig, "Test prompt")).rejects.toThrow( + await expect(singleCompletionHandler(mockApiConfig, "Test prompt")).rejects.toThrow( "The selected API provider does not support prompt enhancement", ) }) @@ -101,7 +113,7 @@ describe("enhancePrompt", () => { }), } as unknown as SingleCompletionHandler) - const result = await enhancePrompt(openRouterConfig, "Test prompt") + const result = await singleCompletionHandler(openRouterConfig, "Test prompt") expect(buildApiHandler).toHaveBeenCalledWith(openRouterConfig) expect(result).toBe("Enhanced prompt") @@ -121,6 +133,6 @@ describe("enhancePrompt", () => { }), } as unknown as SingleCompletionHandler) - await expect(enhancePrompt(mockApiConfig, "Test prompt")).rejects.toThrow("API Error") + await expect(singleCompletionHandler(mockApiConfig, "Test prompt")).rejects.toThrow("API Error") }) }) diff --git a/src/utils/enhance-prompt.ts b/src/utils/single-completion-handler.ts similarity index 71% rename from src/utils/enhance-prompt.ts rename to src/utils/single-completion-handler.ts index 3724757..5e049d4 100644 --- a/src/utils/enhance-prompt.ts +++ b/src/utils/single-completion-handler.ts @@ -1,16 +1,11 @@ import { ApiConfiguration } from "../shared/api" import { buildApiHandler, SingleCompletionHandler } from "../api" -import { defaultPrompts } from "../shared/modes" /** * Enhances a prompt using the configured API without creating a full Cline instance or task history. * This is a lightweight alternative that only uses the API's completion functionality. */ -export async function enhancePrompt( - apiConfiguration: ApiConfiguration, - promptText: string, - enhancePrompt?: string, -): Promise { +export async function singleCompletionHandler(apiConfiguration: ApiConfiguration, promptText: string): Promise { if (!promptText) { throw new Error("No prompt text provided") } @@ -25,7 +20,5 @@ export async function enhancePrompt( throw new Error("The selected API provider does not support prompt enhancement") } - const enhancePromptText = enhancePrompt ?? defaultPrompts.enhance - const prompt = `${enhancePromptText}\n\n${promptText}` - return (handler as SingleCompletionHandler).completePrompt(prompt) + return (handler as SingleCompletionHandler).completePrompt(promptText) } From f86e96d15712dab872d3616877cb80d12c557261 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Fri, 24 Jan 2025 01:46:33 +0700 Subject: [PATCH 61/66] refactor: separate mode and support prompts - Rename customPrompts to customModePrompts for mode-specific prompts - Add new customSupportPrompts type for support action prompts - Update types to be more specific (CustomModePrompts and CustomSupportPrompts) - Fix all related tests and component implementations --- src/core/Cline.ts | 4 +- src/core/prompts/__tests__/system.test.ts | 32 ++++----- src/core/prompts/system.ts | 6 +- src/core/webview/ClineProvider.ts | 68 +++++++++++-------- .../webview/__tests__/ClineProvider.test.ts | 30 ++++---- src/shared/ExtensionMessage.ts | 6 +- src/shared/__tests__/support-prompts.test.ts | 16 ++--- src/shared/modes.ts | 6 +- src/shared/support-prompt.ts | 12 ++-- .../chat/__tests__/AutoApproveMenu.test.tsx | 7 +- .../src/components/prompts/PromptsView.tsx | 17 ++--- .../prompts/__tests__/PromptsView.test.tsx | 2 +- .../src/context/ExtensionStateContext.tsx | 12 ++-- 13 files changed, 119 insertions(+), 99 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 5f8af77..0e2872b 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -809,7 +809,7 @@ export class Cline { }) } - const { browserViewportSize, mode, customPrompts, preferredLanguage } = + const { browserViewportSize, mode, customModePrompts, preferredLanguage } = (await this.providerRef.deref()?.getState()) ?? {} const { customModes } = (await this.providerRef.deref()?.getState()) ?? {} const systemPrompt = await (async () => { @@ -825,7 +825,7 @@ export class Cline { this.diffStrategy, browserViewportSize, mode, - customPrompts, + customModePrompts, customModes, this.customInstructions, preferredLanguage, diff --git a/src/core/prompts/__tests__/system.test.ts b/src/core/prompts/__tests__/system.test.ts index 6ecf7ef..4138d11 100644 --- a/src/core/prompts/__tests__/system.test.ts +++ b/src/core/prompts/__tests__/system.test.ts @@ -162,7 +162,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) @@ -178,7 +178,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy "1280x800", // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) @@ -196,7 +196,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) @@ -212,7 +212,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) @@ -228,7 +228,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy "900x600", // different viewport size defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) @@ -244,7 +244,7 @@ describe("SYSTEM_PROMPT", () => { new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions undefined, // preferredLanguage @@ -264,7 +264,7 @@ describe("SYSTEM_PROMPT", () => { new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions undefined, // preferredLanguage @@ -284,7 +284,7 @@ describe("SYSTEM_PROMPT", () => { new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions undefined, // preferredLanguage @@ -304,7 +304,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy undefined, // browserViewportSize defaultModeSlug, // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions "Spanish", // preferredLanguage @@ -334,7 +334,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // diffStrategy undefined, // browserViewportSize "custom-mode", // mode - undefined, // customPrompts + undefined, // customModePrompts customModes, // customModes "Global instructions", // globalCustomInstructions ) @@ -351,7 +351,7 @@ describe("SYSTEM_PROMPT", () => { }) it("should use promptComponent roleDefinition when available", async () => { - const customPrompts = { + const customModePrompts = { [defaultModeSlug]: { roleDefinition: "Custom prompt role definition", customInstructions: "Custom prompt instructions", @@ -366,7 +366,7 @@ describe("SYSTEM_PROMPT", () => { undefined, undefined, defaultModeSlug, - customPrompts, + customModePrompts, undefined, ) @@ -377,7 +377,7 @@ describe("SYSTEM_PROMPT", () => { }) it("should fallback to modeConfig roleDefinition when promptComponent has no roleDefinition", async () => { - const customPrompts = { + const customModePrompts = { [defaultModeSlug]: { customInstructions: "Custom prompt instructions", // No roleDefinition provided @@ -392,7 +392,7 @@ describe("SYSTEM_PROMPT", () => { undefined, undefined, defaultModeSlug, - customPrompts, + customModePrompts, undefined, ) @@ -432,7 +432,7 @@ describe("addCustomInstructions", () => { undefined, // diffStrategy undefined, // browserViewportSize "architect", // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) @@ -448,7 +448,7 @@ describe("addCustomInstructions", () => { undefined, // diffStrategy undefined, // browserViewportSize "ask", // mode - undefined, // customPrompts + undefined, // customModePrompts undefined, // customModes ) diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 2546adc..b77d243 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -1,7 +1,7 @@ import { Mode, modes, - CustomPrompts, + CustomModePrompts, PromptComponent, getRoleDefinition, defaultModeSlug, @@ -97,7 +97,7 @@ export const SYSTEM_PROMPT = async ( diffStrategy?: DiffStrategy, browserViewportSize?: string, mode: Mode = defaultModeSlug, - customPrompts?: CustomPrompts, + customModePrompts?: CustomModePrompts, customModes?: ModeConfig[], globalCustomInstructions?: string, preferredLanguage?: string, @@ -115,7 +115,7 @@ export const SYSTEM_PROMPT = async ( } // Check if it's a custom mode - const promptComponent = getPromptComponent(customPrompts?.[mode]) + const promptComponent = getPromptComponent(customModePrompts?.[mode]) // 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] diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 797def3..1cf7028 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -22,7 +22,7 @@ import { WebviewMessage } from "../../shared/WebviewMessage" import { Mode, modes, - CustomPrompts, + CustomModePrompts, PromptComponent, ModeConfig, defaultModeSlug, @@ -40,7 +40,7 @@ import { singleCompletionHandler } from "../../utils/single-completion-handler" import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git" import { ConfigManager } from "../config/ConfigManager" import { CustomModesManager } from "../config/CustomModesManager" -import { supportPrompt } from "../../shared/support-prompt" +import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt" import { ACTION_NAMES } from "../CodeActionProvider" @@ -111,7 +111,8 @@ type GlobalStateKey = | "vsCodeLmModelSelector" | "mode" | "modeApiConfigs" - | "customPrompts" + | "customModePrompts" + | "customSupportPrompts" | "enhancementApiConfigId" | "experimentalDiffStrategy" | "autoApprovalEnabled" @@ -203,9 +204,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { return } - const { customPrompts } = await visibleProvider.getState() + const { customSupportPrompts } = await visibleProvider.getState() - const prompt = supportPrompt.create(promptType, params, customPrompts) + const prompt = supportPrompt.create(promptType, params, customSupportPrompts) await visibleProvider.initClineWithTask(prompt) } @@ -296,7 +297,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.clearTask() const { apiConfiguration, - customPrompts, + customModePrompts, diffEnabled, fuzzyMatchThreshold, mode, @@ -304,7 +305,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { experimentalDiffStrategy, } = await this.getState() - const modePrompt = customPrompts?.[mode] as PromptComponent + const modePrompt = customModePrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") this.cline = new Cline( @@ -324,7 +325,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.clearTask() const { apiConfiguration, - customPrompts, + customModePrompts, diffEnabled, fuzzyMatchThreshold, mode, @@ -332,7 +333,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { experimentalDiffStrategy, } = await this.getState() - const modePrompt = customPrompts?.[mode] as PromptComponent + const modePrompt = customModePrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") this.cline = new Cline( @@ -817,14 +818,14 @@ export class ClineProvider implements vscode.WebviewViewProvider { return } - const existingPrompts = (await this.getGlobalState("customPrompts")) || {} + const existingPrompts = (await this.getGlobalState("customSupportPrompts")) || {} const updatedPrompts = { ...existingPrompts, ...message.values, } - await this.updateGlobalState("customPrompts", updatedPrompts) + await this.updateGlobalState("customSupportPrompts", updatedPrompts) await this.postStateToWebview() } catch (error) { console.error("Error update support prompt:", error) @@ -837,10 +838,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { return } - const existingPrompts = ((await this.getGlobalState("customPrompts")) || {}) as Record< - string, - any - > + const existingPrompts = ((await this.getGlobalState("customSupportPrompts")) || + {}) as Record const updatedPrompts = { ...existingPrompts, @@ -848,7 +847,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { updatedPrompts[message.text] = undefined - await this.updateGlobalState("customPrompts", updatedPrompts) + await this.updateGlobalState("customSupportPrompts", updatedPrompts) await this.postStateToWebview() } catch (error) { console.error("Error reset support prompt:", error) @@ -857,21 +856,21 @@ export class ClineProvider implements vscode.WebviewViewProvider { break case "updatePrompt": if (message.promptMode && message.customPrompt !== undefined) { - const existingPrompts = (await this.getGlobalState("customPrompts")) || {} + const existingPrompts = (await this.getGlobalState("customModePrompts")) || {} const updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt, } - await this.updateGlobalState("customPrompts", updatedPrompts) + await this.updateGlobalState("customModePrompts", updatedPrompts) - // Get current state and explicitly include customPrompts + // Get current state and explicitly include customModePrompts const currentState = await this.getState() const stateWithPrompts = { ...currentState, - customPrompts: updatedPrompts, + customModePrompts: updatedPrompts, } // Post state with prompts @@ -981,8 +980,12 @@ export class ClineProvider implements vscode.WebviewViewProvider { case "enhancePrompt": if (message.text) { try { - const { apiConfiguration, customPrompts, listApiConfigMeta, enhancementApiConfigId } = - await this.getState() + const { + apiConfiguration, + customSupportPrompts, + listApiConfigMeta, + enhancementApiConfigId, + } = await this.getState() // Try to get enhancement config first, fall back to current config let configToUse: ApiConfiguration = apiConfiguration @@ -1003,7 +1006,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { { userInput: message.text, }, - customPrompts, + customSupportPrompts, ), ) @@ -1024,7 +1027,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { try { const { apiConfiguration, - customPrompts, + customModePrompts, customInstructions, preferredLanguage, browserViewportSize, @@ -1054,7 +1057,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { diffStrategy, browserViewportSize ?? "900x600", mode, - customPrompts, + customModePrompts, customModes, customInstructions, preferredLanguage, @@ -1802,7 +1805,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { currentApiConfigName, listApiConfigMeta, mode, - customPrompts, + customModePrompts, + customSupportPrompts, enhancementApiConfigId, experimentalDiffStrategy, autoApprovalEnabled, @@ -1841,7 +1845,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { currentApiConfigName: currentApiConfigName ?? "default", listApiConfigMeta: listApiConfigMeta ?? [], mode: mode ?? defaultModeSlug, - customPrompts: customPrompts ?? {}, + customModePrompts: customModePrompts ?? {}, + customSupportPrompts: customSupportPrompts ?? {}, enhancementApiConfigId, experimentalDiffStrategy: experimentalDiffStrategy ?? false, autoApprovalEnabled: autoApprovalEnabled ?? false, @@ -1961,7 +1966,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { vsCodeLmModelSelector, mode, modeApiConfigs, - customPrompts, + customModePrompts, + customSupportPrompts, enhancementApiConfigId, experimentalDiffStrategy, autoApprovalEnabled, @@ -2026,7 +2032,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("vsCodeLmModelSelector") as Promise, this.getGlobalState("mode") as Promise, this.getGlobalState("modeApiConfigs") as Promise | undefined>, - this.getGlobalState("customPrompts") as Promise, + this.getGlobalState("customModePrompts") as Promise, + this.getGlobalState("customSupportPrompts") as Promise, this.getGlobalState("enhancementApiConfigId") as Promise, this.getGlobalState("experimentalDiffStrategy") as Promise, this.getGlobalState("autoApprovalEnabled") as Promise, @@ -2137,7 +2144,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { currentApiConfigName: currentApiConfigName ?? "default", listApiConfigMeta: listApiConfigMeta ?? [], modeApiConfigs: modeApiConfigs ?? ({} as Record), - customPrompts: customPrompts ?? {}, + customModePrompts: customModePrompts ?? {}, + customSupportPrompts: customSupportPrompts ?? {}, enhancementApiConfigId, experimentalDiffStrategy: experimentalDiffStrategy ?? false, autoApprovalEnabled: autoApprovalEnabled ?? false, diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 63dc2d5..cd1f295 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -555,7 +555,7 @@ describe("ClineProvider", () => { architect: "existing architect prompt", } ;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => { - if (key === "customPrompts") { + if (key === "customModePrompts") { return existingPrompts } return undefined @@ -569,7 +569,7 @@ describe("ClineProvider", () => { }) // Verify state was updated correctly - expect(mockContext.globalState.update).toHaveBeenCalledWith("customPrompts", { + expect(mockContext.globalState.update).toHaveBeenCalledWith("customModePrompts", { ...existingPrompts, code: "new code prompt", }) @@ -579,7 +579,7 @@ describe("ClineProvider", () => { expect.objectContaining({ type: "state", state: expect.objectContaining({ - customPrompts: { + customModePrompts: { ...existingPrompts, code: "new code prompt", }, @@ -588,17 +588,17 @@ describe("ClineProvider", () => { ) }) - test("customPrompts defaults to empty object", async () => { - // Mock globalState.get to return undefined for customPrompts + test("customModePrompts defaults to empty object", async () => { + // Mock globalState.get to return undefined for customModePrompts ;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => { - if (key === "customPrompts") { + if (key === "customModePrompts") { return undefined } return null }) const state = await provider.getState() - expect(state.customPrompts).toEqual({}) + expect(state.customModePrompts).toEqual({}) }) test("uses mode-specific custom instructions in Cline initialization", async () => { @@ -611,7 +611,7 @@ describe("ClineProvider", () => { jest.spyOn(provider, "getState").mockResolvedValue({ apiConfiguration: mockApiConfig, - customPrompts: { + customModePrompts: { code: { customInstructions: modeCustomInstructions }, }, mode: "code", @@ -651,7 +651,7 @@ describe("ClineProvider", () => { }, } mockContext.globalState.get = jest.fn((key: string) => { - if (key === "customPrompts") { + if (key === "customModePrompts") { return existingPrompts } return undefined @@ -668,7 +668,7 @@ describe("ClineProvider", () => { }) // Verify state was updated correctly - expect(mockContext.globalState.update).toHaveBeenCalledWith("customPrompts", { + expect(mockContext.globalState.update).toHaveBeenCalledWith("customModePrompts", { code: { roleDefinition: "Code role", customInstructions: "New instructions", @@ -978,7 +978,7 @@ describe("ClineProvider", () => { apiModelId: "test-model", openRouterModelInfo: { supportsComputerUse: true }, }, - customPrompts: {}, + customModePrompts: {}, mode: "code", mcpEnabled: false, browserViewportSize: "900x600", @@ -1007,7 +1007,7 @@ describe("ClineProvider", () => { }), "900x600", // browserViewportSize "code", // mode - {}, // customPrompts + {}, // customModePrompts {}, // customModes undefined, // effectiveInstructions undefined, // preferredLanguage @@ -1027,7 +1027,7 @@ describe("ClineProvider", () => { apiModelId: "test-model", openRouterModelInfo: { supportsComputerUse: true }, }, - customPrompts: {}, + customModePrompts: {}, mode: "code", mcpEnabled: false, browserViewportSize: "900x600", @@ -1056,7 +1056,7 @@ describe("ClineProvider", () => { }), "900x600", // browserViewportSize "code", // mode - {}, // customPrompts + {}, // customModePrompts {}, // customModes undefined, // effectiveInstructions undefined, // preferredLanguage @@ -1071,7 +1071,7 @@ describe("ClineProvider", () => { apiProvider: "openrouter", openRouterModelInfo: { supportsComputerUse: true }, }, - customPrompts: { + customModePrompts: { architect: { customInstructions: "Architect mode instructions" }, }, mode: "architect", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 3bcd8a0..ed5d78c 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -4,7 +4,8 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "./api" import { HistoryItem } from "./HistoryItem" import { McpServer } from "./mcp" import { GitCommit } from "../utils/git" -import { Mode, CustomPrompts, ModeConfig } from "./modes" +import { Mode, CustomModePrompts, ModeConfig } from "./modes" +import { CustomSupportPrompts } from "./support-prompt" export interface LanguageModelChatSelector { vendor?: string @@ -82,7 +83,8 @@ export interface ExtensionState { currentApiConfigName?: string listApiConfigMeta?: ApiConfigMeta[] customInstructions?: string - customPrompts?: CustomPrompts + customModePrompts?: CustomModePrompts + customSupportPrompts?: CustomSupportPrompts alwaysAllowReadOnly?: boolean alwaysAllowWrite?: boolean alwaysAllowExecute?: boolean diff --git a/src/shared/__tests__/support-prompts.test.ts b/src/shared/__tests__/support-prompts.test.ts index cd27a38..edee6c2 100644 --- a/src/shared/__tests__/support-prompts.test.ts +++ b/src/shared/__tests__/support-prompts.test.ts @@ -97,18 +97,18 @@ describe("Code Action Prompts", () => { it("should return custom template when provided", () => { const customTemplate = "Custom template for explaining code" - const customPrompts = { + const customSupportPrompts = { EXPLAIN: customTemplate, } - const template = supportPrompt.get(customPrompts, "EXPLAIN") + const template = supportPrompt.get(customSupportPrompts, "EXPLAIN") expect(template).toBe(customTemplate) }) it("should return default template when custom prompts does not include type", () => { - const customPrompts = { + const customSupportPrompts = { SOMETHING_ELSE: "Other template", } - const template = supportPrompt.get(customPrompts, "EXPLAIN") + const template = supportPrompt.get(customSupportPrompts, "EXPLAIN") expect(template).toBe(supportPrompt.default.EXPLAIN) }) }) @@ -116,7 +116,7 @@ describe("Code Action Prompts", () => { describe("create with custom prompts", () => { it("should use custom template when provided", () => { const customTemplate = "Custom template for ${filePath}" - const customPrompts = { + const customSupportPrompts = { EXPLAIN: customTemplate, } @@ -126,7 +126,7 @@ describe("Code Action Prompts", () => { filePath: testFilePath, selectedText: testCode, }, - customPrompts, + customSupportPrompts, ) expect(prompt).toContain(`Custom template for ${testFilePath}`) @@ -134,7 +134,7 @@ describe("Code Action Prompts", () => { }) it("should use default template when custom prompts does not include type", () => { - const customPrompts = { + const customSupportPrompts = { EXPLAIN: "Other template", } @@ -144,7 +144,7 @@ describe("Code Action Prompts", () => { filePath: testFilePath, selectedText: testCode, }, - customPrompts, + customSupportPrompts, ) expect(prompt).toContain("Other template") diff --git a/src/shared/modes.ts b/src/shared/modes.ts index 451e661..5b3358b 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -18,8 +18,8 @@ export type PromptComponent = { customInstructions?: string } -export type CustomPrompts = { - [key: string]: PromptComponent | undefined | string +export type CustomModePrompts = { + [key: string]: PromptComponent | undefined } // Helper to get all tools for a mode @@ -141,7 +141,7 @@ export function isToolAllowedForMode( } // Create the mode-specific default prompts -export const defaultPrompts: Readonly = Object.freeze( +export const defaultPrompts: Readonly = Object.freeze( Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])), ) diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 715d8c6..9f18b1a 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -84,11 +84,11 @@ type SupportPromptType = keyof typeof defaultTemplates export const supportPrompt = { default: defaultTemplates, - get: (customPrompts: Record | undefined, type: SupportPromptType): string => { - return customPrompts?.[type] ?? defaultTemplates[type] + get: (customSupportPrompts: Record | undefined, type: SupportPromptType): string => { + return customSupportPrompts?.[type] ?? defaultTemplates[type] }, - create: (type: SupportPromptType, params: PromptParams, customPrompts?: Record): string => { - const template = supportPrompt.get(customPrompts, type) + create: (type: SupportPromptType, params: PromptParams, customSupportPrompts?: Record): string => { + const template = supportPrompt.get(customSupportPrompts, type) return createPrompt(template, params) }, } as const @@ -102,3 +102,7 @@ export const supportPromptLabels: Record = { IMPROVE: "Improve Code", ENHANCE: "Enhance Prompt", } as const + +export type CustomSupportPrompts = { + [key: string]: string | undefined +} diff --git a/webview-ui/src/components/chat/__tests__/AutoApproveMenu.test.tsx b/webview-ui/src/components/chat/__tests__/AutoApproveMenu.test.tsx index 76ee929..510b5b2 100644 --- a/webview-ui/src/components/chat/__tests__/AutoApproveMenu.test.tsx +++ b/webview-ui/src/components/chat/__tests__/AutoApproveMenu.test.tsx @@ -1,7 +1,7 @@ import { render, fireEvent, screen } from "@testing-library/react" import { useExtensionState } from "../../../context/ExtensionStateContext" import AutoApproveMenu from "../AutoApproveMenu" -import { codeMode, defaultPrompts } from "../../../../../src/shared/modes" +import { defaultModeSlug, defaultPrompts } from "../../../../../src/shared/modes" // Mock the ExtensionStateContext hook jest.mock("../../../context/ExtensionStateContext") @@ -29,8 +29,9 @@ describe("AutoApproveMenu", () => { requestDelaySeconds: 5, currentApiConfigName: "default", listApiConfigMeta: [], - mode: codeMode, - customPrompts: defaultPrompts, + mode: defaultModeSlug, + customModePrompts: defaultPrompts, + customSupportPrompts: {}, enhancementApiConfigId: "", didHydrateState: true, showWelcome: false, diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index 6f5085a..c2f3cad 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -23,7 +23,8 @@ type PromptsViewProps = { const PromptsView = ({ onDone }: PromptsViewProps) => { const { - customPrompts, + customModePrompts, + customSupportPrompts, listApiConfigMeta, enhancementApiConfigId, setEnhancementApiConfigId, @@ -50,7 +51,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { // Direct update functions const updateAgentPrompt = useCallback( (mode: Mode, promptData: PromptComponent) => { - const existingPrompt = customPrompts?.[mode] as PromptComponent + const existingPrompt = customModePrompts?.[mode] as PromptComponent const updatedPrompt = { ...existingPrompt, ...promptData } // Only include properties that differ from defaults @@ -64,7 +65,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { customPrompt: updatedPrompt, }) }, - [customPrompts], + [customModePrompts], ) const updateCustomMode = useCallback((slug: string, modeConfig: ModeConfig) => { @@ -261,7 +262,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const handleAgentReset = (modeSlug: string) => { // Only reset role definition for built-in modes - const existingPrompt = customPrompts?.[modeSlug] as PromptComponent + const existingPrompt = customModePrompts?.[modeSlug] as PromptComponent updateAgentPrompt(modeSlug, { ...existingPrompt, roleDefinition: undefined, @@ -276,7 +277,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { } const getSupportPromptValue = (type: SupportPromptType): string => { - return supportPrompt.get(customPrompts, type) + return supportPrompt.get(customSupportPrompts, type) } const handleTestEnhancement = () => { @@ -556,7 +557,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { { const customMode = findModeBySlug(mode, customModes) - const prompt = customPrompts?.[mode] as PromptComponent + const prompt = customModePrompts?.[mode] as PromptComponent return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode) })()} onChange={(e) => { @@ -673,7 +674,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { { const customMode = findModeBySlug(mode, customModes) - const prompt = customPrompts?.[mode] as PromptComponent + const prompt = customModePrompts?.[mode] as PromptComponent return customMode?.customInstructions ?? prompt?.customInstructions ?? "" })()} onChange={(e) => { @@ -689,7 +690,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { }) } else { // For built-in modes, update the prompts - const existingPrompt = customPrompts?.[mode] as PromptComponent + const existingPrompt = customModePrompts?.[mode] as PromptComponent updateAgentPrompt(mode, { ...existingPrompt, customInstructions: value.trim() || undefined, diff --git a/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx b/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx index 93e8698..95437a4 100644 --- a/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx +++ b/webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx @@ -12,7 +12,7 @@ jest.mock("../../../utils/vscode", () => ({ })) const mockExtensionState = { - customPrompts: {}, + customModePrompts: {}, listApiConfigMeta: [ { id: "config1", name: "Config 1" }, { id: "config2", name: "Config 2" }, diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index ea00a0c..2d9fda0 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -14,7 +14,8 @@ import { convertTextMateToHljs } from "../utils/textMateToHljs" import { findLastIndex } from "../../../src/shared/array" import { McpServer } from "../../../src/shared/mcp" import { checkExistKey } from "../../../src/shared/checkExistApiConfig" -import { Mode, CustomPrompts, defaultModeSlug, defaultPrompts, ModeConfig } from "../../../src/shared/modes" +import { Mode, CustomModePrompts, defaultModeSlug, defaultPrompts, ModeConfig } from "../../../src/shared/modes" +import { CustomSupportPrompts } from "../../../src/shared/support-prompt" export interface ExtensionStateContextType extends ExtensionState { didHydrateState: boolean @@ -57,7 +58,8 @@ export interface ExtensionStateContextType extends ExtensionState { onUpdateApiConfig: (apiConfig: ApiConfiguration) => void mode: Mode setMode: (value: Mode) => void - setCustomPrompts: (value: CustomPrompts) => void + setCustomModePrompts: (value: CustomModePrompts) => void + setCustomSupportPrompts: (value: CustomSupportPrompts) => void enhancementApiConfigId?: string setEnhancementApiConfigId: (value: string) => void experimentalDiffStrategy: boolean @@ -93,7 +95,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode currentApiConfigName: "default", listApiConfigMeta: [], mode: defaultModeSlug, - customPrompts: defaultPrompts, + customModePrompts: defaultPrompts, + customSupportPrompts: {}, enhancementApiConfigId: "", experimentalDiffStrategy: false, autoApprovalEnabled: false, @@ -270,7 +273,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setListApiConfigMeta, onUpdateApiConfig, setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })), - setCustomPrompts: (value) => setState((prevState) => ({ ...prevState, customPrompts: value })), + setCustomModePrompts: (value) => setState((prevState) => ({ ...prevState, customModePrompts: value })), + setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })), setEnhancementApiConfigId: (value) => setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })), setExperimentalDiffStrategy: (value) => From d93a7a74d569189d81efcde920cafcc617e26d01 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 23 Jan 2025 21:12:59 +0200 Subject: [PATCH 62/66] feat(api): add Llama 3.3 70B Instruct to AWS Bedrock model options --- src/shared/api.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/shared/api.ts b/src/shared/api.ts index 72d6f17..d61c5d7 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -236,6 +236,15 @@ export const bedrockModels = { inputPrice: 0.25, outputPrice: 1.25, }, + "meta.llama3-3-70b-instruct-v1:0": { + maxTokens: 8192, + contextWindow: 128_000, + supportsImages: false, + supportsComputerUse: false, + supportsPromptCache: false, + inputPrice: 0.72, + outputPrice: 0.72, + }, "meta.llama3-2-90b-instruct-v1:0": { maxTokens: 8192, contextWindow: 128_000, From 3257dffa56676db3e48a52b3b97721b5897ba9b7 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 23 Jan 2025 15:56:24 -0800 Subject: [PATCH 63/66] Review feedback --- package.json | 6 +- src/extension.ts | 40 ++-- src/shared/support-prompt.ts | 79 ++++--- .../src/components/prompts/PromptsView.tsx | 194 +++++++++++------- 4 files changed, 179 insertions(+), 140 deletions(-) diff --git a/package.json b/package.json index e5cf882..7eab96e 100644 --- a/package.json +++ b/package.json @@ -104,17 +104,17 @@ }, { "command": "roo-cline.explainCode", - "title": "Explain Code", + "title": "Roo Code: Explain Code", "category": "Roo Code" }, { "command": "roo-cline.fixCode", - "title": "Fix Code", + "title": "Roo Code: Fix Code", "category": "Roo Code" }, { "command": "roo-cline.improveCode", - "title": "Improve Code", + "title": "Roo Code: Improve Code", "category": "Roo Code" } ], diff --git a/src/extension.ts b/src/extension.ts index b03e9e5..472968a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -171,17 +171,21 @@ export function activate(context: vscode.ExtensionContext) { context: vscode.ExtensionContext, command: string, promptType: keyof typeof ACTION_NAMES, - inputPrompt: string, - inputPlaceholder: string, + inputPrompt?: string, + inputPlaceholder?: string, ) => { + let userInput: string | undefined + context.subscriptions.push( vscode.commands.registerCommand( command, async (filePath: string, selectedText: string, diagnostics?: any[]) => { - const userInput = await vscode.window.showInputBox({ - prompt: inputPrompt, - placeHolder: inputPlaceholder, - }) + if (inputPrompt) { + userInput = await vscode.window.showInputBox({ + prompt: inputPrompt, + placeHolder: inputPlaceholder, + }) + } const params = { filePath, @@ -197,29 +201,11 @@ export function activate(context: vscode.ExtensionContext) { } // Register code action commands - registerCodeAction( - context, - "roo-cline.explainCode", - "EXPLAIN", - "What would you like Roo to explain?", - "E.g. How does the error handling work?", - ) + registerCodeAction(context, "roo-cline.explainCode", "EXPLAIN") - registerCodeAction( - context, - "roo-cline.fixCode", - "FIX", - "What would you like Roo to fix?", - "E.g. Maintain backward compatibility", - ) + registerCodeAction(context, "roo-cline.fixCode", "FIX") - registerCodeAction( - context, - "roo-cline.improveCode", - "IMPROVE", - "What would you like Roo to improve?", - "E.g. Focus on performance optimization", - ) + registerCodeAction(context, "roo-cline.improveCode", "IMPROVE") return createClineAPI(outputChannel, sidebarProvider) } diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 9f18b1a..881c060 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -24,7 +24,26 @@ export const createPrompt = (template: string, params: PromptParams): string => return result } -const EXPLAIN_TEMPLATE = `Explain the following code from file path @/\${filePath}: +interface SupportPromptConfig { + label: string + description: string + template: string +} + +const supportPromptConfigs: Record = { + ENHANCE: { + label: "Enhance Prompt", + description: + "Use prompt enhancement to get tailored suggestions or improvements for your inputs. This ensures Roo understands your intent and provides the best possible responses. Available via the ✨ icon in chat.", + template: `Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes): + +\${userInput}`, + }, + EXPLAIN: { + label: "Explain Code", + description: + "Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in the editor context menu (right-click on selected code).", + template: `Explain the following code from file path @/\${filePath}: \${userInput} \`\`\` @@ -34,10 +53,13 @@ const EXPLAIN_TEMPLATE = `Explain the following code from file path @/\${filePat Please provide a clear and concise explanation of what this code does, including: 1. The purpose and functionality 2. Key components and their interactions -3. Important patterns or techniques used -` - -const FIX_TEMPLATE = `Fix any issues in the following code from file path @/\${filePath} +3. Important patterns or techniques used`, + }, + FIX: { + label: "Fix Issues", + description: + "Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in the editor context menu (right-click on selected code).", + template: `Fix any issues in the following code from file path @/\${filePath} \${diagnosticText} \${userInput} @@ -49,10 +71,13 @@ Please: 1. Address all detected problems listed above (if any) 2. Identify any other potential bugs or issues 3. Provide corrected code -4. Explain what was fixed and why -` - -const IMPROVE_TEMPLATE = `Improve the following code from file path @/\${filePath}: +4. Explain what was fixed and why`, + }, + IMPROVE: { + label: "Improve Code", + description: + "Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in the editor context menu (right-click on selected code).", + template: `Improve the following code from file path @/\${filePath}: \${userInput} \`\`\` @@ -65,27 +90,16 @@ Please suggest improvements for: 3. Best practices and patterns 4. Error handling and edge cases -Provide the improved code along with explanations for each enhancement. -` - -const ENHANCE_TEMPLATE = `Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes): - -\${userInput}` - -// Get template based on prompt type -const defaultTemplates = { - EXPLAIN: EXPLAIN_TEMPLATE, - FIX: FIX_TEMPLATE, - IMPROVE: IMPROVE_TEMPLATE, - ENHANCE: ENHANCE_TEMPLATE, +Provide the improved code along with explanations for each enhancement.`, + }, } as const -type SupportPromptType = keyof typeof defaultTemplates +type SupportPromptType = keyof typeof supportPromptConfigs export const supportPrompt = { - default: defaultTemplates, + default: Object.fromEntries(Object.entries(supportPromptConfigs).map(([key, config]) => [key, config.template])), get: (customSupportPrompts: Record | undefined, type: SupportPromptType): string => { - return customSupportPrompts?.[type] ?? defaultTemplates[type] + return customSupportPrompts?.[type] ?? supportPromptConfigs[type].template }, create: (type: SupportPromptType, params: PromptParams, customSupportPrompts?: Record): string => { const template = supportPrompt.get(customSupportPrompts, type) @@ -95,13 +109,14 @@ export const supportPrompt = { export type { SupportPromptType } -// User-friendly labels for support prompt types -export const supportPromptLabels: Record = { - FIX: "Fix Issues", - EXPLAIN: "Explain Code", - IMPROVE: "Improve Code", - ENHANCE: "Enhance Prompt", -} as const +// Expose labels and descriptions for UI +export const supportPromptLabels = Object.fromEntries( + Object.entries(supportPromptConfigs).map(([key, config]) => [key, config.label]), +) as Record + +export const supportPromptDescriptions = Object.fromEntries( + Object.entries(supportPromptConfigs).map(([key, config]) => [key, config.description]), +) as Record export type CustomSupportPrompts = { [key: string]: string | undefined diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index c2f3cad..c05f70f 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -9,7 +9,12 @@ import { } from "@vscode/webview-ui-toolkit/react" import { useExtensionState } from "../../context/ExtensionStateContext" import { Mode, PromptComponent, getRoleDefinition, getAllModes, ModeConfig } from "../../../../src/shared/modes" -import { supportPrompt, SupportPromptType, supportPromptLabels } from "../../../../src/shared/support-prompt" +import { + supportPrompt, + SupportPromptType, + supportPromptLabels, + supportPromptDescriptions, +} from "../../../../src/shared/support-prompt" import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups" import { vscode } from "../../utils/vscode" @@ -46,7 +51,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const [selectedPromptTitle, setSelectedPromptTitle] = useState("") const [isToolsEditMode, setIsToolsEditMode] = useState(false) const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false) - const [activeSupportTab, setActiveSupportTab] = useState("EXPLAIN") + const [activeSupportTab, setActiveSupportTab] = useState("ENHANCE") // Direct update functions const updateAgentPrompt = useCallback( @@ -313,7 +318,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
-
+
Preferred Language