diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c4c59d49a8..7feb2005ae 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # These owners will be the default owners for everything in the repo -* @stea9499 @ColemanRoo @mrubens @cte +* @mrubens @cte diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 501180c3d5..dc66b4f390 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -33,7 +33,7 @@ body: id: model attributes: label: Which Model are you using? - description: Please specify the model you're using (e.g. Claude 3.5 Sonnet) + description: Please specify the model you're using (e.g. Claude 3.7 Sonnet) validations: required: true - type: textarea diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index 06367080fc..fde891f804 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -28,7 +28,22 @@ jobs: - name: Lint run: npm run lint - unit-test: + knip: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + - name: Install dependencies + run: npm run install:all + - name: Run knip checks + run: npm run knip + + test-extension: runs-on: ubuntu-latest steps: - name: Checkout code @@ -41,7 +56,30 @@ jobs: - name: Install dependencies run: npm run install:all - name: Run unit tests - run: npm test + run: npx jest --silent + + test-webview: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + - name: Install dependencies + run: npm run install:all + - name: Run unit tests + working-directory: webview-ui + run: npx jest --silent + + unit-test: + needs: [test-extension, test-webview] + runs-on: ubuntu-latest + steps: + - name: NO-OP + run: echo "All unit tests passed." check-openrouter-api-key: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 3365f29001..02a4a30cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # Roo Code Changelog +## [3.7.5] + +- Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173)) +- Fix various issues with the model picker (thanks @System233!) +- Fix model input / output cost parsing (thanks @System233!) +- Add drag-and-drop for files +- Enable the "Thinking Budget" slider for Claude 3.7 Sonnet on OpenRouter + +## [3.7.4] + +- Fix a bug that prevented the "Thinking" setting from properly updating when switching profiles. + +## [3.7.3] + +- Support for ["Thinking"](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking) Sonnet 3.7 when using the Anthropic provider. + +## [3.7.2] + +- Fix computer use and prompt caching for OpenRouter's `anthropic/claude-3.7-sonnet:beta` (thanks @cte!) +- Fix sliding window calculations for Sonnet 3.7 that were causing a context window overflow (thanks @cte!) +- Encourage diff editing more strongly in the system prompt (thanks @hannesrudolph!) + +## [3.7.1] + +- Add AWS Bedrock support for Sonnet 3.7 and update some defaults to Sonnet 3.7 instead of 3.5 + +## [3.7.0] + +- Introducing Roo Code 3.7, with support for the new Claude Sonnet 3.7. Because who cares about skipping version numbers anymore? Thanks @lupuletic and @cte for the PRs! + +## [3.3.26] + +- Adjust the default prompt for Debug mode to focus more on diagnosis and to require user confirmation before moving on to implementation + +## [3.3.25] + +- Add a "Debug" mode that specializes in debugging tricky problems (thanks [Ted Werbel](https://x.com/tedx_ai/status/1891514191179309457) and [Carlos E. Perez](https://x.com/IntuitMachine/status/1891516362486337739)!) +- Add an experimental "Power Steering" option to significantly improve adherence to role definitions and custom instructions + +## [3.3.24] + +- Fixed a bug with region selection preventing AWS Bedrock profiles from being saved (thanks @oprstchn!) +- Updated the price of gpt-4o (thanks @marvijo-code!) + ## [3.3.23] - Handle errors more gracefully when reading custom instructions from files (thanks @joemanley201!) diff --git a/README.md b/README.md index 53f29e0725..60161110e2 100644 --- a/README.md +++ b/README.md @@ -34,204 +34,78 @@ Check out the [CHANGELOG](CHANGELOG.md) for detailed updates and fixes. --- -## New in 3.3: Code Actions, More Powerful Modes, and a new Discord! 🚀 +## New in 3.7: Claude 3.7 Sonnet Support 🚀 -This release brings significant improvements to how you interact with Roo Code: +We're excited to announce support for Anthropic's latest model, Claude 3.7 Sonnet! The model shows notable improvements in: -### Code Actions +- Front-end development and full-stack updates +- Agentic workflows for multi-step processes +- More accurate math, coding, and instruction-following -Roo Code now integrates directly with VS Code's native code actions system, providing quick fixes and refactoring options right in your editor. Look for the lightbulb 💡 to access Roo Code's capabilities without switching context. - -### Enhanced Mode Capabilities - -- **Markdown Editing**: Addressing one of the most requested features, Ask and Architect modes can now create and edit markdown files! -- **Custom File Restrictions**: In general, custom modes can now be restricted to specific file patterns (for example, a technical writer who can only edit markdown files 👋). There's no UI for this yet, but who needs that when you can just ask Roo to set it up for you? -- **Self-Initiated Mode Switching**: Modes can intelligently request to switch between each other based on the task at hand. For instance, Code mode might request to switch to Test Engineer mode once it's ready to write tests. - -### Join Our Discord! - -We've launched a new Discord community! Join us at [https://roocode.com/discord](https://roocode.com/discord) to: - -- Share your custom modes -- Get help and support -- Connect with other Roo Code users -- Stay updated on the latest features - -## 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 - -Hot off the heels of **v3.0** introducing Code, Architect, and Ask chat modes, one of the most requested features has arrived: **customizable prompts for each mode**! 🎉 - -You can now tailor the **role definition** and **custom instructions** for every chat mode to perfectly fit your workflow. Want to adjust Architect mode to focus more on system scalability? Or tweak Ask mode for deeper research queries? Done. Plus, you can define these via **mode-specific `.clinerules-[mode]` files**. You’ll find all of this in the new **Prompts** tab in the top menu. - -The second big feature in this release is a complete revamp of **prompt enhancements**. This feature helps you craft messages to get even better results from Cline. Here’s what’s new: - -- Works with **any provider** and API configuration, not just OpenRouter. -- Fully customizable prompts to match your unique needs. -- Same simple workflow: just hit the ✨ **Enhance Prompt** button in the chat input to try it out. - -Whether you’re using GPT-4, other APIs, or switching configurations, this gives you total control over how your prompts are optimized. - -As always, we’d love to hear your thoughts and ideas! What features do you want to see in **v3.2**? Drop by https://www.reddit.com/r/roocline and join the discussion - we're building Roo Cline together. 🚀 - -## New in 3.0 - Chat Modes! - -You can now choose between different prompts for Roo Cline to better suit your workflow. Here’s what’s available: - -- **Code:** (existing behavior) The default mode where Cline helps you write code and execute tasks. - -- **Architect:** "You are Cline, a software architecture expert..." Ideal for thinking through high-level technical design and system architecture. Can’t write code or run commands. - -- **Ask:** "You are Cline, a knowledgeable technical assistant..." Perfect for asking questions about the codebase or digging into concepts. Also can’t write code or run commands. - -**Switching Modes:** -It’s super simple! There’s a dropdown in the bottom left of the chat input to switch modes. Right next to it, you’ll find a way to switch between the API configuration profiles associated with the current mode (configured on the settings screen). - -**Why Add This?** - -- It keeps Cline from being overly eager to jump into solving problems when you just want to think or ask questions. -- Each mode remembers the API configuration you last used with it. For example, you can use more thoughtful models like OpenAI o1 for Architect and Ask, while sticking with Sonnet or DeepSeek for coding tasks. -- It builds on research suggesting better results when separating "thinking" from "coding," explained well in this very thoughtful [article](https://aider.chat/2024/09/26/architect.html) from aider. - -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. +Try it today in your provider of choice! --- +## What Can Roo Code Do? + +- 🚀 **Generate Code** from natural language descriptions +- 🔧 **Refactor & Debug** existing code +- 📝 **Write & Update** documentation +- 🤔 **Answer Questions** about your codebase +- 🔄 **Automate** repetitive tasks +- 🏗️ **Create** new files and projects + +## Quick Start + +1. [Install Roo Code](https://docs.roocode.com/getting-started/installing) +2. [Connect Your AI Provider](https://docs.roocode.com/getting-started/connecting-api-provider) +3. [Try Your First Task](https://docs.roocode.com/getting-started/your-first-task) + ## Key Features -### Adaptive Autonomy +### Multiple Modes -Roo Code communicates in **natural language** and proposes actions—file edits, terminal commands, browser tests, etc. You choose how it behaves: +Roo Code adapts to your needs with specialized [modes](https://docs.roocode.com/basic-usage/modes): -- **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). +- **Code Mode:** For general-purpose coding tasks +- **Architect Mode:** For planning and technical leadership +- **Ask Mode:** For answering questions and providing information +- **Debug Mode:** For systematic problem diagnosis +- **[Custom Modes](https://docs.roocode.com/advanced-usage/custom-modes):** Create unlimited specialized personas for security auditing, performance optimization, documentation, or any other task -No matter your preference, you always have the final say on what Roo Code does. +### Smart Tools ---- +Roo Code comes with powerful [tools](https://docs.roocode.com/basic-usage/using-tools) that can: -### Supports Any API or Model +- Read and write files in your project +- Execute commands in your VS Code terminal +- Control a web browser +- Use external tools via [MCP (Model Context Protocol)](https://docs.roocode.com/advanced-usage/mcp) -Use Roo Code with: +MCP extends Roo Code's capabilities by allowing you to add unlimited custom tools. Integrate with external APIs, connect to databases, or create specialized development tools - MCP provides the framework to expand Roo Code's functionality to meet your specific needs. -- **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. +### Customization ---- +Make Roo Code work your way with: -### Custom Modes +- [Custom Instructions](https://docs.roocode.com/advanced-usage/custom-instructions) for personalized behavior +- [Custom Modes](https://docs.roocode.com/advanced-usage/custom-modes) for specialized tasks +- [Local Models](https://docs.roocode.com/advanced-usage/local-models) for offline use +- [Auto-Approval Settings](https://docs.roocode.com/advanced-usage/auto-approving-actions) for faster workflows -**Custom Modes** let you shape Roo Code’s persona, instructions, and permissions: +## Resources -- **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. +### Documentation -Modes can each have unique instructions and skill sets. Manage them in the **Prompts** tab. +- [Basic Usage Guide](https://docs.roocode.com/basic-usage/the-chat-interface) +- [Advanced Features](https://docs.roocode.com/advanced-usage/auto-approving-actions) +- [Frequently Asked Questions](https://docs.roocode.com/faq) -**Advanced Mode Features:** +### Community -- **File Restrictions**: Modes can be restricted to specific file types (e.g., Ask and Architect modes can edit markdown files) -- **Custom File Rules**: Define your own file access patterns (e.g., `.test.ts` for test files only) -- **Direct Mode Switching**: Modes can request to switch to other modes when needed (e.g., switching to Code mode for implementation) -- **Self-Creation**: Roo Code can help create new modes, complete with role definitions and file restrictions - ---- - -### 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. +- **Discord:** [Join our Discord server](https://discord.gg/roocode) for real-time help and discussions +- **Reddit:** [Visit our subreddit](https://www.reddit.com/r/RooCode) to share experiences and tips +- **GitHub:** Report [issues](https://github.com/RooVetGit/Roo-Code/issues) or request [features](https://github.com/RooVetGit/Roo-Code/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop) --- diff --git a/jest.config.js b/jest.config.js index d0efc8eaaf..dbe5ee54eb 100644 --- a/jest.config.js +++ b/jest.config.js @@ -37,5 +37,5 @@ module.exports = { roots: ["/src", "/webview-ui/src"], modulePathIgnorePatterns: [".vscode-test"], reporters: [["jest-simple-dot-reporter", {}]], - setupFiles: [], + setupFiles: ["/src/__mocks__/jest.setup.ts"], } diff --git a/knip.json b/knip.json new file mode 100644 index 0000000000..b0e0839da7 --- /dev/null +++ b/knip.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://unpkg.com/knip@latest/schema.json", + "entry": ["src/extension.ts", "src/activate/index.ts", "webview-ui/src/index.tsx"], + "project": ["src/**/*.ts", "webview-ui/src/**/*.{ts,tsx}"], + "ignore": [ + "**/__mocks__/**", + "**/__tests__/**", + "**/test/**", + "**/*.test.ts", + "**/*.test.tsx", + "**/stories/**", + "coverage/**", + "dist/**", + "out/**", + "bin/**", + "src/activate/**", + "src/exports/**", + "src/extension.ts", + ".vscode-test.mjs" + ], + "workspaces": { + "webview-ui": { + "entry": ["src/index.tsx"], + "project": ["src/**/*.{ts,tsx}"] + } + } +} diff --git a/package-lock.json b/package-lock.json index e3ee551772..a6c75bd69b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "roo-cline", - "version": "3.3.23", + "version": "3.7.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.3.23", + "version": "3.7.5", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", - "@anthropic-ai/sdk": "^0.26.0", + "@anthropic-ai/sdk": "^0.37.0", "@anthropic-ai/vertex-sdk": "^0.4.1", "@aws-sdk/client-bedrock-runtime": "^3.706.0", "@google/generative-ai": "^0.18.0", @@ -76,6 +76,7 @@ "husky": "^9.1.7", "jest": "^29.7.0", "jest-simple-dot-reporter": "^1.0.5", + "knip": "^5.44.4", "lint-staged": "^15.2.11", "mkdirp": "^3.0.1", "mocha": "^11.1.0", @@ -121,9 +122,10 @@ } }, "node_modules/@anthropic-ai/sdk": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.26.1.tgz", - "integrity": "sha512-HeMJP1bDFfQPQS3XTJAmfXkFBdZ88wvfkE05+vsoA9zGn5dHqEaHOPsqkazf/i0gXYg2XlLxxZrf6rUAarSqzw==", + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.37.0.tgz", + "integrity": "sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw==", + "license": "MIT", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -5854,6 +5856,47 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "node_modules/@snyk/github-codeowners": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@snyk/github-codeowners/-/github-codeowners-1.1.0.tgz", + "integrity": "sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw==", + "dev": true, + "dependencies": { + "commander": "^4.1.1", + "ignore": "^5.1.8", + "p-map": "^4.0.0" + }, + "bin": { + "github-codeowners": "dist/cli.js" + }, + "engines": { + "node": ">=8.10" + } + }, + "node_modules/@snyk/github-codeowners/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@snyk/github-codeowners/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -6696,6 +6739,19 @@ "node": ">= 8.0.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -7530,6 +7586,15 @@ "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", "dev": true }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cli-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", @@ -7662,6 +7727,16 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -7957,6 +8032,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "optional": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -8207,6 +8295,27 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/easy-table": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.2.0.tgz", + "integrity": "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "optionalDependencies": { + "wcwidth": "^1.0.1" + } + }, + "node_modules/easy-table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -8299,9 +8408,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -8947,15 +9056,15 @@ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -9957,6 +10066,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -11195,7 +11313,8 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/jest-simple-dot-reporter/-/jest-simple-dot-reporter-1.0.5.tgz", "integrity": "sha512-cZLFG/C7k0+WYoIGGuGXKm0vmJiXlWG/m3uCZ4RaMPYxt8lxjdXMLHYkxXaQ7gVWaSPe7uAPCEUcRxthC5xskg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jest-snapshot": { "version": "29.7.0", @@ -11323,6 +11442,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -11472,6 +11600,114 @@ "node": ">=6" } }, + "node_modules/knip": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/knip/-/knip-5.44.4.tgz", + "integrity": "sha512-Ryn8LwWHLId8jSK1DgtT0hmg5DbzkqAtH+Gg3vZJpmSMgGHMspej9Ag+qKTm8wsPLDjVetuEz/lIsobo0XCMvQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + }, + { + "type": "polar", + "url": "https://polar.sh/webpro-nl" + } + ], + "dependencies": { + "@nodelib/fs.walk": "3.0.1", + "@snyk/github-codeowners": "1.1.0", + "easy-table": "1.2.0", + "enhanced-resolve": "^5.18.0", + "fast-glob": "^3.3.3", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "minimist": "^1.2.8", + "picocolors": "^1.1.0", + "picomatch": "^4.0.1", + "pretty-ms": "^9.0.0", + "smol-toml": "^1.3.1", + "strip-json-comments": "5.0.1", + "summary": "2.1.0", + "zod": "^3.22.4", + "zod-validation-error": "^3.0.3" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": ">=18.18.0" + }, + "peerDependencies": { + "@types/node": ">=18", + "typescript": ">=5.0.4" + } + }, + "node_modules/knip/node_modules/@nodelib/fs.scandir": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-4.0.1.tgz", + "integrity": "sha512-vAkI715yhnmiPupY+dq+xenu5Tdf2TBQ66jLvBIcCddtz+5Q8LbMKaf9CIJJreez8fQ8fgaY+RaywQx8RJIWpw==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "4.0.0", + "run-parallel": "^1.2.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/knip/node_modules/@nodelib/fs.stat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-4.0.0.tgz", + "integrity": "sha512-ctr6bByzksKRCV0bavi8WoQevU6plSp2IkllIsEqaiKe2mwNNnaluhnRhcsgGZHrrHk57B3lf95MkLMO3STYcg==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/knip/node_modules/@nodelib/fs.walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-3.0.1.tgz", + "integrity": "sha512-nIh/M6Kh3ZtOmlY00DaUYB4xeeV6F3/ts1l29iwl3/cfyY/OuCfUx+v08zgx8TKPTifXRcjjqVQ4KB2zOYSbyw==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "4.0.1", + "fastq": "^1.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/knip/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/knip/node_modules/strip-json-comments": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.1.tgz", + "integrity": "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -12205,6 +12441,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -13170,6 +13415,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", @@ -13480,6 +13737,21 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", + "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "dev": true, + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -14340,6 +14612,18 @@ "npm": ">= 3.0.0" } }, + "node_modules/smol-toml": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.1.tgz", + "integrity": "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==", + "dev": true, + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/socks": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", @@ -14761,6 +15045,12 @@ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" }, + "node_modules/summary": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/summary/-/summary-2.1.0.tgz", + "integrity": "sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw==", + "dev": true + }, "node_modules/supports-color": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", @@ -15336,6 +15626,16 @@ "makeerror": "1.0.12" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "optional": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/web-streams-polyfill": { "version": "4.0.0-beta.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", @@ -15962,6 +16262,18 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-validation-error": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } } } } diff --git a/package.json b/package.json index ebf733ed26..40bb6a545d 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "roo-cline", "displayName": "Roo Code (prev. Roo Cline)", - "description": "An AI-powered autonomous coding agent that lives in your editor.", + "description": "A whole dev team of AI agents in your editor.", "publisher": "RooVeterinaryInc", - "version": "3.3.23", + "version": "3.7.5", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", @@ -277,10 +277,11 @@ "build": "npm run build:webview && npm run vsix", "build:webview": "cd webview-ui && npm run build", "changeset": "changeset", - "check-types": "tsc --noEmit", + "check-types": "tsc --noEmit && cd webview-ui && npm run check-types", "compile": "tsc -p . --outDir out && node esbuild.js", "compile:integration": "tsc -p tsconfig.integration.json", "install:all": "npm install && cd webview-ui && npm install", + "knip": "knip --include files", "lint": "eslint src --ext ts && npm run lint --prefix webview-ui", "lint-local": "eslint -c .eslintrc.local.json src --ext ts && npm run lint --prefix webview-ui", "lint-fix": "eslint src --ext ts --fix && npm run lint-fix --prefix webview-ui", @@ -288,8 +289,7 @@ "package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production", "pretest": "npm run compile && npm run compile:integration", "dev": "cd webview-ui && npm run dev", - "test": "jest && npm run test:webview", - "test:webview": "cd webview-ui && npm run test", + "test": "jest && cd webview-ui && npm run test", "test:integration": "npm run build && npm run compile:integration && npx dotenvx run -f .env.integration -- node ./out-integration/test/runTest.js", "prepare": "husky", "publish:marketplace": "vsce publish && ovsx publish", @@ -304,7 +304,7 @@ }, "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", - "@anthropic-ai/sdk": "^0.26.0", + "@anthropic-ai/sdk": "^0.37.0", "@anthropic-ai/vertex-sdk": "^0.4.1", "@aws-sdk/client-bedrock-runtime": "^3.706.0", "@google/generative-ai": "^0.18.0", @@ -371,6 +371,7 @@ "husky": "^9.1.7", "jest": "^29.7.0", "jest-simple-dot-reporter": "^1.0.5", + "knip": "^5.44.4", "lint-staged": "^15.2.11", "mkdirp": "^3.0.1", "mocha": "^11.1.0", diff --git a/src/__mocks__/jest.setup.ts b/src/__mocks__/jest.setup.ts new file mode 100644 index 0000000000..6bd00e9567 --- /dev/null +++ b/src/__mocks__/jest.setup.ts @@ -0,0 +1,17 @@ +// Mock the logger globally for all tests +jest.mock("../utils/logging", () => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), + child: jest.fn().mockReturnValue({ + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), + }), + }, +})) diff --git a/src/api/providers/__tests__/anthropic.test.ts b/src/api/providers/__tests__/anthropic.test.ts index df0050ab9c..ff7bdb4054 100644 --- a/src/api/providers/__tests__/anthropic.test.ts +++ b/src/api/providers/__tests__/anthropic.test.ts @@ -1,50 +1,13 @@ +// npx jest src/api/providers/__tests__/anthropic.test.ts + import { AnthropicHandler } from "../anthropic" import { ApiHandlerOptions } from "../../../shared/api" -import { ApiStream } from "../../transform/stream" -import { Anthropic } from "@anthropic-ai/sdk" -// Mock Anthropic client -const mockBetaCreate = jest.fn() const mockCreate = jest.fn() + jest.mock("@anthropic-ai/sdk", () => { return { Anthropic: jest.fn().mockImplementation(() => ({ - beta: { - promptCaching: { - messages: { - create: mockBetaCreate.mockImplementation(async () => ({ - async *[Symbol.asyncIterator]() { - yield { - type: "message_start", - message: { - usage: { - input_tokens: 100, - output_tokens: 50, - cache_creation_input_tokens: 20, - cache_read_input_tokens: 10, - }, - }, - } - yield { - type: "content_block_start", - index: 0, - content_block: { - type: "text", - text: "Hello", - }, - } - yield { - type: "content_block_delta", - delta: { - type: "text_delta", - text: " world", - }, - } - }, - })), - }, - }, - }, messages: { create: mockCreate.mockImplementation(async (options) => { if (!options.stream) { @@ -65,16 +28,26 @@ jest.mock("@anthropic-ai/sdk", () => { type: "message_start", message: { usage: { - input_tokens: 10, - output_tokens: 5, + input_tokens: 100, + output_tokens: 50, + cache_creation_input_tokens: 20, + cache_read_input_tokens: 10, }, }, } yield { type: "content_block_start", + index: 0, content_block: { type: "text", - text: "Test response", + text: "Hello", + }, + } + yield { + type: "content_block_delta", + delta: { + type: "text_delta", + text: " world", }, } }, @@ -95,7 +68,6 @@ describe("AnthropicHandler", () => { apiModelId: "claude-3-5-sonnet-20241022", } handler = new AnthropicHandler(mockOptions) - mockBetaCreate.mockClear() mockCreate.mockClear() }) @@ -126,17 +98,6 @@ describe("AnthropicHandler", () => { describe("createMessage", () => { const systemPrompt = "You are a helpful assistant." - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "text" as const, - text: "Hello!", - }, - ], - }, - ] it("should handle prompt caching for supported models", async () => { const stream = handler.createMessage(systemPrompt, [ @@ -173,9 +134,8 @@ describe("AnthropicHandler", () => { expect(textChunks[0].text).toBe("Hello") expect(textChunks[1].text).toBe(" world") - // Verify beta API was used - expect(mockBetaCreate).toHaveBeenCalled() - expect(mockCreate).not.toHaveBeenCalled() + // Verify API + expect(mockCreate).toHaveBeenCalled() }) }) diff --git a/src/api/providers/__tests__/glama.test.ts b/src/api/providers/__tests__/glama.test.ts index c3fc90e32b..5e017ccd0a 100644 --- a/src/api/providers/__tests__/glama.test.ts +++ b/src/api/providers/__tests__/glama.test.ts @@ -1,9 +1,11 @@ -import { GlamaHandler } from "../glama" -import { ApiHandlerOptions } from "../../../shared/api" -import OpenAI from "openai" +// npx jest src/api/providers/__tests__/glama.test.ts + import { Anthropic } from "@anthropic-ai/sdk" import axios from "axios" +import { GlamaHandler } from "../glama" +import { ApiHandlerOptions } from "../../../shared/api" + // Mock OpenAI client const mockCreate = jest.fn() const mockWithResponse = jest.fn() @@ -71,8 +73,8 @@ describe("GlamaHandler", () => { beforeEach(() => { mockOptions = { - apiModelId: "anthropic/claude-3-5-sonnet", - glamaModelId: "anthropic/claude-3-5-sonnet", + apiModelId: "anthropic/claude-3-7-sonnet", + glamaModelId: "anthropic/claude-3-7-sonnet", glamaApiKey: "test-api-key", } handler = new GlamaHandler(mockOptions) diff --git a/src/api/providers/__tests__/openrouter.test.ts b/src/api/providers/__tests__/openrouter.test.ts index 18f81ce2fd..aabd7f71a8 100644 --- a/src/api/providers/__tests__/openrouter.test.ts +++ b/src/api/providers/__tests__/openrouter.test.ts @@ -1,3 +1,5 @@ +// npx jest src/api/providers/__tests__/openrouter.test.ts + import { OpenRouterHandler } from "../openrouter" import { ApiHandlerOptions, ModelInfo } from "../../../shared/api" import OpenAI from "openai" @@ -55,7 +57,7 @@ describe("OpenRouterHandler", () => { const handler = new OpenRouterHandler({}) const result = handler.getModel() - expect(result.id).toBe("anthropic/claude-3.5-sonnet:beta") + expect(result.id).toBe("anthropic/claude-3.7-sonnet") expect(result.info.supportsPromptCache).toBe(true) }) diff --git a/src/api/providers/__tests__/vertex.test.ts b/src/api/providers/__tests__/vertex.test.ts index a51033af2d..ebe60ba0c6 100644 --- a/src/api/providers/__tests__/vertex.test.ts +++ b/src/api/providers/__tests__/vertex.test.ts @@ -1,7 +1,10 @@ -import { VertexHandler } from "../vertex" +// npx jest src/api/providers/__tests__/vertex.test.ts + import { Anthropic } from "@anthropic-ai/sdk" import { AnthropicVertex } from "@anthropic-ai/vertex-sdk" +import { VertexHandler } from "../vertex" + // Mock Vertex SDK jest.mock("@anthropic-ai/vertex-sdk", () => ({ AnthropicVertex: jest.fn().mockImplementation(() => ({ @@ -289,7 +292,7 @@ describe("VertexHandler", () => { vertexRegion: "us-central1", }) const modelInfo = invalidHandler.getModel() - expect(modelInfo.id).toBe("claude-3-5-sonnet-v2@20241022") // Default model + expect(modelInfo.id).toBe("claude-3-7-sonnet@20250219") // Default model }) }) }) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 9a14756f5d..ad58a1cf6b 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -1,5 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming" +import { CacheControlEphemeral } from "@anthropic-ai/sdk/resources" +import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta" import { anthropicDefaultModelId, AnthropicModelId, @@ -18,6 +20,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { constructor(options: ApiHandlerOptions) { this.options = options + this.client = new Anthropic({ apiKey: this.options.apiKey, baseURL: this.options.anthropicBaseUrl || undefined, @@ -25,45 +28,60 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { } async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { - let stream: AnthropicStream - const modelId = this.getModel().id + let stream: AnthropicStream + const cacheControl: CacheControlEphemeral = { type: "ephemeral" } + let { id: modelId, info: modelInfo } = this.getModel() + const maxTokens = modelInfo.maxTokens || 8192 + let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE + let thinking: BetaThinkingConfigParam | undefined = undefined + + // Anthropic "Thinking" models require a temperature of 1.0. + if (modelId === "claude-3-7-sonnet-20250219:thinking") { + // The `:thinking` variant is a virtual identifier for the + // `claude-3-7-sonnet-20250219` model with a thinking budget. + // We can handle this more elegantly in the future. + modelId = "claude-3-7-sonnet-20250219" + const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024) + thinking = { type: "enabled", budget_tokens: budgetTokens } + temperature = 1.0 + } + switch (modelId) { - // 'latest' alias does not support cache_control + case "claude-3-7-sonnet-20250219": case "claude-3-5-sonnet-20241022": case "claude-3-5-haiku-20241022": case "claude-3-opus-20240229": case "claude-3-haiku-20240307": { - /* - The latest message will be the new user message, one before will be the assistant message from a previous request, and the user message before that will be a previously cached user message. So we need to mark the latest user message as ephemeral to cache it for the next request, and mark the second to last user message as ephemeral to let the server know the last message to retrieve from the cache for the current request.. - */ + /** + * The latest message will be the new user message, one before will + * be the assistant message from a previous request, and the user message before that will be a previously cached user message. So we need to mark the latest user message as ephemeral to cache it for the next request, and mark the second to last user message as ephemeral to let the server know the last message to retrieve from the cache for the current request.. + */ const userMsgIndices = messages.reduce( (acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), [] as number[], ) + const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1 const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1 - stream = await this.client.beta.promptCaching.messages.create( + + stream = await this.client.messages.create( { model: modelId, - max_tokens: this.getModel().info.maxTokens || 8192, - temperature: this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE, - system: [{ text: systemPrompt, type: "text", cache_control: { type: "ephemeral" } }], // setting cache breakpoint for system prompt so new tasks can reuse it + max_tokens: maxTokens, + temperature, + thinking, + // Setting cache breakpoint for system prompt so new tasks can reuse it. + system: [{ text: systemPrompt, type: "text", cache_control: cacheControl }], messages: messages.map((message, index) => { if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) { return { ...message, content: typeof message.content === "string" - ? [ - { - type: "text", - text: message.content, - cache_control: { type: "ephemeral" }, - }, - ] + ? [{ type: "text", text: message.content, cache_control: cacheControl }] : message.content.map((content, contentIndex) => contentIndex === message.content.length - 1 - ? { ...content, cache_control: { type: "ephemeral" } } + ? { ...content, cache_control: cacheControl } : content, ), } @@ -97,8 +115,8 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { default: { stream = (await this.client.messages.create({ model: modelId, - max_tokens: this.getModel().info.maxTokens || 8192, - temperature: this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE, + max_tokens: maxTokens, + temperature, system: [{ text: systemPrompt, type: "text" }], messages, // tools, @@ -112,8 +130,9 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { for await (const chunk of stream) { switch (chunk.type) { case "message_start": - // tells us cache reads/writes/input/output + // Tells us cache reads/writes/input/output. const usage = chunk.message.usage + yield { type: "usage", inputTokens: usage.input_tokens || 0, @@ -121,45 +140,53 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { cacheWriteTokens: usage.cache_creation_input_tokens || undefined, cacheReadTokens: usage.cache_read_input_tokens || undefined, } + break case "message_delta": - // tells us stop_reason, stop_sequence, and output tokens along the way and at the end of the message - + // Tells us stop_reason, stop_sequence, and output tokens + // along the way and at the end of the message. yield { type: "usage", inputTokens: 0, outputTokens: chunk.usage.output_tokens || 0, } + break case "message_stop": - // no usage data, just an indicator that the message is done + // No usage data, just an indicator that the message is done. break case "content_block_start": switch (chunk.content_block.type) { - case "text": - // we may receive multiple text blocks, in which case just insert a line break between them + case "thinking": + // We may receive multiple text blocks, in which + // case just insert a line break between them. if (chunk.index > 0) { - yield { - type: "text", - text: "\n", - } + yield { type: "reasoning", text: "\n" } } - yield { - type: "text", - text: chunk.content_block.text, + + yield { type: "reasoning", text: chunk.content_block.thinking } + break + case "text": + // We may receive multiple text blocks, in which + // case just insert a line break between them. + if (chunk.index > 0) { + yield { type: "text", text: "\n" } } + + yield { type: "text", text: chunk.content_block.text } break } break case "content_block_delta": switch (chunk.delta.type) { + case "thinking_delta": + yield { type: "reasoning", text: chunk.delta.thinking } + break case "text_delta": - yield { - type: "text", - text: chunk.delta.text, - } + yield { type: "text", text: chunk.delta.text } break } + break case "content_block_stop": break @@ -169,10 +196,12 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { getModel(): { id: AnthropicModelId; info: ModelInfo } { const modelId = this.options.apiModelId + if (modelId && modelId in anthropicModels) { const id = modelId as AnthropicModelId return { id, info: anthropicModels[id] } } + return { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] } } @@ -187,14 +216,17 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler { }) const content = response.content[0] + if (content.type === "text") { return content.text } + return "" } catch (error) { if (error instanceof Error) { throw new Error(`Anthropic completion error: ${error.message}`) } + throw error } } diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 8f897fda2a..3bca70338d 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -9,7 +9,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import { ApiHandler, SingleCompletionHandler } from "../" import { ApiHandlerOptions, BedrockModelId, ModelInfo, bedrockDefaultModelId, bedrockModels } from "../../shared/api" import { ApiStream } from "../transform/stream" -import { convertToBedrockConverseMessages, convertToAnthropicMessage } from "../transform/bedrock-converse-format" +import { convertToBedrockConverseMessages } from "../transform/bedrock-converse-format" const BEDROCK_DEFAULT_TEMPERATURE = 0.3 diff --git a/src/api/providers/glama.ts b/src/api/providers/glama.ts index 72b41e5f58..946d28d24f 100644 --- a/src/api/providers/glama.ts +++ b/src/api/providers/glama.ts @@ -1,10 +1,12 @@ import { Anthropic } from "@anthropic-ai/sdk" import axios from "axios" import OpenAI from "openai" -import { ApiHandler, SingleCompletionHandler } from "../" + import { ApiHandlerOptions, ModelInfo, glamaDefaultModelId, glamaDefaultModelInfo } from "../../shared/api" +import { parseApiPrice } from "../../utils/cost" import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStream } from "../transform/stream" +import { ApiHandler, SingleCompletionHandler } from "../" const GLAMA_DEFAULT_TEMPERATURE = 0 @@ -69,7 +71,7 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler { let maxTokens: number | undefined if (this.getModel().id.startsWith("anthropic/")) { - maxTokens = 8_192 + maxTokens = this.getModel().info.maxTokens } const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = { @@ -177,7 +179,7 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler { } if (this.getModel().id.startsWith("anthropic/")) { - requestOptions.max_tokens = 8192 + requestOptions.max_tokens = this.getModel().info.maxTokens } const response = await this.client.chat.completions.create(requestOptions) @@ -190,3 +192,44 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler { } } } + +export async function getGlamaModels() { + const models: Record = {} + + try { + const response = await axios.get("https://glama.ai/api/gateway/v1/models") + const rawModels = response.data + + for (const rawModel of rawModels) { + const modelInfo: ModelInfo = { + maxTokens: rawModel.maxTokensOutput, + contextWindow: rawModel.maxTokensInput, + supportsImages: rawModel.capabilities?.includes("input:image"), + supportsComputerUse: rawModel.capabilities?.includes("computer_use"), + supportsPromptCache: rawModel.capabilities?.includes("caching"), + inputPrice: parseApiPrice(rawModel.pricePerToken?.input), + outputPrice: parseApiPrice(rawModel.pricePerToken?.output), + description: undefined, + cacheWritesPrice: parseApiPrice(rawModel.pricePerToken?.cacheWrite), + cacheReadsPrice: parseApiPrice(rawModel.pricePerToken?.cacheRead), + } + + switch (rawModel.id) { + case rawModel.id.startsWith("anthropic/claude-3-7-sonnet"): + modelInfo.maxTokens = 16384 + break + case rawModel.id.startsWith("anthropic/"): + modelInfo.maxTokens = 8192 + break + default: + break + } + + models[rawModel.id] = modelInfo + } + } catch (error) { + console.error(`Error fetching Glama models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + } + + return models +} diff --git a/src/api/providers/lmstudio.ts b/src/api/providers/lmstudio.ts index 7efa037f46..beb3bd1b79 100644 --- a/src/api/providers/lmstudio.ts +++ b/src/api/providers/lmstudio.ts @@ -1,5 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" +import axios from "axios" + import { ApiHandler, SingleCompletionHandler } from "../" import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api" import { convertToOpenAiMessages } from "../transform/openai-format" @@ -72,3 +74,17 @@ export class LmStudioHandler implements ApiHandler, SingleCompletionHandler { } } } + +export async function getLmStudioModels(baseUrl = "http://localhost:1234") { + try { + if (!URL.canParse(baseUrl)) { + return [] + } + + const response = await axios.get(`${baseUrl}/v1/models`) + const modelsArray = response.data?.data?.map((model: any) => model.id) || [] + return [...new Set(modelsArray)] + } catch (error) { + return [] + } +} diff --git a/src/api/providers/ollama.ts b/src/api/providers/ollama.ts index afb6117b54..de7df5d261 100644 --- a/src/api/providers/ollama.ts +++ b/src/api/providers/ollama.ts @@ -1,5 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" +import axios from "axios" + import { ApiHandler, SingleCompletionHandler } from "../" import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api" import { convertToOpenAiMessages } from "../transform/openai-format" @@ -88,3 +90,17 @@ export class OllamaHandler implements ApiHandler, SingleCompletionHandler { } } } + +export async function getOllamaModels(baseUrl = "http://localhost:11434") { + try { + if (!URL.canParse(baseUrl)) { + return [] + } + + const response = await axios.get(`${baseUrl}/api/tags`) + const modelsArray = response.data?.models?.map((model: any) => model.name) || [] + return [...new Set(modelsArray)] + } catch (error) { + return [] + } +} diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index cea500df26..f1c404d50a 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -1,5 +1,6 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI, { AzureOpenAI } from "openai" +import axios from "axios" import { ApiHandlerOptions, @@ -166,3 +167,27 @@ export class OpenAiHandler implements ApiHandler, SingleCompletionHandler { } } } + +export async function getOpenAiModels(baseUrl?: string, apiKey?: string) { + try { + if (!baseUrl) { + return [] + } + + if (!URL.canParse(baseUrl)) { + return [] + } + + const config: Record = {} + + if (apiKey) { + config["headers"] = { Authorization: `Bearer ${apiKey}` } + } + + const response = await axios.get(`${baseUrl}/models`, config) + const modelsArray = response.data?.data?.map((model: any) => model.id) || [] + return [...new Set(modelsArray)] + } catch (error) { + return [] + } +} diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 1fcf25260e..0a9488e816 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -1,29 +1,31 @@ import { Anthropic } from "@anthropic-ai/sdk" +import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta" import axios from "axios" import OpenAI from "openai" -import { ApiHandler } from "../" +import delay from "delay" + import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api" +import { parseApiPrice } from "../../utils/cost" import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStreamChunk, ApiStreamUsageChunk } from "../transform/stream" -import delay from "delay" +import { convertToR1Format } from "../transform/r1-format" import { DEEP_SEEK_DEFAULT_TEMPERATURE } from "./openai" +import { ApiHandler, SingleCompletionHandler } from ".." const OPENROUTER_DEFAULT_TEMPERATURE = 0 -// Add custom interface for OpenRouter params +// Add custom interface for OpenRouter params. type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & { transforms?: string[] include_reasoning?: boolean + thinking?: BetaThinkingConfigParam } -// Add custom interface for OpenRouter usage chunk +// Add custom interface for OpenRouter usage chunk. interface OpenRouterApiStreamUsageChunk extends ApiStreamUsageChunk { fullResponseText: string } -import { SingleCompletionHandler } from ".." -import { convertToR1Format } from "../transform/r1-format" - export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { private options: ApiHandlerOptions private client: OpenAI @@ -52,21 +54,12 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { ...convertToOpenAiMessages(messages), ] + const { id: modelId, info: modelInfo } = this.getModel() + // prompt caching: https://openrouter.ai/docs/prompt-caching // this is specifically for claude models (some models may 'support prompt caching' automatically without this) - switch (this.getModel().id) { - case "anthropic/claude-3.5-sonnet": - case "anthropic/claude-3.5-sonnet:beta": - case "anthropic/claude-3.5-sonnet-20240620": - case "anthropic/claude-3.5-sonnet-20240620:beta": - case "anthropic/claude-3-5-haiku": - case "anthropic/claude-3-5-haiku:beta": - case "anthropic/claude-3-5-haiku-20241022": - case "anthropic/claude-3-5-haiku-20241022:beta": - case "anthropic/claude-3-haiku": - case "anthropic/claude-3-haiku:beta": - case "anthropic/claude-3-opus": - case "anthropic/claude-3-opus:beta": + switch (true) { + case modelId.startsWith("anthropic/"): openAiMessages[0] = { role: "system", content: [ @@ -102,30 +95,11 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { break } - // Not sure how openrouter defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192. - // (models usually default to max tokens allowed) - let maxTokens: number | undefined - switch (this.getModel().id) { - case "anthropic/claude-3.5-sonnet": - case "anthropic/claude-3.5-sonnet:beta": - case "anthropic/claude-3.5-sonnet-20240620": - case "anthropic/claude-3.5-sonnet-20240620:beta": - case "anthropic/claude-3-5-haiku": - case "anthropic/claude-3-5-haiku:beta": - case "anthropic/claude-3-5-haiku-20241022": - case "anthropic/claude-3-5-haiku-20241022:beta": - maxTokens = 8_192 - break - } - let defaultTemperature = OPENROUTER_DEFAULT_TEMPERATURE let topP: number | undefined = undefined // Handle models based on deepseek-r1 - if ( - this.getModel().id.startsWith("deepseek/deepseek-r1") || - this.getModel().id === "perplexity/sonar-reasoning" - ) { + if (modelId.startsWith("deepseek/deepseek-r1") || modelId === "perplexity/sonar-reasoning") { // Recommended temperature for DeepSeek reasoning models defaultTemperature = DEEP_SEEK_DEFAULT_TEMPERATURE // DeepSeek highly recommends using user instead of system role @@ -134,24 +108,38 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { topP = 0.95 } + let temperature = this.options.modelTemperature ?? defaultTemperature + let thinking: BetaThinkingConfigParam | undefined = undefined + + if (modelInfo.thinking) { + const maxTokens = modelInfo.maxTokens || 8192 + const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024) + thinking = { type: "enabled", budget_tokens: budgetTokens } + temperature = 1.0 + } + // https://openrouter.ai/docs/transforms let fullResponseText = "" - const stream = await this.client.chat.completions.create({ - model: this.getModel().id, - max_tokens: maxTokens, - temperature: this.options.modelTemperature ?? defaultTemperature, + + const completionParams: OpenRouterChatCompletionParams = { + model: modelId, + max_tokens: modelInfo.maxTokens, + temperature, + thinking, // OpenRouter is temporarily supporting this. top_p: topP, messages: openAiMessages, stream: true, include_reasoning: true, // This way, the transforms field will only be included in the parameters when openRouterUseMiddleOutTransform is true. ...(this.options.openRouterUseMiddleOutTransform && { transforms: ["middle-out"] }), - } as OpenRouterChatCompletionParams) + } + + const stream = await this.client.chat.completions.create(completionParams) let genId: string | undefined for await (const chunk of stream as unknown as AsyncIterable) { - // openrouter returns an error object instead of the openai sdk throwing an error + // OpenRouter returns an error object instead of the OpenAI SDK throwing an error. if ("error" in chunk) { const error = chunk.error as { message?: string; code?: number } console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`) @@ -163,12 +151,14 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { } const delta = chunk.choices[0]?.delta + if ("reasoning" in delta && delta.reasoning) { yield { type: "reasoning", text: delta.reasoning, } as ApiStreamChunk } + if (delta?.content) { fullResponseText += delta.content yield { @@ -176,6 +166,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { text: delta.content, } as ApiStreamChunk } + // if (chunk.usage) { // yield { // type: "usage", @@ -185,10 +176,12 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { // } } - // retry fetching generation details + // Retry fetching generation details. let attempt = 0 + while (attempt++ < 10) { await delay(200) // FIXME: necessary delay to ensure generation endpoint is ready + try { const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, { headers: { @@ -198,7 +191,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { }) const generation = response.data?.data - console.log("OpenRouter generation details:", response.data) + yield { type: "usage", // cacheWriteTokens: 0, @@ -209,6 +202,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { totalCost: generation?.total_cost || 0, fullResponseText, } as OpenRouterApiStreamUsageChunk + return } catch (error) { // ignore if fails @@ -216,13 +210,13 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { } } } - getModel(): { id: string; info: ModelInfo } { + + getModel() { const modelId = this.options.openRouterModelId const modelInfo = this.options.openRouterModelInfo - if (modelId && modelInfo) { - return { id: modelId, info: modelInfo } - } - return { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo } + return modelId && modelInfo + ? { id: modelId, info: modelInfo } + : { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo } } async completePrompt(prompt: string): Promise { @@ -245,7 +239,81 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler { if (error instanceof Error) { throw new Error(`OpenRouter completion error: ${error.message}`) } + throw error } } } + +export async function getOpenRouterModels() { + const models: Record = {} + + try { + const response = await axios.get("https://openrouter.ai/api/v1/models") + const rawModels = response.data.data + + for (const rawModel of rawModels) { + const modelInfo: ModelInfo = { + maxTokens: rawModel.top_provider?.max_completion_tokens, + contextWindow: rawModel.context_length, + supportsImages: rawModel.architecture?.modality?.includes("image"), + supportsPromptCache: false, + inputPrice: parseApiPrice(rawModel.pricing?.prompt), + outputPrice: parseApiPrice(rawModel.pricing?.completion), + description: rawModel.description, + thinking: rawModel.id === "anthropic/claude-3.7-sonnet:thinking", + } + + // NOTE: this needs to be synced with api.ts/openrouter default model info. + switch (true) { + case rawModel.id.startsWith("anthropic/claude-3.7-sonnet"): + modelInfo.supportsComputerUse = true + modelInfo.supportsPromptCache = true + modelInfo.cacheWritesPrice = 3.75 + modelInfo.cacheReadsPrice = 0.3 + modelInfo.maxTokens = 16384 + break + case rawModel.id.startsWith("anthropic/claude-3.5-sonnet-20240620"): + modelInfo.supportsPromptCache = true + modelInfo.cacheWritesPrice = 3.75 + modelInfo.cacheReadsPrice = 0.3 + modelInfo.maxTokens = 8192 + break + case rawModel.id.startsWith("anthropic/claude-3.5-sonnet"): + modelInfo.supportsComputerUse = true + modelInfo.supportsPromptCache = true + modelInfo.cacheWritesPrice = 3.75 + modelInfo.cacheReadsPrice = 0.3 + modelInfo.maxTokens = 8192 + break + case rawModel.id.startsWith("anthropic/claude-3-5-haiku"): + modelInfo.supportsPromptCache = true + modelInfo.cacheWritesPrice = 1.25 + modelInfo.cacheReadsPrice = 0.1 + modelInfo.maxTokens = 8192 + break + case rawModel.id.startsWith("anthropic/claude-3-opus"): + modelInfo.supportsPromptCache = true + modelInfo.cacheWritesPrice = 18.75 + modelInfo.cacheReadsPrice = 1.5 + modelInfo.maxTokens = 8192 + break + case rawModel.id.startsWith("anthropic/claude-3-haiku"): + default: + modelInfo.supportsPromptCache = true + modelInfo.cacheWritesPrice = 0.3 + modelInfo.cacheReadsPrice = 0.03 + modelInfo.maxTokens = 8192 + break + } + + models[rawModel.id] = modelInfo + } + } catch (error) { + console.error( + `Error fetching OpenRouter models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + } + + return models +} diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index 67f43aabc5..5e570ca2a2 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -1,6 +1,9 @@ -import { OpenAiHandler, OpenAiHandlerOptions } from "./openai" +import axios from "axios" + import { ModelInfo, requestyModelInfoSaneDefaults, requestyDefaultModelId } from "../../shared/api" -import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" +import { parseApiPrice } from "../../utils/cost" +import { ApiStreamUsageChunk } from "../transform/stream" +import { OpenAiHandler, OpenAiHandlerOptions } from "./openai" export class RequestyHandler extends OpenAiHandler { constructor(options: OpenAiHandlerOptions) { @@ -38,3 +41,65 @@ export class RequestyHandler extends OpenAiHandler { } } } + +export async function getRequestyModels() { + const models: Record = {} + + try { + const response = await axios.get("https://router.requesty.ai/v1/models") + const rawModels = response.data.data + + for (const rawModel of rawModels) { + // { + // id: "anthropic/claude-3-5-sonnet-20240620", + // object: "model", + // created: 1740552655, + // owned_by: "system", + // input_price: 0.0000028, + // caching_price: 0.00000375, + // cached_price: 3e-7, + // output_price: 0.000015, + // max_output_tokens: 8192, + // context_window: 200000, + // supports_caching: true, + // description: + // "Anthropic's previous most intelligent model. High level of intelligence and capability. Excells in coding.", + // } + + const modelInfo: ModelInfo = { + maxTokens: rawModel.max_output_tokens, + contextWindow: rawModel.context_window, + supportsPromptCache: rawModel.supports_caching, + inputPrice: parseApiPrice(rawModel.input_price), + outputPrice: parseApiPrice(rawModel.output_price), + description: rawModel.description, + cacheWritesPrice: parseApiPrice(rawModel.caching_price), + cacheReadsPrice: parseApiPrice(rawModel.cached_price), + } + + switch (rawModel.id) { + case rawModel.id.startsWith("anthropic/claude-3-7-sonnet"): + modelInfo.supportsComputerUse = true + modelInfo.supportsImages = true + modelInfo.maxTokens = 16384 + break + case rawModel.id.startsWith("anthropic/claude-3-5-sonnet-20241022"): + modelInfo.supportsComputerUse = true + modelInfo.supportsImages = true + modelInfo.maxTokens = 8192 + break + case rawModel.id.startsWith("anthropic/"): + modelInfo.maxTokens = 8192 + break + default: + break + } + + models[rawModel.id] = modelInfo + } + } catch (error) { + console.error(`Error fetching Requesty models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + } + + return models +} diff --git a/src/api/providers/unbound.ts b/src/api/providers/unbound.ts index 0599ffa443..5e3ad8843b 100644 --- a/src/api/providers/unbound.ts +++ b/src/api/providers/unbound.ts @@ -1,9 +1,11 @@ import { Anthropic } from "@anthropic-ai/sdk" +import axios from "axios" import OpenAI from "openai" -import { ApiHandler, SingleCompletionHandler } from "../" + import { ApiHandlerOptions, ModelInfo, unboundDefaultModelId, unboundDefaultModelInfo } from "../../shared/api" import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" +import { ApiHandler, SingleCompletionHandler } from "../" interface UnboundUsage extends OpenAI.CompletionUsage { cache_creation_input_tokens?: number @@ -71,7 +73,7 @@ export class UnboundHandler implements ApiHandler, SingleCompletionHandler { let maxTokens: number | undefined if (this.getModel().id.startsWith("anthropic/")) { - maxTokens = 8_192 + maxTokens = this.getModel().info.maxTokens } const { data: completion, response } = await this.client.chat.completions @@ -150,7 +152,7 @@ export class UnboundHandler implements ApiHandler, SingleCompletionHandler { } if (this.getModel().id.startsWith("anthropic/")) { - requestOptions.max_tokens = 8192 + requestOptions.max_tokens = this.getModel().info.maxTokens } const response = await this.client.chat.completions.create(requestOptions) @@ -163,3 +165,46 @@ export class UnboundHandler implements ApiHandler, SingleCompletionHandler { } } } + +export async function getUnboundModels() { + const models: Record = {} + + try { + const response = await axios.get("https://api.getunbound.ai/models") + + if (response.data) { + const rawModels: Record = response.data + + for (const [modelId, model] of Object.entries(rawModels)) { + const modelInfo: ModelInfo = { + maxTokens: model?.maxTokens ? parseInt(model.maxTokens) : undefined, + contextWindow: model?.contextWindow ? parseInt(model.contextWindow) : 0, + supportsImages: model?.supportsImages ?? false, + supportsPromptCache: model?.supportsPromptCaching ?? false, + supportsComputerUse: model?.supportsComputerUse ?? false, + inputPrice: model?.inputTokenPrice ? parseFloat(model.inputTokenPrice) : undefined, + outputPrice: model?.outputTokenPrice ? parseFloat(model.outputTokenPrice) : undefined, + cacheWritesPrice: model?.cacheWritePrice ? parseFloat(model.cacheWritePrice) : undefined, + cacheReadsPrice: model?.cacheReadPrice ? parseFloat(model.cacheReadPrice) : undefined, + } + + switch (true) { + case modelId.startsWith("anthropic/claude-3-7-sonnet"): + modelInfo.maxTokens = 16384 + break + case modelId.startsWith("anthropic/"): + modelInfo.maxTokens = 8192 + break + default: + break + } + + models[modelId] = modelInfo + } + } + } catch (error) { + console.error(`Error fetching Unbound models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + } + + return models +} diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts index e2bf8609ae..28e24231a2 100644 --- a/src/api/providers/vscode-lm.ts +++ b/src/api/providers/vscode-lm.ts @@ -1,5 +1,6 @@ import { Anthropic } from "@anthropic-ai/sdk" import * as vscode from "vscode" + import { ApiHandler, SingleCompletionHandler } from "../" import { calculateApiCost } from "../../utils/cost" import { ApiStream } from "../transform/stream" @@ -545,3 +546,15 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler { } } } + +export async function getVsCodeLmModels() { + try { + const models = await vscode.lm.selectChatModels({}) + return models || [] + } catch (error) { + console.error( + `Error fetching VS Code LM models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + return [] + } +} diff --git a/src/api/transform/__tests__/bedrock-converse-format.test.ts b/src/api/transform/__tests__/bedrock-converse-format.test.ts index c46eb94a2e..c56b8a07fc 100644 --- a/src/api/transform/__tests__/bedrock-converse-format.test.ts +++ b/src/api/transform/__tests__/bedrock-converse-format.test.ts @@ -1,250 +1,167 @@ -import { convertToBedrockConverseMessages, convertToAnthropicMessage } from "../bedrock-converse-format" +// npx jest src/api/transform/__tests__/bedrock-converse-format.test.ts + +import { convertToBedrockConverseMessages } from "../bedrock-converse-format" import { Anthropic } from "@anthropic-ai/sdk" import { ContentBlock, ToolResultContentBlock } from "@aws-sdk/client-bedrock-runtime" -import { StreamEvent } from "../../providers/bedrock" -describe("bedrock-converse-format", () => { - describe("convertToBedrockConverseMessages", () => { - test("converts simple text messages correctly", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { role: "user", content: "Hello" }, - { role: "assistant", content: "Hi there" }, - ] +describe("convertToBedrockConverseMessages", () => { + test("converts simple text messages correctly", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there" }, + ] - const result = convertToBedrockConverseMessages(messages) + const result = convertToBedrockConverseMessages(messages) - expect(result).toEqual([ - { - role: "user", - content: [{ text: "Hello" }], - }, - { - role: "assistant", - content: [{ text: "Hi there" }], - }, - ]) - }) - - test("converts messages with images correctly", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "text", - text: "Look at this image:", - }, - { - type: "image", - source: { - type: "base64", - data: "SGVsbG8=", // "Hello" in base64 - media_type: "image/jpeg" as const, - }, - }, - ], - }, - ] - - const result = convertToBedrockConverseMessages(messages) - - if (!result[0] || !result[0].content) { - fail("Expected result to have content") - return - } - - expect(result[0].role).toBe("user") - expect(result[0].content).toHaveLength(2) - expect(result[0].content[0]).toEqual({ text: "Look at this image:" }) - - const imageBlock = result[0].content[1] as ContentBlock - if ("image" in imageBlock && imageBlock.image && imageBlock.image.source) { - expect(imageBlock.image.format).toBe("jpeg") - expect(imageBlock.image.source).toBeDefined() - expect(imageBlock.image.source.bytes).toBeDefined() - } else { - fail("Expected image block not found") - } - }) - - test("converts tool use messages correctly", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "assistant", - content: [ - { - type: "tool_use", - id: "test-id", - name: "read_file", - input: { - path: "test.txt", - }, - }, - ], - }, - ] - - const result = convertToBedrockConverseMessages(messages) - - if (!result[0] || !result[0].content) { - fail("Expected result to have content") - return - } - - expect(result[0].role).toBe("assistant") - const toolBlock = result[0].content[0] as ContentBlock - if ("toolUse" in toolBlock && toolBlock.toolUse) { - expect(toolBlock.toolUse).toEqual({ - toolUseId: "test-id", - name: "read_file", - input: "\n\ntest.txt\n\n", - }) - } else { - fail("Expected tool use block not found") - } - }) - - test("converts tool result messages correctly", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "assistant", - content: [ - { - type: "tool_result", - tool_use_id: "test-id", - content: [{ type: "text", text: "File contents here" }], - }, - ], - }, - ] - - const result = convertToBedrockConverseMessages(messages) - - if (!result[0] || !result[0].content) { - fail("Expected result to have content") - return - } - - expect(result[0].role).toBe("assistant") - const resultBlock = result[0].content[0] as ContentBlock - if ("toolResult" in resultBlock && resultBlock.toolResult) { - const expectedContent: ToolResultContentBlock[] = [{ text: "File contents here" }] - expect(resultBlock.toolResult).toEqual({ - toolUseId: "test-id", - content: expectedContent, - status: "success", - }) - } else { - fail("Expected tool result block not found") - } - }) - - test("handles text content correctly", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "text", - text: "Hello world", - }, - ], - }, - ] - - const result = convertToBedrockConverseMessages(messages) - - if (!result[0] || !result[0].content) { - fail("Expected result to have content") - return - } - - expect(result[0].role).toBe("user") - expect(result[0].content).toHaveLength(1) - const textBlock = result[0].content[0] as ContentBlock - expect(textBlock).toEqual({ text: "Hello world" }) - }) + expect(result).toEqual([ + { + role: "user", + content: [{ text: "Hello" }], + }, + { + role: "assistant", + content: [{ text: "Hi there" }], + }, + ]) }) - describe("convertToAnthropicMessage", () => { - test("converts metadata events correctly", () => { - const event: StreamEvent = { - metadata: { - usage: { - inputTokens: 10, - outputTokens: 20, + test("converts messages with images correctly", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "Look at this image:", }, - }, - } - - const result = convertToAnthropicMessage(event, "test-model") - - expect(result).toEqual({ - id: "", - type: "message", - role: "assistant", - model: "test-model", - usage: { - input_tokens: 10, - output_tokens: 20, - }, - }) - }) - - test("converts content block start events correctly", () => { - const event: StreamEvent = { - contentBlockStart: { - start: { - text: "Hello", + { + type: "image", + source: { + type: "base64", + data: "SGVsbG8=", // "Hello" in base64 + media_type: "image/jpeg" as const, + }, }, - }, - } + ], + }, + ] - const result = convertToAnthropicMessage(event, "test-model") + const result = convertToBedrockConverseMessages(messages) - expect(result).toEqual({ - type: "message", + if (!result[0] || !result[0].content) { + fail("Expected result to have content") + return + } + + expect(result[0].role).toBe("user") + expect(result[0].content).toHaveLength(2) + expect(result[0].content[0]).toEqual({ text: "Look at this image:" }) + + const imageBlock = result[0].content[1] as ContentBlock + if ("image" in imageBlock && imageBlock.image && imageBlock.image.source) { + expect(imageBlock.image.format).toBe("jpeg") + expect(imageBlock.image.source).toBeDefined() + expect(imageBlock.image.source.bytes).toBeDefined() + } else { + fail("Expected image block not found") + } + }) + + test("converts tool use messages correctly", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "assistant", - content: [{ type: "text", text: "Hello" }], - model: "test-model", - }) - }) - - test("converts content block delta events correctly", () => { - const event: StreamEvent = { - contentBlockDelta: { - delta: { - text: " world", + content: [ + { + type: "tool_use", + id: "test-id", + name: "read_file", + input: { + path: "test.txt", + }, }, - }, - } + ], + }, + ] - const result = convertToAnthropicMessage(event, "test-model") + const result = convertToBedrockConverseMessages(messages) - expect(result).toEqual({ - type: "message", - role: "assistant", - content: [{ type: "text", text: " world" }], - model: "test-model", + if (!result[0] || !result[0].content) { + fail("Expected result to have content") + return + } + + expect(result[0].role).toBe("assistant") + const toolBlock = result[0].content[0] as ContentBlock + if ("toolUse" in toolBlock && toolBlock.toolUse) { + expect(toolBlock.toolUse).toEqual({ + toolUseId: "test-id", + name: "read_file", + input: "\n\ntest.txt\n\n", }) - }) + } else { + fail("Expected tool use block not found") + } + }) - test("converts message stop events correctly", () => { - const event: StreamEvent = { - messageStop: { - stopReason: "end_turn" as const, - }, - } - - const result = convertToAnthropicMessage(event, "test-model") - - expect(result).toEqual({ - type: "message", + test("converts tool result messages correctly", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "assistant", - stop_reason: "end_turn", - stop_sequence: null, - model: "test-model", + content: [ + { + type: "tool_result", + tool_use_id: "test-id", + content: [{ type: "text", text: "File contents here" }], + }, + ], + }, + ] + + const result = convertToBedrockConverseMessages(messages) + + if (!result[0] || !result[0].content) { + fail("Expected result to have content") + return + } + + expect(result[0].role).toBe("assistant") + const resultBlock = result[0].content[0] as ContentBlock + if ("toolResult" in resultBlock && resultBlock.toolResult) { + const expectedContent: ToolResultContentBlock[] = [{ text: "File contents here" }] + expect(resultBlock.toolResult).toEqual({ + toolUseId: "test-id", + content: expectedContent, + status: "success", }) - }) + } else { + fail("Expected tool result block not found") + } + }) + + test("handles text content correctly", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "Hello world", + }, + ], + }, + ] + + const result = convertToBedrockConverseMessages(messages) + + if (!result[0] || !result[0].content) { + fail("Expected result to have content") + return + } + + expect(result[0].role).toBe("user") + expect(result[0].content).toHaveLength(1) + const textBlock = result[0].content[0] as ContentBlock + expect(textBlock).toEqual({ text: "Hello world" }) }) }) diff --git a/src/api/transform/__tests__/gemini-format.test.ts b/src/api/transform/__tests__/gemini-format.test.ts new file mode 100644 index 0000000000..fe6b256404 --- /dev/null +++ b/src/api/transform/__tests__/gemini-format.test.ts @@ -0,0 +1,338 @@ +// npx jest src/api/transform/__tests__/gemini-format.test.ts + +import { Anthropic } from "@anthropic-ai/sdk" + +import { convertAnthropicMessageToGemini } from "../gemini-format" + +describe("convertAnthropicMessageToGemini", () => { + it("should convert a simple text message", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: "Hello, world!", + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [{ text: "Hello, world!" }], + }) + }) + + it("should convert assistant role to model role", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "assistant", + content: "I'm an assistant", + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "model", + parts: [{ text: "I'm an assistant" }], + }) + }) + + it("should convert a message with text blocks", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { type: "text", text: "First paragraph" }, + { type: "text", text: "Second paragraph" }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [{ text: "First paragraph" }, { text: "Second paragraph" }], + }) + }) + + it("should convert a message with an image", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { type: "text", text: "Check out this image:" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: "base64encodeddata", + }, + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [ + { text: "Check out this image:" }, + { + inlineData: { + data: "base64encodeddata", + mimeType: "image/jpeg", + }, + }, + ], + }) + }) + + it("should throw an error for unsupported image source type", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { + type: "image", + source: { + type: "url", // Not supported + url: "https://example.com/image.jpg", + } as any, + }, + ], + } + + expect(() => convertAnthropicMessageToGemini(anthropicMessage)).toThrow("Unsupported image source type") + }) + + it("should convert a message with tool use", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "assistant", + content: [ + { type: "text", text: "Let me calculate that for you." }, + { + type: "tool_use", + id: "calc-123", + name: "calculator", + input: { operation: "add", numbers: [2, 3] }, + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "model", + parts: [ + { text: "Let me calculate that for you." }, + { + functionCall: { + name: "calculator", + args: { operation: "add", numbers: [2, 3] }, + }, + }, + ], + }) + }) + + it("should convert a message with tool result as string", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { type: "text", text: "Here's the result:" }, + { + type: "tool_result", + tool_use_id: "calculator-123", + content: "The result is 5", + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [ + { text: "Here's the result:" }, + { + functionResponse: { + name: "calculator", + response: { + name: "calculator", + content: "The result is 5", + }, + }, + }, + ], + }) + }) + + it("should handle empty tool result content", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "calculator-123", + content: null as any, // Empty content + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + // Should skip the empty tool result + expect(result).toEqual({ + role: "user", + parts: [], + }) + }) + + it("should convert a message with tool result as array with text only", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "search-123", + content: [ + { type: "text", text: "First result" }, + { type: "text", text: "Second result" }, + ], + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [ + { + functionResponse: { + name: "search", + response: { + name: "search", + content: "First result\n\nSecond result", + }, + }, + }, + ], + }) + }) + + it("should convert a message with tool result as array with text and images", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "search-123", + content: [ + { type: "text", text: "Search results:" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "image1data", + }, + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: "image2data", + }, + }, + ], + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [ + { + functionResponse: { + name: "search", + response: { + name: "search", + content: "Search results:\n\n(See next part for image)", + }, + }, + }, + { + inlineData: { + data: "image1data", + mimeType: "image/png", + }, + }, + { + inlineData: { + data: "image2data", + mimeType: "image/jpeg", + }, + }, + ], + }) + }) + + it("should convert a message with tool result containing only images", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "imagesearch-123", + content: [ + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "onlyimagedata", + }, + }, + ], + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage) + + expect(result).toEqual({ + role: "user", + parts: [ + { + functionResponse: { + name: "imagesearch", + response: { + name: "imagesearch", + content: "\n\n(See next part for image)", + }, + }, + }, + { + inlineData: { + data: "onlyimagedata", + mimeType: "image/png", + }, + }, + ], + }) + }) + + it("should throw an error for unsupported content block type", () => { + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { + type: "unknown_type", // Unsupported type + data: "some data", + } as any, + ], + } + + expect(() => convertAnthropicMessageToGemini(anthropicMessage)).toThrow( + "Unsupported content block type: unknown_type", + ) + }) +}) diff --git a/src/api/transform/__tests__/mistral-format.test.ts b/src/api/transform/__tests__/mistral-format.test.ts new file mode 100644 index 0000000000..b8e9412eda --- /dev/null +++ b/src/api/transform/__tests__/mistral-format.test.ts @@ -0,0 +1,301 @@ +// npx jest src/api/transform/__tests__/mistral-format.test.ts + +import { Anthropic } from "@anthropic-ai/sdk" + +import { convertToMistralMessages } from "../mistral-format" + +describe("convertToMistralMessages", () => { + it("should convert simple text messages for user and assistant roles", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello", + }, + { + role: "assistant", + content: "Hi there!", + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(2) + expect(mistralMessages[0]).toEqual({ + role: "user", + content: "Hello", + }) + expect(mistralMessages[1]).toEqual({ + role: "assistant", + content: "Hi there!", + }) + }) + + it("should handle user messages with image content", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "What is in this image?", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: "base64data", + }, + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(1) + expect(mistralMessages[0].role).toBe("user") + + const content = mistralMessages[0].content as Array<{ + type: string + text?: string + imageUrl?: { url: string } + }> + + expect(Array.isArray(content)).toBe(true) + expect(content).toHaveLength(2) + expect(content[0]).toEqual({ type: "text", text: "What is in this image?" }) + expect(content[1]).toEqual({ + type: "image_url", + imageUrl: { url: "data:image/jpeg;base64,base64data" }, + }) + }) + + it("should handle user messages with only tool results", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "weather-123", + content: "Current temperature in London: 20°C", + }, + ], + }, + ] + + // Based on the implementation, tool results without accompanying text/image + // don't generate any messages + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(0) + }) + + it("should handle user messages with mixed content (text, image, and tool results)", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "Here's the weather data and an image:", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "imagedata123", + }, + }, + { + type: "tool_result", + tool_use_id: "weather-123", + content: "Current temperature in London: 20°C", + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + // Based on the implementation, only the text and image content is included + // Tool results are not converted to separate messages + expect(mistralMessages).toHaveLength(1) + + // Message should be the user message with text and image + expect(mistralMessages[0].role).toBe("user") + const userContent = mistralMessages[0].content as Array<{ + type: string + text?: string + imageUrl?: { url: string } + }> + expect(Array.isArray(userContent)).toBe(true) + expect(userContent).toHaveLength(2) + expect(userContent[0]).toEqual({ type: "text", text: "Here's the weather data and an image:" }) + expect(userContent[1]).toEqual({ + type: "image_url", + imageUrl: { url: "data:image/png;base64,imagedata123" }, + }) + }) + + it("should handle assistant messages with text content", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "text", + text: "I'll help you with that question.", + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(1) + expect(mistralMessages[0].role).toBe("assistant") + expect(mistralMessages[0].content).toBe("I'll help you with that question.") + }) + + it("should handle assistant messages with tool use", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "text", + text: "Let me check the weather for you.", + }, + { + type: "tool_use", + id: "weather-123", + name: "get_weather", + input: { city: "London" }, + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(1) + expect(mistralMessages[0].role).toBe("assistant") + expect(mistralMessages[0].content).toBe("Let me check the weather for you.") + }) + + it("should handle multiple text blocks in assistant messages", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "text", + text: "First paragraph of information.", + }, + { + type: "text", + text: "Second paragraph with more details.", + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(1) + expect(mistralMessages[0].role).toBe("assistant") + expect(mistralMessages[0].content).toBe("First paragraph of information.\nSecond paragraph with more details.") + }) + + it("should handle a conversation with mixed message types", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "What's in this image?", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: "imagedata", + }, + }, + ], + }, + { + role: "assistant", + content: [ + { + type: "text", + text: "This image shows a landscape with mountains.", + }, + { + type: "tool_use", + id: "search-123", + name: "search_info", + input: { query: "mountain types" }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "search-123", + content: "Found information about different mountain types.", + }, + ], + }, + { + role: "assistant", + content: "Based on the search results, I can tell you more about the mountains in the image.", + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + // Based on the implementation, user messages with only tool results don't generate messages + expect(mistralMessages).toHaveLength(3) + + // User message with image + expect(mistralMessages[0].role).toBe("user") + const userContent = mistralMessages[0].content as Array<{ + type: string + text?: string + imageUrl?: { url: string } + }> + expect(Array.isArray(userContent)).toBe(true) + expect(userContent).toHaveLength(2) + + // Assistant message with text (tool_use is not included in Mistral format) + expect(mistralMessages[1].role).toBe("assistant") + expect(mistralMessages[1].content).toBe("This image shows a landscape with mountains.") + + // Final assistant message + expect(mistralMessages[2]).toEqual({ + role: "assistant", + content: "Based on the search results, I can tell you more about the mountains in the image.", + }) + }) + + it("should handle empty content in assistant messages", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "search-123", + name: "search_info", + input: { query: "test query" }, + }, + ], + }, + ] + + const mistralMessages = convertToMistralMessages(anthropicMessages) + expect(mistralMessages).toHaveLength(1) + expect(mistralMessages[0].role).toBe("assistant") + expect(mistralMessages[0].content).toBeUndefined() + }) +}) diff --git a/src/api/transform/__tests__/openai-format.test.ts b/src/api/transform/__tests__/openai-format.test.ts index f37d369d70..f0aa5e1a56 100644 --- a/src/api/transform/__tests__/openai-format.test.ts +++ b/src/api/transform/__tests__/openai-format.test.ts @@ -1,275 +1,131 @@ -import { convertToOpenAiMessages, convertToAnthropicMessage } from "../openai-format" +// npx jest src/api/transform/__tests__/openai-format.test.ts + import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" -type PartialChatCompletion = Omit & { - choices: Array< - Partial & { - message: OpenAI.Chat.Completions.ChatCompletion.Choice["message"] - finish_reason: string - index: number - } - > -} +import { convertToOpenAiMessages } from "../openai-format" -describe("OpenAI Format Transformations", () => { - describe("convertToOpenAiMessages", () => { - it("should convert simple text messages", () => { - const anthropicMessages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: "Hello", - }, - { - role: "assistant", - content: "Hi there!", - }, - ] - - const openAiMessages = convertToOpenAiMessages(anthropicMessages) - expect(openAiMessages).toHaveLength(2) - expect(openAiMessages[0]).toEqual({ +describe("convertToOpenAiMessages", () => { + it("should convert simple text messages", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello", - }) - expect(openAiMessages[1]).toEqual({ + }, + { role: "assistant", content: "Hi there!", - }) + }, + ] + + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages).toHaveLength(2) + expect(openAiMessages[0]).toEqual({ + role: "user", + content: "Hello", }) - - it("should handle messages with image content", () => { - const anthropicMessages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "text", - text: "What is in this image?", - }, - { - type: "image", - source: { - type: "base64", - media_type: "image/jpeg", - data: "base64data", - }, - }, - ], - }, - ] - - const openAiMessages = convertToOpenAiMessages(anthropicMessages) - expect(openAiMessages).toHaveLength(1) - expect(openAiMessages[0].role).toBe("user") - - const content = openAiMessages[0].content as Array<{ - type: string - text?: string - image_url?: { url: string } - }> - - expect(Array.isArray(content)).toBe(true) - expect(content).toHaveLength(2) - expect(content[0]).toEqual({ type: "text", text: "What is in this image?" }) - expect(content[1]).toEqual({ - type: "image_url", - image_url: { url: "data:image/jpeg;base64,base64data" }, - }) - }) - - it("should handle assistant messages with tool use", () => { - const anthropicMessages: Anthropic.Messages.MessageParam[] = [ - { - role: "assistant", - content: [ - { - type: "text", - text: "Let me check the weather.", - }, - { - type: "tool_use", - id: "weather-123", - name: "get_weather", - input: { city: "London" }, - }, - ], - }, - ] - - const openAiMessages = convertToOpenAiMessages(anthropicMessages) - expect(openAiMessages).toHaveLength(1) - - const assistantMessage = openAiMessages[0] as OpenAI.Chat.ChatCompletionAssistantMessageParam - expect(assistantMessage.role).toBe("assistant") - expect(assistantMessage.content).toBe("Let me check the weather.") - expect(assistantMessage.tool_calls).toHaveLength(1) - expect(assistantMessage.tool_calls![0]).toEqual({ - id: "weather-123", - type: "function", - function: { - name: "get_weather", - arguments: JSON.stringify({ city: "London" }), - }, - }) - }) - - it("should handle user messages with tool results", () => { - const anthropicMessages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { - type: "tool_result", - tool_use_id: "weather-123", - content: "Current temperature in London: 20°C", - }, - ], - }, - ] - - const openAiMessages = convertToOpenAiMessages(anthropicMessages) - expect(openAiMessages).toHaveLength(1) - - const toolMessage = openAiMessages[0] as OpenAI.Chat.ChatCompletionToolMessageParam - expect(toolMessage.role).toBe("tool") - expect(toolMessage.tool_call_id).toBe("weather-123") - expect(toolMessage.content).toBe("Current temperature in London: 20°C") + expect(openAiMessages[1]).toEqual({ + role: "assistant", + content: "Hi there!", }) }) - describe("convertToAnthropicMessage", () => { - it("should convert simple completion", () => { - const openAiCompletion: PartialChatCompletion = { - id: "completion-123", - model: "gpt-4", - choices: [ + it("should handle messages with image content", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ { - message: { - role: "assistant", - content: "Hello there!", - refusal: null, + type: "text", + text: "What is in this image?", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/jpeg", + data: "base64data", }, - finish_reason: "stop", - index: 0, }, ], - usage: { - prompt_tokens: 10, - completion_tokens: 5, - total_tokens: 15, - }, - created: 123456789, - object: "chat.completion", - } + }, + ] - const anthropicMessage = convertToAnthropicMessage( - openAiCompletion as OpenAI.Chat.Completions.ChatCompletion, - ) - expect(anthropicMessage.id).toBe("completion-123") - expect(anthropicMessage.role).toBe("assistant") - expect(anthropicMessage.content).toHaveLength(1) - expect(anthropicMessage.content[0]).toEqual({ - type: "text", - text: "Hello there!", - }) - expect(anthropicMessage.stop_reason).toBe("end_turn") - expect(anthropicMessage.usage).toEqual({ - input_tokens: 10, - output_tokens: 5, - }) + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages).toHaveLength(1) + expect(openAiMessages[0].role).toBe("user") + + const content = openAiMessages[0].content as Array<{ + type: string + text?: string + image_url?: { url: string } + }> + + expect(Array.isArray(content)).toBe(true) + expect(content).toHaveLength(2) + expect(content[0]).toEqual({ type: "text", text: "What is in this image?" }) + expect(content[1]).toEqual({ + type: "image_url", + image_url: { url: "data:image/jpeg;base64,base64data" }, }) + }) - it("should handle tool calls in completion", () => { - const openAiCompletion: PartialChatCompletion = { - id: "completion-123", - model: "gpt-4", - choices: [ + it("should handle assistant messages with tool use", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ { - message: { - role: "assistant", - content: "Let me check the weather.", - tool_calls: [ - { - id: "weather-123", - type: "function", - function: { - name: "get_weather", - arguments: '{"city":"London"}', - }, - }, - ], - refusal: null, - }, - finish_reason: "tool_calls", - index: 0, + type: "text", + text: "Let me check the weather.", + }, + { + type: "tool_use", + id: "weather-123", + name: "get_weather", + input: { city: "London" }, }, ], - usage: { - prompt_tokens: 15, - completion_tokens: 8, - total_tokens: 23, - }, - created: 123456789, - object: "chat.completion", - } + }, + ] - const anthropicMessage = convertToAnthropicMessage( - openAiCompletion as OpenAI.Chat.Completions.ChatCompletion, - ) - expect(anthropicMessage.content).toHaveLength(2) - expect(anthropicMessage.content[0]).toEqual({ - type: "text", - text: "Let me check the weather.", - }) - expect(anthropicMessage.content[1]).toEqual({ - type: "tool_use", - id: "weather-123", + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages).toHaveLength(1) + + const assistantMessage = openAiMessages[0] as OpenAI.Chat.ChatCompletionAssistantMessageParam + expect(assistantMessage.role).toBe("assistant") + expect(assistantMessage.content).toBe("Let me check the weather.") + expect(assistantMessage.tool_calls).toHaveLength(1) + expect(assistantMessage.tool_calls![0]).toEqual({ + id: "weather-123", + type: "function", + function: { name: "get_weather", - input: { city: "London" }, - }) - expect(anthropicMessage.stop_reason).toBe("tool_use") + arguments: JSON.stringify({ city: "London" }), + }, }) + }) - it("should handle invalid tool call arguments", () => { - const openAiCompletion: PartialChatCompletion = { - id: "completion-123", - model: "gpt-4", - choices: [ + it("should handle user messages with tool results", () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ { - message: { - role: "assistant", - content: "Testing invalid arguments", - tool_calls: [ - { - id: "test-123", - type: "function", - function: { - name: "test_function", - arguments: "invalid json", - }, - }, - ], - refusal: null, - }, - finish_reason: "tool_calls", - index: 0, + type: "tool_result", + tool_use_id: "weather-123", + content: "Current temperature in London: 20°C", }, ], - created: 123456789, - object: "chat.completion", - } + }, + ] - const anthropicMessage = convertToAnthropicMessage( - openAiCompletion as OpenAI.Chat.Completions.ChatCompletion, - ) - expect(anthropicMessage.content).toHaveLength(2) - expect(anthropicMessage.content[1]).toEqual({ - type: "tool_use", - id: "test-123", - name: "test_function", - input: {}, // Should default to empty object for invalid JSON - }) - }) + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages).toHaveLength(1) + + const toolMessage = openAiMessages[0] as OpenAI.Chat.ChatCompletionToolMessageParam + expect(toolMessage.role).toBe("tool") + expect(toolMessage.tool_call_id).toBe("weather-123") + expect(toolMessage.content).toBe("Current temperature in London: 20°C") }) }) diff --git a/src/api/transform/__tests__/vscode-lm-format.test.ts b/src/api/transform/__tests__/vscode-lm-format.test.ts index b27097fd17..eea8de7c9a 100644 --- a/src/api/transform/__tests__/vscode-lm-format.test.ts +++ b/src/api/transform/__tests__/vscode-lm-format.test.ts @@ -1,6 +1,8 @@ +// npx jest src/api/transform/__tests__/vscode-lm-format.test.ts + import { Anthropic } from "@anthropic-ai/sdk" -import * as vscode from "vscode" -import { convertToVsCodeLmMessages, convertToAnthropicRole, convertToAnthropicMessage } from "../vscode-lm-format" + +import { convertToVsCodeLmMessages, convertToAnthropicRole } from "../vscode-lm-format" // Mock crypto const mockCrypto = { @@ -27,14 +29,6 @@ interface MockLanguageModelToolResultPart { parts: MockLanguageModelTextPart[] } -type MockMessageContent = MockLanguageModelTextPart | MockLanguageModelToolCallPart | MockLanguageModelToolResultPart - -interface MockLanguageModelChatMessage { - role: string - name?: string - content: MockMessageContent[] -} - // Mock vscode namespace jest.mock("vscode", () => { const LanguageModelChatMessageRole = { @@ -84,173 +78,115 @@ jest.mock("vscode", () => { } }) -describe("vscode-lm-format", () => { - describe("convertToVsCodeLmMessages", () => { - it("should convert simple string messages", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { role: "user", content: "Hello" }, - { role: "assistant", content: "Hi there" }, - ] +describe("convertToVsCodeLmMessages", () => { + it("should convert simple string messages", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there" }, + ] - const result = convertToVsCodeLmMessages(messages) + const result = convertToVsCodeLmMessages(messages) - expect(result).toHaveLength(2) - expect(result[0].role).toBe("user") - expect((result[0].content[0] as MockLanguageModelTextPart).value).toBe("Hello") - expect(result[1].role).toBe("assistant") - expect((result[1].content[0] as MockLanguageModelTextPart).value).toBe("Hi there") - }) - - it("should handle complex user messages with tool results", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { type: "text", text: "Here is the result:" }, - { - type: "tool_result", - tool_use_id: "tool-1", - content: "Tool output", - }, - ], - }, - ] - - const result = convertToVsCodeLmMessages(messages) - - expect(result).toHaveLength(1) - expect(result[0].role).toBe("user") - expect(result[0].content).toHaveLength(2) - const [toolResult, textContent] = result[0].content as [ - MockLanguageModelToolResultPart, - MockLanguageModelTextPart, - ] - expect(toolResult.type).toBe("tool_result") - expect(textContent.type).toBe("text") - }) - - it("should handle complex assistant messages with tool calls", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "assistant", - content: [ - { type: "text", text: "Let me help you with that." }, - { - type: "tool_use", - id: "tool-1", - name: "calculator", - input: { operation: "add", numbers: [2, 2] }, - }, - ], - }, - ] - - const result = convertToVsCodeLmMessages(messages) - - expect(result).toHaveLength(1) - expect(result[0].role).toBe("assistant") - expect(result[0].content).toHaveLength(2) - const [toolCall, textContent] = result[0].content as [ - MockLanguageModelToolCallPart, - MockLanguageModelTextPart, - ] - expect(toolCall.type).toBe("tool_call") - expect(textContent.type).toBe("text") - }) - - it("should handle image blocks with appropriate placeholders", () => { - const messages: Anthropic.Messages.MessageParam[] = [ - { - role: "user", - content: [ - { type: "text", text: "Look at this:" }, - { - type: "image", - source: { - type: "base64", - media_type: "image/png", - data: "base64data", - }, - }, - ], - }, - ] - - const result = convertToVsCodeLmMessages(messages) - - expect(result).toHaveLength(1) - const imagePlaceholder = result[0].content[1] as MockLanguageModelTextPart - expect(imagePlaceholder.value).toContain("[Image (base64): image/png not supported by VSCode LM API]") - }) + expect(result).toHaveLength(2) + expect(result[0].role).toBe("user") + expect((result[0].content[0] as MockLanguageModelTextPart).value).toBe("Hello") + expect(result[1].role).toBe("assistant") + expect((result[1].content[0] as MockLanguageModelTextPart).value).toBe("Hi there") }) - describe("convertToAnthropicRole", () => { - it("should convert assistant role correctly", () => { - const result = convertToAnthropicRole("assistant" as any) - expect(result).toBe("assistant") - }) - - it("should convert user role correctly", () => { - const result = convertToAnthropicRole("user" as any) - expect(result).toBe("user") - }) - - it("should return null for unknown roles", () => { - const result = convertToAnthropicRole("unknown" as any) - expect(result).toBeNull() - }) - }) - - describe("convertToAnthropicMessage", () => { - it("should convert assistant message with text content", async () => { - const vsCodeMessage = { - role: "assistant", - name: "assistant", - content: [new vscode.LanguageModelTextPart("Hello")], - } - - const result = await convertToAnthropicMessage(vsCodeMessage as any) - - expect(result.role).toBe("assistant") - expect(result.content).toHaveLength(1) - expect(result.content[0]).toEqual({ - type: "text", - text: "Hello", - }) - expect(result.id).toBe("test-uuid") - }) - - it("should convert assistant message with tool calls", async () => { - const vsCodeMessage = { - role: "assistant", - name: "assistant", - content: [ - new vscode.LanguageModelToolCallPart("call-1", "calculator", { operation: "add", numbers: [2, 2] }), - ], - } - - const result = await convertToAnthropicMessage(vsCodeMessage as any) - - expect(result.content).toHaveLength(1) - expect(result.content[0]).toEqual({ - type: "tool_use", - id: "call-1", - name: "calculator", - input: { operation: "add", numbers: [2, 2] }, - }) - expect(result.id).toBe("test-uuid") - }) - - it("should throw error for non-assistant messages", async () => { - const vsCodeMessage = { + it("should handle complex user messages with tool results", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", - name: "user", - content: [new vscode.LanguageModelTextPart("Hello")], - } + content: [ + { type: "text", text: "Here is the result:" }, + { + type: "tool_result", + tool_use_id: "tool-1", + content: "Tool output", + }, + ], + }, + ] - await expect(convertToAnthropicMessage(vsCodeMessage as any)).rejects.toThrow( - "Roo Code : Only assistant messages are supported.", - ) - }) + const result = convertToVsCodeLmMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0].role).toBe("user") + expect(result[0].content).toHaveLength(2) + const [toolResult, textContent] = result[0].content as [ + MockLanguageModelToolResultPart, + MockLanguageModelTextPart, + ] + expect(toolResult.type).toBe("tool_result") + expect(textContent.type).toBe("text") + }) + + it("should handle complex assistant messages with tool calls", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "text", text: "Let me help you with that." }, + { + type: "tool_use", + id: "tool-1", + name: "calculator", + input: { operation: "add", numbers: [2, 2] }, + }, + ], + }, + ] + + const result = convertToVsCodeLmMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0].role).toBe("assistant") + expect(result[0].content).toHaveLength(2) + const [toolCall, textContent] = result[0].content as [MockLanguageModelToolCallPart, MockLanguageModelTextPart] + expect(toolCall.type).toBe("tool_call") + expect(textContent.type).toBe("text") + }) + + it("should handle image blocks with appropriate placeholders", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Look at this:" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "base64data", + }, + }, + ], + }, + ] + + const result = convertToVsCodeLmMessages(messages) + + expect(result).toHaveLength(1) + const imagePlaceholder = result[0].content[1] as MockLanguageModelTextPart + expect(imagePlaceholder.value).toContain("[Image (base64): image/png not supported by VSCode LM API]") + }) +}) + +describe("convertToAnthropicRole", () => { + it("should convert assistant role correctly", () => { + const result = convertToAnthropicRole("assistant" as any) + expect(result).toBe("assistant") + }) + + it("should convert user role correctly", () => { + const result = convertToAnthropicRole("user" as any) + expect(result).toBe("user") + }) + + it("should return null for unknown roles", () => { + const result = convertToAnthropicRole("unknown" as any) + expect(result).toBeNull() }) }) diff --git a/src/api/transform/bedrock-converse-format.ts b/src/api/transform/bedrock-converse-format.ts index 07529db1bc..68d21e4d5b 100644 --- a/src/api/transform/bedrock-converse-format.ts +++ b/src/api/transform/bedrock-converse-format.ts @@ -1,9 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" -import { MessageContent } from "../../shared/api" import { ConversationRole, Message, ContentBlock } from "@aws-sdk/client-bedrock-runtime" -// Import StreamEvent type from bedrock.ts -import { StreamEvent } from "../providers/bedrock" +import { MessageContent } from "../../shared/api" /** * Convert Anthropic messages to Bedrock Converse format @@ -175,49 +173,3 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me } }) } - -/** - * Convert Bedrock Converse stream events to Anthropic message format - */ -export function convertToAnthropicMessage( - streamEvent: StreamEvent, - modelId: string, -): Partial { - // Handle metadata events - if (streamEvent.metadata?.usage) { - return { - id: "", // Bedrock doesn't provide message IDs - type: "message", - role: "assistant", - model: modelId, - usage: { - input_tokens: streamEvent.metadata.usage.inputTokens || 0, - output_tokens: streamEvent.metadata.usage.outputTokens || 0, - }, - } - } - - // Handle content blocks - const text = streamEvent.contentBlockStart?.start?.text || streamEvent.contentBlockDelta?.delta?.text - if (text !== undefined) { - return { - type: "message", - role: "assistant", - content: [{ type: "text", text: text }], - model: modelId, - } - } - - // Handle message stop - if (streamEvent.messageStop) { - return { - type: "message", - role: "assistant", - stop_reason: streamEvent.messageStop.stopReason || null, - stop_sequence: null, - model: modelId, - } - } - - return {} -} diff --git a/src/api/transform/gemini-format.ts b/src/api/transform/gemini-format.ts index 935e47147a..c8fc80d769 100644 --- a/src/api/transform/gemini-format.ts +++ b/src/api/transform/gemini-format.ts @@ -1,29 +1,11 @@ import { Anthropic } from "@anthropic-ai/sdk" -import { - Content, - EnhancedGenerateContentResponse, - FunctionCallPart, - FunctionDeclaration, - FunctionResponsePart, - InlineDataPart, - Part, - SchemaType, - TextPart, -} from "@google/generative-ai" +import { Content, FunctionCallPart, FunctionResponsePart, InlineDataPart, Part, TextPart } from "@google/generative-ai" -export function convertAnthropicContentToGemini( - content: - | string - | Array< - | Anthropic.Messages.TextBlockParam - | Anthropic.Messages.ImageBlockParam - | Anthropic.Messages.ToolUseBlockParam - | Anthropic.Messages.ToolResultBlockParam - >, -): Part[] { +function convertAnthropicContentToGemini(content: Anthropic.Messages.MessageParam["content"]): Part[] { if (typeof content === "string") { return [{ text: content } as TextPart] } + return content.flatMap((block) => { switch (block.type) { case "text": @@ -99,97 +81,3 @@ export function convertAnthropicMessageToGemini(message: Anthropic.Messages.Mess parts: convertAnthropicContentToGemini(message.content), } } - -export function convertAnthropicToolToGemini(tool: Anthropic.Messages.Tool): FunctionDeclaration { - return { - name: tool.name, - description: tool.description || "", - parameters: { - type: SchemaType.OBJECT, - properties: Object.fromEntries( - Object.entries(tool.input_schema.properties || {}).map(([key, value]) => [ - key, - { - type: (value as any).type.toUpperCase(), - description: (value as any).description || "", - }, - ]), - ), - required: (tool.input_schema.required as string[]) || [], - }, - } -} - -/* -It looks like gemini likes to double escape certain characters when writing file contents: https://discuss.ai.google.dev/t/function-call-string-property-is-double-escaped/37867 -*/ -export function unescapeGeminiContent(content: string) { - return content - .replace(/\\n/g, "\n") - .replace(/\\'/g, "'") - .replace(/\\"/g, '"') - .replace(/\\r/g, "\r") - .replace(/\\t/g, "\t") -} - -export function convertGeminiResponseToAnthropic( - response: EnhancedGenerateContentResponse, -): Anthropic.Messages.Message { - const content: Anthropic.Messages.ContentBlock[] = [] - - // Add the main text response - const text = response.text() - if (text) { - content.push({ type: "text", text }) - } - - // Add function calls as tool_use blocks - const functionCalls = response.functionCalls() - if (functionCalls) { - functionCalls.forEach((call, index) => { - if ("content" in call.args && typeof call.args.content === "string") { - call.args.content = unescapeGeminiContent(call.args.content) - } - content.push({ - type: "tool_use", - id: `${call.name}-${index}-${Date.now()}`, - name: call.name, - input: call.args, - }) - }) - } - - // Determine stop reason - let stop_reason: Anthropic.Messages.Message["stop_reason"] = null - const finishReason = response.candidates?.[0]?.finishReason - if (finishReason) { - switch (finishReason) { - case "STOP": - stop_reason = "end_turn" - break - case "MAX_TOKENS": - stop_reason = "max_tokens" - break - case "SAFETY": - case "RECITATION": - case "OTHER": - stop_reason = "stop_sequence" - break - // Add more cases if needed - } - } - - return { - id: `msg_${Date.now()}`, // Generate a unique ID - type: "message", - role: "assistant", - content, - model: "", - stop_reason, - stop_sequence: null, // Gemini doesn't provide this information - usage: { - input_tokens: response.usageMetadata?.promptTokenCount ?? 0, - output_tokens: response.usageMetadata?.candidatesTokenCount ?? 0, - }, - } -} diff --git a/src/api/transform/mistral-format.ts b/src/api/transform/mistral-format.ts index 16c6aaf238..baf81ef24d 100644 --- a/src/api/transform/mistral-format.ts +++ b/src/api/transform/mistral-format.ts @@ -1,5 +1,4 @@ import { Anthropic } from "@anthropic-ai/sdk" -import { Mistral } from "@mistralai/mistralai" import { AssistantMessage } from "@mistralai/mistralai/models/components/assistantmessage" import { SystemMessage } from "@mistralai/mistralai/models/components/systemmessage" import { ToolMessage } from "@mistralai/mistralai/models/components/toolmessage" @@ -13,6 +12,7 @@ export type MistralMessage = export function convertToMistralMessages(anthropicMessages: Anthropic.Messages.MessageParam[]): MistralMessage[] { const mistralMessages: MistralMessage[] = [] + for (const anthropicMessage of anthropicMessages) { if (typeof anthropicMessage.content === "string") { mistralMessages.push({ diff --git a/src/api/transform/o1-format.ts b/src/api/transform/o1-format.ts deleted file mode 100644 index 1346fdbd54..0000000000 --- a/src/api/transform/o1-format.ts +++ /dev/null @@ -1,429 +0,0 @@ -import { Anthropic } from "@anthropic-ai/sdk" -import OpenAI from "openai" - -const o1SystemPrompt = (systemPrompt: string) => ` -# System Prompt - -${systemPrompt} - -# Instructions for Formulating Your Response - -You must respond to the user's request by using at least one tool call. When formulating your response, follow these guidelines: - -1. Begin your response with normal text, explaining your thoughts, analysis, or plan of action. -2. If you need to use any tools, place ALL tool calls at the END of your message, after your normal text explanation. -3. You can use multiple tool calls if needed, but they should all be grouped together at the end of your message. -4. After placing the tool calls, do not add any additional normal text. The tool calls should be the final content in your message. - -Here's the general structure your responses should follow: - -\`\`\` -[Your normal text response explaining your thoughts and actions] - -[Tool Call 1] -[Tool Call 2 if needed] -[Tool Call 3 if needed] -... -\`\`\` - -Remember: -- Choose the most appropriate tool(s) based on the task and the tool descriptions provided. -- Formulate your tool calls using the XML format specified for each tool. -- Provide clear explanations in your normal text about what actions you're taking and why you're using particular tools. -- Act as if the tool calls will be executed immediately after your message, and your next response will have access to their results. - -# Tool Descriptions and XML Formats - -1. execute_command: - -Your command here - -Description: 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. - -2. list_files: - -Directory path here -true or false (optional) - -Description: 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. - -3. list_code_definition_names: - -Directory path here - -Description: Lists 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. - -4. search_files: - -Directory path here -Your regex pattern here -Optional file pattern here - -Description: 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. - -5. read_file: - -File path here - -Description: Read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file, for example to analyze code, review text files, or extract information from configuration files. 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. - -6. write_to_file: - -File path here - -Your file content here - - -Description: Write 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. Always provide the full intended content of the file, without any truncation. This tool will automatically create any directories needed to write the file. - -7. ask_followup_question: - -Your question here - -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. - -8. attempt_completion: - -Optional command to demonstrate result - -Your final result description here - - -Description: Once you've completed the task, use this tool to present the result to the user. They may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again. - -# Examples - -Here are some examples of how to structure your responses with tool calls: - -Example 1: Using a single tool - -Let's run the test suite for our project. This will help us ensure that all our components are functioning correctly. - - -npm test - - -Example 2: Using multiple tools - -Let's create two new configuration files for the web application: one for the frontend and one for the backend. - - -./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" -} - - - - -./backend-config.yaml - -database: - host: localhost - port: 5432 - name: myapp_db - user: admin - -server: - port: 3000 - environment: development - logLevel: debug - -security: - jwtSecret: your-secret-key-here - passwordSaltRounds: 10 - -caching: - enabled: true - provider: redis - ttl: 3600 - -externalServices: - emailProvider: sendgrid - storageProvider: aws-s3 - - - -Example 3: Asking a follow-up question - -I've analyzed the project structure, but I need more information to proceed. Let me ask the user for clarification. - - -Which specific feature would you like me to implement in the example.py file? - -` - -export function convertToO1Messages( - openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[], - systemPrompt: string, -): OpenAI.Chat.ChatCompletionMessageParam[] { - const toolsReplaced = openAiMessages.reduce((acc, message) => { - if (message.role === "tool") { - // Convert tool messages to user messages - acc.push({ - role: "user", - content: message.content || "", - }) - } else if (message.role === "assistant" && message.tool_calls) { - // Convert tool calls to content and remove tool_calls - let content = message.content || "" - message.tool_calls.forEach((toolCall) => { - if (toolCall.type === "function") { - content += `\nTool Call: ${toolCall.function.name}\nArguments: ${toolCall.function.arguments}` - } - }) - acc.push({ - role: "assistant", - content: content, - tool_calls: undefined, - }) - } else { - // Keep other messages as they are - acc.push(message) - } - return acc - }, [] as OpenAI.Chat.ChatCompletionMessageParam[]) - - // Find the index of the last assistant message - // const lastAssistantIndex = findLastIndex(toolsReplaced, (message) => message.role === "assistant") - - // Create a new array to hold the modified messages - const messagesWithSystemPrompt = [ - { - role: "user", - content: o1SystemPrompt(systemPrompt), - } as OpenAI.Chat.ChatCompletionUserMessageParam, - ...toolsReplaced, - ] - - // If there's an assistant message, insert the system prompt after it - // if (lastAssistantIndex !== -1) { - // const insertIndex = lastAssistantIndex + 1 - // if (insertIndex < messagesWithSystemPrompt.length && messagesWithSystemPrompt[insertIndex].role === "user") { - // messagesWithSystemPrompt.splice(insertIndex, 0, { - // role: "user", - // content: o1SystemPrompt(systemPrompt), - // }) - // } - // } else { - // // If there were no assistant messages, prepend the system prompt - // messagesWithSystemPrompt.unshift({ - // role: "user", - // content: o1SystemPrompt(systemPrompt), - // }) - // } - - return messagesWithSystemPrompt -} - -interface ToolCall { - tool: string - tool_input: Record -} - -const toolNames = [ - "execute_command", - "list_files", - "list_code_definition_names", - "search_files", - "read_file", - "write_to_file", - "ask_followup_question", - "attempt_completion", -] - -function parseAIResponse(response: string): { normalText: string; toolCalls: ToolCall[] } { - // Create a regex pattern to match any tool call opening tag - const toolCallPattern = new RegExp(`<(${toolNames.join("|")})`, "i") - const match = response.match(toolCallPattern) - - if (!match) { - // No tool calls found - return { normalText: response.trim(), toolCalls: [] } - } - - const toolCallStart = match.index! - const normalText = response.slice(0, toolCallStart).trim() - const toolCallsText = response.slice(toolCallStart) - - const toolCalls = parseToolCalls(toolCallsText) - - return { normalText, toolCalls } -} - -function parseToolCalls(toolCallsText: string): ToolCall[] { - const toolCalls: ToolCall[] = [] - - let remainingText = toolCallsText - - while (remainingText.length > 0) { - const toolMatch = toolNames.find((tool) => new RegExp(`<${tool}`, "i").test(remainingText)) - - if (!toolMatch) { - break // No more tool calls found - } - - const startTag = `<${toolMatch}` - const endTag = `` - const startIndex = remainingText.indexOf(startTag) - const endIndex = remainingText.indexOf(endTag, startIndex) - - if (endIndex === -1) { - break // Malformed XML, no closing tag found - } - - const toolCallContent = remainingText.slice(startIndex, endIndex + endTag.length) - remainingText = remainingText.slice(endIndex + endTag.length).trim() - - const toolCall = parseToolCall(toolMatch, toolCallContent) - if (toolCall) { - toolCalls.push(toolCall) - } - } - - return toolCalls -} - -function parseToolCall(toolName: string, content: string): ToolCall | null { - const tool_input: Record = {} - - // Remove the outer tool tags - const innerContent = content.replace(new RegExp(`^<${toolName}>|$`, "g"), "").trim() - - // Parse nested XML elements - const paramRegex = /<(\w+)>([\s\S]*?)<\/\1>/gs - let match - - while ((match = paramRegex.exec(innerContent)) !== null) { - const [, paramName, paramValue] = match - // Preserve newlines and trim only leading/trailing whitespace - tool_input[paramName] = paramValue.replace(/^\s+|\s+$/g, "") - } - - // Validate required parameters - if (!validateToolInput(toolName, tool_input)) { - console.error(`Invalid tool call for ${toolName}:`, content) - return null - } - - return { tool: toolName, tool_input } -} - -function validateToolInput(toolName: string, tool_input: Record): boolean { - switch (toolName) { - case "execute_command": - return "command" in tool_input - case "read_file": - case "list_code_definition_names": - case "list_files": - return "path" in tool_input - case "search_files": - return "path" in tool_input && "regex" in tool_input - case "write_to_file": - return "path" in tool_input && "content" in tool_input - case "ask_followup_question": - return "question" in tool_input - case "attempt_completion": - return "result" in tool_input - default: - return false - } -} - -// Example usage: -// const aiResponse = `Here's my analysis of the situation... - -// -// ls -la -// - -// -// ./example.txt -// Hello, World! -// `; -// -// const { normalText, toolCalls } = parseAIResponse(aiResponse); -// console.log(normalText); -// console.log(toolCalls); - -// Convert OpenAI response to Anthropic format -export function convertO1ResponseToAnthropicMessage( - completion: OpenAI.Chat.Completions.ChatCompletion, -): Anthropic.Messages.Message { - const openAiMessage = completion.choices[0].message - const { normalText, toolCalls } = parseAIResponse(openAiMessage.content || "") - - const anthropicMessage: Anthropic.Messages.Message = { - id: completion.id, - type: "message", - role: openAiMessage.role, // always "assistant" - content: [ - { - type: "text", - text: normalText, - }, - ], - model: completion.model, - stop_reason: (() => { - switch (completion.choices[0].finish_reason) { - case "stop": - return "end_turn" - case "length": - return "max_tokens" - case "tool_calls": - return "tool_use" - case "content_filter": // Anthropic doesn't have an exact equivalent - default: - return null - } - })(), - stop_sequence: null, // which custom stop_sequence was generated, if any (not applicable if you don't use stop_sequence) - usage: { - input_tokens: completion.usage?.prompt_tokens || 0, - output_tokens: completion.usage?.completion_tokens || 0, - }, - } - - if (toolCalls.length > 0) { - anthropicMessage.content.push( - ...toolCalls.map((toolCall: ToolCall, index: number): Anthropic.ToolUseBlock => { - return { - type: "tool_use", - id: `call_${index}_${Date.now()}`, // Generate a unique ID for each tool call - name: toolCall.tool, - input: toolCall.tool_input, - } - }), - ) - } - - return anthropicMessage -} - -// Example usage: -// const openAICompletion = { -// id: "cmpl-123", -// choices: [{ -// message: { -// role: "assistant", -// content: "Here's my analysis...\n\n\n ls -la\n" -// }, -// finish_reason: "stop" -// }], -// model: "gpt-3.5-turbo", -// usage: { prompt_tokens: 50, completion_tokens: 100 } -// }; -// const anthropicMessage = convertO1ResponseToAnthropicMessage(openAICompletion); -// console.log(anthropicMessage); diff --git a/src/api/transform/openai-format.ts b/src/api/transform/openai-format.ts index fe23b9b2ff..134f9f2ed6 100644 --- a/src/api/transform/openai-format.ts +++ b/src/api/transform/openai-format.ts @@ -144,60 +144,3 @@ export function convertToOpenAiMessages( return openAiMessages } - -// Convert OpenAI response to Anthropic format -export function convertToAnthropicMessage( - completion: OpenAI.Chat.Completions.ChatCompletion, -): Anthropic.Messages.Message { - const openAiMessage = completion.choices[0].message - const anthropicMessage: Anthropic.Messages.Message = { - id: completion.id, - type: "message", - role: openAiMessage.role, // always "assistant" - content: [ - { - type: "text", - text: openAiMessage.content || "", - }, - ], - model: completion.model, - stop_reason: (() => { - switch (completion.choices[0].finish_reason) { - case "stop": - return "end_turn" - case "length": - return "max_tokens" - case "tool_calls": - return "tool_use" - case "content_filter": // Anthropic doesn't have an exact equivalent - default: - return null - } - })(), - stop_sequence: null, // which custom stop_sequence was generated, if any (not applicable if you don't use stop_sequence) - usage: { - input_tokens: completion.usage?.prompt_tokens || 0, - output_tokens: completion.usage?.completion_tokens || 0, - }, - } - - if (openAiMessage.tool_calls && openAiMessage.tool_calls.length > 0) { - anthropicMessage.content.push( - ...openAiMessage.tool_calls.map((toolCall): Anthropic.ToolUseBlock => { - let parsedInput = {} - try { - parsedInput = JSON.parse(toolCall.function.arguments || "{}") - } catch (error) { - console.error("Failed to parse tool arguments:", error) - } - return { - type: "tool_use", - id: toolCall.id, - name: toolCall.function.name, - input: parsedInput, - } - }), - ) - } - return anthropicMessage -} diff --git a/src/api/transform/simple-format.ts b/src/api/transform/simple-format.ts index c1e4895bba..39049f76c2 100644 --- a/src/api/transform/simple-format.ts +++ b/src/api/transform/simple-format.ts @@ -3,16 +3,7 @@ import { Anthropic } from "@anthropic-ai/sdk" /** * Convert complex content blocks to simple string content */ -export function convertToSimpleContent( - content: - | string - | Array< - | Anthropic.Messages.TextBlockParam - | Anthropic.Messages.ImageBlockParam - | Anthropic.Messages.ToolUseBlockParam - | Anthropic.Messages.ToolResultBlockParam - >, -): string { +export function convertToSimpleContent(content: Anthropic.Messages.MessageParam["content"]): string { if (typeof content === "string") { return content } diff --git a/src/api/transform/vscode-lm-format.ts b/src/api/transform/vscode-lm-format.ts index 6d7bea92ba..73716cf912 100644 --- a/src/api/transform/vscode-lm-format.ts +++ b/src/api/transform/vscode-lm-format.ts @@ -155,46 +155,3 @@ export function convertToAnthropicRole(vsCodeLmMessageRole: vscode.LanguageModel return null } } - -export async function convertToAnthropicMessage( - vsCodeLmMessage: vscode.LanguageModelChatMessage, -): Promise { - const anthropicRole: string | null = convertToAnthropicRole(vsCodeLmMessage.role) - if (anthropicRole !== "assistant") { - throw new Error("Roo Code : Only assistant messages are supported.") - } - - return { - id: crypto.randomUUID(), - type: "message", - model: "vscode-lm", - role: anthropicRole, - content: vsCodeLmMessage.content - .map((part): Anthropic.ContentBlock | null => { - if (part instanceof vscode.LanguageModelTextPart) { - return { - type: "text", - text: part.value, - } - } - - if (part instanceof vscode.LanguageModelToolCallPart) { - return { - type: "tool_use", - id: part.callId || crypto.randomUUID(), - name: part.name, - input: asObjectSafe(part.input), - } - } - - return null - }) - .filter((part): part is Anthropic.ContentBlock => part !== null), - stop_reason: null, - stop_sequence: null, - usage: { - input_tokens: 0, - output_tokens: 0, - }, - } -} diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 9c2977a266..073bd10911 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -47,6 +47,8 @@ import { import { getApiMetrics } from "../shared/getApiMetrics" import { HistoryItem } from "../shared/HistoryItem" import { ClineAskResponse } from "../shared/WebviewMessage" +import { GlobalFileNames } from "../shared/globalFileNames" +import { defaultModeSlug, getModeBySlug, getFullModeDetails } from "../shared/modes" import { calculateApiCost } from "../utils/cost" import { fileExistsAtPath } from "../utils/fs" import { arePathsEqual, getReadablePath } from "../utils/path" @@ -54,24 +56,34 @@ import { parseMentions } from "./mentions" import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message" import { formatResponse } from "./prompts/responses" import { SYSTEM_PROMPT } from "./prompts/system" -import { modes, defaultModeSlug, getModeBySlug } from "../shared/modes" import { truncateConversationIfNeeded } from "./sliding-window" -import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider" +import { ClineProvider } from "./webview/ClineProvider" import { detectCodeOmission } from "../integrations/editor/detect-omission" import { BrowserSession } from "../services/browser/BrowserSession" -import { OpenRouterHandler } from "../api/providers/openrouter" import { McpHub } from "../services/mcp/McpHub" import crypto from "crypto" import { insertGroups } from "./diff/insert-groups" -import { EXPERIMENT_IDS, experiments as Experiments } from "../shared/experiments" +import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../shared/experiments" const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution type ToolResponse = string | Array -type UserContent = Array< - Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam -> +type UserContent = Array + +export type ClineOptions = { + provider: ClineProvider + apiConfiguration: ApiConfiguration + customInstructions?: string + enableDiff?: boolean + enableCheckpoints?: boolean + fuzzyMatchThreshold?: number + task?: string + images?: string[] + historyItem?: HistoryItem + experiments?: Record + startTask?: boolean +} export class Cline { readonly taskId: string @@ -118,19 +130,20 @@ export class Cline { private didAlreadyUseTool = false private didCompleteReadingStream = false - constructor( - provider: ClineProvider, - apiConfiguration: ApiConfiguration, - customInstructions?: string, - enableDiff?: boolean, - enableCheckpoints?: boolean, - fuzzyMatchThreshold?: number, - task?: string | undefined, - images?: string[] | undefined, - historyItem?: HistoryItem | undefined, - experiments?: Record, - ) { - if (!task && !images && !historyItem) { + constructor({ + provider, + apiConfiguration, + customInstructions, + enableDiff, + enableCheckpoints, + fuzzyMatchThreshold, + task, + images, + historyItem, + experiments, + startTask = true, + }: ClineOptions) { + if (startTask && !task && !images && !historyItem) { throw new Error("Either historyItem or task/images must be provided") } @@ -153,13 +166,33 @@ export class Cline { // Initialize diffStrategy based on current state this.updateDiffStrategy(Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.DIFF_STRATEGY)) - if (task || images) { - this.startTask(task, images) - } else if (historyItem) { - this.resumeTaskFromHistory() + if (startTask) { + if (task || images) { + this.startTask(task, images) + } else if (historyItem) { + this.resumeTaskFromHistory() + } else { + throw new Error("Either historyItem or task/images must be provided") + } } } + static create(options: ClineOptions): [Cline, Promise] { + const instance = new Cline({ ...options, startTask: false }) + const { images, task, historyItem } = options + let promise + + if (images || task) { + promise = instance.startTask(task, images) + } else if (historyItem) { + promise = instance.resumeTaskFromHistory() + } else { + throw new Error("Either historyItem or task/images must be provided") + } + + return [instance, promise] + } + // Add method to update diffStrategy async updateDiffStrategy(experimentalDiffStrategy?: boolean) { // If not provided, get from current state @@ -745,8 +778,12 @@ export class Cline { } } - async abortTask() { + async abortTask(isAbandoned = false) { // Will stop any autonomously running promises. + if (isAbandoned) { + this.abandoned = true + } + this.abort = true this.terminalManager.disposeAll() @@ -2753,7 +2790,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").` - : "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.", + : "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.7 Sonnet for its advanced agentic coding capabilities.", ) if (response === "messageResponse") { userContent.push( @@ -2836,8 +2873,6 @@ export class Cline { } const abortStream = async (cancelReason: ClineApiReqCancelReason, streamingFailedMessage?: string) => { - console.log(`[Cline#abortStream] cancelReason = ${cancelReason}`) - if (this.diffViewProvider.isEditing) { await this.diffViewProvider.revertChanges() // closes diff view } @@ -2969,7 +3004,7 @@ export class Cline { } // need to call here in case the stream was aborted - if (this.abort) { + if (this.abort || this.abandoned) { throw new Error("Roo Code instance aborted") } @@ -3237,9 +3272,29 @@ export class Cline { details += `\n\n# Current Context Size (Tokens)\n${contextTokens ? `${contextTokens.toLocaleString()} (${contextPercentage}%)` : "(Not available)"}` // Add current mode and any mode-specific warnings - const { mode, customModes } = (await this.providerRef.deref()?.getState()) ?? {} + const { + mode, + customModes, + customModePrompts, + experiments = {} as Record, + customInstructions: globalCustomInstructions, + preferredLanguage, + } = (await this.providerRef.deref()?.getState()) ?? {} const currentMode = mode ?? defaultModeSlug - details += `\n\n# Current Mode\n${currentMode}` + const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, { + cwd, + globalCustomInstructions, + preferredLanguage, + }) + details += `\n\n# Current Mode\n` + details += `${currentMode}\n` + details += `${modeDetails.name}\n` + if (Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING)) { + details += `${modeDetails.roleDefinition}\n` + if (modeDetails.customInstructions) { + details += `${modeDetails.customInstructions}\n` + } + } // Add warning if not in code mode if ( diff --git a/src/core/__tests__/Cline.test.ts b/src/core/__tests__/Cline.test.ts index 3da0c8cdd3..9910896ebb 100644 --- a/src/core/__tests__/Cline.test.ts +++ b/src/core/__tests__/Cline.test.ts @@ -1,3 +1,5 @@ +// npx jest src/core/__tests__/Cline.test.ts + import { Cline } from "../Cline" import { ClineProvider } from "../webview/ClineProvider" import { ApiConfiguration, ModelInfo } from "../../shared/api" @@ -82,7 +84,20 @@ jest.mock("fs/promises", () => ({ return Promise.resolve(JSON.stringify(mockMessages)) } if (filePath.includes("api_conversation_history.json")) { - return Promise.resolve("[]") + return Promise.resolve( + JSON.stringify([ + { + role: "user", + content: [{ type: "text", text: "historical task" }], + ts: Date.now(), + }, + { + role: "assistant", + content: [{ type: "text", text: "I'll help you with that task." }], + ts: Date.now(), + }, + ]), + ) } return Promise.resolve("[]") }), @@ -295,93 +310,102 @@ describe("Cline", () => { taskDirPath: "/mock/storage/path/tasks/123", apiConversationHistoryFilePath: "/mock/storage/path/tasks/123/api_conversation_history.json", uiMessagesFilePath: "/mock/storage/path/tasks/123/ui_messages.json", - apiConversationHistory: [], + apiConversationHistory: [ + { + role: "user", + content: [{ type: "text", text: "historical task" }], + ts: Date.now(), + }, + { + role: "assistant", + content: [{ type: "text", text: "I'll help you with that task." }], + ts: Date.now(), + }, + ], })) }) describe("constructor", () => { - it("should respect provided settings", () => { - const cline = new Cline( - mockProvider, - mockApiConfig, - "custom instructions", - false, - false, - 0.95, // 95% threshold - "test task", - ) + it("should respect provided settings", async () => { + const [cline, task] = Cline.create({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + customInstructions: "custom instructions", + fuzzyMatchThreshold: 0.95, + task: "test task", + }) expect(cline.customInstructions).toBe("custom instructions") expect(cline.diffEnabled).toBe(false) + + await cline.abortTask(true) + await task.catch(() => {}) }) - it("should use default fuzzy match threshold when not provided", () => { - const cline = new Cline( - mockProvider, - mockApiConfig, - "custom instructions", - true, - false, - undefined, - "test task", - ) + it("should use default fuzzy match threshold when not provided", async () => { + const [cline, task] = await Cline.create({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + customInstructions: "custom instructions", + enableDiff: true, + fuzzyMatchThreshold: 0.95, + task: "test task", + }) expect(cline.diffEnabled).toBe(true) // The diff strategy should be created with default threshold (1.0) expect(cline.diffStrategy).toBeDefined() + + await cline.abortTask(true) + await task.catch(() => {}) }) - it("should use provided fuzzy match threshold", () => { + it("should use provided fuzzy match threshold", async () => { const getDiffStrategySpy = jest.spyOn(require("../diff/DiffStrategy"), "getDiffStrategy") - const cline = new Cline( - mockProvider, - mockApiConfig, - "custom instructions", - true, - false, - 0.9, // 90% threshold - "test task", - ) + const [cline, task] = await Cline.create({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + customInstructions: "custom instructions", + enableDiff: true, + fuzzyMatchThreshold: 0.9, + task: "test task", + }) expect(cline.diffEnabled).toBe(true) expect(cline.diffStrategy).toBeDefined() expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 0.9, false) getDiffStrategySpy.mockRestore() + + await cline.abortTask(true) + await task.catch(() => {}) }) - it("should pass default threshold to diff strategy when not provided", () => { + it("should pass default threshold to diff strategy when not provided", async () => { const getDiffStrategySpy = jest.spyOn(require("../diff/DiffStrategy"), "getDiffStrategy") - const cline = new Cline( - mockProvider, - mockApiConfig, - "custom instructions", - true, - false, - undefined, - "test task", - ) + const [cline, task] = Cline.create({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + customInstructions: "custom instructions", + enableDiff: true, + task: "test task", + }) expect(cline.diffEnabled).toBe(true) expect(cline.diffStrategy).toBeDefined() expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 1.0, false) getDiffStrategySpy.mockRestore() + + await cline.abortTask(true) + await task.catch(() => {}) }) it("should require either task or historyItem", () => { expect(() => { - new Cline( - mockProvider, - mockApiConfig, - undefined, // customInstructions - false, // diffEnabled - false, // checkpointsEnabled - undefined, // fuzzyMatchThreshold - undefined, // task - ) + new Cline({ provider: mockProvider, apiConfiguration: mockApiConfig }) }).toThrow("Either historyItem or task/images must be provided") }) }) @@ -431,7 +455,11 @@ describe("Cline", () => { }) it("should include timezone information in environment details", async () => { - const cline = new Cline(mockProvider, mockApiConfig, undefined, false, false, undefined, "test task") + const [cline, task] = Cline.create({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + }) const details = await cline["getEnvironmentDetails"](false) @@ -440,11 +468,21 @@ describe("Cline", () => { expect(details).toMatch(/UTC-7:00/) // Fixed offset for America/Los_Angeles expect(details).toContain("# Current Time") expect(details).toMatch(/1\/1\/2024.*5:00:00 AM.*\(America\/Los_Angeles, UTC-7:00\)/) // Full time string format + + await cline.abortTask(true) + await task.catch(() => {}) }) describe("API conversation handling", () => { it("should clean conversation history before sending to API", async () => { - const cline = new Cline(mockProvider, mockApiConfig, undefined, false, false, undefined, "test task") + const [cline, task] = Cline.create({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + }) + + cline.abandoned = true + await task // Mock the API's createMessage method to capture the conversation history const createMessageSpy = jest.fn() @@ -552,15 +590,12 @@ describe("Cline", () => { ] // Test with model that supports images - const clineWithImages = new Cline( - mockProvider, - configWithImages, - undefined, - false, - false, - undefined, - "test task", - ) + const [clineWithImages, taskWithImages] = Cline.create({ + provider: mockProvider, + apiConfiguration: configWithImages, + task: "test task", + }) + // Mock the model info to indicate image support jest.spyOn(clineWithImages.api, "getModel").mockReturnValue({ id: "claude-3-sonnet", @@ -574,18 +609,16 @@ describe("Cline", () => { outputPrice: 0.75, } as ModelInfo, }) + clineWithImages.apiConversationHistory = conversationHistory // Test with model that doesn't support images - const clineWithoutImages = new Cline( - mockProvider, - configWithoutImages, - undefined, - false, - false, - undefined, - "test task", - ) + const [clineWithoutImages, taskWithoutImages] = Cline.create({ + provider: mockProvider, + apiConfiguration: configWithoutImages, + task: "test task", + }) + // Mock the model info to indicate no image support jest.spyOn(clineWithoutImages.api, "getModel").mockReturnValue({ id: "gpt-3.5-turbo", @@ -599,6 +632,7 @@ describe("Cline", () => { outputPrice: 0.2, } as ModelInfo, }) + clineWithoutImages.apiConversationHistory = conversationHistory // Mock abort state for both instances @@ -607,6 +641,7 @@ describe("Cline", () => { set: () => {}, configurable: true, }) + Object.defineProperty(clineWithoutImages, "abort", { get: () => false, set: () => {}, @@ -621,6 +656,7 @@ describe("Cline", () => { content, "", ]) + // Set up mock streams const mockStreamWithImages = (async function* () { yield { type: "text", text: "test response" } @@ -648,6 +684,12 @@ describe("Cline", () => { }, ] + clineWithImages.abandoned = true + await taskWithImages.catch(() => {}) + + clineWithoutImages.abandoned = true + await taskWithoutImages.catch(() => {}) + // Trigger API requests await clineWithImages.recursivelyMakeClineRequests([{ type: "text", text: "test request" }]) await clineWithoutImages.recursivelyMakeClineRequests([{ type: "text", text: "test request" }]) @@ -670,8 +712,12 @@ describe("Cline", () => { }) }) - it("should handle API retry with countdown", async () => { - const cline = new Cline(mockProvider, mockApiConfig, undefined, false, false, undefined, "test task") + it.skip("should handle API retry with countdown", async () => { + const [cline, task] = Cline.create({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + }) // Mock delay to track countdown timing const mockDelay = jest.fn().mockResolvedValue(undefined) @@ -785,10 +831,17 @@ describe("Cline", () => { expect(errorMessage).toBe( `${mockError.message}\n\nRetry attempt 1\nRetrying in ${baseDelay} seconds...`, ) + + await cline.abortTask(true) + await task.catch(() => {}) }) - it("should not apply retry delay twice", async () => { - const cline = new Cline(mockProvider, mockApiConfig, undefined, false, false, undefined, "test task") + it.skip("should not apply retry delay twice", async () => { + const [cline, task] = Cline.create({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + }) // Mock delay to track countdown timing const mockDelay = jest.fn().mockResolvedValue(undefined) @@ -901,19 +954,18 @@ describe("Cline", () => { undefined, false, ) + + await cline.abortTask(true) + await task.catch(() => {}) }) describe("loadContext", () => { it("should process mentions in task and feedback tags", async () => { - const cline = new Cline( - mockProvider, - mockApiConfig, - undefined, - false, - false, - undefined, - "test task", - ) + const [cline, task] = Cline.create({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + }) // Mock parseMentions to track calls const mockParseMentions = jest.fn().mockImplementation((text) => `processed: ${text}`) @@ -978,6 +1030,9 @@ describe("Cline", () => { const toolResult2 = processedContent[3] as Anthropic.ToolResultBlockParam const content2 = Array.isArray(toolResult2.content) ? toolResult2.content[0] : toolResult2.content expect((content2 as Anthropic.TextBlockParam).text).toBe("Regular tool result with @/path") + + await cline.abortTask(true) + await task.catch(() => {}) }) }) }) diff --git a/src/core/config/__tests__/CustomModesManager.test.ts b/src/core/config/__tests__/CustomModesManager.test.ts index 3c8236e920..4031bff906 100644 --- a/src/core/config/__tests__/CustomModesManager.test.ts +++ b/src/core/config/__tests__/CustomModesManager.test.ts @@ -1,3 +1,5 @@ +// npx jest src/core/config/__tests__/CustomModesManager.test.ts + import * as vscode from "vscode" import * as path from "path" import * as fs from "fs/promises" @@ -15,9 +17,10 @@ describe("CustomModesManager", () => { let mockOnUpdate: jest.Mock let mockWorkspaceFolders: { uri: { fsPath: string } }[] - const mockStoragePath = "/mock/settings" + // Use path.sep to ensure correct path separators for the current platform + const mockStoragePath = `${path.sep}mock${path.sep}settings` const mockSettingsPath = path.join(mockStoragePath, "settings", "cline_custom_modes.json") - const mockRoomodes = "/mock/workspace/.roomodes" + const mockRoomodes = `${path.sep}mock${path.sep}workspace${path.sep}.roomodes` beforeEach(() => { mockOnUpdate = jest.fn() @@ -243,7 +246,15 @@ describe("CustomModesManager", () => { await manager.updateCustomMode("project-mode", projectMode) // Verify .roomodes was created with the project mode - expect(fs.writeFile).toHaveBeenCalledWith(mockRoomodes, expect.stringContaining("project-mode"), "utf-8") + expect(fs.writeFile).toHaveBeenCalledWith( + expect.any(String), // Don't check exact path as it may have different separators on different platforms + expect.stringContaining("project-mode"), + "utf-8", + ) + + // Verify the path is correct regardless of separators + const writeCall = (fs.writeFile as jest.Mock).mock.calls[0] + expect(path.normalize(writeCall[0])).toBe(path.normalize(mockRoomodes)) // Verify the content written to .roomodes expect(roomodesContent).toEqual({ diff --git a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap index 2abc613861..b8050adb77 100644 --- a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap +++ b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap @@ -2543,43 +2543,6 @@ 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 - - ## apply_diff Description: Request to replace existing code using a search and replace block. This tool allows for precise, surgical replaces to files by specifying exactly what content to search for and what to replace it with. @@ -2640,6 +2603,43 @@ Your search/replace content here 5 +## 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. For command chaining, use the appropriate chaining syntax for the user's shell. 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: @@ -2758,7 +2758,7 @@ CAPABILITIES - 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 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 apply_diff or 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. ==== @@ -2775,11 +2775,11 @@ RULES - 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 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 apply_diff or 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. -- For editing files, you have access to these tools: write_to_file (for creating new files or complete file rewrites), apply_diff (for replacing lines in existing files). -- When using the write_to_file tool to modify a file, use the tool directly with the desired content. You do not need to display the content before using the 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. +- For editing files, you have access to these tools: apply_diff (for replacing lines in existing files), write_to_file (for creating new files or complete file rewrites). - You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files. +- When using the write_to_file tool to modify a file, use the tool directly with the desired content. You do not need to display the content before using the 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. - Some modes have restrictions on which files they can edit. If you attempt to edit a restricted file, the operation will be rejected with a FileRestrictionError that will specify which file patterns are allowed for the current mode. - 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. * For example, in architect mode trying to edit app.js would be rejected because architect mode can only edit files matching "\\.md$" diff --git a/src/core/prompts/__tests__/sections.test.ts b/src/core/prompts/__tests__/sections.test.ts index 2100016e46..fe92e63f92 100644 --- a/src/core/prompts/__tests__/sections.test.ts +++ b/src/core/prompts/__tests__/sections.test.ts @@ -42,15 +42,15 @@ describe("getCapabilitiesSection", () => { 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") + expect(result).toContain("apply_diff or") + expect(result).toContain("then use the apply_diff or write_to_file 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).not.toContain("apply_diff or") expect(result).toContain("then use the write_to_file tool") - expect(result).not.toContain("write_to_file or apply_diff") + expect(result).not.toContain("apply_diff or write_to_file") }) }) diff --git a/src/core/prompts/sections/capabilities.ts b/src/core/prompts/sections/capabilities.ts index c292eeffbc..983d07bf76 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 ${diffStrategy ? "the apply_diff or write_to_file" : "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.${ 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/sections/modes.ts b/src/core/prompts/sections/modes.ts index eff950c2c2..f3863870db 100644 --- a/src/core/prompts/sections/modes.ts +++ b/src/core/prompts/sections/modes.ts @@ -45,7 +45,7 @@ Both files should follow this structure: "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) - allows editing any file + "edit", // Edit files group (apply_diff, write_to_file) - allows editing any file // Or with file restrictions: // ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], // Edit group that only allows editing markdown files "browser", // Browser group (browser_action) diff --git a/src/core/prompts/sections/rules.ts b/src/core/prompts/sections/rules.ts index b6e19eb08c..86e554a157 100644 --- a/src/core/prompts/sections/rules.ts +++ b/src/core/prompts/sections/rules.ts @@ -5,11 +5,16 @@ import * as path from "path" function getEditingInstructions(diffStrategy?: DiffStrategy, experiments?: Record): string { const instructions: string[] = [] - const availableTools: string[] = ["write_to_file (for creating new files or complete file rewrites)"] + const availableTools: string[] = [] // Collect available editing tools if (diffStrategy) { - availableTools.push("apply_diff (for replacing lines in existing files)") + availableTools.push( + "apply_diff (for replacing lines in existing files)", + "write_to_file (for creating new files or complete file rewrites)", + ) + } else { + availableTools.push("write_to_file (for creating new files or complete file rewrites)") } if (experiments?.["insert_content"]) { availableTools.push("insert_content (for adding lines to existing files)") @@ -36,16 +41,16 @@ function getEditingInstructions(diffStrategy?: DiffStrategy, experiments?: Recor ) } - instructions.push( - "- When using the write_to_file tool to modify a file, use the tool directly with the desired content. You do not need to display the content before using the 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.", - ) - if (availableTools.length > 1) { instructions.push( "- You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.", ) } + instructions.push( + "- When using the write_to_file tool to modify a file, use the tool directly with the desired content. You do not need to display the content before using the 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.", + ) + return instructions.join("\n") } @@ -63,7 +68,7 @@ RULES - You cannot \`cd\` into a different directory to complete a task. You are stuck operating from '${cwd.toPosix()}', 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 '${cwd.toPosix()}', and if so prepend with \`cd\`'ing into that directory && then executing the command (as one command since you are stuck operating from '${cwd.toPosix()}'). For example, if you needed to run \`npm install\` in a project outside of '${cwd.toPosix()}', you would need to prepend with a \`cd\` i.e. pseudocode for this would be \`cd (path to project) && (command, in this case npm install)\`. -- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes. +- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using ${diffStrategy ? "apply_diff or write_to_file" : "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. ${getEditingInstructions(diffStrategy, experiments)} - Some modes have restrictions on which files they can edit. If you attempt to edit a restricted file, the operation will be rejected with a FileRestrictionError that will specify which file patterns are allowed for the current mode. diff --git a/src/core/sliding-window/__tests__/sliding-window.test.ts b/src/core/sliding-window/__tests__/sliding-window.test.ts new file mode 100644 index 0000000000..3dcf9e5fd2 --- /dev/null +++ b/src/core/sliding-window/__tests__/sliding-window.test.ts @@ -0,0 +1,231 @@ +// npx jest src/core/sliding-window/__tests__/sliding-window.test.ts + +import { Anthropic } from "@anthropic-ai/sdk" + +import { ModelInfo } from "../../../shared/api" +import { truncateConversation, truncateConversationIfNeeded } from "../index" + +/** + * Tests for the truncateConversation function + */ +describe("truncateConversation", () => { + it("should retain the first message", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "First message" }, + { role: "assistant", content: "Second message" }, + { role: "user", content: "Third message" }, + ] + + const result = truncateConversation(messages, 0.5) + + // With 2 messages after the first, 0.5 fraction means remove 1 message + // But 1 is odd, so it rounds down to 0 (to make it even) + expect(result.length).toBe(3) // First message + 2 remaining messages + expect(result[0]).toEqual(messages[0]) + expect(result[1]).toEqual(messages[1]) + expect(result[2]).toEqual(messages[2]) + }) + + it("should remove the specified fraction of messages (rounded to even number)", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "First message" }, + { role: "assistant", content: "Second message" }, + { role: "user", content: "Third message" }, + { role: "assistant", content: "Fourth message" }, + { role: "user", content: "Fifth message" }, + ] + + // 4 messages excluding first, 0.5 fraction = 2 messages to remove + // 2 is already even, so no rounding needed + const result = truncateConversation(messages, 0.5) + + expect(result.length).toBe(3) + expect(result[0]).toEqual(messages[0]) + expect(result[1]).toEqual(messages[3]) + expect(result[2]).toEqual(messages[4]) + }) + + it("should round to an even number of messages to remove", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "First message" }, + { role: "assistant", content: "Second message" }, + { role: "user", content: "Third message" }, + { role: "assistant", content: "Fourth message" }, + { role: "user", content: "Fifth message" }, + { role: "assistant", content: "Sixth message" }, + { role: "user", content: "Seventh message" }, + ] + + // 6 messages excluding first, 0.3 fraction = 1.8 messages to remove + // 1.8 rounds down to 1, then to 0 to make it even + const result = truncateConversation(messages, 0.3) + + expect(result.length).toBe(7) // No messages removed + expect(result).toEqual(messages) + }) + + it("should handle edge case with fracToRemove = 0", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "First message" }, + { role: "assistant", content: "Second message" }, + { role: "user", content: "Third message" }, + ] + + const result = truncateConversation(messages, 0) + + expect(result).toEqual(messages) + }) + + it("should handle edge case with fracToRemove = 1", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "First message" }, + { role: "assistant", content: "Second message" }, + { role: "user", content: "Third message" }, + { role: "assistant", content: "Fourth message" }, + ] + + // 3 messages excluding first, 1.0 fraction = 3 messages to remove + // But 3 is odd, so it rounds down to 2 to make it even + const result = truncateConversation(messages, 1) + + expect(result.length).toBe(2) + expect(result[0]).toEqual(messages[0]) + expect(result[1]).toEqual(messages[3]) + }) +}) + +/** + * Tests for the getMaxTokens function (private but tested through truncateConversationIfNeeded) + */ +describe("getMaxTokens", () => { + // We'll test this indirectly through truncateConversationIfNeeded + const createModelInfo = (contextWindow: number, maxTokens?: number): ModelInfo => ({ + contextWindow, + supportsPromptCache: true, // Not relevant for getMaxTokens + maxTokens, + }) + + // Reuse across tests for consistency + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "First message" }, + { role: "assistant", content: "Second message" }, + { role: "user", content: "Third message" }, + { role: "assistant", content: "Fourth message" }, + { role: "user", content: "Fifth message" }, + ] + + it("should use maxTokens as buffer when specified", () => { + const modelInfo = createModelInfo(100000, 50000) + // Max tokens = 100000 - 50000 = 50000 + + // Below max tokens - no truncation + const result1 = truncateConversationIfNeeded(messages, 49999, modelInfo) + expect(result1).toEqual(messages) + + // Above max tokens - truncate + const result2 = truncateConversationIfNeeded(messages, 50001, modelInfo) + expect(result2).not.toEqual(messages) + expect(result2.length).toBe(3) // Truncated with 0.5 fraction + }) + + it("should use 20% of context window as buffer when maxTokens is undefined", () => { + const modelInfo = createModelInfo(100000, undefined) + // Max tokens = 100000 - (100000 * 0.2) = 80000 + + // Below max tokens - no truncation + const result1 = truncateConversationIfNeeded(messages, 79999, modelInfo) + expect(result1).toEqual(messages) + + // Above max tokens - truncate + const result2 = truncateConversationIfNeeded(messages, 80001, modelInfo) + expect(result2).not.toEqual(messages) + expect(result2.length).toBe(3) // Truncated with 0.5 fraction + }) + + it("should handle small context windows appropriately", () => { + const modelInfo = createModelInfo(50000, 10000) + // Max tokens = 50000 - 10000 = 40000 + + // Below max tokens - no truncation + const result1 = truncateConversationIfNeeded(messages, 39999, modelInfo) + expect(result1).toEqual(messages) + + // Above max tokens - truncate + const result2 = truncateConversationIfNeeded(messages, 40001, modelInfo) + expect(result2).not.toEqual(messages) + expect(result2.length).toBe(3) // Truncated with 0.5 fraction + }) + + it("should handle large context windows appropriately", () => { + const modelInfo = createModelInfo(200000, 30000) + // Max tokens = 200000 - 30000 = 170000 + + // Below max tokens - no truncation + const result1 = truncateConversationIfNeeded(messages, 169999, modelInfo) + expect(result1).toEqual(messages) + + // Above max tokens - truncate + const result2 = truncateConversationIfNeeded(messages, 170001, modelInfo) + expect(result2).not.toEqual(messages) + expect(result2.length).toBe(3) // Truncated with 0.5 fraction + }) +}) + +/** + * Tests for the truncateConversationIfNeeded function + */ +describe("truncateConversationIfNeeded", () => { + const createModelInfo = (contextWindow: number, supportsPromptCache: boolean, maxTokens?: number): ModelInfo => ({ + contextWindow, + supportsPromptCache, + maxTokens, + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "First message" }, + { role: "assistant", content: "Second message" }, + { role: "user", content: "Third message" }, + { role: "assistant", content: "Fourth message" }, + { role: "user", content: "Fifth message" }, + ] + + it("should not truncate if tokens are below max tokens threshold", () => { + const modelInfo = createModelInfo(100000, true, 30000) + const maxTokens = 100000 - 30000 // 70000 + const totalTokens = 69999 // Below threshold + + const result = truncateConversationIfNeeded(messages, totalTokens, modelInfo) + expect(result).toEqual(messages) // No truncation occurs + }) + + it("should truncate if tokens are above max tokens threshold", () => { + const modelInfo = createModelInfo(100000, true, 30000) + const maxTokens = 100000 - 30000 // 70000 + const totalTokens = 70001 // Above threshold + + // When truncating, always uses 0.5 fraction + // With 4 messages after the first, 0.5 fraction means remove 2 messages + const expectedResult = [messages[0], messages[3], messages[4]] + + const result = truncateConversationIfNeeded(messages, totalTokens, modelInfo) + expect(result).toEqual(expectedResult) + }) + + it("should work with non-prompt caching models the same as prompt caching models", () => { + // The implementation no longer differentiates between prompt caching and non-prompt caching models + const modelInfo1 = createModelInfo(100000, true, 30000) + const modelInfo2 = createModelInfo(100000, false, 30000) + + // Test below threshold + const belowThreshold = 69999 + expect(truncateConversationIfNeeded(messages, belowThreshold, modelInfo1)).toEqual( + truncateConversationIfNeeded(messages, belowThreshold, modelInfo2), + ) + + // Test above threshold + const aboveThreshold = 70001 + expect(truncateConversationIfNeeded(messages, aboveThreshold, modelInfo1)).toEqual( + truncateConversationIfNeeded(messages, aboveThreshold, modelInfo2), + ) + }) +}) diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts index ee4a1543e7..a0fff05ea5 100644 --- a/src/core/sliding-window/index.ts +++ b/src/core/sliding-window/index.ts @@ -1,4 +1,5 @@ import { Anthropic } from "@anthropic-ai/sdk" + import { ModelInfo } from "../../shared/api" /** @@ -27,13 +28,9 @@ export function truncateConversation( /** * Conditionally truncates the conversation messages if the total token count exceeds the model's limit. * - * Depending on whether the model supports prompt caching, different maximum token thresholds - * and truncation fractions are used. If the current total tokens exceed the threshold, - * the conversation is truncated using the appropriate fraction. - * * @param {Anthropic.Messages.MessageParam[]} messages - The conversation messages. * @param {number} totalTokens - The total number of tokens in the conversation. - * @param {ModelInfo} modelInfo - Model metadata including context window size and prompt cache support. + * @param {ModelInfo} modelInfo - Model metadata including context window size. * @returns {Anthropic.Messages.MessageParam[]} The original or truncated conversation messages. */ export function truncateConversationIfNeeded( @@ -41,57 +38,16 @@ export function truncateConversationIfNeeded( totalTokens: number, modelInfo: ModelInfo, ): Anthropic.Messages.MessageParam[] { - if (modelInfo.supportsPromptCache) { - return totalTokens < getMaxTokensForPromptCachingModels(modelInfo) - ? messages - : truncateConversation(messages, getTruncFractionForPromptCachingModels(modelInfo)) - } else { - return totalTokens < getMaxTokensForNonPromptCachingModels(modelInfo) - ? messages - : truncateConversation(messages, getTruncFractionForNonPromptCachingModels(modelInfo)) - } + return totalTokens < getMaxTokens(modelInfo) ? messages : truncateConversation(messages, 0.5) } /** - * Calculates the maximum allowed tokens for models that support prompt caching. - * - * The maximum is computed as the greater of (contextWindow - 40000) and 80% of the contextWindow. + * Calculates the maximum allowed tokens * * @param {ModelInfo} modelInfo - The model information containing the context window size. - * @returns {number} The maximum number of tokens allowed for prompt caching models. + * @returns {number} The maximum number of tokens allowed */ -function getMaxTokensForPromptCachingModels(modelInfo: ModelInfo): number { - return Math.max(modelInfo.contextWindow - 40_000, modelInfo.contextWindow * 0.8) -} - -/** - * Provides the fraction of messages to remove for models that support prompt caching. - * - * @param {ModelInfo} modelInfo - The model information (unused in current implementation). - * @returns {number} The truncation fraction for prompt caching models (fixed at 0.5). - */ -function getTruncFractionForPromptCachingModels(modelInfo: ModelInfo): number { - return 0.5 -} - -/** - * Calculates the maximum allowed tokens for models that do not support prompt caching. - * - * The maximum is computed as the greater of (contextWindow - 40000) and 80% of the contextWindow. - * - * @param {ModelInfo} modelInfo - The model information containing the context window size. - * @returns {number} The maximum number of tokens allowed for non-prompt caching models. - */ -function getMaxTokensForNonPromptCachingModels(modelInfo: ModelInfo): number { - return Math.max(modelInfo.contextWindow - 40_000, modelInfo.contextWindow * 0.8) -} - -/** - * Provides the fraction of messages to remove for models that do not support prompt caching. - * - * @param {ModelInfo} modelInfo - The model information. - * @returns {number} The truncation fraction for non-prompt caching models (fixed at 0.1). - */ -function getTruncFractionForNonPromptCachingModels(modelInfo: ModelInfo): number { - return Math.min(40_000 / modelInfo.contextWindow, 0.2) +function getMaxTokens(modelInfo: ModelInfo): number { + // The buffer needs to be at least as large as `modelInfo.maxTokens`, or 20% of the context window if for some reason it's not set. + return modelInfo.contextWindow - (modelInfo.maxTokens || modelInfo.contextWindow * 0.2) } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 05faa13834..bc6f457868 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -8,137 +8,51 @@ import * as path from "path" import * as vscode from "vscode" import simpleGit from "simple-git" -import { buildApiHandler } from "../../api" +import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api" +import { findLast } from "../../shared/array" +import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt" +import { GlobalFileNames } from "../../shared/globalFileNames" +import type { SecretKey, GlobalStateKey } from "../../shared/globalState" +import { HistoryItem } from "../../shared/HistoryItem" +import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage" +import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage" +import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug } from "../../shared/modes" +import { checkExistKey } from "../../shared/checkExistApiConfig" +import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments" 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" -import { findLast } from "../../shared/array" -import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage" -import { HistoryItem } from "../../shared/HistoryItem" -import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage" -import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug } from "../../shared/modes" -import { SYSTEM_PROMPT } from "../prompts/system" +import { McpServerManager } from "../../services/mcp/McpServerManager" import { fileExistsAtPath } from "../../utils/fs" +import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound" +import { singleCompletionHandler } from "../../utils/single-completion-handler" +import { searchCommits } from "../../utils/git" +import { getDiffStrategy } from "../diff/DiffStrategy" +import { SYSTEM_PROMPT } from "../prompts/system" +import { ConfigManager } from "../config/ConfigManager" +import { CustomModesManager } from "../config/CustomModesManager" +import { buildApiHandler } from "../../api" +import { getOpenRouterModels } from "../../api/providers/openrouter" +import { getGlamaModels } from "../../api/providers/glama" +import { getUnboundModels } from "../../api/providers/unbound" +import { getRequestyModels } from "../../api/providers/requesty" +import { getOpenAiModels } from "../../api/providers/openai" +import { getOllamaModels } from "../../api/providers/ollama" +import { getVsCodeLmModels } from "../../api/providers/vscode-lm" +import { getLmStudioModels } from "../../api/providers/lmstudio" +import { ACTION_NAMES } from "../CodeActionProvider" import { Cline } from "../Cline" import { openMention } from "../mentions" import { getNonce } from "./getNonce" import { getUri } from "./getUri" -import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound" -import { checkExistKey } from "../../shared/checkExistApiConfig" -import { singleCompletionHandler } from "../../utils/single-completion-handler" -import { searchCommits } from "../../utils/git" -import { ConfigManager } from "../config/ConfigManager" -import { CustomModesManager } from "../config/CustomModesManager" -import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments" -import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt" -import { ACTION_NAMES } from "../CodeActionProvider" -import { McpServerManager } from "../../services/mcp/McpServerManager" - -/* -https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts - -https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts -*/ - -type SecretKey = - | "apiKey" - | "glamaApiKey" - | "openRouterApiKey" - | "awsAccessKey" - | "awsSecretKey" - | "awsSessionToken" - | "openAiApiKey" - | "geminiApiKey" - | "openAiNativeApiKey" - | "deepSeekApiKey" - | "mistralApiKey" - | "unboundApiKey" - | "requestyApiKey" -type GlobalStateKey = - | "apiProvider" - | "apiModelId" - | "glamaModelId" - | "glamaModelInfo" - | "awsRegion" - | "awsUseCrossRegionInference" - | "awsProfile" - | "awsUseProfile" - | "vertexProjectId" - | "vertexRegion" - | "lastShownAnnouncementId" - | "customInstructions" - | "alwaysAllowReadOnly" - | "alwaysAllowWrite" - | "alwaysAllowExecute" - | "alwaysAllowBrowser" - | "alwaysAllowMcp" - | "alwaysAllowModeSwitch" - | "taskHistory" - | "openAiBaseUrl" - | "openAiModelId" - | "openAiCustomModelInfo" - | "openAiUseAzure" - | "ollamaModelId" - | "ollamaBaseUrl" - | "lmStudioModelId" - | "lmStudioBaseUrl" - | "anthropicBaseUrl" - | "azureApiVersion" - | "openAiStreamingEnabled" - | "openRouterModelId" - | "openRouterModelInfo" - | "openRouterBaseUrl" - | "openRouterUseMiddleOutTransform" - | "allowedCommands" - | "soundEnabled" - | "soundVolume" - | "diffEnabled" - | "checkpointsEnabled" - | "browserViewportSize" - | "screenshotQuality" - | "fuzzyMatchThreshold" - | "preferredLanguage" // Language setting for Cline's communication - | "writeDelayMs" - | "terminalOutputLineLimit" - | "mcpEnabled" - | "enableMcpServerCreation" - | "alwaysApproveResubmit" - | "requestDelaySeconds" - | "rateLimitSeconds" - | "currentApiConfigName" - | "listApiConfigMeta" - | "vsCodeLmModelSelector" - | "mode" - | "modeApiConfigs" - | "customModePrompts" - | "customSupportPrompts" - | "enhancementApiConfigId" - | "experiments" // Map of experiment IDs to their enabled state - | "autoApprovalEnabled" - | "customModes" // Array of custom modes - | "unboundModelId" - | "requestyModelId" - | "requestyModelInfo" - | "unboundModelInfo" - | "modelTemperature" - | "mistralCodestralUrl" - | "maxOpenTabsContext" - -export const GlobalFileNames = { - apiConversationHistory: "api_conversation_history.json", - uiMessages: "ui_messages.json", - glamaModels: "glama_models.json", - openRouterModels: "openrouter_models.json", - requestyModels: "requesty_models.json", - mcpSettings: "cline_mcp_settings.json", - unboundModels: "unbound_models.json", -} +/** + * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts + * https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts + */ export class ClineProvider implements vscode.WebviewViewProvider { public static readonly sideBarId = "roo-cline.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension. @@ -413,18 +327,17 @@ export class ClineProvider implements vscode.WebviewViewProvider { const modePrompt = customModePrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") - this.cline = new Cline( - this, + this.cline = new Cline({ + provider: this, apiConfiguration, - effectiveInstructions, - diffEnabled, - checkpointsEnabled, + customInstructions: effectiveInstructions, + enableDiff: diffEnabled, + enableCheckpoints: checkpointsEnabled, fuzzyMatchThreshold, task, images, - undefined, experiments, - ) + }) } public async initClineWithHistoryItem(historyItem: HistoryItem) { @@ -444,18 +357,16 @@ export class ClineProvider implements vscode.WebviewViewProvider { const modePrompt = customModePrompts?.[mode] as PromptComponent const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n") - this.cline = new Cline( - this, + this.cline = new Cline({ + provider: this, apiConfiguration, - effectiveInstructions, - diffEnabled, - checkpointsEnabled, + customInstructions: effectiveInstructions, + enableDiff: diffEnabled, + enableCheckpoints: checkpointsEnabled, fuzzyMatchThreshold, - undefined, - undefined, historyItem, experiments, - ) + }) } public async postMessageToWebview(message: ExtensionMessage) { @@ -621,15 +532,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.postStateToWebview() this.workspaceTracker?.initializeFilePaths() // don't await + getTheme().then((theme) => this.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) }), ) - // post last cached models in case the call to endpoint fails - this.readOpenRouterModels().then((openRouterModels) => { - if (openRouterModels) { - this.postMessageToWebview({ type: "openRouterModels", openRouterModels }) - } - }) // If MCP Hub is already initialized, update the webview with current server list if (this.mcpHub) { @@ -639,13 +545,37 @@ export class ClineProvider implements vscode.WebviewViewProvider { }) } - // gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch. - // we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point - // (see normalizeApiConfiguration > openrouter) - this.refreshOpenRouterModels().then(async (openRouterModels) => { + const cacheDir = await this.ensureCacheDirectoryExists() + + // Post last cached models in case the call to endpoint fails. + this.readModelsFromCache(GlobalFileNames.openRouterModels).then((openRouterModels) => { if (openRouterModels) { - // update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there) + this.postMessageToWebview({ type: "openRouterModels", openRouterModels }) + } + }) + + // GUI relies on model info to be up-to-date to provide + // the most accurate pricing, so we need to fetch the + // latest details on launch. + // We do this for all users since many users switch + // between api providers and if they were to switch back + // to OpenRouter it would be showing outdated model info + // if we hadn't retrieved the latest at this point + // (see normalizeApiConfiguration > openrouter). + getOpenRouterModels().then(async (openRouterModels) => { + if (Object.keys(openRouterModels).length > 0) { + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.openRouterModels), + JSON.stringify(openRouterModels), + ) + await this.postMessageToWebview({ type: "openRouterModels", openRouterModels }) + + // Update model info in state (this needs to be + // done here since we don't want to update state + // while settings is open, and we may refresh + // models there). const { apiConfiguration } = await this.getState() + if (apiConfiguration.openRouterModelId) { await this.updateGlobalState( "openRouterModelInfo", @@ -655,15 +585,23 @@ export class ClineProvider implements vscode.WebviewViewProvider { } } }) - this.readGlamaModels().then((glamaModels) => { + + this.readModelsFromCache(GlobalFileNames.glamaModels).then((glamaModels) => { if (glamaModels) { this.postMessageToWebview({ type: "glamaModels", glamaModels }) } }) - this.refreshGlamaModels().then(async (glamaModels) => { - if (glamaModels) { - // update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there) + + getGlamaModels().then(async (glamaModels) => { + if (Object.keys(glamaModels).length > 0) { + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.glamaModels), + JSON.stringify(glamaModels), + ) + await this.postMessageToWebview({ type: "glamaModels", glamaModels }) + const { apiConfiguration } = await this.getState() + if (apiConfiguration.glamaModelId) { await this.updateGlobalState( "glamaModelInfo", @@ -674,14 +612,22 @@ export class ClineProvider implements vscode.WebviewViewProvider { } }) - this.readUnboundModels().then((unboundModels) => { + this.readModelsFromCache(GlobalFileNames.unboundModels).then((unboundModels) => { if (unboundModels) { this.postMessageToWebview({ type: "unboundModels", unboundModels }) } }) - this.refreshUnboundModels().then(async (unboundModels) => { - if (unboundModels) { + + getUnboundModels().then(async (unboundModels) => { + if (Object.keys(unboundModels).length > 0) { + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.unboundModels), + JSON.stringify(unboundModels), + ) + await this.postMessageToWebview({ type: "unboundModels", unboundModels }) + const { apiConfiguration } = await this.getState() + if (apiConfiguration?.unboundModelId) { await this.updateGlobalState( "unboundModelInfo", @@ -692,15 +638,22 @@ export class ClineProvider implements vscode.WebviewViewProvider { } }) - this.readRequestyModels().then((requestyModels) => { + this.readModelsFromCache(GlobalFileNames.requestyModels).then((requestyModels) => { if (requestyModels) { this.postMessageToWebview({ type: "requestyModels", requestyModels }) } }) - this.refreshRequestyModels().then(async (requestyModels) => { - if (requestyModels) { - // update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there) + + getRequestyModels().then(async (requestyModels) => { + if (Object.keys(requestyModels).length > 0) { + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.requestyModels), + JSON.stringify(requestyModels), + ) + await this.postMessageToWebview({ type: "requestyModels", requestyModels }) + const { apiConfiguration } = await this.getState() + if (apiConfiguration.requestyModelId) { await this.updateGlobalState( "requestyModelInfo", @@ -843,41 +796,82 @@ export class ClineProvider implements vscode.WebviewViewProvider { case "resetState": await this.resetState() break - case "requestOllamaModels": - const ollamaModels = await this.getOllamaModels(message.text) - this.postMessageToWebview({ type: "ollamaModels", ollamaModels }) - break - case "requestLmStudioModels": - const lmStudioModels = await this.getLmStudioModels(message.text) - this.postMessageToWebview({ type: "lmStudioModels", lmStudioModels }) - break - case "requestVsCodeLmModels": - const vsCodeLmModels = await this.getVsCodeLmModels() - this.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels }) + case "refreshOpenRouterModels": + const openRouterModels = await getOpenRouterModels() + + if (Object.keys(openRouterModels).length > 0) { + const cacheDir = await this.ensureCacheDirectoryExists() + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.openRouterModels), + JSON.stringify(openRouterModels), + ) + await this.postMessageToWebview({ type: "openRouterModels", openRouterModels }) + } + break case "refreshGlamaModels": - await this.refreshGlamaModels() + const glamaModels = await getGlamaModels() + + if (Object.keys(glamaModels).length > 0) { + const cacheDir = await this.ensureCacheDirectoryExists() + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.glamaModels), + JSON.stringify(glamaModels), + ) + await this.postMessageToWebview({ type: "glamaModels", glamaModels }) + } + break - case "refreshOpenRouterModels": - await this.refreshOpenRouterModels() + case "refreshUnboundModels": + const unboundModels = await getUnboundModels() + + if (Object.keys(unboundModels).length > 0) { + const cacheDir = await this.ensureCacheDirectoryExists() + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.unboundModels), + JSON.stringify(unboundModels), + ) + await this.postMessageToWebview({ type: "unboundModels", unboundModels }) + } + + break + case "refreshRequestyModels": + const requestyModels = await getRequestyModels() + + if (Object.keys(requestyModels).length > 0) { + const cacheDir = await this.ensureCacheDirectoryExists() + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.requestyModels), + JSON.stringify(requestyModels), + ) + await this.postMessageToWebview({ type: "requestyModels", requestyModels }) + } + break case "refreshOpenAiModels": if (message?.values?.baseUrl && message?.values?.apiKey) { - const openAiModels = await this.getOpenAiModels( + const openAiModels = await getOpenAiModels( message?.values?.baseUrl, message?.values?.apiKey, ) this.postMessageToWebview({ type: "openAiModels", openAiModels }) } + break - case "refreshUnboundModels": - await this.refreshUnboundModels() + case "requestOllamaModels": + const ollamaModels = await getOllamaModels(message.text) + // TODO: Cache like we do for OpenRouter, etc? + this.postMessageToWebview({ type: "ollamaModels", ollamaModels }) break - case "refreshRequestyModels": - if (message?.values?.apiKey) { - const requestyModels = await this.refreshRequestyModels(message?.values?.apiKey) - this.postMessageToWebview({ type: "requestyModels", requestyModels: requestyModels }) - } + case "requestLmStudioModels": + const lmStudioModels = await getLmStudioModels(message.text) + // TODO: Cache like we do for OpenRouter, etc? + this.postMessageToWebview({ type: "lmStudioModels", lmStudioModels }) + break + case "requestVsCodeLmModels": + const vsCodeLmModels = await getVsCodeLmModels() + // TODO: Cache like we do for OpenRouter, etc? + this.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels }) break case "openImage": openImage(message.text!) @@ -1657,6 +1651,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { lmStudioModelId, lmStudioBaseUrl, anthropicBaseUrl, + anthropicThinking, geminiApiKey, openAiNativeApiKey, deepSeekApiKey, @@ -1704,6 +1699,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.updateGlobalState("lmStudioModelId", lmStudioModelId), this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl), this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl), + this.updateGlobalState("anthropicThinking", anthropicThinking), this.storeSecret("geminiApiKey", geminiApiKey), this.storeSecret("openAiNativeApiKey", openAiNativeApiKey), this.storeSecret("deepSeekApiKey", deepSeekApiKey), @@ -1789,175 +1785,24 @@ export class ClineProvider implements vscode.WebviewViewProvider { return settingsDir } - // Ollama - - async getOllamaModels(baseUrl?: string) { - try { - if (!baseUrl) { - baseUrl = "http://localhost:11434" - } - if (!URL.canParse(baseUrl)) { - return [] - } - const response = await axios.get(`${baseUrl}/api/tags`) - const modelsArray = response.data?.models?.map((model: any) => model.name) || [] - const models = [...new Set(modelsArray)] - return models - } catch (error) { - return [] - } + private async ensureCacheDirectoryExists() { + const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache") + await fs.mkdir(cacheDir, { recursive: true }) + return cacheDir } - // LM Studio + private async readModelsFromCache(filename: string): Promise | undefined> { + const filePath = path.join(await this.ensureCacheDirectoryExists(), filename) + const fileExists = await fileExistsAtPath(filePath) - async getLmStudioModels(baseUrl?: string) { - try { - if (!baseUrl) { - baseUrl = "http://localhost:1234" - } - if (!URL.canParse(baseUrl)) { - return [] - } - const response = await axios.get(`${baseUrl}/v1/models`) - const modelsArray = response.data?.data?.map((model: any) => model.id) || [] - const models = [...new Set(modelsArray)] - return models - } catch (error) { - return [] - } - } - - // VSCode LM API - private async getVsCodeLmModels() { - try { - const models = await vscode.lm.selectChatModels({}) - return models || [] - } catch (error) { - this.outputChannel.appendLine( - `Error fetching VS Code LM models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - return [] - } - } - - // OpenAi - - async getOpenAiModels(baseUrl?: string, apiKey?: string) { - try { - if (!baseUrl) { - return [] - } - - if (!URL.canParse(baseUrl)) { - return [] - } - - const config: Record = {} - if (apiKey) { - config["headers"] = { Authorization: `Bearer ${apiKey}` } - } - - const response = await axios.get(`${baseUrl}/models`, config) - const modelsArray = response.data?.data?.map((model: any) => model.id) || [] - const models = [...new Set(modelsArray)] - return models - } catch (error) { - return [] - } - } - - // Requesty - async readRequestyModels(): Promise | undefined> { - const requestyModelsFilePath = path.join( - await this.ensureCacheDirectoryExists(), - GlobalFileNames.requestyModels, - ) - const fileExists = await fileExistsAtPath(requestyModelsFilePath) if (fileExists) { - const fileContents = await fs.readFile(requestyModelsFilePath, "utf8") + const fileContents = await fs.readFile(filePath, "utf8") return JSON.parse(fileContents) } + return undefined } - async refreshRequestyModels(apiKey?: string) { - const requestyModelsFilePath = path.join( - await this.ensureCacheDirectoryExists(), - GlobalFileNames.requestyModels, - ) - - const models: Record = {} - try { - const config: Record = {} - if (!apiKey) { - apiKey = (await this.getSecret("requestyApiKey")) as string - } - - if (!apiKey) { - this.outputChannel.appendLine("No Requesty API key found") - return models - } - - if (apiKey) { - config["headers"] = { Authorization: `Bearer ${apiKey}` } - } - - const response = await axios.get("https://router.requesty.ai/v1/models", config) - /* - { - "id": "anthropic/claude-3-5-sonnet-20240620", - "object": "model", - "created": 1738243330, - "owned_by": "system", - "input_price": 0.000003, - "caching_price": 0.00000375, - "cached_price": 3E-7, - "output_price": 0.000015, - "max_output_tokens": 8192, - "context_window": 200000, - "supports_caching": true, - "description": "Anthropic's most intelligent model. Highest level of intelligence and capability" - }, - } - */ - if (response.data) { - const rawModels = response.data.data - const parsePrice = (price: any) => { - if (price) { - return parseFloat(price) * 1_000_000 - } - return undefined - } - for (const rawModel of rawModels) { - const modelInfo: ModelInfo = { - maxTokens: rawModel.max_output_tokens, - contextWindow: rawModel.context_window, - supportsImages: rawModel.support_image, - supportsComputerUse: rawModel.support_computer_use, - supportsPromptCache: rawModel.supports_caching, - inputPrice: parsePrice(rawModel.input_price), - outputPrice: parsePrice(rawModel.output_price), - description: rawModel.description, - cacheWritesPrice: parsePrice(rawModel.caching_price), - cacheReadsPrice: parsePrice(rawModel.cached_price), - } - - models[rawModel.id] = modelInfo - } - } else { - this.outputChannel.appendLine("Invalid response from Requesty API") - } - await fs.writeFile(requestyModelsFilePath, JSON.stringify(models)) - } catch (error) { - this.outputChannel.appendLine( - `Error fetching Requesty models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - } - - await this.postMessageToWebview({ type: "requestyModels", requestyModels: models }) - return models - } - // OpenRouter async handleOpenRouterCallback(code: string) { @@ -1986,11 +1831,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { // await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome } - private async ensureCacheDirectoryExists(): Promise { - const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache") - await fs.mkdir(cacheDir, { recursive: true }) - return cacheDir - } + // Glama async handleGlamaCallback(code: string) { let apiKey: string @@ -2021,246 +1862,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { // await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome } - private async readModelsFromCache(filename: string): Promise | undefined> { - const filePath = path.join(await this.ensureCacheDirectoryExists(), filename) - const fileExists = await fileExistsAtPath(filePath) - if (fileExists) { - const fileContents = await fs.readFile(filePath, "utf8") - return JSON.parse(fileContents) - } - return undefined - } - - async readGlamaModels(): Promise | undefined> { - return this.readModelsFromCache(GlobalFileNames.glamaModels) - } - - async refreshGlamaModels() { - const glamaModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.glamaModels) - - const models: Record = {} - try { - const response = await axios.get("https://glama.ai/api/gateway/v1/models") - /* - { - "added": "2024-12-24T15:12:49.324Z", - "capabilities": [ - "adjustable_safety_settings", - "caching", - "code_execution", - "function_calling", - "json_mode", - "json_schema", - "system_instructions", - "tuning", - "input:audio", - "input:image", - "input:text", - "input:video", - "output:text" - ], - "id": "google-vertex/gemini-1.5-flash-002", - "maxTokensInput": 1048576, - "maxTokensOutput": 8192, - "pricePerToken": { - "cacheRead": null, - "cacheWrite": null, - "input": "0.000000075", - "output": "0.0000003" - } - } - */ - if (response.data) { - const rawModels = response.data - const parsePrice = (price: any) => { - if (price) { - return parseFloat(price) * 1_000_000 - } - return undefined - } - for (const rawModel of rawModels) { - const modelInfo: ModelInfo = { - maxTokens: rawModel.maxTokensOutput, - contextWindow: rawModel.maxTokensInput, - supportsImages: rawModel.capabilities?.includes("input:image"), - supportsComputerUse: rawModel.capabilities?.includes("computer_use"), - supportsPromptCache: rawModel.capabilities?.includes("caching"), - inputPrice: parsePrice(rawModel.pricePerToken?.input), - outputPrice: parsePrice(rawModel.pricePerToken?.output), - description: undefined, - cacheWritesPrice: parsePrice(rawModel.pricePerToken?.cacheWrite), - cacheReadsPrice: parsePrice(rawModel.pricePerToken?.cacheRead), - } - - models[rawModel.id] = modelInfo - } - } else { - this.outputChannel.appendLine("Invalid response from Glama API") - } - await fs.writeFile(glamaModelsFilePath, JSON.stringify(models)) - } catch (error) { - this.outputChannel.appendLine( - `Error fetching Glama models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - } - - await this.postMessageToWebview({ type: "glamaModels", glamaModels: models }) - return models - } - - async readOpenRouterModels(): Promise | undefined> { - return this.readModelsFromCache(GlobalFileNames.openRouterModels) - } - - async refreshOpenRouterModels() { - const openRouterModelsFilePath = path.join( - await this.ensureCacheDirectoryExists(), - GlobalFileNames.openRouterModels, - ) - - const models: Record = {} - try { - const response = await axios.get("https://openrouter.ai/api/v1/models") - /* - { - "id": "anthropic/claude-3.5-sonnet", - "name": "Anthropic: Claude 3.5 Sonnet", - "created": 1718841600, - "description": "Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: Autonomously writes, edits, and runs code with reasoning and troubleshooting\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal", - "context_length": 200000, - "architecture": { - "modality": "text+image-\u003Etext", - "tokenizer": "Claude", - "instruct_type": null - }, - "pricing": { - "prompt": "0.000003", - "completion": "0.000015", - "image": "0.0048", - "request": "0" - }, - "top_provider": { - "context_length": 200000, - "max_completion_tokens": 8192, - "is_moderated": true - }, - "per_request_limits": null - }, - */ - if (response.data?.data) { - const rawModels = response.data.data - const parsePrice = (price: any) => { - if (price) { - return parseFloat(price) * 1_000_000 - } - return undefined - } - for (const rawModel of rawModels) { - const modelInfo: ModelInfo = { - maxTokens: rawModel.top_provider?.max_completion_tokens, - contextWindow: rawModel.context_length, - supportsImages: rawModel.architecture?.modality?.includes("image"), - supportsPromptCache: false, - inputPrice: parsePrice(rawModel.pricing?.prompt), - outputPrice: parsePrice(rawModel.pricing?.completion), - description: rawModel.description, - } - - switch (rawModel.id) { - case "anthropic/claude-3.5-sonnet": - case "anthropic/claude-3.5-sonnet:beta": - // NOTE: this needs to be synced with api.ts/openrouter default model info - modelInfo.supportsComputerUse = true - modelInfo.supportsPromptCache = true - modelInfo.cacheWritesPrice = 3.75 - modelInfo.cacheReadsPrice = 0.3 - break - case "anthropic/claude-3.5-sonnet-20240620": - case "anthropic/claude-3.5-sonnet-20240620:beta": - modelInfo.supportsPromptCache = true - modelInfo.cacheWritesPrice = 3.75 - modelInfo.cacheReadsPrice = 0.3 - break - case "anthropic/claude-3-5-haiku": - case "anthropic/claude-3-5-haiku:beta": - case "anthropic/claude-3-5-haiku-20241022": - case "anthropic/claude-3-5-haiku-20241022:beta": - case "anthropic/claude-3.5-haiku": - case "anthropic/claude-3.5-haiku:beta": - case "anthropic/claude-3.5-haiku-20241022": - case "anthropic/claude-3.5-haiku-20241022:beta": - modelInfo.supportsPromptCache = true - modelInfo.cacheWritesPrice = 1.25 - modelInfo.cacheReadsPrice = 0.1 - break - case "anthropic/claude-3-opus": - case "anthropic/claude-3-opus:beta": - modelInfo.supportsPromptCache = true - modelInfo.cacheWritesPrice = 18.75 - modelInfo.cacheReadsPrice = 1.5 - break - case "anthropic/claude-3-haiku": - case "anthropic/claude-3-haiku:beta": - modelInfo.supportsPromptCache = true - modelInfo.cacheWritesPrice = 0.3 - modelInfo.cacheReadsPrice = 0.03 - break - } - - models[rawModel.id] = modelInfo - } - } else { - this.outputChannel.appendLine("Invalid response from OpenRouter API") - } - await fs.writeFile(openRouterModelsFilePath, JSON.stringify(models)) - } catch (error) { - this.outputChannel.appendLine( - `Error fetching OpenRouter models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - } - - await this.postMessageToWebview({ type: "openRouterModels", openRouterModels: models }) - return models - } - - async readUnboundModels(): Promise | undefined> { - return this.readModelsFromCache(GlobalFileNames.unboundModels) - } - - async refreshUnboundModels() { - const unboundModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.unboundModels) - - const models: Record = {} - try { - const response = await axios.get("https://api.getunbound.ai/models") - - if (response.data) { - const rawModels: Record = response.data - for (const [modelId, model] of Object.entries(rawModels)) { - models[modelId] = { - maxTokens: model?.maxTokens ? parseInt(model.maxTokens) : undefined, - contextWindow: model?.contextWindow ? parseInt(model.contextWindow) : 0, - supportsImages: model?.supportsImages ?? false, - supportsPromptCache: model?.supportsPromptCaching ?? false, - supportsComputerUse: model?.supportsComputerUse ?? false, - inputPrice: model?.inputTokenPrice ? parseFloat(model.inputTokenPrice) : undefined, - outputPrice: model?.outputTokenPrice ? parseFloat(model.outputTokenPrice) : undefined, - cacheWritesPrice: model?.cacheWritePrice ? parseFloat(model.cacheWritePrice) : undefined, - cacheReadsPrice: model?.cacheReadPrice ? parseFloat(model.cacheReadPrice) : undefined, - } - } - } - await fs.writeFile(unboundModelsFilePath, JSON.stringify(models)) - } catch (error) { - this.outputChannel.appendLine( - `Error fetching Unbound models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - } - - await this.postMessageToWebview({ type: "unboundModels", unboundModels: models }) - return models - } - // Task history async getTaskWithId(id: string): Promise<{ @@ -2423,6 +2024,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get("allowedCommands") || [] + const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || "" + return { version: this.context.extension?.packageJSON?.version ?? "", apiConfiguration, @@ -2469,6 +2072,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { experiments: experiments ?? experimentDefault, mcpServers: this.mcpHub?.getAllServers() ?? [], maxOpenTabsContext: maxOpenTabsContext ?? 20, + cwd: cwd, } } @@ -2551,6 +2155,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { lmStudioModelId, lmStudioBaseUrl, anthropicBaseUrl, + anthropicThinking, geminiApiKey, openAiNativeApiKey, deepSeekApiKey, @@ -2633,6 +2238,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("lmStudioModelId") as Promise, this.getGlobalState("lmStudioBaseUrl") as Promise, this.getGlobalState("anthropicBaseUrl") as Promise, + this.getGlobalState("anthropicThinking") as Promise, this.getSecret("geminiApiKey") as Promise, this.getSecret("openAiNativeApiKey") as Promise, this.getSecret("deepSeekApiKey") as Promise, @@ -2732,6 +2338,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { lmStudioModelId, lmStudioBaseUrl, anthropicBaseUrl, + anthropicThinking, geminiApiKey, openAiNativeApiKey, deepSeekApiKey, @@ -2844,26 +2451,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { return await this.context.globalState.get(key) } - // workspace - - private async updateWorkspaceState(key: string, value: any) { - await this.context.workspaceState.update(key, value) - } - - private async getWorkspaceState(key: string) { - return await this.context.workspaceState.get(key) - } - - // private async clearState() { - // this.context.workspaceState.keys().forEach((key) => { - // this.context.workspaceState.update(key, undefined) - // }) - // this.context.globalState.keys().forEach((key) => { - // this.context.globalState.update(key, undefined) - // }) - // this.context.secrets.delete("apiKey") - // } - // secrets public async storeSecret(key: SecretKey, value?: string) { diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 0a8f73308f..6449cc93be 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -9,12 +9,41 @@ import { setSoundEnabled } from "../../../utils/sound" import { defaultModeSlug } from "../../../shared/modes" import { experimentDefault } from "../../../shared/experiments" -// Mock custom-instructions module -const mockAddCustomInstructions = jest.fn() +// Mock setup must come before imports +jest.mock("../../prompts/sections/custom-instructions") -jest.mock("../../prompts/sections/custom-instructions", () => ({ - addCustomInstructions: mockAddCustomInstructions, -})) +// Mock dependencies +jest.mock("vscode") +jest.mock("delay") +jest.mock( + "@modelcontextprotocol/sdk/types.js", + () => ({ + CallToolResultSchema: {}, + ListResourcesResultSchema: {}, + ListResourceTemplatesResultSchema: {}, + ListToolsResultSchema: {}, + ReadResourceResultSchema: {}, + ErrorCode: { + InvalidRequest: "InvalidRequest", + MethodNotFound: "MethodNotFound", + InternalError: "InternalError", + }, + McpError: class McpError extends Error { + code: string + constructor(code: string, message: string) { + super(message) + this.code = code + this.name = "McpError" + } + }, + }), + { virtual: true }, +) + +// Initialize mocks +const mockAddCustomInstructions = jest.fn().mockResolvedValue("Combined instructions") +;(jest.requireMock("../../prompts/sections/custom-instructions") as any).addCustomInstructions = + mockAddCustomInstructions // Mock delay module jest.mock("delay", () => { @@ -661,19 +690,18 @@ describe("ClineProvider", () => { await provider.initClineWithTask("Test task") // Verify Cline was initialized with mode-specific instructions - expect(Cline).toHaveBeenCalledWith( + expect(Cline).toHaveBeenCalledWith({ provider, - mockApiConfig, - modeCustomInstructions, - true, - false, - 1.0, - "Test task", - undefined, - undefined, - experimentDefault, - ) + apiConfiguration: mockApiConfig, + customInstructions: modeCustomInstructions, + enableDiff: true, + enableCheckpoints: false, + fuzzyMatchThreshold: 1.0, + task: "Test task", + experiments: experimentDefault, + }) }) + test("handles mode-specific custom instructions updates", async () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] diff --git a/src/integrations/diagnostics/DiagnosticsMonitor.ts b/src/integrations/diagnostics/DiagnosticsMonitor.ts deleted file mode 100644 index 6773567484..0000000000 --- a/src/integrations/diagnostics/DiagnosticsMonitor.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* -import * as vscode from "vscode" -import deepEqual from "fast-deep-equal" - -type FileDiagnostics = [vscode.Uri, vscode.Diagnostic[]][] - - -About Diagnostics: -The Problems tab shows diagnostics that have been reported for your project. These diagnostics are categorized into: -Errors: Critical issues that usually prevent your code from compiling or running correctly. -Warnings: Potential problems in the code that may not prevent it from running but could cause issues (e.g., bad practices, unused variables). -Information: Non-critical suggestions or tips (e.g., formatting issues or notes from linters). -The Problems tab displays diagnostics from various sources: -1. Language Servers: - - TypeScript: Type errors, missing imports, syntax issues - - Python: Syntax errors, invalid type hints, undefined variables - - JavaScript/Node.js: Parsing and execution errors -2. Linters: - - ESLint: Code style, best practices, potential bugs - - Pylint: Unused imports, naming conventions - - TSLint: Style and correctness issues in TypeScript -3. Build Tools: - - Webpack: Module resolution failures, build errors - - Gulp: Build errors during task execution -4. Custom Validators: - - Extensions can generate custom diagnostics for specific languages or tools -Each problem typically indicates its source (e.g., language server, linter, build tool). -Diagnostics update in real-time as you edit code, helping identify issues quickly. For example, if you introduce a syntax error in a TypeScript file, the Problems tab will immediately display the new error. - -Notes on diagnostics: -- linter diagnostics are only captured for open editors -- this works great for us since when cline edits/creates files its through vscode's textedit api's and we get those diagnostics for free -- some tools might require you to save the file or manually refresh to clear the problem from the list. - -System Prompt -- You will automatically receive workspace error diagnostics in environment_details. Be mindful that this may include issues beyond the scope of your task or the user's request. Only address errors relevant to your work, and avoid fixing pre-existing or unrelated issues unless the user specifically instructs you to do so. -- If you are unable to resolve errors provided in environment_details after two attempts, consider using ask_followup_question to ask the user for additional information, such as the latest documentation related to a problematic framework, to help you make progress on the task. If the error remains unresolved after this step, proceed with your task while disregarding the error. - -class DiagnosticsMonitor { - private diagnosticsChangeEmitter: vscode.EventEmitter = new vscode.EventEmitter() - private disposables: vscode.Disposable[] = [] - private lastDiagnostics: FileDiagnostics = [] - - constructor() { - this.disposables.push( - vscode.languages.onDidChangeDiagnostics(() => { - this.diagnosticsChangeEmitter.fire() - }) - ) - } - - public async getCurrentDiagnostics(shouldWaitForChanges: boolean): Promise { - const currentDiagnostics = this.getDiagnostics() - if (!shouldWaitForChanges) { - this.lastDiagnostics = currentDiagnostics - return currentDiagnostics - } - - if (!deepEqual(this.lastDiagnostics, currentDiagnostics)) { - this.lastDiagnostics = currentDiagnostics - return currentDiagnostics - } - - let timeout = 300 // only way this happens is if theres no errors - - // if diagnostics contain existing errors (since the check above didn't trigger) then it's likely cline just did something that should have fixed the error, so we'll give a longer grace period for diagnostics to catch up - const hasErrors = currentDiagnostics.some(([_, diagnostics]) => - diagnostics.some((d) => d.severity === vscode.DiagnosticSeverity.Error) - ) - if (hasErrors) { - console.log("Existing errors detected, extending timeout", currentDiagnostics) - timeout = 10_000 - } - - return this.waitForUpdatedDiagnostics(timeout) - } - - private async waitForUpdatedDiagnostics(timeout: number): Promise { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - cleanup() - const finalDiagnostics = this.getDiagnostics() - this.lastDiagnostics = finalDiagnostics - resolve(finalDiagnostics) - }, timeout) - - const disposable = this.diagnosticsChangeEmitter.event(() => { - const updatedDiagnostics = this.getDiagnostics() // I thought this would only trigger when diagnostics changed, but that's not the case. - if (deepEqual(this.lastDiagnostics, updatedDiagnostics)) { - // diagnostics have not changed, ignoring... - return - } - cleanup() - this.lastDiagnostics = updatedDiagnostics - resolve(updatedDiagnostics) - }) - - const cleanup = () => { - clearTimeout(timer) - disposable.dispose() - } - }) - } - - private getDiagnostics(): FileDiagnostics { - const allDiagnostics = vscode.languages.getDiagnostics() - return allDiagnostics - .filter(([_, diagnostics]) => diagnostics.some((d) => d.severity === vscode.DiagnosticSeverity.Error)) - .map(([uri, diagnostics]) => [ - uri, - diagnostics.filter((d) => d.severity === vscode.DiagnosticSeverity.Error), - ]) - } - - public dispose() { - this.disposables.forEach((d) => d.dispose()) - this.disposables = [] - this.diagnosticsChangeEmitter.dispose() - } -} - -export default DiagnosticsMonitor -*/ diff --git a/src/integrations/misc/export-markdown.ts b/src/integrations/misc/export-markdown.ts index 2aa9d7b6ed..05b31671d8 100644 --- a/src/integrations/misc/export-markdown.ts +++ b/src/integrations/misc/export-markdown.ts @@ -41,14 +41,7 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi } } -export function formatContentBlockToMarkdown( - block: - | Anthropic.TextBlockParam - | Anthropic.ImageBlockParam - | Anthropic.ToolUseBlockParam - | Anthropic.ToolResultBlockParam, - // messages: Anthropic.MessageParam[] -): string { +export function formatContentBlockToMarkdown(block: Anthropic.Messages.ContentBlockParam): string { switch (block.type) { case "text": return block.text diff --git a/src/integrations/terminal/TerminalActions.ts b/src/integrations/terminal/TerminalActions.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/integrations/workspace/get-python-env.ts b/src/integrations/workspace/get-python-env.ts deleted file mode 100644 index 92575b408a..0000000000 --- a/src/integrations/workspace/get-python-env.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as vscode from "vscode" - -/* -Used to get user's current python environment (unnecessary now that we use the IDE's terminal) -${await (async () => { - try { - const pythonEnvPath = await getPythonEnvPath() - if (pythonEnvPath) { - return `\nPython Environment: ${pythonEnvPath}` - } - } catch {} - return "" - })()} -*/ -export async function getPythonEnvPath(): Promise { - const pythonExtension = vscode.extensions.getExtension("ms-python.python") - - if (!pythonExtension) { - return undefined - } - - // Ensure the Python extension is activated - if (!pythonExtension.isActive) { - // if the python extension is not active, we can assume the project is not a python project - return undefined - } - - // Access the Python extension API - const pythonApi = pythonExtension.exports - // Get the active environment path for the current workspace - const workspaceFolder = vscode.workspace.workspaceFolders?.[0] - if (!workspaceFolder) { - return undefined - } - // Get the active python environment path for the current workspace - const pythonEnv = await pythonApi?.environments?.getActiveEnvironmentPath(workspaceFolder.uri) - if (pythonEnv && pythonEnv.path) { - return pythonEnv.path - } else { - return undefined - } -} diff --git a/src/services/checkpoints/__tests__/LocalCheckpointService.test.ts b/src/services/checkpoints/__tests__/LocalCheckpointService.test.ts index 5ba2721b8c..29ab7133a5 100644 --- a/src/services/checkpoints/__tests__/LocalCheckpointService.test.ts +++ b/src/services/checkpoints/__tests__/LocalCheckpointService.test.ts @@ -9,6 +9,8 @@ import { simpleGit, SimpleGit } from "simple-git" import { CheckpointServiceFactory } from "../CheckpointServiceFactory" import { LocalCheckpointService } from "../LocalCheckpointService" +const tmpDir = path.join(os.tmpdir(), "test-LocalCheckpointService") + describe("LocalCheckpointService", () => { const taskId = "test-task" @@ -29,7 +31,7 @@ describe("LocalCheckpointService", () => { textFileContent?: string }) => { // Create a temporary directory for testing. - await fs.mkdir(workspaceDir) + await fs.mkdir(workspaceDir, { recursive: true }) // Initialize git repo. const git = simpleGit(workspaceDir) @@ -49,7 +51,7 @@ describe("LocalCheckpointService", () => { } beforeEach(async () => { - const workspaceDir = path.join(os.tmpdir(), `checkpoint-service-test-${Date.now()}`) + const workspaceDir = path.join(tmpDir, `checkpoint-service-test-${Date.now()}`) const repo = await initRepo({ workspaceDir }) testFile = repo.testFile @@ -60,10 +62,13 @@ describe("LocalCheckpointService", () => { }) afterEach(async () => { - await fs.rm(service.workspaceDir, { recursive: true, force: true }) jest.restoreAllMocks() }) + afterAll(async () => { + await fs.rm(tmpDir, { recursive: true, force: true }) + }) + describe("getDiff", () => { it("returns the correct diff between commits", async () => { await fs.writeFile(testFile, "Ahoy, world!") @@ -316,7 +321,7 @@ describe("LocalCheckpointService", () => { describe("create", () => { it("initializes a git repository if one does not already exist", async () => { - const workspaceDir = path.join(os.tmpdir(), `checkpoint-service-test2-${Date.now()}`) + const workspaceDir = path.join(tmpDir, `checkpoint-service-test2-${Date.now()}`) await fs.mkdir(workspaceDir) const newTestFile = path.join(workspaceDir, "test.txt") await fs.writeFile(newTestFile, "Hello, world!") @@ -364,7 +369,7 @@ describe("LocalCheckpointService", () => { }) it("respects existing git user configuration", async () => { - const workspaceDir = path.join(os.tmpdir(), `checkpoint-service-test-config2-${Date.now()}`) + const workspaceDir = path.join(tmpDir, `checkpoint-service-test-config2-${Date.now()}`) const userName = "Custom User" const userEmail = "custom@example.com" await initRepo({ workspaceDir, userName, userEmail }) diff --git a/src/services/checkpoints/__tests__/ShadowCheckpointService.test.ts b/src/services/checkpoints/__tests__/ShadowCheckpointService.test.ts index 0e32e82fdf..5bcc5d444c 100644 --- a/src/services/checkpoints/__tests__/ShadowCheckpointService.test.ts +++ b/src/services/checkpoints/__tests__/ShadowCheckpointService.test.ts @@ -13,11 +13,12 @@ jest.mock("globby", () => ({ globby: jest.fn().mockResolvedValue([]), })) +const tmpDir = path.join(os.tmpdir(), "test-ShadowCheckpointService") + describe("ShadowCheckpointService", () => { const taskId = "test-task" let workspaceGit: SimpleGit - let shadowGit: SimpleGit let testFile: string let service: ShadowCheckpointService @@ -35,7 +36,7 @@ describe("ShadowCheckpointService", () => { textFileContent?: string }) => { // Create a temporary directory for testing. - await fs.mkdir(workspaceDir) + await fs.mkdir(workspaceDir, { recursive: true }) // Initialize git repo. const git = simpleGit(workspaceDir) @@ -57,8 +58,8 @@ describe("ShadowCheckpointService", () => { beforeEach(async () => { jest.mocked(require("globby").globby).mockClear().mockResolvedValue([]) - const shadowDir = path.join(os.tmpdir(), `shadow-${Date.now()}`) - const workspaceDir = path.join(os.tmpdir(), `workspace-${Date.now()}`) + const shadowDir = path.join(tmpDir, `shadow-${Date.now()}`) + const workspaceDir = path.join(tmpDir, `workspace-${Date.now()}`) const repo = await initRepo({ workspaceDir }) testFile = repo.testFile @@ -69,15 +70,16 @@ describe("ShadowCheckpointService", () => { }) workspaceGit = repo.git - shadowGit = service.git }) afterEach(async () => { - await fs.rm(service.shadowDir, { recursive: true, force: true }) - await fs.rm(service.workspaceDir, { recursive: true, force: true }) jest.restoreAllMocks() }) + afterAll(async () => { + await fs.rm(tmpDir, { recursive: true, force: true }) + }) + describe("getDiff", () => { it("returns the correct diff between commits", async () => { await fs.writeFile(testFile, "Ahoy, world!") @@ -299,8 +301,8 @@ describe("ShadowCheckpointService", () => { describe("create", () => { it("initializes a git repository if one does not already exist", async () => { - const shadowDir = path.join(os.tmpdir(), `shadow2-${Date.now()}`) - const workspaceDir = path.join(os.tmpdir(), `workspace2-${Date.now()}`) + const shadowDir = path.join(tmpDir, `shadow2-${Date.now()}`) + const workspaceDir = path.join(tmpDir, `workspace2-${Date.now()}`) await fs.mkdir(workspaceDir) const newTestFile = path.join(workspaceDir, "test.txt") diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 6e3f39fa7d..6c906c7cf8 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -14,7 +14,9 @@ import * as fs from "fs/promises" import * as path from "path" import * as vscode from "vscode" import { z } from "zod" -import { ClineProvider, GlobalFileNames } from "../../core/webview/ClineProvider" + +import { ClineProvider } from "../../core/webview/ClineProvider" +import { GlobalFileNames } from "../../shared/globalFileNames" import { McpResource, McpResourceResponse, diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index fe9fa39427..e87edffed1 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -27,10 +27,11 @@ export interface ExtensionMessage { | "workspaceUpdated" | "invoke" | "partialMessage" - | "glamaModels" | "openRouterModels" - | "openAiModels" + | "glamaModels" + | "unboundModels" | "requestyModels" + | "openAiModels" | "mcpServers" | "enhancedPrompt" | "commitSearchResults" @@ -43,8 +44,6 @@ export interface ExtensionMessage { | "autoApprovalEnabled" | "updateCustomMode" | "deleteCustomMode" - | "unboundModels" - | "refreshUnboundModels" | "currentCheckpointUpdated" text?: string action?: @@ -67,11 +66,11 @@ export interface ExtensionMessage { path?: string }> partialMessage?: ClineMessage - glamaModels?: Record - requestyModels?: Record openRouterModels?: Record - openAiModels?: string[] + glamaModels?: Record unboundModels?: Record + requestyModels?: Record + openAiModels?: string[] mcpServers?: McpServer[] commits?: GitCommit[] listApiConfig?: ApiConfigMeta[] @@ -129,6 +128,7 @@ export interface ExtensionState { customModes: ModeConfig[] toolRequirements?: Record // Map of tool names to their requirements (e.g. {"apply_diff": true} if diffEnabled) maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500) + cwd?: string // Current working directory } export interface ClineMessage { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 106e6d243b..fde7442cc1 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -40,11 +40,11 @@ export interface WebviewMessage { | "openFile" | "openMention" | "cancelTask" - | "refreshGlamaModels" | "refreshOpenRouterModels" - | "refreshOpenAiModels" + | "refreshGlamaModels" | "refreshUnboundModels" | "refreshRequestyModels" + | "refreshOpenAiModels" | "alwaysAllowBrowser" | "alwaysAllowMcp" | "alwaysAllowModeSwitch" @@ -71,7 +71,6 @@ export interface WebviewMessage { | "mcpEnabled" | "enableMcpServerCreation" | "searchCommits" - | "refreshGlamaModels" | "alwaysApproveResubmit" | "requestDelaySeconds" | "rateLimitSeconds" diff --git a/src/shared/__tests__/checkExistApiConfig.test.ts b/src/shared/__tests__/checkExistApiConfig.test.ts index 914f4933d6..62517d6958 100644 --- a/src/shared/__tests__/checkExistApiConfig.test.ts +++ b/src/shared/__tests__/checkExistApiConfig.test.ts @@ -32,6 +32,7 @@ describe("checkExistKey", () => { apiKey: "test-key", apiProvider: undefined, anthropicBaseUrl: undefined, + anthropicThinking: undefined, } expect(checkExistKey(config)).toBe(true) }) diff --git a/src/shared/__tests__/experiments.test.ts b/src/shared/__tests__/experiments.test.ts new file mode 100644 index 0000000000..c5b999a1a3 --- /dev/null +++ b/src/shared/__tests__/experiments.test.ts @@ -0,0 +1,47 @@ +import { EXPERIMENT_IDS, experimentConfigsMap, experiments as Experiments, ExperimentId } from "../experiments" + +describe("experiments", () => { + describe("POWER_STEERING", () => { + it("is configured correctly", () => { + expect(EXPERIMENT_IDS.POWER_STEERING).toBe("powerSteering") + expect(experimentConfigsMap.POWER_STEERING).toMatchObject({ + name: 'Use experimental "power steering" mode', + description: + "When enabled, Roo will remind the model about the details of its current mode definition more frequently. This will lead to stronger adherence to role definitions and custom instructions, but will use more tokens per message.", + enabled: false, + }) + }) + }) + + describe("isEnabled", () => { + it("returns false when experiment is not enabled", () => { + const experiments: Record = { + powerSteering: false, + experimentalDiffStrategy: false, + search_and_replace: false, + insert_content: false, + } + expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) + }) + + it("returns true when experiment is enabled", () => { + const experiments: Record = { + powerSteering: true, + experimentalDiffStrategy: false, + search_and_replace: false, + insert_content: false, + } + expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true) + }) + + it("returns false when experiment is not present", () => { + const experiments: Record = { + experimentalDiffStrategy: false, + search_and_replace: false, + insert_content: false, + powerSteering: false, + } + expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) + }) + }) +}) diff --git a/src/shared/__tests__/modes.test.ts b/src/shared/__tests__/modes.test.ts index aaf7338b5e..3bd89c4ecb 100644 --- a/src/shared/__tests__/modes.test.ts +++ b/src/shared/__tests__/modes.test.ts @@ -1,4 +1,13 @@ -import { isToolAllowedForMode, FileRestrictionError, ModeConfig } from "../modes" +// Mock setup must come before imports +jest.mock("vscode") +const mockAddCustomInstructions = jest.fn().mockResolvedValue("Combined instructions") +jest.mock("../../core/prompts/sections/custom-instructions", () => ({ + addCustomInstructions: mockAddCustomInstructions, +})) + +import { isToolAllowedForMode, FileRestrictionError, ModeConfig, getFullModeDetails, modes } from "../modes" +import * as vscode from "vscode" +import { addCustomInstructions } from "../../core/prompts/sections/custom-instructions" describe("isToolAllowedForMode", () => { const customModes: ModeConfig[] = [ @@ -324,6 +333,98 @@ describe("FileRestrictionError", () => { expect(error.name).toBe("FileRestrictionError") }) + describe("debug mode", () => { + it("is configured correctly", () => { + const debugMode = modes.find((mode) => mode.slug === "debug") + expect(debugMode).toBeDefined() + expect(debugMode).toMatchObject({ + slug: "debug", + name: "Debug", + roleDefinition: + "You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.", + groups: ["read", "edit", "browser", "command", "mcp"], + }) + expect(debugMode?.customInstructions).toContain( + "Reflect on 5-7 different possible sources of the problem, distill those down to 1-2 most likely sources, and then add logs to validate your assumptions. Explicitly ask the user to confirm the diagnosis before fixing the problem.", + ) + }) + }) + + describe("getFullModeDetails", () => { + beforeEach(() => { + jest.clearAllMocks() + ;(addCustomInstructions as jest.Mock).mockResolvedValue("Combined instructions") + }) + + it("returns base mode when no overrides exist", async () => { + const result = await getFullModeDetails("debug") + expect(result).toMatchObject({ + slug: "debug", + name: "Debug", + roleDefinition: + "You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.", + }) + }) + + it("applies custom mode overrides", async () => { + const customModes = [ + { + slug: "debug", + name: "Custom Debug", + roleDefinition: "Custom debug role", + groups: ["read"], + }, + ] + + const result = await getFullModeDetails("debug", customModes) + expect(result).toMatchObject({ + slug: "debug", + name: "Custom Debug", + roleDefinition: "Custom debug role", + groups: ["read"], + }) + }) + + it("applies prompt component overrides", async () => { + const customModePrompts = { + debug: { + roleDefinition: "Overridden role", + customInstructions: "Overridden instructions", + }, + } + + const result = await getFullModeDetails("debug", undefined, customModePrompts) + expect(result.roleDefinition).toBe("Overridden role") + expect(result.customInstructions).toBe("Overridden instructions") + }) + + it("combines custom instructions when cwd provided", async () => { + const options = { + cwd: "/test/path", + globalCustomInstructions: "Global instructions", + preferredLanguage: "en", + } + + await getFullModeDetails("debug", undefined, undefined, options) + + expect(addCustomInstructions).toHaveBeenCalledWith( + expect.any(String), + "Global instructions", + "/test/path", + "debug", + { preferredLanguage: "en" }, + ) + }) + + it("falls back to first mode for non-existent mode", async () => { + const result = await getFullModeDetails("non-existent") + expect(result).toMatchObject({ + ...modes[0], + customInstructions: "", + }) + }) + }) + it("formats error message with description when provided", () => { const error = new FileRestrictionError("Markdown Editor", "\\.md$", "Markdown files only", "test.js") expect(error.message).toBe( diff --git a/src/shared/api.ts b/src/shared/api.ts index 9ecb12c140..5d4b8b120d 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -21,6 +21,7 @@ export interface ApiHandlerOptions { apiModelId?: string apiKey?: string // anthropic anthropicBaseUrl?: string + anthropicThinking?: number vsCodeLmModelSelector?: vscode.LanguageModelChatSelector glamaModelId?: string glamaModelInfo?: ModelInfo @@ -88,13 +89,44 @@ export interface ModelInfo { cacheReadsPrice?: number description?: string reasoningEffort?: "low" | "medium" | "high" + thinking?: boolean +} + +export const THINKING_BUDGET = { + step: 1024, + min: 1024, + default: 8 * 1024, } // Anthropic // https://docs.anthropic.com/en/docs/about-claude/models export type AnthropicModelId = keyof typeof anthropicModels -export const anthropicDefaultModelId: AnthropicModelId = "claude-3-5-sonnet-20241022" +export const anthropicDefaultModelId: AnthropicModelId = "claude-3-7-sonnet-20250219" export const anthropicModels = { + "claude-3-7-sonnet-20250219:thinking": { + maxTokens: 16384, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 3.0, // $3 per million input tokens + outputPrice: 15.0, // $15 per million output tokens + cacheWritesPrice: 3.75, // $3.75 per million tokens + cacheReadsPrice: 0.3, // $0.30 per million tokens + thinking: true, + }, + "claude-3-7-sonnet-20250219": { + maxTokens: 16384, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 3.0, // $3 per million input tokens + outputPrice: 15.0, // $15 per million output tokens + cacheWritesPrice: 3.75, // $3.75 per million tokens + cacheReadsPrice: 0.3, // $0.30 per million tokens + thinking: false, + }, "claude-3-5-sonnet-20241022": { maxTokens: 8192, contextWindow: 200_000, @@ -162,7 +194,7 @@ export interface MessageContent { } export type BedrockModelId = keyof typeof bedrockModels -export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-5-sonnet-20241022-v2:0" +export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-7-sonnet-20250219-v1:0" export const bedrockModels = { "amazon.nova-pro-v1:0": { maxTokens: 5000, @@ -197,6 +229,17 @@ export const bedrockModels = { cacheWritesPrice: 0.035, // per million tokens cacheReadsPrice: 0.00875, // per million tokens }, + "anthropic.claude-3-7-sonnet-20250219-v1:0": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + }, "anthropic.claude-3-5-sonnet-20241022-v2:0": { maxTokens: 8192, contextWindow: 200_000, @@ -205,8 +248,8 @@ export const bedrockModels = { supportsPromptCache: false, inputPrice: 3.0, outputPrice: 15.0, - cacheWritesPrice: 3.75, // per million tokens - cacheReadsPrice: 0.3, // per million tokens + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, }, "anthropic.claude-3-5-haiku-20241022-v1:0": { maxTokens: 8192, @@ -344,7 +387,7 @@ export const bedrockModels = { // Glama // https://glama.ai/models -export const glamaDefaultModelId = "anthropic/claude-3-5-sonnet" +export const glamaDefaultModelId = "anthropic/claude-3-7-sonnet" export const glamaDefaultModelInfo: ModelInfo = { maxTokens: 8192, contextWindow: 200_000, @@ -356,9 +399,12 @@ export const glamaDefaultModelInfo: ModelInfo = { cacheWritesPrice: 3.75, cacheReadsPrice: 0.3, description: - "The new Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: New Sonnet scores ~49% on SWE-Bench Verified, higher than the last best score, and without any fancy prompt scaffolding\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal\n\n_This is a faster endpoint, made available in collaboration with Anthropic, that is self-moderated: response moderation happens on the provider's side instead of OpenRouter's. For requests that pass moderation, it's identical to the [Standard](/anthropic/claude-3.5-sonnet) variant._", + "Claude 3.7 Sonnet is an advanced large language model with improved reasoning, coding, and problem-solving capabilities. It introduces a hybrid reasoning approach, allowing users to choose between rapid responses and extended, step-by-step processing for complex tasks. The model demonstrates notable improvements in coding, particularly in front-end development and full-stack updates, and excels in agentic workflows, where it can autonomously navigate multi-step processes. Claude 3.7 Sonnet maintains performance parity with its predecessor in standard mode while offering an extended reasoning mode for enhanced accuracy in math, coding, and instruction-following tasks. Read more at the [blog post here](https://www.anthropic.com/news/claude-3-7-sonnet)", } +// Requesty +// https://requesty.ai/router-2 +export const requestyDefaultModelId = "anthropic/claude-3-7-sonnet-latest" export const requestyDefaultModelInfo: ModelInfo = { maxTokens: 8192, contextWindow: 200_000, @@ -370,13 +416,12 @@ export const requestyDefaultModelInfo: ModelInfo = { cacheWritesPrice: 3.75, cacheReadsPrice: 0.3, description: - "The new Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: New Sonnet scores ~49% on SWE-Bench Verified, higher than the last best score, and without any fancy prompt scaffolding\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal\n\n_This is a faster endpoint, made available in collaboration with Anthropic, that is self-moderated: response moderation happens on the provider's side instead of OpenRouter's. For requests that pass moderation, it's identical to the [Standard](/anthropic/claude-3.5-sonnet) variant._", + "Claude 3.7 Sonnet is an advanced large language model with improved reasoning, coding, and problem-solving capabilities. It introduces a hybrid reasoning approach, allowing users to choose between rapid responses and extended, step-by-step processing for complex tasks. The model demonstrates notable improvements in coding, particularly in front-end development and full-stack updates, and excels in agentic workflows, where it can autonomously navigate multi-step processes. Claude 3.7 Sonnet maintains performance parity with its predecessor in standard mode while offering an extended reasoning mode for enhanced accuracy in math, coding, and instruction-following tasks. Read more at the [blog post here](https://www.anthropic.com/news/claude-3-7-sonnet)", } -export const requestyDefaultModelId = "anthropic/claude-3-5-sonnet" // OpenRouter // https://openrouter.ai/models?order=newest&supported_parameters=tools -export const openRouterDefaultModelId = "anthropic/claude-3.5-sonnet:beta" // will always exist in openRouterModels +export const openRouterDefaultModelId = "anthropic/claude-3.7-sonnet" export const openRouterDefaultModelInfo: ModelInfo = { maxTokens: 8192, contextWindow: 200_000, @@ -388,14 +433,23 @@ export const openRouterDefaultModelInfo: ModelInfo = { cacheWritesPrice: 3.75, cacheReadsPrice: 0.3, description: - "The new Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: New Sonnet scores ~49% on SWE-Bench Verified, higher than the last best score, and without any fancy prompt scaffolding\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal\n\n_This is a faster endpoint, made available in collaboration with Anthropic, that is self-moderated: response moderation happens on the provider's side instead of OpenRouter's. For requests that pass moderation, it's identical to the [Standard](/anthropic/claude-3.5-sonnet) variant._", + "Claude 3.7 Sonnet is an advanced large language model with improved reasoning, coding, and problem-solving capabilities. It introduces a hybrid reasoning approach, allowing users to choose between rapid responses and extended, step-by-step processing for complex tasks. The model demonstrates notable improvements in coding, particularly in front-end development and full-stack updates, and excels in agentic workflows, where it can autonomously navigate multi-step processes. Claude 3.7 Sonnet maintains performance parity with its predecessor in standard mode while offering an extended reasoning mode for enhanced accuracy in math, coding, and instruction-following tasks. Read more at the [blog post here](https://www.anthropic.com/news/claude-3-7-sonnet)", } // Vertex AI // https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude export type VertexModelId = keyof typeof vertexModels -export const vertexDefaultModelId: VertexModelId = "claude-3-5-sonnet-v2@20241022" +export const vertexDefaultModelId: VertexModelId = "claude-3-7-sonnet@20250219" export const vertexModels = { + "claude-3-7-sonnet@20250219": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: false, + inputPrice: 3.0, + outputPrice: 15.0, + }, "claude-3-5-sonnet-v2@20241022": { maxTokens: 8192, contextWindow: 200_000, diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index 9849e22526..2f946283c0 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -2,6 +2,7 @@ export const EXPERIMENT_IDS = { DIFF_STRATEGY: "experimentalDiffStrategy", SEARCH_AND_REPLACE: "search_and_replace", INSERT_BLOCK: "insert_content", + POWER_STEERING: "powerSteering", } as const export type ExperimentKey = keyof typeof EXPERIMENT_IDS @@ -35,6 +36,12 @@ export const experimentConfigsMap: Record = { "Enable the experimental insert content tool, allowing Roo to insert content at specific line numbers without needing to create a diff.", enabled: false, }, + POWER_STEERING: { + name: 'Use experimental "power steering" mode', + description: + "When enabled, Roo will remind the model about the details of its current mode definition more frequently. This will lead to stronger adherence to role definitions and custom instructions, but will use more tokens per message.", + enabled: false, + }, } export const experimentDefault = Object.fromEntries( diff --git a/src/shared/globalFileNames.ts b/src/shared/globalFileNames.ts new file mode 100644 index 0000000000..6088e95d99 --- /dev/null +++ b/src/shared/globalFileNames.ts @@ -0,0 +1,9 @@ +export const GlobalFileNames = { + apiConversationHistory: "api_conversation_history.json", + uiMessages: "ui_messages.json", + glamaModels: "glama_models.json", + openRouterModels: "openrouter_models.json", + requestyModels: "requesty_models.json", + mcpSettings: "cline_mcp_settings.json", + unboundModels: "unbound_models.json", +} diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts new file mode 100644 index 0000000000..7b6b4f8274 --- /dev/null +++ b/src/shared/globalState.ts @@ -0,0 +1,85 @@ +export type SecretKey = + | "apiKey" + | "glamaApiKey" + | "openRouterApiKey" + | "awsAccessKey" + | "awsSecretKey" + | "awsSessionToken" + | "openAiApiKey" + | "geminiApiKey" + | "openAiNativeApiKey" + | "deepSeekApiKey" + | "mistralApiKey" + | "unboundApiKey" + | "requestyApiKey" + +export type GlobalStateKey = + | "apiProvider" + | "apiModelId" + | "glamaModelId" + | "glamaModelInfo" + | "awsRegion" + | "awsUseCrossRegionInference" + | "awsProfile" + | "awsUseProfile" + | "vertexProjectId" + | "vertexRegion" + | "lastShownAnnouncementId" + | "customInstructions" + | "alwaysAllowReadOnly" + | "alwaysAllowWrite" + | "alwaysAllowExecute" + | "alwaysAllowBrowser" + | "alwaysAllowMcp" + | "alwaysAllowModeSwitch" + | "taskHistory" + | "openAiBaseUrl" + | "openAiModelId" + | "openAiCustomModelInfo" + | "openAiUseAzure" + | "ollamaModelId" + | "ollamaBaseUrl" + | "lmStudioModelId" + | "lmStudioBaseUrl" + | "anthropicBaseUrl" + | "anthropicThinking" + | "azureApiVersion" + | "openAiStreamingEnabled" + | "openRouterModelId" + | "openRouterModelInfo" + | "openRouterBaseUrl" + | "openRouterUseMiddleOutTransform" + | "allowedCommands" + | "soundEnabled" + | "soundVolume" + | "diffEnabled" + | "checkpointsEnabled" + | "browserViewportSize" + | "screenshotQuality" + | "fuzzyMatchThreshold" + | "preferredLanguage" // Language setting for Cline's communication + | "writeDelayMs" + | "terminalOutputLineLimit" + | "mcpEnabled" + | "enableMcpServerCreation" + | "alwaysApproveResubmit" + | "requestDelaySeconds" + | "rateLimitSeconds" + | "currentApiConfigName" + | "listApiConfigMeta" + | "vsCodeLmModelSelector" + | "mode" + | "modeApiConfigs" + | "customModePrompts" + | "customSupportPrompts" + | "enhancementApiConfigId" + | "experiments" // Map of experiment IDs to their enabled state + | "autoApprovalEnabled" + | "customModes" // Array of custom modes + | "unboundModelId" + | "requestyModelId" + | "requestyModelInfo" + | "unboundModelInfo" + | "modelTemperature" + | "mistralCodestralUrl" + | "maxOpenTabsContext" diff --git a/src/shared/modes.ts b/src/shared/modes.ts index 72f7785f13..ceb1e3e11f 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode" import { TOOL_GROUPS, ToolGroup, ALWAYS_AVAILABLE_TOOLS } from "./tool-groups" +import { addCustomInstructions } from "../core/prompts/sections/custom-instructions" // Mode types export type Mode = string @@ -98,6 +99,15 @@ export const modes: readonly ModeConfig[] = [ customInstructions: "You can analyze code, explain concepts, and access external resources. Make sure to answer the user's questions and don't rush to switch to implementing code.", }, + { + slug: "debug", + name: "Debug", + roleDefinition: + "You are Roo, an expert software debugger specializing in systematic problem diagnosis and resolution.", + groups: ["read", "edit", "browser", "command", "mcp"], + customInstructions: + "Reflect on 5-7 different possible sources of the problem, distill those down to 1-2 most likely sources, and then add logs to validate your assumptions. Explicitly ask the user to confirm the diagnosis before fixing the problem.", + }, ] as const // Export the default mode slug @@ -253,6 +263,46 @@ export async function getAllModesWithPrompts(context: vscode.ExtensionContext): })) } +// Helper function to get complete mode details with all overrides +export async function getFullModeDetails( + modeSlug: string, + customModes?: ModeConfig[], + customModePrompts?: CustomModePrompts, + options?: { + cwd?: string + globalCustomInstructions?: string + preferredLanguage?: string + }, +): Promise { + // First get the base mode config from custom modes or built-in modes + const baseMode = getModeBySlug(modeSlug, customModes) || modes.find((m) => m.slug === modeSlug) || modes[0] + + // Check for any prompt component overrides + const promptComponent = customModePrompts?.[modeSlug] + + // Get the base custom instructions + const baseCustomInstructions = promptComponent?.customInstructions || baseMode.customInstructions || "" + + // If we have cwd, load and combine all custom instructions + let fullCustomInstructions = baseCustomInstructions + if (options?.cwd) { + fullCustomInstructions = await addCustomInstructions( + baseCustomInstructions, + options.globalCustomInstructions || "", + options.cwd, + modeSlug, + { preferredLanguage: options.preferredLanguage }, + ) + } + + // Return mode with any overrides applied + return { + ...baseMode, + roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition, + customInstructions: fullCustomInstructions, + } +} + // Helper function to safely get role definition export function getRoleDefinition(modeSlug: string, customModes?: ModeConfig[]): string { const mode = getModeBySlug(modeSlug, customModes) diff --git a/src/shared/tool-groups.ts b/src/shared/tool-groups.ts index 2728d42319..50c7b80ca9 100644 --- a/src/shared/tool-groups.ts +++ b/src/shared/tool-groups.ts @@ -28,7 +28,7 @@ export const TOOL_GROUPS: Record = { tools: ["read_file", "search_files", "list_files", "list_code_definition_names"], }, edit: { - tools: ["write_to_file", "apply_diff", "insert_content", "search_and_replace"], + tools: ["apply_diff", "write_to_file", "insert_content", "search_and_replace"], }, browser: { tools: ["browser_action"], diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index ffb8de7473..cc487b0bf7 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -13,23 +13,23 @@ declare global { } export async function run(): Promise { - // Create the mocha test const mocha = new Mocha({ ui: "tdd", - timeout: 600000, // 10 minutes to compensate for time communicating with LLM while running in GHA + timeout: 600000, // 10 minutes to compensate for time communicating with LLM while running in GHA. }) const testsRoot = path.resolve(__dirname, "..") try { - // Find all test files + // Find all test files. const files = await glob("**/**.test.js", { cwd: testsRoot }) - // Add files to the test suite + // Add files to the test suite. files.forEach((f: string) => mocha.addFile(path.resolve(testsRoot, f))) - //Set up global extension, api, provider, and panel + // Set up global extension, api, provider, and panel. globalThis.extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline") + if (!globalThis.extension) { throw new Error("Extension not found") } @@ -37,9 +37,12 @@ export async function run(): Promise { globalThis.api = globalThis.extension.isActive ? globalThis.extension.exports : await globalThis.extension.activate() + globalThis.provider = globalThis.api.sidebarProvider + await globalThis.provider.updateGlobalState("apiProvider", "openrouter") await globalThis.provider.updateGlobalState("openRouterModelId", "anthropic/claude-3.5-sonnet") + await globalThis.provider.storeSecret( "openRouterApiKey", process.env.OPENROUTER_API_KEY || "sk-or-v1-fake-api-key", @@ -71,7 +74,7 @@ export async function run(): Promise { await new Promise((resolve) => setTimeout(resolve, interval)) } - // Run the mocha test + // Run the mocha test. return new Promise((resolve, reject) => { try { mocha.run((failures: number) => { diff --git a/src/test/suite/modes.test.ts b/src/test/suite/modes.test.ts index 2fe0eaa597..b94e71d110 100644 --- a/src/test/suite/modes.test.ts +++ b/src/test/suite/modes.test.ts @@ -1,101 +1,105 @@ import * as assert from "assert" -import * as vscode from "vscode" suite("Roo Code Modes", () => { test("Should handle switching modes correctly", async function () { const timeout = 30000 const interval = 1000 + const testPrompt = "For each mode (Code, Architect, Ask) respond with the mode name and what it specializes in after switching to that mode, do not start with the current mode, be sure to say 'I AM DONE' after the task is complete" + if (!globalThis.extension) { assert.fail("Extension not found") } - try { - let startTime = Date.now() + let startTime = Date.now() - // Ensure the webview is launched. - while (Date.now() - startTime < timeout) { - if (globalThis.provider.viewLaunched) { - break - } - - await new Promise((resolve) => setTimeout(resolve, interval)) + // Ensure the webview is launched. + while (Date.now() - startTime < timeout) { + if (globalThis.provider.viewLaunched) { + break } - await globalThis.provider.updateGlobalState("mode", "Ask") - await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true) - await globalThis.provider.updateGlobalState("autoApprovalEnabled", true) - - // Start a new task. - await globalThis.api.startNewTask(testPrompt) - - // Wait for task to appear in history with tokens. - startTime = Date.now() - - while (Date.now() - startTime < timeout) { - const messages = globalThis.provider.messages - - if ( - messages.some( - ({ type, text }) => - type === "say" && text?.includes("I AM DONE") && !text?.includes("be sure to say"), - ) - ) { - break - } - - await new Promise((resolve) => setTimeout(resolve, interval)) - } - if (globalThis.provider.messages.length === 0) { - assert.fail("No messages received") - } - - //Log the messages to the console - globalThis.provider.messages.forEach(({ type, text }) => { - if (type === "say") { - console.log(text) - } - }) - - //Start Grading Portion of test to grade the response from 1 to 10 - await globalThis.provider.updateGlobalState("mode", "Ask") - let output = globalThis.provider.messages.map(({ type, text }) => (type === "say" ? text : "")).join("\n") - await globalThis.api.startNewTask( - `Given this prompt: ${testPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ${output} \n Be sure to say 'I AM DONE GRADING' after the task is complete`, - ) - - startTime = Date.now() - - while (Date.now() - startTime < timeout) { - const messages = globalThis.provider.messages - - if ( - messages.some( - ({ type, text }) => - type === "say" && text?.includes("I AM DONE GRADING") && !text?.includes("be sure to say"), - ) - ) { - break - } - - await new Promise((resolve) => setTimeout(resolve, interval)) - } - if (globalThis.provider.messages.length === 0) { - assert.fail("No messages received") - } - globalThis.provider.messages.forEach(({ type, text }) => { - if (type === "say" && text?.includes("Grade:")) { - console.log(text) - } - }) - const gradeMessage = globalThis.provider.messages.find( - ({ type, text }) => type === "say" && !text?.includes("Grade: (1-10)") && text?.includes("Grade:"), - )?.text - const gradeMatch = gradeMessage?.match(/Grade: (\d+)/) - const gradeNum = gradeMatch ? parseInt(gradeMatch[1]) : undefined - assert.ok(gradeNum !== undefined && gradeNum >= 7 && gradeNum <= 10, "Grade must be between 7 and 10") - } finally { + await new Promise((resolve) => setTimeout(resolve, interval)) } + + await globalThis.provider.updateGlobalState("mode", "Ask") + await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true) + await globalThis.provider.updateGlobalState("autoApprovalEnabled", true) + + // Start a new task. + await globalThis.api.startNewTask(testPrompt) + + // Wait for task to appear in history with tokens. + startTime = Date.now() + + while (Date.now() - startTime < timeout) { + const messages = globalThis.provider.messages + + if ( + messages.some( + ({ type, text }) => + type === "say" && text?.includes("I AM DONE") && !text?.includes("be sure to say"), + ) + ) { + break + } + + await new Promise((resolve) => setTimeout(resolve, interval)) + } + + if (globalThis.provider.messages.length === 0) { + assert.fail("No messages received") + } + + // Log the messages to the console. + globalThis.provider.messages.forEach(({ type, text }) => { + if (type === "say") { + console.log(text) + } + }) + + // Start Grading Portion of test to grade the response from 1 to 10. + await globalThis.provider.updateGlobalState("mode", "Ask") + let output = globalThis.provider.messages.map(({ type, text }) => (type === "say" ? text : "")).join("\n") + + await globalThis.api.startNewTask( + `Given this prompt: ${testPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ${output} \n Be sure to say 'I AM DONE GRADING' after the task is complete`, + ) + + startTime = Date.now() + + while (Date.now() - startTime < timeout) { + const messages = globalThis.provider.messages + + if ( + messages.some( + ({ type, text }) => + type === "say" && text?.includes("I AM DONE GRADING") && !text?.includes("be sure to say"), + ) + ) { + break + } + + await new Promise((resolve) => setTimeout(resolve, interval)) + } + + if (globalThis.provider.messages.length === 0) { + assert.fail("No messages received") + } + + globalThis.provider.messages.forEach(({ type, text }) => { + if (type === "say" && text?.includes("Grade:")) { + console.log(text) + } + }) + + const gradeMessage = globalThis.provider.messages.find( + ({ type, text }) => type === "say" && !text?.includes("Grade: (1-10)") && text?.includes("Grade:"), + )?.text + + const gradeMatch = gradeMessage?.match(/Grade: (\d+)/) + const gradeNum = gradeMatch ? parseInt(gradeMatch[1]) : undefined + assert.ok(gradeNum !== undefined && gradeNum >= 7 && gradeNum <= 10, "Grade must be between 7 and 10") }) }) diff --git a/src/test/suite/task.test.ts b/src/test/suite/task.test.ts index 2d34bc78ff..6bdedcde00 100644 --- a/src/test/suite/task.test.ts +++ b/src/test/suite/task.test.ts @@ -1,5 +1,4 @@ import * as assert from "assert" -import * as vscode from "vscode" suite("Roo Code Task", () => { test("Should handle prompt and response correctly", async function () { @@ -10,48 +9,43 @@ suite("Roo Code Task", () => { assert.fail("Extension not found") } - try { - // Ensure the webview is launched. - let startTime = Date.now() + // Ensure the webview is launched. + let startTime = Date.now() - while (Date.now() - startTime < timeout) { - if (globalThis.provider.viewLaunched) { - break - } - - await new Promise((resolve) => setTimeout(resolve, interval)) + while (Date.now() - startTime < timeout) { + if (globalThis.provider.viewLaunched) { + break } - await globalThis.provider.updateGlobalState("mode", "Code") - await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true) - await globalThis.provider.updateGlobalState("autoApprovalEnabled", true) - - await globalThis.api.startNewTask("Hello world, what is your name? Respond with 'My name is ...'") - - // Wait for task to appear in history with tokens. - startTime = Date.now() - - while (Date.now() - startTime < timeout) { - const messages = globalThis.provider.messages - - if (messages.some(({ type, text }) => type === "say" && text?.includes("My name is Roo"))) { - break - } - - await new Promise((resolve) => setTimeout(resolve, interval)) - } - - if (globalThis.provider.messages.length === 0) { - assert.fail("No messages received") - } - - assert.ok( - globalThis.provider.messages.some( - ({ type, text }) => type === "say" && text?.includes("My name is Roo"), - ), - "Did not receive expected response containing 'My name is Roo'", - ) - } finally { + await new Promise((resolve) => setTimeout(resolve, interval)) } + + await globalThis.provider.updateGlobalState("mode", "Code") + await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true) + await globalThis.provider.updateGlobalState("autoApprovalEnabled", true) + + await globalThis.api.startNewTask("Hello world, what is your name? Respond with 'My name is ...'") + + // Wait for task to appear in history with tokens. + startTime = Date.now() + + while (Date.now() - startTime < timeout) { + const messages = globalThis.provider.messages + + if (messages.some(({ type, text }) => type === "say" && text?.includes("My name is Roo"))) { + break + } + + await new Promise((resolve) => setTimeout(resolve, interval)) + } + + if (globalThis.provider.messages.length === 0) { + assert.fail("No messages received") + } + + assert.ok( + globalThis.provider.messages.some(({ type, text }) => type === "say" && text?.includes("My name is Roo")), + "Did not receive expected response containing 'My name is Roo'", + ) }) }) diff --git a/src/utils/__tests__/path.test.ts b/src/utils/__tests__/path.test.ts index 1d20e86c69..8c8a8cc672 100644 --- a/src/utils/__tests__/path.test.ts +++ b/src/utils/__tests__/path.test.ts @@ -1,6 +1,9 @@ -import { arePathsEqual, getReadablePath } from "../path" -import * as path from "path" +// npx jest src/utils/__tests__/path.test.ts + import os from "os" +import * as path from "path" + +import { arePathsEqual, getReadablePath } from "../path" describe("Path Utilities", () => { const originalPlatform = process.platform @@ -92,22 +95,24 @@ describe("Path Utilities", () => { describe("getReadablePath", () => { const homeDir = os.homedir() const desktop = path.join(homeDir, "Desktop") + const cwd = process.platform === "win32" ? "C:\\Users\\test\\project" : "/Users/test/project" it("should return basename when path equals cwd", () => { - const cwd = "/Users/test/project" expect(getReadablePath(cwd, cwd)).toBe("project") }) it("should return relative path when inside cwd", () => { - const cwd = "/Users/test/project" - const filePath = "/Users/test/project/src/file.txt" + const filePath = + process.platform === "win32" + ? "C:\\Users\\test\\project\\src\\file.txt" + : "/Users/test/project/src/file.txt" expect(getReadablePath(cwd, filePath)).toBe("src/file.txt") }) it("should return absolute path when outside cwd", () => { - const cwd = "/Users/test/project" - const filePath = "/Users/test/other/file.txt" - expect(getReadablePath(cwd, filePath)).toBe("/Users/test/other/file.txt") + const filePath = + process.platform === "win32" ? "C:\\Users\\test\\other\\file.txt" : "/Users/test/other/file.txt" + expect(getReadablePath(cwd, filePath)).toBe(filePath.toPosix()) }) it("should handle Desktop as cwd", () => { @@ -116,19 +121,20 @@ describe("Path Utilities", () => { }) it("should handle undefined relative path", () => { - const cwd = "/Users/test/project" expect(getReadablePath(cwd)).toBe("project") }) it("should handle parent directory traversal", () => { - const cwd = "/Users/test/project" - const filePath = "../../other/file.txt" - expect(getReadablePath(cwd, filePath)).toBe("/Users/other/file.txt") + const filePath = + process.platform === "win32" ? "C:\\Users\\test\\other\\file.txt" : "/Users/test/other/file.txt" + expect(getReadablePath(cwd, filePath)).toBe(filePath.toPosix()) }) it("should normalize paths with redundant segments", () => { - const cwd = "/Users/test/project" - const filePath = "/Users/test/project/./src/../src/file.txt" + const filePath = + process.platform === "win32" + ? "C:\\Users\\test\\project\\src\\file.txt" + : "/Users/test/project/./src/../src/file.txt" expect(getReadablePath(cwd, filePath)).toBe("src/file.txt") }) }) diff --git a/src/utils/cost.ts b/src/utils/cost.ts index f8f5f2b125..adc2ded0a8 100644 --- a/src/utils/cost.ts +++ b/src/utils/cost.ts @@ -22,3 +22,5 @@ export function calculateApiCost( const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost return totalCost } + +export const parseApiPrice = (price: any) => (price ? parseFloat(price) * 1_000_000 : undefined) diff --git a/webview-ui/.eslintrc.json b/webview-ui/.eslintrc.json index e6aa150f11..4309a7c3ed 100644 --- a/webview-ui/.eslintrc.json +++ b/webview-ui/.eslintrc.json @@ -1,3 +1,4 @@ { - "extends": "react-app" + "extends": "react-app", + "ignorePatterns": ["!.storybook"] } diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 325b8100ee..22564d01a6 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -68,13 +68,13 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-storybook": "^0.11.2", "identity-obj-proxy": "^3.0.0", - "jest": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "jest-simple-dot-reporter": "^1.0.5", "shiki": "^2.3.2", "storybook": "^8.5.6", "storybook-dark-mode": "^4.0.2", - "ts-jest": "^27.1.5", + "ts-jest": "^29.2.5", "typescript": "^4.9.5", "vite": "6.0.11" } @@ -3006,61 +3006,61 @@ } }, "node_modules/@jest/console": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", - "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^27.5.1", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/core": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", - "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "^27.5.1", - "@jest/reporters": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "emittery": "^0.8.1", + "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^27.5.1", - "jest-config": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-resolve-dependencies": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "jest-watcher": "^27.5.1", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", - "rimraf": "^3.0.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -3071,90 +3071,162 @@ } } }, - "node_modules/@jest/environment": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", - "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", - "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", - "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/types": "^27.5.1", - "expect": "^27.5.1" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", - "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", - "glob": "^7.1.2", + "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-haste-map": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "slash": "^3.0.0", - "source-map": "^0.6.0", "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^8.1.0" + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -3165,102 +3237,109 @@ } } }, - "node_modules/@jest/source-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", - "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "license": "MIT", "dependencies": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9", - "source-map": "^0.6.0" + "@sinclair/typebox": "^0.27.8" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/test-result": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", - "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/test-sequencer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", - "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "^27.5.1", + "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-runtime": "^27.5.1" + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/transform": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", - "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.5.1", + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-util": "^27.5.1", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" + "write-file-atomic": "^4.0.2" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/transform/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true, - "license": "MIT" - }, "node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "license": "MIT", "dependencies": { + "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", - "@types/yargs": "^16.0.0", + "@types/yargs": "^17.0.8", "chalk": "^4.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { @@ -3595,6 +3674,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz", "integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", @@ -4640,6 +4720,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, @@ -5358,10 +5439,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5369,13 +5457,13 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^1.7.0" + "@sinonjs/commons": "^3.0.0" } }, "node_modules/@storybook/addon-actions": { @@ -5692,28 +5780,6 @@ "node": ">=10" } }, - "node_modules/@storybook/core/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@storybook/csf": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.12.tgz", @@ -6356,13 +6422,13 @@ } }, "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">= 10" } }, "node_modules/@types/aria-query": { @@ -6512,6 +6578,18 @@ "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==", "license": "MIT" }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -6571,13 +6649,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/prettier": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -6648,6 +6719,13 @@ "@types/jest": "*" } }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -6669,9 +6747,9 @@ "license": "MIT" }, "node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "license": "MIT", "dependencies": { @@ -7290,27 +7368,14 @@ } }, "node_modules/acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - } - }, - "node_modules/acorn-globals/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" } }, "node_modules/acorn-jsx": { @@ -7324,11 +7389,14 @@ } }, "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, "engines": { "node": ">=0.4.0" } @@ -7647,6 +7715,13 @@ "dev": true, "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -7701,23 +7776,22 @@ } }, "node_modules/babel-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", - "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^27.5.1", + "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" @@ -7740,20 +7814,37 @@ "node": ">=8" } }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/babel-plugin-jest-hoist": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", - "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", + "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/babel-plugin-macros": { @@ -7849,17 +7940,17 @@ } }, "node_modules/babel-preset-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", - "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "^27.5.1", + "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" @@ -7950,13 +8041,6 @@ "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", "dev": true }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -8245,9 +8329,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", - "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "dev": true, "license": "MIT" }, @@ -8264,15 +8348,18 @@ } }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/clsx": { @@ -8848,6 +8935,28 @@ "node": ">= 6" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8913,9 +9022,9 @@ "license": "MIT" }, "node_modules/cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", "dev": true, "license": "MIT" }, @@ -8953,18 +9062,18 @@ "license": "BSD-2-Clause" }, "node_modules/data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", "dev": true, "license": "MIT", "dependencies": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/data-view-buffer": { @@ -9081,11 +9190,19 @@ } }, "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } }, "node_modules/deep-eql": { "version": "5.0.2", @@ -9267,27 +9384,17 @@ "peer": true }, "node_modules/domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", "deprecated": "Use your platform's native DOMException instead", "dev": true, "license": "MIT", "dependencies": { - "webidl-conversions": "^5.0.0" + "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/domexception/node_modules/webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=8" + "node": ">=12" } }, "node_modules/dunder-proto": { @@ -9312,6 +9419,22 @@ "dev": true, "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.88", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.88.tgz", @@ -9320,13 +9443,13 @@ "license": "ISC" }, "node_modules/emittery": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", - "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sindresorhus/emittery?sponsor=1" @@ -9359,6 +9482,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -10814,21 +10950,109 @@ } }, "node_modules/expect": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", - "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1" + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expect/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/expect/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -10929,6 +11153,39 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -11028,14 +11285,15 @@ } }, "node_modules/form-data": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.2.tgz", - "integrity": "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" }, "engines": { @@ -11694,16 +11952,16 @@ } }, "node_modules/html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", "dev": true, "license": "MIT", "dependencies": { - "whatwg-encoding": "^1.0.5" + "whatwg-encoding": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/html-escaper": { @@ -11735,13 +11993,13 @@ } }, "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "dev": true, "license": "MIT", "dependencies": { - "@tootallnate/once": "1", + "@tootallnate/once": "2", "agent-base": "6", "debug": "4" }, @@ -11780,13 +12038,13 @@ "license": "BSD-3-Clause" }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -12419,13 +12677,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true, - "license": "MIT" - }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -12510,20 +12761,33 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" + "semver": "^7.5.4" }, "engines": { - "node": ">=8" + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/istanbul-lib-report": { @@ -12604,22 +12868,42 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", - "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "^27.5.1", + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", "import-local": "^3.0.2", - "jest-cli": "^27.5.1" + "jest-cli": "^29.7.0" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -12631,76 +12915,163 @@ } }, "node_modules/jest-changed-files": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", - "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^27.5.1", "execa": "^5.0.0", - "throat": "^6.0.1" + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-circus": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", - "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^27.5.1", + "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" + "stack-utils": "^2.0.3" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-cli": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", - "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "prompts": "^2.0.1", - "yargs": "^16.2.0" + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -12712,49 +13083,96 @@ } }, "node_modules/jest-config": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", - "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.8.0", - "@jest/test-sequencer": "^27.5.1", - "@jest/types": "^27.5.1", - "babel-jest": "^27.5.1", + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", - "glob": "^7.1.1", + "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-jasmine2": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^27.5.1", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { + "@types/node": "*", "ts-node": ">=9.0.0" }, "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, "ts-node": { "optional": true } } }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-diff": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", @@ -12772,70 +13190,124 @@ } }, "node_modules/jest-docblock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", - "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-each": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", - "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^27.5.1", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1" + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-environment-jsdom": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", - "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1", - "jsdom": "^16.6.0" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, "node_modules/jest-environment-node": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", - "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-get-type": { @@ -12849,75 +13321,90 @@ } }, "node_modules/jest-haste-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", - "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^27.5.1", - "@types/graceful-fs": "^4.1.2", + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^27.5.1", - "jest-serializer": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "micromatch": "^4.0.4", - "walker": "^1.0.7" + "walker": "^1.0.8" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, - "node_modules/jest-jasmine2": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", - "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "throat": "^6.0.1" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-leak-detector": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", - "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", "dependencies": { - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-matcher-utils": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", @@ -12935,38 +13422,74 @@ } }, "node_modules/jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", + "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-pnp-resolver": { @@ -12988,131 +13511,115 @@ } }, "node_modules/jest-regex-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", - "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, "license": "MIT", "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", - "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^27.5.1", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", + "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", + "resolve.exports": "^2.0.0", "slash": "^3.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve-dependencies": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", - "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-snapshot": "^27.5.1" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", - "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "^27.5.1", - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "emittery": "^0.8.1", + "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-docblock": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-leak-detector": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "source-map-support": "^0.5.6", - "throat": "^6.0.1" + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runtime": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", - "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/globals": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-serializer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", - "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-simple-dot-reporter": { @@ -13123,43 +13630,128 @@ "license": "MIT" }, "node_modules/jest-snapshot": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", - "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.7.2", + "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^27.5.1", + "expect": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "natural-compare": "^1.4.0", - "pretty-format": "^27.5.1", - "semver": "^7.3.2" + "pretty-format": "^29.7.0", + "semver": "^7.5.3" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -13170,13 +13762,13 @@ } }, "node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^27.5.1", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -13184,25 +13776,38 @@ "picomatch": "^2.2.3" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-validate": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", - "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^27.5.1", + "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", + "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^27.5.1" + "pretty-format": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-validate/node_modules/camelcase": { @@ -13218,38 +13823,72 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-watcher": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", - "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", + "node_modules/jest-validate/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "jest-util": "^27.5.1", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", "string-length": "^4.0.1" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker/node_modules/supports-color": { @@ -13304,42 +13943,41 @@ } }, "node_modules/jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", "dev": true, "license": "MIT", "dependencies": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=14" }, "peerDependencies": { "canvas": "^2.5.0" @@ -13866,9 +14504,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -16285,11 +16923,17 @@ } }, "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } }, "node_modules/path-exists": { "version": "4.0.0", @@ -16646,6 +17290,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -17736,9 +18397,9 @@ } }, "node_modules/resolve.exports": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", - "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "dev": true, "license": "MIT", "engines": { @@ -17907,16 +18568,16 @@ "license": "MIT" }, "node_modules/saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" }, "engines": { - "node": ">=10" + "node": ">=v12.22.7" } }, "node_modules/scheduler": { @@ -18185,9 +18846,9 @@ } }, "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, "license": "MIT", "dependencies": { @@ -18682,20 +19343,6 @@ "node": ">=8" } }, - "node_modules/supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -18756,23 +19403,6 @@ "node": ">=6" } }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -18795,13 +19425,6 @@ "dev": true, "license": "MIT" }, - "node_modules/throat": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", - "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==", - "dev": true, - "license": "MIT" - }, "node_modules/throttle-debounce": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", @@ -18885,16 +19508,16 @@ } }, "node_modules/tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.1.1" }, "engines": { - "node": ">=8" + "node": ">=12" } }, "node_modules/trim-lines": { @@ -18947,39 +19570,44 @@ "license": "Unlicense" }, "node_modules/ts-jest": { - "version": "27.1.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz", - "integrity": "sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==", + "version": "29.2.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", + "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", "dev": true, "license": "MIT", "dependencies": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", - "jest-util": "^27.0.0", - "json5": "2.x", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "7.x", - "yargs-parser": "20.x" + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.1", + "yargs-parser": "^21.1.1" }, "bin": { "ts-jest": "cli.js" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", - "@types/jest": "^27.0.0", - "babel-jest": ">=27.0.0 <28", - "jest": "^27.0.0", - "typescript": ">=3.8 <5.0" + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" }, "peerDependenciesMeta": { "@babel/core": { "optional": true }, - "@types/jest": { + "@jest/transform": { + "optional": true + }, + "@jest/types": { "optional": true }, "babel-jest": { @@ -18991,9 +19619,9 @@ } }, "node_modules/ts-jest/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -19171,16 +19799,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -19653,37 +20271,20 @@ } }, "node_modules/v8-to-istanbul": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", - "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, "license": "ISC", "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" + "convert-source-map": "^2.0.0" }, "engines": { "node": ">=10.12.0" } }, - "node_modules/v8-to-istanbul/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" - } - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -19810,28 +20411,17 @@ "react": "^17 || ^18 || ^19" } }, - "node_modules/w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", - "dev": true, - "license": "MIT", - "dependencies": { - "browser-process-hrtime": "^1.0.0" - } - }, "node_modules/w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", "dev": true, "license": "MIT", "dependencies": { - "xml-name-validator": "^3.0.0" + "xml-name-validator": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=14" } }, "node_modules/walker": { @@ -19855,13 +20445,13 @@ } }, "node_modules/webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=10.4" + "node": ">=12" } }, "node_modules/webpack-virtual-modules": { @@ -19872,35 +20462,40 @@ "license": "MIT" }, "node_modules/whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", "dev": true, "license": "MIT", "dependencies": { - "iconv-lite": "0.4.24" + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" } }, "node_modules/whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12" + } }, "node_modules/whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", "dev": true, "license": "MIT", "dependencies": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/which": { @@ -20062,30 +20657,31 @@ "license": "ISC" }, "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -20097,11 +20693,14 @@ } }, "node_modules/xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } }, "node_modules/xmlchars": { "version": "2.2.0", @@ -20137,32 +20736,32 @@ "license": "ISC" }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yocto-queue": { diff --git a/webview-ui/package.json b/webview-ui/package.json index 49bd0decbc..2206fb35c9 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -75,13 +75,13 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-storybook": "^0.11.2", "identity-obj-proxy": "^3.0.0", - "jest": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "jest-simple-dot-reporter": "^1.0.5", "shiki": "^2.3.2", "storybook": "^8.5.6", "storybook-dark-mode": "^4.0.2", - "ts-jest": "^27.1.5", + "ts-jest": "^29.2.5", "typescript": "^4.9.5", "vite": "6.0.11" } diff --git a/webview-ui/src/__mocks__/lucide-react.ts b/webview-ui/src/__mocks__/lucide-react.ts new file mode 100644 index 0000000000..d85cd25d6a --- /dev/null +++ b/webview-ui/src/__mocks__/lucide-react.ts @@ -0,0 +1,6 @@ +import React from "react" + +export const Check = () => React.createElement("div") +export const ChevronsUpDown = () => React.createElement("div") +export const Loader = () => React.createElement("div") +export const X = () => React.createElement("div") diff --git a/webview-ui/src/__mocks__/vscrui.ts b/webview-ui/src/__mocks__/vscrui.ts index 76760ba5cc..9b4a20f4d6 100644 --- a/webview-ui/src/__mocks__/vscrui.ts +++ b/webview-ui/src/__mocks__/vscrui.ts @@ -8,6 +8,9 @@ export const Dropdown = ({ children, value, onChange }: any) => export const Pane = ({ children }: any) => React.createElement("div", { "data-testid": "mock-pane" }, children) +export const Button = ({ children, ...props }: any) => + React.createElement("div", { "data-testid": "mock-button", ...props }, children) + export type DropdownOption = { label: string value: string diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index d317efb96e..dc78a3fdb3 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -16,6 +16,7 @@ import { vscode } from "../../utils/vscode" import { WebviewMessage } from "../../../../src/shared/WebviewMessage" import { Mode, getAllModes } from "../../../../src/shared/modes" import { CaretIcon } from "../common/CaretIcon" +import { convertToMentionPath } from "../../utils/path-mentions" interface ChatTextAreaProps { inputValue: string @@ -50,7 +51,7 @@ const ChatTextArea = forwardRef( }, ref, ) => { - const { filePaths, openedTabs, currentApiConfigName, listApiConfigMeta, customModes } = useExtensionState() + const { filePaths, openedTabs, currentApiConfigName, listApiConfigMeta, customModes, cwd } = useExtensionState() const [gitCommits, setGitCommits] = useState([]) const [showDropdown, setShowDropdown] = useState(false) @@ -589,18 +590,24 @@ const ChatTextArea = forwardRef( const files = Array.from(e.dataTransfer.files) const text = e.dataTransfer.getData("text") if (text) { - const newValue = inputValue.slice(0, cursorPosition) + text + inputValue.slice(cursorPosition) + // Convert the path to a mention-friendly format + const mentionText = convertToMentionPath(text, cwd) + + const newValue = + inputValue.slice(0, cursorPosition) + mentionText + " " + inputValue.slice(cursorPosition) setInputValue(newValue) - const newCursorPosition = cursorPosition + text.length + const newCursorPosition = cursorPosition + mentionText.length + 1 setCursorPosition(newCursorPosition) setIntendedCursorPosition(newCursorPosition) return } + const acceptedTypes = ["png", "jpeg", "webp"] const imageFiles = files.filter((file) => { const [type, subtype] = file.type.split("/") return type === "image" && acceptedTypes.includes(subtype) }) + if (!shouldDisableImages && imageFiles.length > 0) { const imagePromises = imageFiles.map((file) => { return new Promise((resolve) => { diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index b9fc215a1c..98369cf095 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -880,9 +880,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie const placeholderText = useMemo(() => { const baseText = task ? "Type a message..." : "Type your task here..." const contextText = "(@ to add context, / to switch modes" - const imageText = shouldDisableImages ? "" : ", hold shift to drag in images" - const helpText = imageText ? `\n${contextText}${imageText})` : `\n${contextText})` - return baseText + helpText + const imageText = shouldDisableImages ? "hold shift to drag in files" : ", hold shift to drag in files/images" + return baseText + `\n${contextText}${imageText})` }, [task, shouldDisableImages]) const itemContent = useCallback( diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index b35be0cd2a..341855f796 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -351,7 +351,7 @@ const TaskActions = ({ item }: { item: HistoryItem | undefined }) => ( - {item?.size && ( + {!!item?.size && item.size > 0 && ( + + + + + + + + ) +} diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx index 38ca14df46..ca60e1fcb8 100644 --- a/webview-ui/src/components/history/HistoryView.tsx +++ b/webview-ui/src/components/history/HistoryView.tsx @@ -1,4 +1,5 @@ import React, { memo, useMemo, useState, useEffect } from "react" +import { DeleteTaskDialog } from "./DeleteTaskDialog" import { Fzf } from "fzf" import prettyBytes from "pretty-bytes" import { Virtuoso } from "react-virtuoso" @@ -37,8 +38,12 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { vscode.postMessage({ type: "showTaskWithId", text: id }) } + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [taskToDelete, setTaskToDelete] = useState(null) + const handleDeleteHistoryItem = (id: string) => { - vscode.postMessage({ type: "deleteTaskWithId", text: id }) + setTaskToDelete(id) + setDeleteDialogOpen(true) } const formatDate = (timestamp: number) => { @@ -398,6 +403,18 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { )} /> + {taskToDelete && ( + { + setDeleteDialogOpen(open) + if (!open) { + setTaskToDelete(null) + } + }} + /> + )} ) } diff --git a/webview-ui/src/components/history/__tests__/HistoryView.test.tsx b/webview-ui/src/components/history/__tests__/HistoryView.test.tsx index 7408b26878..12b0181af6 100644 --- a/webview-ui/src/components/history/__tests__/HistoryView.test.tsx +++ b/webview-ui/src/components/history/__tests__/HistoryView.test.tsx @@ -135,7 +135,7 @@ describe("HistoryView", () => { }) }) - it("handles task deletion", () => { + it("handles task deletion", async () => { const onDone = jest.fn() render() @@ -143,9 +143,14 @@ describe("HistoryView", () => { const taskContainer = screen.getByTestId("virtuoso-item-1") fireEvent.mouseEnter(taskContainer) + // Click delete button to open confirmation dialog const deleteButton = within(taskContainer).getByTitle("Delete Task") fireEvent.click(deleteButton) + // Find and click the confirm delete button in the dialog + const confirmDeleteButton = screen.getByRole("button", { name: /delete/i }) + fireEvent.click(confirmDeleteButton) + // Verify vscode message was sent expect(vscode.postMessage).toHaveBeenCalledWith({ type: "deleteTaskWithId", diff --git a/webview-ui/src/components/settings/ApiErrorMessage.tsx b/webview-ui/src/components/settings/ApiErrorMessage.tsx new file mode 100644 index 0000000000..06764a1bfa --- /dev/null +++ b/webview-ui/src/components/settings/ApiErrorMessage.tsx @@ -0,0 +1,16 @@ +import React from "react" + +interface ApiErrorMessageProps { + errorMessage: string | undefined + children?: React.ReactNode +} + +export const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => ( +
+
+
+
{errorMessage}
+
+ {children} +
+) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 19813a1f6d..c30035cef0 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -1,8 +1,7 @@ -import { memo, useCallback, useMemo, useState } from "react" +import React, { memo, useCallback, useEffect, useMemo, useState } from "react" import { useDebounce, useEvent } from "react-use" import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui" import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import { TemperatureControl } from "./TemperatureControl" import * as vscodemodels from "vscode" import { @@ -34,45 +33,76 @@ import { requestyDefaultModelInfo, } from "../../../../src/shared/api" import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage" + import { vscode } from "../../utils/vscode" import VSCodeButtonLink from "../common/VSCodeButtonLink" -import { OpenRouterModelPicker } from "./OpenRouterModelPicker" -import OpenAiModelPicker from "./OpenAiModelPicker" -import { GlamaModelPicker } from "./GlamaModelPicker" -import { UnboundModelPicker } from "./UnboundModelPicker" import { ModelInfoView } from "./ModelInfoView" import { DROPDOWN_Z_INDEX } from "./styles" -import { RequestyModelPicker } from "./RequestyModelPicker" +import { ModelPicker } from "./ModelPicker" +import { TemperatureControl } from "./TemperatureControl" +import { validateApiConfiguration, validateModelId } from "@/utils/validate" +import { ApiErrorMessage } from "./ApiErrorMessage" +import { ThinkingBudget } from "./ThinkingBudget" + +const modelsByProvider: Record> = { + anthropic: anthropicModels, + bedrock: bedrockModels, + vertex: vertexModels, + gemini: geminiModels, + "openai-native": openAiNativeModels, + deepseek: deepSeekModels, + mistral: mistralModels, +} interface ApiOptionsProps { uriScheme: string | undefined - apiConfiguration: ApiConfiguration | undefined + apiConfiguration: ApiConfiguration setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void - apiErrorMessage?: string - modelIdErrorMessage?: string fromWelcomeView?: boolean + errorMessage: string | undefined + setErrorMessage: React.Dispatch> } const ApiOptions = ({ uriScheme, apiConfiguration, setApiConfigurationField, - apiErrorMessage, - modelIdErrorMessage, fromWelcomeView, + errorMessage, + setErrorMessage, }: ApiOptionsProps) => { const [ollamaModels, setOllamaModels] = useState([]) const [lmStudioModels, setLmStudioModels] = useState([]) const [vsCodeLmModels, setVsCodeLmModels] = useState([]) + + const [openRouterModels, setOpenRouterModels] = useState>({ + [openRouterDefaultModelId]: openRouterDefaultModelInfo, + }) + + const [glamaModels, setGlamaModels] = useState>({ + [glamaDefaultModelId]: glamaDefaultModelInfo, + }) + + const [unboundModels, setUnboundModels] = useState>({ + [unboundDefaultModelId]: unboundDefaultModelInfo, + }) + + const [requestyModels, setRequestyModels] = useState>({ + [requestyDefaultModelId]: requestyDefaultModelInfo, + }) + + const [openAiModels, setOpenAiModels] = useState | null>(null) + const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion) const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl) const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) - const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any const noTransform = (value: T) => value + const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any const dropdownEventTransform = (event: DropdownOption | string | undefined) => (typeof event == "string" ? event : event?.value) as T + const handleInputChange = useCallback( ( field: K, @@ -84,15 +114,32 @@ const ApiOptions = ({ [setApiConfigurationField], ) - const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => { - return normalizeApiConfiguration(apiConfiguration) - }, [apiConfiguration]) + const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo( + () => normalizeApiConfiguration(apiConfiguration), + [apiConfiguration], + ) - // Pull ollama/lmstudio models - // Debounced model updates, only executed 250ms after the user stops typing + // Debounced refresh model updates, only executed 250ms after the user + // stops typing. useDebounce( () => { - if (selectedProvider === "ollama") { + if (selectedProvider === "openrouter") { + vscode.postMessage({ type: "refreshOpenRouterModels" }) + } else if (selectedProvider === "glama") { + vscode.postMessage({ type: "refreshGlamaModels" }) + } else if (selectedProvider === "unbound") { + vscode.postMessage({ type: "refreshUnboundModels" }) + } else if (selectedProvider === "requesty") { + vscode.postMessage({ + type: "refreshRequestyModels", + values: { apiKey: apiConfiguration?.requestyApiKey }, + }) + } else if (selectedProvider === "openai") { + vscode.postMessage({ + type: "refreshOpenAiModels", + values: { baseUrl: apiConfiguration?.openAiBaseUrl, apiKey: apiConfiguration?.openAiApiKey }, + }) + } else if (selectedProvider === "ollama") { vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl }) } else if (selectedProvider === "lmstudio") { vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl }) @@ -101,49 +148,95 @@ const ApiOptions = ({ } }, 250, - [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl], + [ + selectedProvider, + apiConfiguration?.requestyApiKey, + apiConfiguration?.openAiBaseUrl, + apiConfiguration?.openAiApiKey, + apiConfiguration?.ollamaBaseUrl, + apiConfiguration?.lmStudioBaseUrl, + ], ) - const handleMessage = useCallback((event: MessageEvent) => { + + useEffect(() => { + const apiValidationResult = + validateApiConfiguration(apiConfiguration) || + validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels, requestyModels) + + setErrorMessage(apiValidationResult) + }, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels, requestyModels]) + + const onMessage = useCallback((event: MessageEvent) => { const message: ExtensionMessage = event.data - if (message.type === "ollamaModels" && Array.isArray(message.ollamaModels)) { - const newModels = message.ollamaModels - setOllamaModels(newModels) - } else if (message.type === "lmStudioModels" && Array.isArray(message.lmStudioModels)) { - const newModels = message.lmStudioModels - setLmStudioModels(newModels) - } else if (message.type === "vsCodeLmModels" && Array.isArray(message.vsCodeLmModels)) { - const newModels = message.vsCodeLmModels - setVsCodeLmModels(newModels) + + switch (message.type) { + case "openRouterModels": { + const updatedModels = message.openRouterModels ?? {} + setOpenRouterModels({ [openRouterDefaultModelId]: openRouterDefaultModelInfo, ...updatedModels }) + break + } + case "glamaModels": { + const updatedModels = message.glamaModels ?? {} + setGlamaModels({ [glamaDefaultModelId]: glamaDefaultModelInfo, ...updatedModels }) + break + } + case "unboundModels": { + const updatedModels = message.unboundModels ?? {} + setUnboundModels({ [unboundDefaultModelId]: unboundDefaultModelInfo, ...updatedModels }) + break + } + case "requestyModels": { + const updatedModels = message.requestyModels ?? {} + setRequestyModels({ [requestyDefaultModelId]: requestyDefaultModelInfo, ...updatedModels }) + break + } + case "openAiModels": { + const updatedModels = message.openAiModels ?? [] + setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults]))) + break + } + case "ollamaModels": + { + const newModels = message.ollamaModels ?? [] + setOllamaModels(newModels) + } + break + case "lmStudioModels": + { + const newModels = message.lmStudioModels ?? [] + setLmStudioModels(newModels) + } + break + case "vsCodeLmModels": + { + const newModels = message.vsCodeLmModels ?? [] + setVsCodeLmModels(newModels) + } + break } }, []) - useEvent("message", handleMessage) - const createDropdown = (models: Record) => { - const options: DropdownOption[] = [ - { value: "", label: "Select a model..." }, - ...Object.keys(models).map((modelId) => ({ - value: modelId, - label: modelId, - })), - ] - return ( - { - setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value) - }} - style={{ width: "100%" }} - options={options} - /> - ) - } + useEvent("message", onMessage) + + const selectedProviderModelOptions: DropdownOption[] = useMemo( + () => + modelsByProvider[selectedProvider] + ? [ + { value: "", label: "Select a model..." }, + ...Object.keys(modelsByProvider[selectedProvider]).map((modelId) => ({ + value: modelId, + label: modelId, + })), + ] + : [], + [selectedProvider], + ) return (
-
+ {errorMessage && } + {selectedProvider === "anthropic" && (
- Anthropic API Key + Anthropic API Key { setAnthropicBaseUrlSelected(checked) + if (!checked) { setApiConfigurationField("anthropicBaseUrl", "") } @@ -228,7 +324,7 @@ const ApiOptions = ({ type="password" onInput={handleInputChange("glamaApiKey")} placeholder="Enter API Key..."> - Glama API Key + Glama API Key {!apiConfiguration?.glamaApiKey && ( - Requesty API Key + Requesty API Key

- OpenAI API Key + OpenAI API Key

- Mistral API Key + Mistral API Key

- Codestral Base URL (Optional) + Codestral Base URL (Optional)

- OpenRouter API Key + OpenRouter API Key {!apiConfiguration?.openRouterApiKey && (

@@ -384,6 +480,7 @@ const ApiOptions = ({ checked={openRouterBaseUrlSelected} onChange={(checked: boolean) => { setOpenRouterBaseUrlSelected(checked) + if (!checked) { setApiConfigurationField("openRouterBaseUrl", "") } @@ -429,7 +526,7 @@ const ApiOptions = ({ style={{ width: "100%" }} onInput={handleInputChange("awsProfile")} placeholder="Enter profile name"> - AWS Profile Name + AWS Profile Name ) : ( <> @@ -440,7 +537,7 @@ const ApiOptions = ({ type="password" onInput={handleInputChange("awsAccessKey")} placeholder="Enter Access Key..."> - AWS Access Key + AWS Access Key - AWS Secret Key + AWS Secret Key - AWS Session Token + AWS Session Token )}

{ - handleInputChange("awsRegion", dropdownEventTransform) - }} + onChange={handleInputChange("awsRegion", dropdownEventTransform)} options={[ { value: "", label: "Select a region..." }, { value: "us-east-1", label: "us-east-1" }, @@ -509,18 +604,18 @@ const ApiOptions = ({
)} - {apiConfiguration?.apiProvider === "vertex" && ( + {selectedProvider === "vertex" && (
- Google Cloud Project ID + Google Cloud Project ID
- Gemini API Key + Gemini API Key

- Base URL + Base URL - API Key + API Key - +

{ setAzureApiVersionSelected(checked) + if (!checked) { setApiConfigurationField("azureApiVersion", "") } @@ -637,12 +743,7 @@ const ApiOptions = ({ placeholder={`Default: ${azureOpenAiDefaultApiVersion}`} /> )} - -
+
{ + onInput={handleInputChange("openAiCustomModelInfo", (e) => { const value = parseInt((e.target as HTMLInputElement).value) return { ...(apiConfiguration?.openAiCustomModelInfo || @@ -702,7 +803,7 @@ const ApiOptions = ({ } })} placeholder="e.g. 4096"> - Max Output Tokens + Max Output Tokens
{ + onInput={handleInputChange("openAiCustomModelInfo", (e) => { const value = (e.target as HTMLInputElement).value const parsed = parseInt(value) return { @@ -752,7 +853,7 @@ const ApiOptions = ({ } })} placeholder="e.g. 128000"> - Context Window Size + Context Window Size
- Image Support + Image Support - Computer Use + Computer Use { + onInput={handleInputChange("openAiCustomModelInfo", (e) => { const value = (e.target as HTMLInputElement).value - const parsed = parseInt(value) + const parsed = parseFloat(value) return { ...(apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults), @@ -899,7 +1000,7 @@ const ApiOptions = ({ })} placeholder="e.g. 0.0001">
- Input Price + Input Price { + onInput={handleInputChange("openAiCustomModelInfo", (e) => { const value = (e.target as HTMLInputElement).value - const parsed = parseInt(value) + const parsed = parseFloat(value) return { ...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults), @@ -944,7 +1045,7 @@ const ApiOptions = ({ })} placeholder="e.g. 0.0002">
- Output Price + Output Price {/* end Model Info Configuration */} - -

- - (Note: Roo Code uses complex prompts and works best - with Claude models. Less capable models may not work as expected.) - -

)} @@ -991,14 +1080,14 @@ const ApiOptions = ({ type="url" onInput={handleInputChange("lmStudioBaseUrl")} placeholder={"Default: http://localhost:1234"}> - Base URL (optional) + Base URL (optional) - Model ID + Model ID {lmStudioModels.length > 0 && ( {" "} feature to use it with this extension.{" "} - (Note: Roo Code 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.)

@@ -1052,7 +1141,7 @@ const ApiOptions = ({ type="password" onInput={handleInputChange("deepSeekApiKey")} placeholder="Enter API Key..."> - DeepSeek API Key + DeepSeek API Key

{vsCodeLmModels.length > 0 ? ( - Base URL (optional) + Base URL (optional) - Model ID + Model ID + {errorMessage && ( +
+ + {errorMessage} +
+ )} {ollamaModels.length > 0 && ( - (Note: Roo Code 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.)

@@ -1191,7 +1286,7 @@ const ApiOptions = ({ type="password" onChange={handleInputChange("unboundApiKey")} placeholder="Enter API Key..."> - Unbound API Key + Unbound API Key {!apiConfiguration?.unboundApiKey && ( This key is stored locally and only used to make API requests from this extension.

-
)} - {apiErrorMessage && ( -

- - {apiErrorMessage} -

+ {selectedProvider === "openrouter" && ( + )} - {selectedProvider === "glama" && } + {selectedProvider === "glama" && ( + + )} - {selectedProvider === "openrouter" && } - {selectedProvider === "requesty" && } + {selectedProvider === "unbound" && ( + + )} - {selectedProvider !== "glama" && - selectedProvider !== "openrouter" && - selectedProvider !== "requesty" && - selectedProvider !== "openai" && - selectedProvider !== "ollama" && - selectedProvider !== "lmstudio" && - selectedProvider !== "unbound" && ( - <> -
- - {selectedProvider === "anthropic" && createDropdown(anthropicModels)} - {selectedProvider === "bedrock" && createDropdown(bedrockModels)} - {selectedProvider === "vertex" && createDropdown(vertexModels)} - {selectedProvider === "gemini" && createDropdown(geminiModels)} - {selectedProvider === "openai-native" && createDropdown(openAiNativeModels)} - {selectedProvider === "deepseek" && createDropdown(deepSeekModels)} - {selectedProvider === "mistral" && createDropdown(mistralModels)} -
+ {selectedProvider === "requesty" && ( + + )} - 0 && ( + <> +
+ + { + setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value) + }} + options={selectedProviderModelOptions} + className="w-full" /> - - )} +
+ + + + )} {!fromWelcomeView && ( -
+
)} - - {modelIdErrorMessage && ( -

- - {modelIdErrorMessage} -

- )}
) } @@ -1302,6 +1423,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { const getProviderData = (models: Record, defaultId: string) => { let selectedModelId: string let selectedModelInfo: ModelInfo + if (modelId && modelId in models) { selectedModelId = modelId selectedModelInfo = models[modelId] @@ -1309,8 +1431,10 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { selectedModelId = defaultId selectedModelInfo = models[defaultId] } + return { selectedProvider: provider, selectedModelId, selectedModelInfo } } + switch (provider) { case "anthropic": return getProviderData(anthropicModels, anthropicDefaultModelId) @@ -1324,12 +1448,6 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { return getProviderData(deepSeekModels, deepSeekDefaultModelId) case "openai-native": return getProviderData(openAiNativeModels, openAiNativeDefaultModelId) - case "glama": - return { - selectedProvider: provider, - selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId, - selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo, - } case "mistral": return getProviderData(mistralModels, mistralDefaultModelId) case "openrouter": @@ -1338,6 +1456,24 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId, selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo, } + case "glama": + return { + selectedProvider: provider, + selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId, + selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo, + } + case "unbound": + return { + selectedProvider: provider, + selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId, + selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo, + } + case "requesty": + return { + selectedProvider: provider, + selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId, + selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo, + } case "openai": return { selectedProvider: provider, @@ -1364,21 +1500,9 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { : "", selectedModelInfo: { ...openAiModelInfoSaneDefaults, - supportsImages: false, // VSCode LM API currently doesn't support images + supportsImages: false, // VSCode LM API currently doesn't support images. }, } - case "unbound": - return { - selectedProvider: provider, - selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId, - selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo, - } - case "requesty": - return { - selectedProvider: provider, - selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId, - selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo, - } default: return getProviderData(anthropicModels, anthropicDefaultModelId) } diff --git a/webview-ui/src/components/settings/GlamaModelPicker.tsx b/webview-ui/src/components/settings/GlamaModelPicker.tsx deleted file mode 100644 index cb813a0d05..0000000000 --- a/webview-ui/src/components/settings/GlamaModelPicker.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ModelPicker } from "./ModelPicker" -import { glamaDefaultModelId } from "../../../../src/shared/api" - -export const GlamaModelPicker = () => ( - -) diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index b21b37ef0f..5a7737edd5 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -1,186 +1,95 @@ -import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import debounce from "debounce" import { useMemo, useState, useCallback, useEffect, useRef } from "react" -import { useMount } from "react-use" -import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons" +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import { cn } from "@/lib/utils" -import { - Button, - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui" +import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "@/components/ui/combobox" + +import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api" -import { useExtensionState } from "../../context/ExtensionStateContext" -import { vscode } from "../../utils/vscode" import { normalizeApiConfiguration } from "./ApiOptions" +import { ThinkingBudget } from "./ThinkingBudget" import { ModelInfoView } from "./ModelInfoView" -type ModelProvider = "glama" | "openRouter" | "unbound" | "requesty" | "openAi" +type ExtractType = NonNullable< + { [K in keyof ApiConfiguration]: Required[K] extends T ? K : never }[keyof ApiConfiguration] +> -type ModelKeys = `${T}Models` -type ConfigKeys = `${T}ModelId` -type InfoKeys = `${T}ModelInfo` -type RefreshMessageType = `refresh${Capitalize}Models` +type ModelIdKeys = NonNullable< + { [K in keyof ApiConfiguration]: K extends `${string}ModelId` ? K : never }[keyof ApiConfiguration] +> -interface ModelPickerProps { +interface ModelPickerProps { defaultModelId: string - modelsKey: ModelKeys - configKey: ConfigKeys - infoKey: InfoKeys - refreshMessageType: RefreshMessageType - refreshValues?: Record + defaultModelInfo?: ModelInfo + models: Record | null + modelIdKey: ModelIdKeys + modelInfoKey: ExtractType serviceName: string serviceUrl: string - recommendedModel: string - allowCustomModel?: boolean + apiConfiguration: ApiConfiguration + setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void } export const ModelPicker = ({ defaultModelId, - modelsKey, - configKey, - infoKey, - refreshMessageType, - refreshValues, + models, + modelIdKey, + modelInfoKey, serviceName, serviceUrl, - recommendedModel, - allowCustomModel = false, + apiConfiguration, + setApiConfigurationField, + defaultModelInfo, }: ModelPickerProps) => { - const [customModelId, setCustomModelId] = useState("") - const [isCustomModel, setIsCustomModel] = useState(false) - const [open, setOpen] = useState(false) - const [value, setValue] = useState(defaultModelId) const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) - const prevRefreshValuesRef = useRef | undefined>() + const isInitialized = useRef(false) - const { apiConfiguration, [modelsKey]: models, onUpdateApiConfig, setApiConfiguration } = useExtensionState() - - const modelIds = useMemo( - () => (Array.isArray(models) ? models : Object.keys(models)).sort((a, b) => a.localeCompare(b)), - [models], - ) + const modelIds = useMemo(() => Object.keys(models ?? {}).sort((a, b) => a.localeCompare(b)), [models]) const { selectedModelId, selectedModelInfo } = useMemo( () => normalizeApiConfiguration(apiConfiguration), [apiConfiguration], ) - const onSelectCustomModel = useCallback( - (modelId: string) => { - setCustomModelId(modelId) - const modelInfo = { id: modelId } - const apiConfig = { ...apiConfiguration, [configKey]: modelId, [infoKey]: modelInfo } - setApiConfiguration(apiConfig) - onUpdateApiConfig(apiConfig) - setValue(modelId) - setOpen(false) - setIsCustomModel(false) - }, - [apiConfiguration, configKey, infoKey, onUpdateApiConfig, setApiConfiguration], - ) - const onSelect = useCallback( (modelId: string) => { - const modelInfo = Array.isArray(models) - ? { id: modelId } // For OpenAI models which are just strings - : models[modelId] // For other models that have full info objects - const apiConfig = { ...apiConfiguration, [configKey]: modelId, [infoKey]: modelInfo } - setApiConfiguration(apiConfig) - onUpdateApiConfig(apiConfig) - setValue(modelId) - setOpen(false) + const modelInfo = models?.[modelId] + setApiConfigurationField(modelIdKey, modelId) + setApiConfigurationField(modelInfoKey, modelInfo ?? defaultModelInfo) }, - [apiConfiguration, configKey, infoKey, models, onUpdateApiConfig, setApiConfiguration], + [modelIdKey, modelInfoKey, models, setApiConfigurationField, defaultModelInfo], ) - const debouncedRefreshModels = useMemo(() => { - return debounce(() => { - const message = refreshValues - ? { type: refreshMessageType, values: refreshValues } - : { type: refreshMessageType } - vscode.postMessage(message) - }, 100) - }, [refreshMessageType, refreshValues]) - - useMount(() => { - debouncedRefreshModels() - return () => debouncedRefreshModels.clear() - }) + const inputValue = apiConfiguration[modelIdKey] useEffect(() => { - if (!refreshValues) { - prevRefreshValuesRef.current = undefined - return + if (!inputValue && !isInitialized.current) { + const initialValue = modelIds.includes(selectedModelId) ? selectedModelId : defaultModelId + setApiConfigurationField(modelIdKey, initialValue) } - // Check if all values in refreshValues are truthy - if (Object.values(refreshValues).some((value) => !value)) { - prevRefreshValuesRef.current = undefined - return - } - - // Compare with previous values - const prevValues = prevRefreshValuesRef.current - if (prevValues && JSON.stringify(prevValues) === JSON.stringify(refreshValues)) { - return - } - - prevRefreshValuesRef.current = refreshValues - debouncedRefreshModels() - }, [debouncedRefreshModels, refreshValues]) - - useEffect(() => setValue(selectedModelId), [selectedModelId]) + isInitialized.current = true + }, [inputValue, modelIds, setApiConfigurationField, modelIdKey, selectedModelId, defaultModelId]) return ( <>
Model
- - - - - - - - - No model found. - - {modelIds.map((model) => ( - - {model} - - - ))} - - {allowCustomModel && ( - - { - setIsCustomModel(true) - setOpen(false) - }}> - + Add custom model - - - )} - - - - - {selectedModelId && selectedModelInfo && ( + + + + No model found. + {modelIds.map((model) => ( + + {model} + + ))} + + + + {selectedModelId && selectedModelInfo && selectedModelId === inputValue && ( If you're unsure which model to choose, Roo Code works best with{" "} - onSelect(recommendedModel)}>{recommendedModel}. + onSelect(defaultModelId)}>{defaultModelId}. You can also try searching "free" for no-cost options currently available.

- {allowCustomModel && isCustomModel && ( -
-
-

Add Custom Model

- setCustomModelId(e.target.value)} - /> -
- - -
-
-
- )} ) } diff --git a/webview-ui/src/components/settings/OpenAiModelPicker.tsx b/webview-ui/src/components/settings/OpenAiModelPicker.tsx deleted file mode 100644 index 040da1d421..0000000000 --- a/webview-ui/src/components/settings/OpenAiModelPicker.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react" -import { useExtensionState } from "../../context/ExtensionStateContext" -import { ModelPicker } from "./ModelPicker" - -const OpenAiModelPicker: React.FC = () => { - const { apiConfiguration } = useExtensionState() - - return ( - - ) -} - -export default OpenAiModelPicker diff --git a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx deleted file mode 100644 index 9111407cd6..0000000000 --- a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ModelPicker } from "./ModelPicker" -import { openRouterDefaultModelId } from "../../../../src/shared/api" - -export const OpenRouterModelPicker = () => ( - -) diff --git a/webview-ui/src/components/settings/RequestyModelPicker.tsx b/webview-ui/src/components/settings/RequestyModelPicker.tsx deleted file mode 100644 index e0759a43ba..0000000000 --- a/webview-ui/src/components/settings/RequestyModelPicker.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ModelPicker } from "./ModelPicker" -import { requestyDefaultModelId } from "../../../../src/shared/api" -import { useExtensionState } from "@/context/ExtensionStateContext" - -export const RequestyModelPicker = () => { - const { apiConfiguration } = useExtensionState() - return ( - - ) -} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 495bf49bd7..d3e65a99ea 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -1,15 +1,7 @@ +import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react" import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react" -import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext" -import { validateApiConfiguration, validateModelId } from "../../utils/validate" -import { vscode } from "../../utils/vscode" -import ApiOptions from "./ApiOptions" -import ExperimentalFeature from "./ExperimentalFeature" -import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments" -import ApiConfigManager from "./ApiConfigManager" -import { Dropdown } from "vscrui" -import type { DropdownOption } from "vscrui" -import { ApiConfiguration } from "../../../../src/shared/api" +import { Button, Dropdown, type DropdownOption } from "vscrui" + import { AlertDialog, AlertDialogContent, @@ -19,7 +11,16 @@ import { AlertDialogAction, AlertDialogHeader, AlertDialogFooter, -} from "../ui/alert-dialog" +} from "@/components/ui" + +import { vscode } from "../../utils/vscode" +import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext" +import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments" +import { ApiConfiguration } from "../../../../src/shared/api" + +import ExperimentalFeature from "./ExperimentalFeature" +import ApiConfigManager from "./ApiConfigManager" +import ApiOptions from "./ApiOptions" type SettingsViewProps = { onDone: () => void @@ -31,19 +32,17 @@ export interface SettingsViewRef { const SettingsView = forwardRef(({ onDone }, ref) => { const extensionState = useExtensionState() - const [apiErrorMessage, setApiErrorMessage] = useState(undefined) - const [modelIdErrorMessage, setModelIdErrorMessage] = useState(undefined) const [commandInput, setCommandInput] = useState("") const [isDiscardDialogShow, setDiscardDialogShow] = useState(false) const [cachedState, setCachedState] = useState(extensionState) const [isChangeDetected, setChangeDetected] = useState(false) const prevApiConfigName = useRef(extensionState.currentApiConfigName) const confirmDialogHandler = useRef<() => void>() + const [errorMessage, setErrorMessage] = useState(undefined) // TODO: Reduce WebviewMessage/ExtensionState complexity const { currentApiConfigName } = extensionState const { - apiConfiguration, alwaysAllowReadOnly, allowedCommands, alwaysAllowBrowser, @@ -68,17 +67,19 @@ const SettingsView = forwardRef(({ onDone }, writeDelayMs, } = cachedState + //Make sure apiConfiguration is initialized and managed by SettingsView + const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration]) + useEffect(() => { - // Update only when currentApiConfigName is changed - // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration + // Update only when currentApiConfigName is changed. + // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration. if (prevApiConfigName.current === currentApiConfigName) { return } - setCachedState((prevCachedState) => ({ - ...prevCachedState, - ...extensionState, - })) + + setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState })) prevApiConfigName.current = currentApiConfigName + // console.log("useEffect: currentApiConfigName changed, setChangeDetected -> false") setChangeDetected(false) }, [currentApiConfigName, extensionState, isChangeDetected]) @@ -88,11 +89,10 @@ const SettingsView = forwardRef(({ onDone }, if (prevState[field] === value) { return prevState } + + // console.log(`setCachedStateField(${field} -> ${value}): setChangeDetected -> true`) setChangeDetected(true) - return { - ...prevState, - [field]: value, - } + return { ...prevState, [field]: value } }) }, [], @@ -104,14 +104,11 @@ const SettingsView = forwardRef(({ onDone }, if (prevState.apiConfiguration?.[field] === value) { return prevState } + + // console.log(`setApiConfigurationField(${field} -> ${value}): setChangeDetected -> true`) setChangeDetected(true) - return { - ...prevState, - apiConfiguration: { - ...prevState.apiConfiguration, - [field]: value, - }, - } + + return { ...prevState, apiConfiguration: { ...prevState.apiConfiguration, [field]: value } } }) }, [], @@ -122,7 +119,10 @@ const SettingsView = forwardRef(({ onDone }, if (prevState.experiments?.[id] === enabled) { return prevState } + + // console.log("setExperimentEnabled: setChangeDetected -> true") setChangeDetected(true) + return { ...prevState, experiments: { ...prevState.experiments, [id]: enabled }, @@ -130,17 +130,10 @@ const SettingsView = forwardRef(({ onDone }, }) }, []) - const handleSubmit = () => { - const apiValidationResult = validateApiConfiguration(apiConfiguration) - const modelIdValidationResult = validateModelId( - apiConfiguration, - extensionState.glamaModels, - extensionState.openRouterModels, - ) + const isSettingValid = !errorMessage - setApiErrorMessage(apiValidationResult) - setModelIdErrorMessage(modelIdValidationResult) - if (!apiValidationResult && !modelIdValidationResult) { + const handleSubmit = () => { + if (isSettingValid) { vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly }) vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite }) vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute }) @@ -162,39 +155,14 @@ const SettingsView = forwardRef(({ onDone }, vscode.postMessage({ type: "rateLimitSeconds", value: rateLimitSeconds }) vscode.postMessage({ type: "maxOpenTabsContext", value: maxOpenTabsContext }) vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName }) - vscode.postMessage({ - type: "updateExperimental", - values: experiments, - }) + vscode.postMessage({ type: "updateExperimental", values: experiments }) vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch }) - - vscode.postMessage({ - type: "upsertApiConfiguration", - text: currentApiConfigName, - apiConfiguration, - }) - // onDone() + vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) + // console.log("handleSubmit: setChangeDetected -> false") setChangeDetected(false) } } - useEffect(() => { - setApiErrorMessage(undefined) - setModelIdErrorMessage(undefined) - }, [apiConfiguration]) - - // Initial validation on mount - useEffect(() => { - const apiValidationResult = validateApiConfiguration(apiConfiguration) - const modelIdValidationResult = validateModelId( - apiConfiguration, - extensionState.glamaModels, - extensionState.openRouterModels, - ) - setApiErrorMessage(apiValidationResult) - setModelIdErrorMessage(modelIdValidationResult) - }, [apiConfiguration, extensionState.glamaModels, extensionState.openRouterModels]) - const checkUnsaveChanges = useCallback( (then: () => void) => { if (isChangeDetected) { @@ -207,13 +175,7 @@ const SettingsView = forwardRef(({ onDone }, [isChangeDetected], ) - useImperativeHandle( - ref, - () => ({ - checkUnsaveChanges, - }), - [checkUnsaveChanges], - ) + useImperativeHandle(ref, () => ({ checkUnsaveChanges }), [checkUnsaveChanges]) const onConfirmDialogResult = useCallback((confirm: boolean) => { if (confirm) { @@ -231,10 +193,7 @@ const SettingsView = forwardRef(({ onDone }, const newCommands = [...currentCommands, commandInput] setCachedStateField("allowedCommands", newCommands) setCommandInput("") - vscode.postMessage({ - type: "allowedCommands", - commands: newCommands, - }) + vscode.postMessage({ type: "allowedCommands", commands: newCommands }) } } @@ -288,13 +247,14 @@ const SettingsView = forwardRef(({ onDone }, justifyContent: "space-between", gap: "6px", }}> - + disabled={!isChangeDetected || !isSettingValid}> Save - + (({ onDone }, uriScheme={extensionState.uriScheme} apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} - apiErrorMessage={apiErrorMessage} - modelIdErrorMessage={modelIdErrorMessage} + errorMessage={errorMessage} + setErrorMessage={setErrorMessage} />
@@ -765,7 +725,7 @@ const SettingsView = forwardRef(({ onDone }, color: "var(--vscode-descriptionForeground)", }}> When enabled, Roo will be able to edit files more quickly and will automatically reject - truncated full-file writes. Works best with the latest Claude 3.5 Sonnet model. + truncated full-file writes. Works best with the latest Claude 3.7 Sonnet model.

{diffEnabled && ( diff --git a/webview-ui/src/components/settings/TabNavbar.tsx b/webview-ui/src/components/settings/TabNavbar.tsx deleted file mode 100644 index cdf40c64d2..0000000000 --- a/webview-ui/src/components/settings/TabNavbar.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" -import React, { useState } from "react" - -export const TAB_NAVBAR_HEIGHT = 24 -const BUTTON_MARGIN_RIGHT = "3px" -const LAST_BUTTON_MARGIN_RIGHT = "13px" - -type TabNavbarProps = { - onPlusClick: () => void - onHistoryClick: () => void - onSettingsClick: () => void -} - -type TooltipProps = { - text: string - isVisible: boolean - position: { x: number; y: number } - align?: "left" | "center" | "right" -} - -const Tooltip: React.FC = ({ text, isVisible, position, align = "center" }) => { - let leftPosition = position.x - let triangleStyle: React.CSSProperties = { - left: "50%", - marginLeft: "-5px", - } - - if (align === "right") { - leftPosition = position.x - 10 // Adjust this value as needed - triangleStyle = { - right: "10px", // Adjust this value to match the tooltip's right padding - marginLeft: "0", - } - } else if (align === "left") { - leftPosition = position.x + 10 // Adjust this value as needed - triangleStyle = { - left: "10px", // Adjust this value to match the tooltip's left padding - marginLeft: "0", - } - } - - return ( -
-
-
- {text} -
- ) -} - -const TabNavbar = ({ onPlusClick, onHistoryClick, onSettingsClick }: TabNavbarProps) => { - const [tooltip, setTooltip] = useState({ - text: "", - isVisible: false, - position: { x: 0, y: 0 }, - align: "center", - }) - - const showTooltip = (text: string, event: React.MouseEvent, align: "left" | "center" | "right" = "center") => { - const rect = event.currentTarget.getBoundingClientRect() - setTooltip({ - text, - isVisible: true, - position: { x: rect.left + rect.width / 2, y: rect.bottom + 7 }, - align, - }) - } - - const hideTooltip = () => { - setTooltip((prev) => ({ ...prev, isVisible: false })) - } - - const buttonStyle = { - marginRight: BUTTON_MARGIN_RIGHT, - } - - const lastButtonStyle = { - ...buttonStyle, - marginRight: LAST_BUTTON_MARGIN_RIGHT, - } - - return ( - <> -
- showTooltip("New Chat", e, "center")} - onMouseLeave={hideTooltip} - onMouseMove={(e) => showTooltip("New Chat", e, "center")}> - - - showTooltip("History", e, "center")} - onMouseLeave={hideTooltip} - onMouseMove={(e) => showTooltip("History", e, "center")}> - - - showTooltip("Settings", e, "right")} - onMouseLeave={hideTooltip} - onMouseMove={(e) => showTooltip("Settings", e, "right")}> - - -
- - - ) -} - -export default TabNavbar diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx new file mode 100644 index 0000000000..efaa90dc39 --- /dev/null +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -0,0 +1,29 @@ +import { Slider } from "@/components/ui" + +import { ApiConfiguration, ModelInfo, THINKING_BUDGET } from "../../../../src/shared/api" + +interface ThinkingBudgetProps { + apiConfiguration: ApiConfiguration + setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void + modelInfo?: ModelInfo +} + +export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => { + const budget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default + + return modelInfo && modelInfo.thinking ? ( +
+
Thinking Budget
+
+ setApiConfigurationField("anthropicThinking", value[0])} + /> +
{budget}
+
+
+ ) : null +} diff --git a/webview-ui/src/components/settings/UnboundModelPicker.tsx b/webview-ui/src/components/settings/UnboundModelPicker.tsx deleted file mode 100644 index 4901884f1e..0000000000 --- a/webview-ui/src/components/settings/UnboundModelPicker.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ModelPicker } from "./ModelPicker" -import { unboundDefaultModelId } from "../../../../src/shared/api" - -export const UnboundModelPicker = () => ( - -) diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx index 8f2d0dff89..73394bae10 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx @@ -51,6 +51,8 @@ describe("ApiOptions", () => { render( {}} uriScheme={undefined} apiConfiguration={{}} setApiConfigurationField={() => {}} @@ -69,4 +71,6 @@ describe("ApiOptions", () => { renderApiOptions({ fromWelcomeView: true }) expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument() }) + + //TODO: More test cases needed }) diff --git a/webview-ui/src/components/settings/__tests__/ModelPicker.test.tsx b/webview-ui/src/components/settings/__tests__/ModelPicker.test.tsx index 4e7c67c187..49d60c55c4 100644 --- a/webview-ui/src/components/settings/__tests__/ModelPicker.test.tsx +++ b/webview-ui/src/components/settings/__tests__/ModelPicker.test.tsx @@ -3,7 +3,6 @@ import { screen, fireEvent, render } from "@testing-library/react" import { act } from "react" import { ModelPicker } from "../ModelPicker" -import { useExtensionState } from "../../../context/ExtensionStateContext" jest.mock("../../../context/ExtensionStateContext", () => ({ useExtensionState: jest.fn(), @@ -20,36 +19,40 @@ global.ResizeObserver = MockResizeObserver Element.prototype.scrollIntoView = jest.fn() describe("ModelPicker", () => { - const mockOnUpdateApiConfig = jest.fn() - const mockSetApiConfiguration = jest.fn() - + const mockSetApiConfigurationField = jest.fn() + const modelInfo = { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + } + const mockModels = { + model1: { name: "Model 1", description: "Test model 1", ...modelInfo }, + model2: { name: "Model 2", description: "Test model 2", ...modelInfo }, + } const defaultProps = { + apiConfiguration: {}, defaultModelId: "model1", - modelsKey: "glamaModels" as const, - configKey: "glamaModelId" as const, - infoKey: "glamaModelInfo" as const, - refreshMessageType: "refreshGlamaModels" as const, + defaultModelInfo: modelInfo, + modelIdKey: "glamaModelId" as const, + modelInfoKey: "glamaModelInfo" as const, serviceName: "Test Service", serviceUrl: "https://test.service", recommendedModel: "recommended-model", - } - - const mockModels = { - model1: { name: "Model 1", description: "Test model 1" }, - model2: { name: "Model 2", description: "Test model 2" }, + models: mockModels, + setApiConfigurationField: mockSetApiConfigurationField, } beforeEach(() => { jest.clearAllMocks() - ;(useExtensionState as jest.Mock).mockReturnValue({ - apiConfiguration: {}, - setApiConfiguration: mockSetApiConfiguration, - glamaModels: mockModels, - onUpdateApiConfig: mockOnUpdateApiConfig, - }) }) - it("calls onUpdateApiConfig when a model is selected", async () => { + it("calls setApiConfigurationField when a model is selected", async () => { await act(async () => { render() }) @@ -67,20 +70,12 @@ describe("ModelPicker", () => { await act(async () => { // Find and click the model item by its value. - const modelItem = screen.getByRole("option", { name: "model2" }) - fireEvent.click(modelItem) + const modelItem = screen.getByTestId("model-input") + fireEvent.input(modelItem, { target: { value: "model2" } }) }) // Verify the API config was updated. - expect(mockSetApiConfiguration).toHaveBeenCalledWith({ - glamaModelId: "model2", - glamaModelInfo: mockModels["model2"], - }) - - // Verify onUpdateApiConfig was called with the new config. - expect(mockOnUpdateApiConfig).toHaveBeenCalledWith({ - glamaModelId: "model2", - glamaModelInfo: mockModels["model2"], - }) + expect(mockSetApiConfigurationField).toHaveBeenCalledWith(defaultProps.modelIdKey, "model2") + expect(mockSetApiConfigurationField).toHaveBeenCalledWith(defaultProps.modelInfoKey, mockModels.model2) }) }) diff --git a/webview-ui/src/components/ui/alert-dialog.tsx b/webview-ui/src/components/ui/alert-dialog.tsx index 7530cae54d..82a25bf8f7 100644 --- a/webview-ui/src/components/ui/alert-dialog.tsx +++ b/webview-ui/src/components/ui/alert-dialog.tsx @@ -4,94 +4,97 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" -const AlertDialog = AlertDialogPrimitive.Root +function AlertDialog({ ...props }: React.ComponentProps) { + return +} -const AlertDialogTrigger = AlertDialogPrimitive.Trigger +function AlertDialogTrigger({ ...props }: React.ComponentProps) { + return +} -const AlertDialogPortal = AlertDialogPrimitive.Portal +function AlertDialogPortal({ ...props }: React.ComponentProps) { + return +} -const AlertDialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName - -const AlertDialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - ) { + return ( + - -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + ) +} -const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -AlertDialogHeader.displayName = "AlertDialogHeader" +function AlertDialogContent({ className, ...props }: React.ComponentProps) { + return ( + + + + + ) +} -const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -AlertDialogFooter.displayName = "AlertDialogFooter" +function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} -const AlertDialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName +function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} -const AlertDialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName +function AlertDialogTitle({ className, ...props }: React.ComponentProps) { + return ( + + ) +} -const AlertDialogAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AlertDialogCancel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName +function AlertDialogAction({ className, ...props }: React.ComponentProps) { + return +} + +function AlertDialogCancel({ className, ...props }: React.ComponentProps) { + return +} export { AlertDialog, diff --git a/webview-ui/src/components/ui/combobox-primitive.tsx b/webview-ui/src/components/ui/combobox-primitive.tsx new file mode 100644 index 0000000000..13bad87aba --- /dev/null +++ b/webview-ui/src/components/ui/combobox-primitive.tsx @@ -0,0 +1,522 @@ +/* eslint-disable react/jsx-pascal-case */ +"use client" + +import * as React from "react" +import { composeEventHandlers } from "@radix-ui/primitive" +import { useComposedRefs } from "@radix-ui/react-compose-refs" +import * as PopoverPrimitive from "@radix-ui/react-popover" +import { Primitive } from "@radix-ui/react-primitive" +import * as RovingFocusGroupPrimitive from "@radix-ui/react-roving-focus" +import { useControllableState } from "@radix-ui/react-use-controllable-state" +import { Command as CommandPrimitive } from "cmdk" + +export type ComboboxContextProps = { + inputValue: string + onInputValueChange: (inputValue: string, reason: "inputChange" | "itemSelect" | "clearClick") => void + onInputBlur?: (e: React.FocusEvent) => void + open: boolean + onOpenChange: (open: boolean) => void + currentTabStopId: string | null + onCurrentTabStopIdChange: (currentTabStopId: string | null) => void + inputRef: React.RefObject + tagGroupRef: React.RefObject> + disabled?: boolean + required?: boolean +} & ( + | Required> + | Required> +) + +const ComboboxContext = React.createContext({ + type: "single", + value: "", + onValueChange: () => {}, + inputValue: "", + onInputValueChange: () => {}, + onInputBlur: () => {}, + open: false, + onOpenChange: () => {}, + currentTabStopId: null, + onCurrentTabStopIdChange: () => {}, + inputRef: { current: null }, + tagGroupRef: { current: null }, + disabled: false, + required: false, +}) + +export const useComboboxContext = () => React.useContext(ComboboxContext) + +export type ComboboxType = "single" | "multiple" + +export interface ComboboxBaseProps + extends React.ComponentProps, + Omit, "value" | "defaultValue" | "onValueChange"> { + type?: ComboboxType | undefined + inputValue?: string + defaultInputValue?: string + onInputValueChange?: (inputValue: string, reason: "inputChange" | "itemSelect" | "clearClick") => void + onInputBlur?: (e: React.FocusEvent) => void + disabled?: boolean + required?: boolean +} + +export type ComboboxValue = T extends "single" + ? string + : T extends "multiple" + ? string[] + : never + +export interface ComboboxSingleProps { + type: "single" + value?: string + defaultValue?: string + onValueChange?: (value: string) => void +} + +export interface ComboboxMultipleProps { + type: "multiple" + value?: string[] + defaultValue?: string[] + onValueChange?: (value: string[]) => void +} + +export type ComboboxProps = ComboboxBaseProps & (ComboboxSingleProps | ComboboxMultipleProps) + +export const Combobox = React.forwardRef( + ( + { + type = "single" as T, + open: openProp, + onOpenChange, + defaultOpen, + modal, + children, + value: valueProp, + defaultValue, + onValueChange, + inputValue: inputValueProp, + defaultInputValue, + onInputValueChange, + onInputBlur, + disabled, + required, + ...props + }: ComboboxProps, + ref: React.ForwardedRef>, + ) => { + const [value = type === "multiple" ? [] : "", setValue] = useControllableState>({ + prop: valueProp as ComboboxValue, + defaultProp: defaultValue as ComboboxValue, + onChange: onValueChange as (value: ComboboxValue) => void, + }) + const [inputValue = "", setInputValue] = useControllableState({ + prop: inputValueProp, + defaultProp: defaultInputValue, + }) + const [open = false, setOpen] = useControllableState({ + prop: openProp, + defaultProp: defaultOpen, + onChange: onOpenChange, + }) + const [currentTabStopId, setCurrentTabStopId] = React.useState(null) + const inputRef = React.useRef(null) + const tagGroupRef = React.useRef>(null) + + const handleInputValueChange: ComboboxContextProps["onInputValueChange"] = React.useCallback( + (inputValue, reason) => { + setInputValue(inputValue) + onInputValueChange?.(inputValue, reason) + }, + [setInputValue, onInputValueChange], + ) + + return ( + + + + {children} + {!open && + + + ) + }, +) +Combobox.displayName = "Combobox" + +export const ComboboxTagGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => { + const { currentTabStopId, onCurrentTabStopIdChange, tagGroupRef, type } = useComboboxContext() + + if (type !== "multiple") { + throw new Error(' should only be used when type is "multiple"') + } + + const composedRefs = useComposedRefs(ref, tagGroupRef) + + return ( + onCurrentTabStopIdChange(null)} + {...props} + /> + ) +}) +ComboboxTagGroup.displayName = "ComboboxTagGroup" + +export interface ComboboxTagGroupItemProps + extends React.ComponentPropsWithoutRef { + value: string + disabled?: boolean +} + +const ComboboxTagGroupItemContext = React.createContext>({ + value: "", + disabled: false, +}) + +const useComboboxTagGroupItemContext = () => React.useContext(ComboboxTagGroupItemContext) + +export const ComboboxTagGroupItem = React.forwardRef< + React.ElementRef, + ComboboxTagGroupItemProps +>(({ onClick, onKeyDown, value: valueProp, disabled, ...props }, ref) => { + const { value, onValueChange, inputRef, currentTabStopId, type } = useComboboxContext() + + if (type !== "multiple") { + throw new Error(' should only be used when type is "multiple"') + } + + const lastItemValue = value.at(-1) + + return ( + + { + if (event.key === "Escape") { + inputRef.current?.focus() + } + if (event.key === "ArrowUp" || event.key === "ArrowDown") { + event.preventDefault() + inputRef.current?.focus() + } + if (event.key === "ArrowRight" && currentTabStopId === lastItemValue) { + inputRef.current?.focus() + } + if (event.key === "Backspace" || event.key === "Delete") { + onValueChange(value.filter((v) => v !== currentTabStopId)) + inputRef.current?.focus() + } + })} + onClick={composeEventHandlers(onClick, () => disabled && inputRef.current?.focus())} + tabStopId={valueProp} + focusable={!disabled} + data-disabled={disabled} + active={valueProp === lastItemValue} + {...props} + /> + + ) +}) +ComboboxTagGroupItem.displayName = "ComboboxTagGroupItem" + +export const ComboboxTagGroupItemRemove = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ onClick, ...props }, ref) => { + const { value, onValueChange, type } = useComboboxContext() + + if (type !== "multiple") { + throw new Error(' should only be used when type is "multiple"') + } + + const { value: valueProp, disabled } = useComboboxTagGroupItemContext() + + return ( + onValueChange(value.filter((v) => v !== valueProp)))} + {...props} + /> + ) +}) +ComboboxTagGroupItemRemove.displayName = "ComboboxTagGroupItemRemove" + +export const ComboboxInput = React.forwardRef< + React.ElementRef, + Omit, "value" | "onValueChange"> +>(({ onKeyDown, onMouseDown, onFocus, onBlur, ...props }, ref) => { + const { + type, + inputValue, + onInputValueChange, + onInputBlur, + open, + onOpenChange, + value, + onValueChange, + inputRef, + disabled, + required, + tagGroupRef, + } = useComboboxContext() + + const composedRefs = useComposedRefs(ref, inputRef) + + return ( + { + if (!open) { + onOpenChange(true) + } + // Schedule input value change to the next tick. + setTimeout(() => onInputValueChange(search, "inputChange")) + if (!search && type === "single") { + onValueChange("") + } + }} + onKeyDown={composeEventHandlers(onKeyDown, (event) => { + if (event.key === "ArrowUp" || event.key === "ArrowDown") { + if (!open) { + event.preventDefault() + onOpenChange(true) + } + } + if (type !== "multiple") { + return + } + if (event.key === "ArrowLeft" && !inputValue && value.length) { + tagGroupRef.current?.focus() + } + if (event.key === "Backspace" && !inputValue) { + onValueChange(value.slice(0, -1)) + } + })} + onMouseDown={composeEventHandlers(onMouseDown, () => onOpenChange(!!inputValue || !open))} + onFocus={composeEventHandlers(onFocus, () => onOpenChange(true))} + onBlur={composeEventHandlers(onBlur, (event) => { + if (!event.relatedTarget?.hasAttribute("cmdk-list")) { + onInputBlur?.(event) + } + })} + {...props} + /> + ) +}) +ComboboxInput.displayName = "ComboboxInput" + +export const ComboboxClear = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ onClick, ...props }, ref) => { + const { value, onValueChange, inputValue, onInputValueChange, type } = useComboboxContext() + + const isValueEmpty = type === "single" ? !value : !value.length + + return ( + { + if (type === "single") { + onValueChange("") + } else { + onValueChange([]) + } + onInputValueChange("", "clearClick") + })} + {...props} + /> + ) +}) +ComboboxClear.displayName = "ComboboxClear" + +export const ComboboxTrigger = PopoverPrimitive.Trigger + +export const ComboboxAnchor = PopoverPrimitive.Anchor + +export const ComboboxPortal = PopoverPrimitive.Portal + +export const ComboboxContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, onOpenAutoFocus, onInteractOutside, ...props }, ref) => ( + event.preventDefault())} + onCloseAutoFocus={composeEventHandlers(onOpenAutoFocus, (event) => event.preventDefault())} + onInteractOutside={composeEventHandlers(onInteractOutside, (event) => { + if (event.target instanceof Element && event.target.hasAttribute("cmdk-input")) { + event.preventDefault() + } + })} + {...props}> + {children} + +)) +ComboboxContent.displayName = "ComboboxContent" + +export const ComboboxEmpty = CommandPrimitive.Empty + +export const ComboboxLoading = CommandPrimitive.Loading + +export interface ComboboxItemProps extends Omit, "value"> { + value: string +} + +const ComboboxItemContext = React.createContext({ isSelected: false }) + +const useComboboxItemContext = () => React.useContext(ComboboxItemContext) + +const findComboboxItemText = (children: React.ReactNode) => { + let text = "" + + React.Children.forEach(children, (child) => { + if (text) { + return + } + + if (React.isValidElement<{ children: React.ReactNode }>(child)) { + if (child.type === ComboboxItemText) { + text = child.props.children as string + } else { + text = findComboboxItemText(child.props.children) + } + } + }) + + return text +} + +export const ComboboxItem = React.forwardRef, ComboboxItemProps>( + ({ value: valueProp, children, onMouseDown, ...props }, ref) => { + const { type, value, onValueChange, onInputValueChange, onOpenChange } = useComboboxContext() + + const inputValue = React.useMemo(() => findComboboxItemText(children), [children]) + + const isSelected = type === "single" ? value === valueProp : value.includes(valueProp) + + return ( + + event.preventDefault())} + onSelect={() => { + if (type === "multiple") { + onValueChange( + value.includes(valueProp) + ? value.filter((v) => v !== valueProp) + : [...value, valueProp], + ) + onInputValueChange("", "itemSelect") + } else { + onValueChange(valueProp) + onInputValueChange(inputValue, "itemSelect") + // Schedule open change to the next tick. + setTimeout(() => onOpenChange(false)) + } + }} + value={inputValue} + {...props}> + {children} + + + ) + }, +) +ComboboxItem.displayName = "ComboboxItem" + +export const ComboboxItemIndicator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => { + const { isSelected } = useComboboxItemContext() + + if (!isSelected) { + return null + } + + return +}) +ComboboxItemIndicator.displayName = "ComboboxItemIndicator" + +export interface ComboboxItemTextProps extends React.ComponentPropsWithoutRef { + children: string +} + +export const ComboboxItemText = (props: ComboboxItemTextProps) => +ComboboxItemText.displayName = "ComboboxItemText" + +export const ComboboxGroup = CommandPrimitive.Group + +export const ComboboxSeparator = CommandPrimitive.Separator + +const Root = Combobox +const TagGroup = ComboboxTagGroup +const TagGroupItem = ComboboxTagGroupItem +const TagGroupItemRemove = ComboboxTagGroupItemRemove +const Input = ComboboxInput +const Clear = ComboboxClear +const Trigger = ComboboxTrigger +const Anchor = ComboboxAnchor +const Portal = ComboboxPortal +const Content = ComboboxContent +const Empty = ComboboxEmpty +const Loading = ComboboxLoading +const Item = ComboboxItem +const ItemIndicator = ComboboxItemIndicator +const ItemText = ComboboxItemText +const Group = ComboboxGroup +const Separator = ComboboxSeparator + +export { + Root, + TagGroup, + TagGroupItem, + TagGroupItemRemove, + Input, + Clear, + Trigger, + Anchor, + Portal, + Content, + Empty, + Loading, + Item, + ItemIndicator, + ItemText, + Group, + Separator, +} diff --git a/webview-ui/src/components/ui/combobox.tsx b/webview-ui/src/components/ui/combobox.tsx new file mode 100644 index 0000000000..24b2f7be1f --- /dev/null +++ b/webview-ui/src/components/ui/combobox.tsx @@ -0,0 +1,177 @@ +"use client" + +import * as React from "react" +import { Slottable } from "@radix-ui/react-slot" +import { cva } from "class-variance-authority" +import { Check, ChevronsUpDown, Loader, X } from "lucide-react" + +import { cn } from "@/lib/utils" +import * as ComboboxPrimitive from "@/components/ui/combobox-primitive" +import { badgeVariants } from "@/components/ui/badge" +// import * as ComboboxPrimitive from "@/registry/default/ui/combobox-primitive" +import { + InputBase, + InputBaseAdornmentButton, + InputBaseControl, + InputBaseFlexWrapper, + InputBaseInput, +} from "@/components/ui/input-base" + +export const Combobox = ComboboxPrimitive.Root + +const ComboboxInputBase = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, ...props }, ref) => ( + + + {children} + + + + + + + + + + + + +)) +ComboboxInputBase.displayName = "ComboboxInputBase" + +export const ComboboxInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + + + + + + + +)) +ComboboxInput.displayName = "ComboboxInput" + +export const ComboboxTagsInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, ...props }, ref) => ( + + + + {children} + + + + + + + + +)) +ComboboxTagsInput.displayName = "ComboboxTagsInput" + +export const ComboboxTag = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ children, className, ...props }, ref) => ( + + {children} + + + Remove + + +)) +ComboboxTag.displayName = "ComboboxTag" + +export const ComboboxContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "start", alignOffset = 0, ...props }, ref) => ( + + + +)) +ComboboxContent.displayName = "ComboboxContent" + +export const ComboboxEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ComboboxEmpty.displayName = "ComboboxEmpty" + +export const ComboboxLoading = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ComboboxLoading.displayName = "ComboboxLoading" + +export const ComboboxGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ComboboxGroup.displayName = "ComboboxGroup" + +const ComboboxSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ComboboxSeparator.displayName = "ComboboxSeparator" + +export const comboboxItemStyle = cva( + "relative flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-vscode-dropdown-foreground data-[disabled=true]:opacity-50", +) + +export const ComboboxItem = React.forwardRef< + React.ElementRef, + Omit, "children"> & + Pick, "children"> +>(({ className, children, ...props }, ref) => ( + + {children} + + + + +)) +ComboboxItem.displayName = "ComboboxItem" diff --git a/webview-ui/src/components/ui/dialog.tsx b/webview-ui/src/components/ui/dialog.tsx index 11d5e2d3b0..ed3160f692 100644 --- a/webview-ui/src/components/ui/dialog.tsx +++ b/webview-ui/src/components/ui/dialog.tsx @@ -1,96 +1,108 @@ -"use client" - import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog" -import { Cross2Icon } from "@radix-ui/react-icons" +import { XIcon } from "lucide-react" import { cn } from "@/lib/utils" -const Dialog = DialogPrimitive.Root +function Dialog({ ...props }: React.ComponentProps) { + return +} -const DialogTrigger = DialogPrimitive.Trigger +function DialogTrigger({ ...props }: React.ComponentProps) { + return +} -const DialogPortal = DialogPrimitive.Portal +function DialogPortal({ ...props }: React.ComponentProps) { + return +} -const DialogClose = DialogPrimitive.Close +function DialogClose({ ...props }: React.ComponentProps) { + return +} -const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName - -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - ) { + return ( + - {children} - - - Close - - - -)) -DialogContent.displayName = DialogPrimitive.Content.displayName + {...props} + /> + ) +} -const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -DialogHeader.displayName = "DialogHeader" +function DialogContent({ className, children, ...props }: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} -const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( -
-) -DialogFooter.displayName = "DialogFooter" +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} -const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} -const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName +function DialogTitle({ className, ...props }: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ className, ...props }: React.ComponentProps) { + return ( + + ) +} export { Dialog, - DialogPortal, - DialogOverlay, - DialogTrigger, DialogClose, DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, } diff --git a/webview-ui/src/components/ui/index.ts b/webview-ui/src/components/ui/index.ts index bf00aa6442..6eb8dd25ba 100644 --- a/webview-ui/src/components/ui/index.ts +++ b/webview-ui/src/components/ui/index.ts @@ -1,3 +1,4 @@ +export * from "./alert-dialog" export * from "./autosize-textarea" export * from "./badge" export * from "./button" diff --git a/webview-ui/src/components/ui/input-base.tsx b/webview-ui/src/components/ui/input-base.tsx new file mode 100644 index 0000000000..9dbda6eb13 --- /dev/null +++ b/webview-ui/src/components/ui/input-base.tsx @@ -0,0 +1,157 @@ +/* eslint-disable react/jsx-no-comment-textnodes */ +/* eslint-disable react/jsx-pascal-case */ +"use client" + +import * as React from "react" +import { composeEventHandlers } from "@radix-ui/primitive" +import { composeRefs } from "@radix-ui/react-compose-refs" +import { Primitive } from "@radix-ui/react-primitive" +import { Slot } from "@radix-ui/react-slot" + +import { cn } from "@/lib/utils" +import { Button } from "./button" + +export type InputBaseContextProps = Pick & { + controlRef: React.RefObject + onFocusedChange: (focused: boolean) => void +} + +const InputBaseContext = React.createContext({ + autoFocus: false, + controlRef: { current: null }, + disabled: false, + onFocusedChange: () => {}, +}) + +const useInputBaseContext = () => React.useContext(InputBaseContext) + +export interface InputBaseProps extends React.ComponentPropsWithoutRef { + autoFocus?: boolean + disabled?: boolean +} + +export const InputBase = React.forwardRef, InputBaseProps>( + ({ autoFocus, disabled, className, onClick, ...props }, ref) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [focused, setFocused] = React.useState(false) + + const controlRef = React.useRef(null) + + return ( + + { + // Based on MUI's implementation. + // https://github.com/mui/material-ui/blob/master/packages/mui-material/src/InputBase/InputBase.js#L458~L460 + if (controlRef.current && event.currentTarget === event.target) { + controlRef.current.focus() + } + })} + className={cn( + "flex w-full text-vscode-input-foreground border border-vscode-dropdown-border bg-vscode-input-background rounded-xs px-3 py-0.5 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus:outline-0 focus-visible:outline-none focus-visible:border-vscode-focusBorder disabled:cursor-not-allowed disabled:opacity-50", + disabled && "cursor-not-allowed opacity-50", + className, + )} + {...props} + /> + + ) + }, +) +InputBase.displayName = "InputBase" + +export const InputBaseFlexWrapper = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +InputBaseFlexWrapper.displayName = "InputBaseFlexWrapper" + +export const InputBaseControl = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ onFocus, onBlur, ...props }, ref) => { + const { controlRef, autoFocus, disabled, onFocusedChange } = useInputBaseContext() + + return ( + onFocusedChange(true))} + onBlur={composeEventHandlers(onBlur, () => onFocusedChange(false))} + {...{ disabled }} + {...props} + /> + ) +}) +InputBaseControl.displayName = "InputBaseControl" + +export interface InputBaseAdornmentProps extends React.ComponentPropsWithoutRef<"div"> { + asChild?: boolean + disablePointerEvents?: boolean +} + +export const InputBaseAdornment = React.forwardRef, InputBaseAdornmentProps>( + ({ className, disablePointerEvents, asChild, children, ...props }, ref) => { + const Comp = asChild ? Slot : typeof children === "string" ? "p" : "div" + + const isAction = React.isValidElement(children) && children.type === InputBaseAdornmentButton + + return ( + + {children} + + ) + }, +) +InputBaseAdornment.displayName = "InputBaseAdornment" + +export const InputBaseAdornmentButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ type = "button", variant = "ghost", size = "icon", disabled: disabledProp, className, ...props }, ref) => { + const { disabled } = useInputBaseContext() + + return ( +
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 3dca8d5f51..ae5c5b9539 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -1,18 +1,7 @@ import React, { createContext, useCallback, useContext, useEffect, useState } from "react" import { useEvent } from "react-use" import { ApiConfigMeta, ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage" -import { - ApiConfiguration, - ModelInfo, - glamaDefaultModelId, - glamaDefaultModelInfo, - openRouterDefaultModelId, - openRouterDefaultModelInfo, - unboundDefaultModelId, - unboundDefaultModelInfo, - requestyDefaultModelId, - requestyDefaultModelInfo, -} from "../../../src/shared/api" +import { ApiConfiguration } from "../../../src/shared/api" import { vscode } from "../utils/vscode" import { convertTextMateToHljs } from "../utils/textMateToHljs" import { findLastIndex } from "../../../src/shared/array" @@ -26,11 +15,6 @@ export interface ExtensionStateContextType extends ExtensionState { didHydrateState: boolean showWelcome: boolean theme: any - glamaModels: Record - requestyModels: Record - openRouterModels: Record - unboundModels: Record - openAiModels: string[] mcpServers: McpServer[] currentCheckpoint?: string filePaths: string[] @@ -70,7 +54,6 @@ export interface ExtensionStateContextType extends ExtensionState { setRateLimitSeconds: (value: number) => void setCurrentApiConfigName: (value: string) => void setListApiConfigMeta: (value: ApiConfigMeta[]) => void - onUpdateApiConfig: (apiConfig: ApiConfiguration) => void mode: Mode setMode: (value: Mode) => void setCustomModePrompts: (value: CustomModePrompts) => void @@ -118,27 +101,15 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode autoApprovalEnabled: false, customModes: [], maxOpenTabsContext: 20, + cwd: "", }) const [didHydrateState, setDidHydrateState] = useState(false) const [showWelcome, setShowWelcome] = useState(false) const [theme, setTheme] = useState(undefined) const [filePaths, setFilePaths] = useState([]) - const [glamaModels, setGlamaModels] = useState>({ - [glamaDefaultModelId]: glamaDefaultModelInfo, - }) const [openedTabs, setOpenedTabs] = useState>([]) - const [openRouterModels, setOpenRouterModels] = useState>({ - [openRouterDefaultModelId]: openRouterDefaultModelInfo, - }) - const [unboundModels, setUnboundModels] = useState>({ - [unboundDefaultModelId]: unboundDefaultModelInfo, - }) - const [requestyModels, setRequestyModels] = useState>({ - [requestyDefaultModelId]: requestyDefaultModelInfo, - }) - const [openAiModels, setOpenAiModels] = useState([]) const [mcpServers, setMcpServers] = useState([]) const [currentCheckpoint, setCurrentCheckpoint] = useState() @@ -146,18 +117,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode (value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), [], ) - - const onUpdateApiConfig = useCallback((apiConfig: ApiConfiguration) => { - setState((currentState) => { - vscode.postMessage({ - type: "upsertApiConfiguration", - text: currentState.currentApiConfigName, - apiConfiguration: { ...currentState.apiConfiguration, ...apiConfig }, - }) - return currentState // No state update needed - }) - }, []) - const handleMessage = useCallback( (event: MessageEvent) => { const message: ExtensionMessage = event.data @@ -202,40 +161,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }) break } - case "glamaModels": { - const updatedModels = message.glamaModels ?? {} - setGlamaModels({ - [glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model - ...updatedModels, - }) - break - } - case "openRouterModels": { - const updatedModels = message.openRouterModels ?? {} - setOpenRouterModels({ - [openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model - ...updatedModels, - }) - break - } - case "openAiModels": { - const updatedModels = message.openAiModels ?? [] - setOpenAiModels(updatedModels) - break - } - case "unboundModels": { - const updatedModels = message.unboundModels ?? {} - setUnboundModels(updatedModels) - break - } - case "requestyModels": { - const updatedModels = message.requestyModels ?? {} - setRequestyModels({ - [requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model - ...updatedModels, - }) - break - } case "mcpServers": { setMcpServers(message.mcpServers ?? []) break @@ -264,11 +189,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode didHydrateState, showWelcome, theme, - glamaModels, - requestyModels, - openRouterModels, - openAiModels, - unboundModels, mcpServers, currentCheckpoint, filePaths, @@ -316,7 +236,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })), setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })), setListApiConfigMeta, - onUpdateApiConfig, setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })), setCustomModePrompts: (value) => setState((prevState) => ({ ...prevState, customModePrompts: value })), setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })), diff --git a/webview-ui/src/services/GitService.ts b/webview-ui/src/services/GitService.ts deleted file mode 100644 index 57a83e5ad9..0000000000 --- a/webview-ui/src/services/GitService.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { vscode } from "../utils/vscode" - -export interface GitCommit { - hash: string - shortHash: string - subject: string - author: string - date: string -} - -class GitService { - private commits: GitCommit[] | null = null - private lastQuery: string = "" - - async searchCommits(query: string = ""): Promise { - if (query === this.lastQuery && this.commits) { - return this.commits - } - - // Request search from extension - vscode.postMessage({ type: "searchCommits", query }) - - // Wait for response - const response = await new Promise((resolve) => { - const handler = (event: MessageEvent) => { - const message = event.data - if (message.type === "commitSearchResults") { - window.removeEventListener("message", handler) - resolve(message.commits) - } - } - window.addEventListener("message", handler) - }) - - this.commits = response - this.lastQuery = query - return response - } - - clearCache() { - this.commits = null - this.lastQuery = "" - } -} - -export const gitService = new GitService() diff --git a/webview-ui/src/utils/__tests__/path-mentions.test.ts b/webview-ui/src/utils/__tests__/path-mentions.test.ts new file mode 100644 index 0000000000..bb5591fbe5 --- /dev/null +++ b/webview-ui/src/utils/__tests__/path-mentions.test.ts @@ -0,0 +1,45 @@ +import { convertToMentionPath } from "../path-mentions" + +describe("path-mentions", () => { + describe("convertToMentionPath", () => { + it("should convert an absolute path to a mention path when it starts with cwd", () => { + // Windows-style paths + expect(convertToMentionPath("C:\\Users\\user\\project\\file.txt", "C:\\Users\\user\\project")).toBe( + "@/file.txt", + ) + + // Unix-style paths + expect(convertToMentionPath("/Users/user/project/file.txt", "/Users/user/project")).toBe("@/file.txt") + }) + + it("should handle paths with trailing slashes in cwd", () => { + expect(convertToMentionPath("/Users/user/project/file.txt", "/Users/user/project/")).toBe("@/file.txt") + }) + + it("should be case-insensitive when matching paths", () => { + expect(convertToMentionPath("/Users/User/Project/file.txt", "/users/user/project")).toBe("@/file.txt") + }) + + it("should return the original path when cwd is not provided", () => { + expect(convertToMentionPath("/Users/user/project/file.txt")).toBe("/Users/user/project/file.txt") + }) + + it("should return the original path when it does not start with cwd", () => { + expect(convertToMentionPath("/Users/other/project/file.txt", "/Users/user/project")).toBe( + "/Users/other/project/file.txt", + ) + }) + + it("should normalize backslashes to forward slashes", () => { + expect(convertToMentionPath("C:\\Users\\user\\project\\subdir\\file.txt", "C:\\Users\\user\\project")).toBe( + "@/subdir/file.txt", + ) + }) + + it("should handle nested paths correctly", () => { + expect(convertToMentionPath("/Users/user/project/nested/deeply/file.txt", "/Users/user/project")).toBe( + "@/nested/deeply/file.txt", + ) + }) + }) +}) diff --git a/webview-ui/src/utils/path-mentions.ts b/webview-ui/src/utils/path-mentions.ts new file mode 100644 index 0000000000..960483f593 --- /dev/null +++ b/webview-ui/src/utils/path-mentions.ts @@ -0,0 +1,38 @@ +/** + * Utilities for handling path-related operations in mentions + */ + +/** + * Converts an absolute path to a mention-friendly path + * If the provided path starts with the current working directory, + * it's converted to a relative path prefixed with @ + * + * @param path The path to convert + * @param cwd The current working directory + * @returns A mention-friendly path + */ +export function convertToMentionPath(path: string, cwd?: string): string { + const normalizedPath = path.replace(/\\/g, "/") + let normalizedCwd = cwd ? cwd.replace(/\\/g, "/") : "" + + if (!normalizedCwd) { + return path + } + + // Remove trailing slash from cwd if it exists + if (normalizedCwd.endsWith("/")) { + normalizedCwd = normalizedCwd.slice(0, -1) + } + + // Always use case-insensitive comparison for path matching + const lowerPath = normalizedPath.toLowerCase() + const lowerCwd = normalizedCwd.toLowerCase() + + if (lowerPath.startsWith(lowerCwd)) { + const relativePath = normalizedPath.substring(normalizedCwd.length) + // Ensure there's a slash after the @ symbol when we create the mention path + return "@" + (relativePath.startsWith("/") ? relativePath : "/" + relativePath) + } + + return path +} diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 19b13e2c6c..82af23ab49 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -1,79 +1,83 @@ -import { - ApiConfiguration, - glamaDefaultModelId, - openRouterDefaultModelId, - unboundDefaultModelId, -} from "../../../src/shared/api" -import { ModelInfo } from "../../../src/shared/api" +import { ApiConfiguration, ModelInfo } from "../../../src/shared/api" + export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined { - if (apiConfiguration) { - switch (apiConfiguration.apiProvider) { - case "anthropic": - if (!apiConfiguration.apiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "glama": - if (!apiConfiguration.glamaApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "bedrock": - if (!apiConfiguration.awsRegion) { - return "You must choose a region to use with AWS Bedrock." - } - break - case "openrouter": - if (!apiConfiguration.openRouterApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "vertex": - if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) { - return "You must provide a valid Google Cloud Project ID and Region." - } - break - case "gemini": - if (!apiConfiguration.geminiApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "openai-native": - if (!apiConfiguration.openAiNativeApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "mistral": - if (!apiConfiguration.mistralApiKey) { - return "You must provide a valid API key or choose a different provider." - } - break - case "openai": - if ( - !apiConfiguration.openAiBaseUrl || - !apiConfiguration.openAiApiKey || - !apiConfiguration.openAiModelId - ) { - return "You must provide a valid base URL, API key, and model ID." - } - break - case "ollama": - if (!apiConfiguration.ollamaModelId) { - return "You must provide a valid model ID." - } - break - case "lmstudio": - if (!apiConfiguration.lmStudioModelId) { - return "You must provide a valid model ID." - } - break - case "vscode-lm": - if (!apiConfiguration.vsCodeLmModelSelector) { - return "You must provide a valid model selector." - } - break - } + if (!apiConfiguration) { + return undefined } + + switch (apiConfiguration.apiProvider) { + case "openrouter": + if (!apiConfiguration.openRouterApiKey) { + return "You must provide a valid API key." + } + break + case "glama": + if (!apiConfiguration.glamaApiKey) { + return "You must provide a valid API key." + } + break + case "unbound": + if (!apiConfiguration.unboundApiKey) { + return "You must provide a valid API key." + } + break + case "requesty": + if (!apiConfiguration.requestyApiKey) { + return "You must provide a valid API key." + } + break + case "anthropic": + if (!apiConfiguration.apiKey) { + return "You must provide a valid API key." + } + break + case "bedrock": + if (!apiConfiguration.awsRegion) { + return "You must choose a region to use with AWS Bedrock." + } + break + case "vertex": + if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) { + return "You must provide a valid Google Cloud Project ID and Region." + } + break + case "gemini": + if (!apiConfiguration.geminiApiKey) { + return "You must provide a valid API key." + } + break + case "openai-native": + if (!apiConfiguration.openAiNativeApiKey) { + return "You must provide a valid API key." + } + break + case "mistral": + if (!apiConfiguration.mistralApiKey) { + return "You must provide a valid API key." + } + break + case "openai": + if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) { + return "You must provide a valid base URL, API key, and model ID." + } + break + case "ollama": + if (!apiConfiguration.ollamaModelId) { + return "You must provide a valid model ID." + } + break + case "lmstudio": + if (!apiConfiguration.lmStudioModelId) { + return "You must provide a valid model ID." + } + break + case "vscode-lm": + if (!apiConfiguration.vsCodeLmModelSelector) { + return "You must provide a valid model selector." + } + break + } + return undefined } @@ -82,40 +86,81 @@ export function validateModelId( glamaModels?: Record, openRouterModels?: Record, unboundModels?: Record, + requestyModels?: Record, ): string | undefined { - if (apiConfiguration) { - switch (apiConfiguration.apiProvider) { - case "glama": - const glamaModelId = apiConfiguration.glamaModelId || glamaDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default - if (!glamaModelId) { - return "You must provide a model ID." - } - if (glamaModels && !Object.keys(glamaModels).includes(glamaModelId)) { - // even if the model list endpoint failed, extensionstatecontext will always have the default model info - return "The model ID you provided is not available. Please choose a different model." - } - break - case "openrouter": - const modelId = apiConfiguration.openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default - if (!modelId) { - return "You must provide a model ID." - } - if (openRouterModels && !Object.keys(openRouterModels).includes(modelId)) { - // even if the model list endpoint failed, extensionstatecontext will always have the default model info - return "The model ID you provided is not available. Please choose a different model." - } - break - case "unbound": - const unboundModelId = apiConfiguration.unboundModelId || unboundDefaultModelId - if (!unboundModelId) { - return "You must provide a model ID." - } - if (unboundModels && !Object.keys(unboundModels).includes(unboundModelId)) { - // even if the model list endpoint failed, extensionstatecontext will always have the default model info - return "The model ID you provided is not available. Please choose a different model." - } - break - } + if (!apiConfiguration) { + return undefined } + + switch (apiConfiguration.apiProvider) { + case "openrouter": + const modelId = apiConfiguration.openRouterModelId + + if (!modelId) { + return "You must provide a model ID." + } + + if ( + openRouterModels && + Object.keys(openRouterModels).length > 1 && + !Object.keys(openRouterModels).includes(modelId) + ) { + return `The model ID (${modelId}) you provided is not available. Please choose a different model.` + } + + break + + case "glama": + const glamaModelId = apiConfiguration.glamaModelId + + if (!glamaModelId) { + return "You must provide a model ID." + } + + if ( + glamaModels && + Object.keys(glamaModels).length > 1 && + !Object.keys(glamaModels).includes(glamaModelId) + ) { + return `The model ID (${glamaModelId}) you provided is not available. Please choose a different model.` + } + + break + + case "unbound": + const unboundModelId = apiConfiguration.unboundModelId + + if (!unboundModelId) { + return "You must provide a model ID." + } + + if ( + unboundModels && + Object.keys(unboundModels).length > 1 && + !Object.keys(unboundModels).includes(unboundModelId) + ) { + return `The model ID (${unboundModelId}) you provided is not available. Please choose a different model.` + } + + break + + case "requesty": + const requestyModelId = apiConfiguration.requestyModelId + + if (!requestyModelId) { + return "You must provide a model ID." + } + + if ( + requestyModels && + Object.keys(requestyModels).length > 1 && + !Object.keys(requestyModels).includes(requestyModelId) + ) { + return `The model ID (${requestyModelId}) you provided is not available. Please choose a different model.` + } + + break + } + return undefined }