diff --git a/.changeset/blue-masks-camp.md b/.changeset/blue-masks-camp.md deleted file mode 100644 index a67e1c4d53..0000000000 --- a/.changeset/blue-masks-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Add shortcuts to the currently open tabs in the "Add File" section of @-mentions (thanks @olup!) diff --git a/.github/workflows/discord-pr-notify.yml b/.github/workflows/discord-pr-notify.yml index 7c38b43bfd..88c918edfe 100644 --- a/.github/workflows/discord-pr-notify.yml +++ b/.github/workflows/discord-pr-notify.yml @@ -11,11 +11,16 @@ jobs: if: github.head_ref != 'changeset-release/main' steps: - name: Send Discord Notification - uses: Ilshidur/action-discord@master - with: - args: | - šŸš€ **New PR:** ${{ github.event.pull_request.title }} - šŸ”— <${{ github.event.pull_request.html_url }}> - šŸ‘¤ **Author:** ${{ github.event.pull_request.user.login }} - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + run: | + PAYLOAD=$(jq -n \ + --arg title "${{ github.event.pull_request.title }}" \ + --arg url "${{ github.event.pull_request.html_url }}" \ + --arg author "${{ github.event.pull_request.user.login }}" \ + '{ + content: ("šŸš€ **New PR:** " + $title + "\nšŸ”— <" + $url + ">\nšŸ‘¤ **Author:** " + $author), + thread_name: ($title + " by " + $author) + }') + + curl -X POST "${{ secrets.DISCORD_WEBHOOK }}" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml deleted file mode 100644 index ac551ebc85..0000000000 --- a/.github/workflows/pages.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Deploy Jekyll site to Pages - -on: - push: - branches: ["main"] - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Build job - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Pages - uses: actions/configure-pages@v5 - - name: Build with Jekyll - uses: actions/jekyll-build-pages@v1 - with: - source: ./docs/ - destination: ./_site - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - - # Deployment job - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.vscode/launch.json b/.vscode/launch.json index ead1700291..a7e8c580a5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,20 +10,20 @@ "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - ], + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], "sourceMaps": true, "outFiles": ["${workspaceFolder}/dist/**/*.js"], - "preLaunchTask": "compile", + "preLaunchTask": "debug-mode", "env": { "NODE_ENV": "development", "VSCODE_DEBUG_MODE": "true" }, - "resolveSourceMapLocations": [ - "${workspaceFolder}/**", - "!**/node_modules/**" - ] - }, + "resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"], + "presentation": { + "hidden": false, + "group": "tasks", + "order": 1 + } + } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5804f54cd6..4b49a7859f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,6 @@ "label": "compile", "type": "npm", "script": "compile", - "dependsOn": ["npm: build:webview"], "group": { "kind": "build", "isDefault": true @@ -30,56 +29,75 @@ } }, { + "label": "debug-mode", + "dependsOn": ["compile", "npm: dev"], + "group": { + "kind": "build", + "isDefault": false + }, + "dependsOrder": "parallel", + "presentation": { + "reveal": "always", + "panel": "new" + } + }, + { + "label": "npm: dev", + "type": "npm", + "script": "dev", + "group": "build", + "problemMatcher": { + "owner": "vite", + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": ".*VITE.*", + "endsPattern": ".*Local:.*" + } + }, + "isBackground": true, + "presentation": { + "group": "watch", + "reveal": "never" + } + }, + { + "label": "npm: build:webview", "type": "npm", "script": "build:webview", "group": "build", "problemMatcher": [], "isBackground": true, - "label": "npm: build:webview", "presentation": { "group": "watch", "reveal": "never" } }, { + "label": "npm: watch:esbuild", "type": "npm", "script": "watch:esbuild", "group": "build", "problemMatcher": "$esbuild-watch", "isBackground": true, - "label": "npm: watch:esbuild", "presentation": { "group": "watch", "reveal": "never" } }, { + "label": "npm: watch:tsc", "type": "npm", "script": "watch:tsc", "group": "build", "problemMatcher": "$tsc-watch", "isBackground": true, - "label": "npm: watch:tsc", "presentation": { "group": "watch", "reveal": "never" } - }, - { - "type": "npm", - "script": "watch-tests", - "problemMatcher": "$tsc-watch", - "isBackground": true, - "presentation": { - "reveal": "never", - "group": "watchers" - }, - "group": "build" - }, - { - "label": "tasks: watch-tests", - "dependsOn": ["npm: watch", "npm: watch-tests"], - "problemMatcher": [] } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd17bd2b9..0a3bbfef82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Roo Code Changelog +## [3.3.11] + +- Safer shell profile path check to avoid an error on Windows +- Autocomplete for slash commands + +## [3.3.10] + +- Add shortcuts to the currently open tabs in the "Add File" section of @-mentions (thanks @olup!) +- Fix pricing for o1-mini (thanks @hesara!) +- Fix context window size calculation (thanks @MuriloFP!) +- Improvements to experimental unified diff strategy and selection logic in code actions (thanks @nissa-seru!) +- Enable markdown formatting in o3 and o1 (thanks @nissa-seru!) +- Improved terminal shell detection logic (thanks @canvrno for the original and @nissa-seru for the port!) +- Fix occasional errors when switching between API profiles (thanks @samhvw8!) +- Visual improvements to the list of modes on the prompts tab +- Fix double-scrollbar in provider dropdown +- Visual cleanup to the list of modes on the prompts tab +- Improvements to the default prompts for Architect and Ask mode +- Allow switching between modes with slash messages like `/ask why is the sky blue?` + ## [3.3.9] - Add o3-mini-high and o3-mini-low diff --git a/docs/Gemfile b/docs/Gemfile deleted file mode 100644 index 91ceacd3cb..0000000000 --- a/docs/Gemfile +++ /dev/null @@ -1,2 +0,0 @@ -source 'https://rubygems.org' -gem 'github-pages', group: :jekyll_plugins \ No newline at end of file diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock deleted file mode 100644 index a08fc8bb22..0000000000 --- a/docs/Gemfile.lock +++ /dev/null @@ -1,308 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - activesupport (8.0.1) - base64 - benchmark (>= 0.3) - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - logger (>= 1.4.2) - minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) - base64 (0.2.0) - benchmark (0.4.0) - bigdecimal (3.1.9) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.12.2) - colorator (1.1.0) - commonmarker (0.23.11) - concurrent-ruby (1.3.5) - connection_pool (2.5.0) - csv (3.3.2) - dnsruby (1.72.3) - base64 (~> 0.2.0) - simpleidn (~> 0.2.1) - drb (2.2.1) - em-websocket (0.5.3) - eventmachine (>= 0.12.9) - http_parser.rb (~> 0) - ethon (0.16.0) - ffi (>= 1.15.0) - eventmachine (1.2.7) - execjs (2.10.0) - faraday (2.12.2) - faraday-net_http (>= 2.0, < 3.5) - json - logger - faraday-net_http (3.4.0) - net-http (>= 0.5.0) - ffi (1.17.1-aarch64-linux-gnu) - ffi (1.17.1-aarch64-linux-musl) - ffi (1.17.1-arm-linux-gnu) - ffi (1.17.1-arm-linux-musl) - ffi (1.17.1-arm64-darwin) - ffi (1.17.1-x86_64-darwin) - ffi (1.17.1-x86_64-linux-gnu) - ffi (1.17.1-x86_64-linux-musl) - forwardable-extended (2.6.0) - gemoji (4.1.0) - github-pages (232) - github-pages-health-check (= 1.18.2) - jekyll (= 3.10.0) - jekyll-avatar (= 0.8.0) - jekyll-coffeescript (= 1.2.2) - jekyll-commonmark-ghpages (= 0.5.1) - jekyll-default-layout (= 0.1.5) - jekyll-feed (= 0.17.0) - jekyll-gist (= 1.5.0) - jekyll-github-metadata (= 2.16.1) - jekyll-include-cache (= 0.2.1) - jekyll-mentions (= 1.6.0) - jekyll-optional-front-matter (= 0.3.2) - jekyll-paginate (= 1.1.0) - jekyll-readme-index (= 0.3.0) - jekyll-redirect-from (= 0.16.0) - jekyll-relative-links (= 0.6.1) - jekyll-remote-theme (= 0.4.3) - jekyll-sass-converter (= 1.5.2) - jekyll-seo-tag (= 2.8.0) - jekyll-sitemap (= 1.4.0) - jekyll-swiss (= 1.0.0) - jekyll-theme-architect (= 0.2.0) - jekyll-theme-cayman (= 0.2.0) - jekyll-theme-dinky (= 0.2.0) - jekyll-theme-hacker (= 0.2.0) - jekyll-theme-leap-day (= 0.2.0) - jekyll-theme-merlot (= 0.2.0) - jekyll-theme-midnight (= 0.2.0) - jekyll-theme-minimal (= 0.2.0) - jekyll-theme-modernist (= 0.2.0) - jekyll-theme-primer (= 0.6.0) - jekyll-theme-slate (= 0.2.0) - jekyll-theme-tactile (= 0.2.0) - jekyll-theme-time-machine (= 0.2.0) - jekyll-titles-from-headings (= 0.5.3) - jemoji (= 0.13.0) - kramdown (= 2.4.0) - kramdown-parser-gfm (= 1.1.0) - liquid (= 4.0.4) - mercenary (~> 0.3) - minima (= 2.5.1) - nokogiri (>= 1.16.2, < 2.0) - rouge (= 3.30.0) - terminal-table (~> 1.4) - webrick (~> 1.8) - github-pages-health-check (1.18.2) - addressable (~> 2.3) - dnsruby (~> 1.60) - octokit (>= 4, < 8) - public_suffix (>= 3.0, < 6.0) - typhoeus (~> 1.3) - html-pipeline (2.14.3) - activesupport (>= 2) - nokogiri (>= 1.4) - http_parser.rb (0.8.0) - i18n (1.14.7) - concurrent-ruby (~> 1.0) - jekyll (3.10.0) - addressable (~> 2.4) - colorator (~> 1.0) - csv (~> 3.0) - em-websocket (~> 0.5) - i18n (>= 0.7, < 2) - jekyll-sass-converter (~> 1.0) - jekyll-watch (~> 2.0) - kramdown (>= 1.17, < 3) - liquid (~> 4.0) - mercenary (~> 0.3.3) - pathutil (~> 0.9) - rouge (>= 1.7, < 4) - safe_yaml (~> 1.0) - webrick (>= 1.0) - jekyll-avatar (0.8.0) - jekyll (>= 3.0, < 5.0) - jekyll-coffeescript (1.2.2) - coffee-script (~> 2.2) - coffee-script-source (~> 1.12) - jekyll-commonmark (1.4.0) - commonmarker (~> 0.22) - jekyll-commonmark-ghpages (0.5.1) - commonmarker (>= 0.23.7, < 1.1.0) - jekyll (>= 3.9, < 4.0) - jekyll-commonmark (~> 1.4.0) - rouge (>= 2.0, < 5.0) - jekyll-default-layout (0.1.5) - jekyll (>= 3.0, < 5.0) - jekyll-feed (0.17.0) - jekyll (>= 3.7, < 5.0) - jekyll-gist (1.5.0) - octokit (~> 4.2) - jekyll-github-metadata (2.16.1) - jekyll (>= 3.4, < 5.0) - octokit (>= 4, < 7, != 4.4.0) - jekyll-include-cache (0.2.1) - jekyll (>= 3.7, < 5.0) - jekyll-mentions (1.6.0) - html-pipeline (~> 2.3) - jekyll (>= 3.7, < 5.0) - jekyll-optional-front-matter (0.3.2) - jekyll (>= 3.0, < 5.0) - jekyll-paginate (1.1.0) - jekyll-readme-index (0.3.0) - jekyll (>= 3.0, < 5.0) - jekyll-redirect-from (0.16.0) - jekyll (>= 3.3, < 5.0) - jekyll-relative-links (0.6.1) - jekyll (>= 3.3, < 5.0) - jekyll-remote-theme (0.4.3) - addressable (~> 2.0) - jekyll (>= 3.5, < 5.0) - jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) - rubyzip (>= 1.3.0, < 3.0) - jekyll-sass-converter (1.5.2) - sass (~> 3.4) - jekyll-seo-tag (2.8.0) - jekyll (>= 3.8, < 5.0) - jekyll-sitemap (1.4.0) - jekyll (>= 3.7, < 5.0) - jekyll-swiss (1.0.0) - jekyll-theme-architect (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-cayman (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-dinky (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-hacker (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-leap-day (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-merlot (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-midnight (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-minimal (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-modernist (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-primer (0.6.0) - jekyll (> 3.5, < 5.0) - jekyll-github-metadata (~> 2.9) - jekyll-seo-tag (~> 2.0) - jekyll-theme-slate (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-tactile (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-time-machine (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-titles-from-headings (0.5.3) - jekyll (>= 3.3, < 5.0) - jekyll-watch (2.2.1) - listen (~> 3.0) - jemoji (0.13.0) - gemoji (>= 3, < 5) - html-pipeline (~> 2.2) - jekyll (>= 3.0, < 5.0) - json (2.9.1) - kramdown (2.4.0) - rexml - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - liquid (4.0.4) - listen (3.9.0) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) - logger (1.6.5) - mercenary (0.3.6) - minima (2.5.1) - jekyll (>= 3.5, < 5.0) - jekyll-feed (~> 0.9) - jekyll-seo-tag (~> 2.1) - minitest (5.25.4) - net-http (0.6.0) - uri - nokogiri (1.18.2-aarch64-linux-gnu) - racc (~> 1.4) - nokogiri (1.18.2-aarch64-linux-musl) - racc (~> 1.4) - nokogiri (1.18.2-arm-linux-gnu) - racc (~> 1.4) - nokogiri (1.18.2-arm-linux-musl) - racc (~> 1.4) - nokogiri (1.18.2-arm64-darwin) - racc (~> 1.4) - nokogiri (1.18.2-x86_64-darwin) - racc (~> 1.4) - nokogiri (1.18.2-x86_64-linux-gnu) - racc (~> 1.4) - nokogiri (1.18.2-x86_64-linux-musl) - racc (~> 1.4) - octokit (4.25.1) - faraday (>= 1, < 3) - sawyer (~> 0.9) - pathutil (0.16.2) - forwardable-extended (~> 2.6) - public_suffix (5.1.1) - racc (1.8.1) - rb-fsevent (0.11.2) - rb-inotify (0.11.1) - ffi (~> 1.0) - rexml (3.4.0) - rouge (3.30.0) - rubyzip (2.4.1) - safe_yaml (1.0.5) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sawyer (0.9.2) - addressable (>= 2.3.5) - faraday (>= 0.17.3, < 3) - securerandom (0.4.1) - simpleidn (0.2.3) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) - typhoeus (1.4.1) - ethon (>= 0.9.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - unicode-display_width (1.8.0) - uri (1.0.2) - webrick (1.9.1) - -PLATFORMS - aarch64-linux-gnu - aarch64-linux-musl - arm-linux-gnu - arm-linux-musl - arm64-darwin - x86_64-darwin - x86_64-linux-gnu - x86_64-linux-musl - -DEPENDENCIES - github-pages - -BUNDLED WITH - 2.5.18 diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 8d2cfe4a38..0000000000 --- a/docs/_config.yml +++ /dev/null @@ -1,15 +0,0 @@ -title: Roo Code Documentation -description: Documentation for the Roo Code project -remote_theme: just-the-docs/just-the-docs - -url: https://docs.roocode.com - -aux_links: - "Roo Code on GitHub": - - "//github.com/RooVetGit/Roo-Code" - -# Enable search -search_enabled: true - -# Enable dark mode -color_scheme: dark diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md deleted file mode 100644 index 9b0f385436..0000000000 --- a/docs/getting-started/index.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Getting Started -layout: default -nav_order: 2 -has_children: true ---- - -# Getting Started with Roo Code - -This section will help you get up and running with Roo Code quickly. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 0cf6022436..0000000000 --- a/docs/index.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Home -layout: home -nav_order: 1 ---- - -# Welcome to Roo Code Documentation - -This is the documentation for Roo Code. Choose a section from the navigation menu to get started. diff --git a/jest.config.js b/jest.config.js index 02a4a78289..d0efc8eaaf 100644 --- a/jest.config.js +++ b/jest.config.js @@ -34,6 +34,7 @@ module.exports = { transformIgnorePatterns: [ "node_modules/(?!(@modelcontextprotocol|delay|p-wait-for|globby|serialize-error|strip-ansi|default-shell|os-name)/)", ], + roots: ["/src", "/webview-ui/src"], modulePathIgnorePatterns: [".vscode-test"], reporters: [["jest-simple-dot-reporter", {}]], setupFiles: [], diff --git a/package-lock.json b/package-lock.json index eecc0e681e..59949791f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.3.9", + "version": "3.3.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.3.9", + "version": "3.3.11", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index a95ace9ee3..69d391a282 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Code (prev. Roo Cline)", "description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.", "publisher": "RooVeterinaryInc", - "version": "3.3.9", + "version": "3.3.11", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index fa650c6e38..bbfc716f6d 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -75,7 +75,7 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit() + /** + * Computes the effective range of text from the given document based on the user's selection. + * If the selection is non-empty, returns that directly. + * Otherwise, if the current line is non-empty, expands the range to include the adjacent lines. + * + * @param document - The text document to extract text from. + * @param range - The user selected range or selection. + * @returns An EffectiveRange object containing the effective range and its text, or null if no valid text is found. + */ static getEffectiveRange( document: vscode.TextDocument, range: vscode.Range | vscode.Selection, @@ -39,16 +73,12 @@ export class EditorUtils { return null } - // Optimize range creation by checking bounds first - const startLine = Math.max(0, currentLine.lineNumber - 1) - const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1) + const startLineIndex = Math.max(0, currentLine.lineNumber - 1) + const endLineIndex = Math.min(document.lineCount - 1, currentLine.lineNumber + 1) - // Only create new positions if needed const effectiveRange = new vscode.Range( - startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0), - endLine === currentLine.lineNumber - ? range.end - : new vscode.Position(endLine, document.lineAt(endLine).text.length), + new vscode.Position(startLineIndex, 0), + new vscode.Position(endLineIndex, document.lineAt(endLineIndex).text.length), ) return { @@ -61,8 +91,15 @@ export class EditorUtils { } } + /** + * Retrieves the file path of a given text document. + * Utilizes an internal cache to avoid redundant computations. + * If the document belongs to a workspace, attempts to compute a relative path; otherwise, returns the absolute fsPath. + * + * @param document - The text document for which to retrieve the file path. + * @returns The file path as a string. + */ static getFilePath(document: vscode.TextDocument): string { - // Check cache first let filePath = this.filePathCache.get(document) if (filePath) { return filePath @@ -77,7 +114,6 @@ export class EditorUtils { filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath } - // Cache the result this.filePathCache.set(document, filePath) return filePath } catch (error) { @@ -86,6 +122,12 @@ export class EditorUtils { } } + /** + * Converts a VSCode Diagnostic object to a local DiagnosticData instance. + * + * @param diagnostic - The VSCode diagnostic to convert. + * @returns The corresponding DiagnosticData object. + */ static createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData { return { message: diagnostic.message, @@ -96,15 +138,36 @@ export class EditorUtils { } } + /** + * Determines whether two VSCode ranges intersect. + * + * @param range1 - The first range. + * @param range2 - The second range. + * @returns True if the ranges intersect; otherwise, false. + */ static hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean { - return !( + if ( + range1.end.line < range2.start.line || + (range1.end.line === range2.start.line && range1.end.character <= range2.start.character) + ) { + return false + } + if ( range2.end.line < range1.start.line || - range2.start.line > range1.end.line || - (range2.end.line === range1.start.line && range2.end.character < range1.start.character) || - (range2.start.line === range1.end.line && range2.start.character > range1.end.character) - ) + (range2.end.line === range1.start.line && range2.end.character <= range1.start.character) + ) { + return false + } + return true } + /** + * Builds the editor context from the provided text editor or from the active text editor. + * The context includes file path, effective selected text, and any diagnostics that intersect with the effective range. + * + * @param editor - (Optional) A specific text editor instance. If not provided, the active text editor is used. + * @returns An EditorContext object if successful; otherwise, null. + */ static getEditorContext(editor?: vscode.TextEditor): EditorContext | null { try { if (!editor) { diff --git a/src/core/__tests__/EditorUtils.test.ts b/src/core/__tests__/EditorUtils.test.ts index 88d64e6da8..1a01838693 100644 --- a/src/core/__tests__/EditorUtils.test.ts +++ b/src/core/__tests__/EditorUtils.test.ts @@ -1,20 +1,35 @@ import * as vscode from "vscode" import { EditorUtils } from "../EditorUtils" -// Mock VSCode API -jest.mock("vscode", () => ({ - Range: jest.fn().mockImplementation((startLine, startChar, endLine, endChar) => ({ - start: { line: startLine, character: startChar }, - end: { line: endLine, character: endChar }, - })), - Position: jest.fn().mockImplementation((line, character) => ({ - line, - character, - })), - workspace: { - getWorkspaceFolder: jest.fn(), - }, -})) +// Use simple classes to simulate VSCode's Range and Position behavior. +jest.mock("vscode", () => { + class MockPosition { + constructor( + public line: number, + public character: number, + ) {} + } + class MockRange { + start: MockPosition + end: MockPosition + constructor(start: MockPosition, end: MockPosition) { + this.start = start + this.end = end + } + } + + return { + Range: MockRange, + Position: MockPosition, + workspace: { + getWorkspaceFolder: jest.fn(), + }, + window: { activeTextEditor: undefined }, + languages: { + getDiagnostics: jest.fn(() => []), + }, + } +}) describe("EditorUtils", () => { let mockDocument: any @@ -30,7 +45,7 @@ describe("EditorUtils", () => { describe("getEffectiveRange", () => { it("should return selected text when available", () => { - const mockRange = new vscode.Range(0, 0, 0, 10) + const mockRange = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10)) mockDocument.getText.mockReturnValue("selected text") const result = EditorUtils.getEffectiveRange(mockDocument, mockRange) @@ -42,7 +57,7 @@ describe("EditorUtils", () => { }) it("should return null for empty line", () => { - const mockRange = new vscode.Range(0, 0, 0, 10) + const mockRange = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10)) mockDocument.getText.mockReturnValue("") mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 }) @@ -50,6 +65,58 @@ describe("EditorUtils", () => { expect(result).toBeNull() }) + + it("should expand empty selection to full lines", () => { + // Simulate a caret (empty selection) on line 2 at character 5. + const initialRange = new vscode.Range(new vscode.Position(2, 5), new vscode.Position(2, 5)) + // Return non-empty text for any line with text (lines 1, 2, and 3). + mockDocument.lineAt.mockImplementation((line: number) => { + return { text: `Line ${line} text`, lineNumber: line } + }) + mockDocument.getText.mockImplementation((range: any) => { + // If the range is exactly the empty initial selection, return an empty string. + if ( + range.start.line === initialRange.start.line && + range.start.character === initialRange.start.character && + range.end.line === initialRange.end.line && + range.end.character === initialRange.end.character + ) { + return "" + } + return "expanded text" + }) + + const result = EditorUtils.getEffectiveRange(mockDocument, initialRange) + + expect(result).not.toBeNull() + // Expected effective range: from the beginning of line 1 to the end of line 3. + expect(result?.range.start).toEqual({ line: 1, character: 0 }) + expect(result?.range.end).toEqual({ line: 3, character: 11 }) + expect(result?.text).toBe("expanded text") + }) + }) + + describe("hasIntersectingRange", () => { + it("should return false for ranges that only touch boundaries", () => { + // Range1: [0, 0) - [0, 10) and Range2: [0, 10) - [0, 20) + const range1 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10)) + const range2 = new vscode.Range(new vscode.Position(0, 10), new vscode.Position(0, 20)) + expect(EditorUtils.hasIntersectingRange(range1, range2)).toBe(false) + }) + + it("should return true for overlapping ranges", () => { + // Range1: [0, 0) - [0, 15) and Range2: [0, 10) - [0, 20) + const range1 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 15)) + const range2 = new vscode.Range(new vscode.Position(0, 10), new vscode.Position(0, 20)) + expect(EditorUtils.hasIntersectingRange(range1, range2)).toBe(true) + }) + + it("should return false for non-overlapping ranges", () => { + // Range1: [0, 0) - [0, 10) and Range2: [1, 0) - [1, 5) + const range1 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10)) + const range2 = new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 5)) + expect(EditorUtils.hasIntersectingRange(range1, range2)).toBe(false) + }) }) describe("getFilePath", () => { diff --git a/src/core/__tests__/mode-validator.test.ts b/src/core/__tests__/mode-validator.test.ts index bd6d3237fd..632ca8a8ab 100644 --- a/src/core/__tests__/mode-validator.test.ts +++ b/src/core/__tests__/mode-validator.test.ts @@ -9,8 +9,8 @@ describe("mode-validator", () => { it("allows all code mode tools", () => { const mode = getModeConfig(codeMode) // Code mode has all groups - Object.entries(TOOL_GROUPS).forEach(([_, tools]) => { - tools.forEach((tool) => { + Object.entries(TOOL_GROUPS).forEach(([_, config]) => { + config.tools.forEach((tool: string) => { expect(isToolAllowedForMode(tool, codeMode, [])).toBe(true) }) }) @@ -25,7 +25,11 @@ describe("mode-validator", () => { it("allows configured tools", () => { const mode = getModeConfig(architectMode) // Architect mode has read, browser, and mcp groups - const architectTools = [...TOOL_GROUPS.read, ...TOOL_GROUPS.browser, ...TOOL_GROUPS.mcp] + const architectTools = [ + ...TOOL_GROUPS.read.tools, + ...TOOL_GROUPS.browser.tools, + ...TOOL_GROUPS.mcp.tools, + ] architectTools.forEach((tool) => { expect(isToolAllowedForMode(tool, architectMode, [])).toBe(true) }) @@ -36,7 +40,7 @@ describe("mode-validator", () => { it("allows configured tools", () => { const mode = getModeConfig(askMode) // Ask mode has read, browser, and mcp groups - const askTools = [...TOOL_GROUPS.read, ...TOOL_GROUPS.browser, ...TOOL_GROUPS.mcp] + const askTools = [...TOOL_GROUPS.read.tools, ...TOOL_GROUPS.browser.tools, ...TOOL_GROUPS.mcp.tools] askTools.forEach((tool) => { expect(isToolAllowedForMode(tool, askMode, [])).toBe(true) }) diff --git a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap index 21c11352fa..20eff3ed27 100644 --- a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap +++ b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap @@ -132,7 +132,7 @@ Example: Requesting to write to frontend-config.json ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +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: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Usage: @@ -291,7 +291,7 @@ RULES SYSTEM INFORMATION Operating System: Linux -Default Shell: /bin/bash +Default Shell: /bin/zsh Home Directory: /home/user Current Working Directory: /test/path @@ -455,7 +455,7 @@ Example: Requesting to write to frontend-config.json ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +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: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Usage: @@ -614,7 +614,7 @@ RULES SYSTEM INFORMATION Operating System: Linux -Default Shell: /bin/bash +Default Shell: /bin/zsh Home Directory: /home/user Current Working Directory: /test/path @@ -778,7 +778,7 @@ Example: Requesting to write to frontend-config.json ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +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: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Usage: @@ -937,7 +937,7 @@ RULES SYSTEM INFORMATION Operating System: Linux -Default Shell: /bin/bash +Default Shell: /bin/zsh Home Directory: /home/user Current Working Directory: /test/path @@ -1147,7 +1147,7 @@ Example: Requesting to click on the element at coordinates 450,300 ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +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: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Usage: @@ -1309,7 +1309,7 @@ RULES SYSTEM INFORMATION Operating System: Linux -Default Shell: /bin/bash +Default Shell: /bin/zsh Home Directory: /home/user Current Working Directory: /test/path @@ -1473,7 +1473,7 @@ Example: Requesting to write to frontend-config.json ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +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: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Usage: @@ -2045,7 +2045,7 @@ RULES SYSTEM INFORMATION Operating System: Linux -Default Shell: /bin/bash +Default Shell: /bin/zsh Home Directory: /home/user Current Working Directory: /test/path @@ -2255,7 +2255,7 @@ Example: Requesting to click on the element at coordinates 450,300 ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +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: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Usage: @@ -2417,7 +2417,7 @@ RULES SYSTEM INFORMATION Operating System: Linux -Default Shell: /bin/bash +Default Shell: /bin/zsh Home Directory: /home/user Current Working Directory: /test/path @@ -2641,7 +2641,7 @@ Your search/replace content here ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +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: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Usage: @@ -2802,7 +2802,7 @@ RULES SYSTEM INFORMATION Operating System: Linux -Default Shell: /bin/bash +Default Shell: /bin/zsh Home Directory: /home/user Current Working Directory: /test/path @@ -2966,7 +2966,7 @@ Example: Requesting to write to frontend-config.json ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +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: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Usage: @@ -3125,7 +3125,7 @@ RULES SYSTEM INFORMATION Operating System: Linux -Default Shell: /bin/bash +Default Shell: /bin/zsh Home Directory: /home/user Current Working Directory: /test/path @@ -3331,7 +3331,7 @@ Example: Requesting to write to frontend-config.json ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +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: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Usage: @@ -3549,7 +3549,7 @@ RULES SYSTEM INFORMATION Operating System: Linux -Default Shell: /bin/bash +Default Shell: /bin/zsh Home Directory: /home/user Current Working Directory: /test/path @@ -3597,7 +3597,7 @@ Mock generic rules" `; exports[`addCustomInstructions should generate correct prompt for architect mode 1`] = ` -"You are Roo, a software architecture expert specializing in analyzing codebases, identifying patterns, and providing high-level technical guidance. You excel at understanding complex systems, evaluating architectural decisions, and suggesting improvements. You can edit markdown documentation files to help document architectural decisions and patterns. +"You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution. ==== @@ -3873,7 +3873,7 @@ RULES SYSTEM INFORMATION Operating System: Linux -Default Shell: /bin/bash +Default Shell: /bin/zsh Home Directory: /home/user Current Working Directory: /test/path @@ -3898,6 +3898,11 @@ USER'S CUSTOM INSTRUCTIONS The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines. +Mode-specific Instructions: +Depending on the user's request, you may need to do some information gathering (for example using read_file or search_files) to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. (You can write the plan to a markdown file if it seems appropriate.) + +Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it. Finally once it seems like you've reached a good plan, use the switch_mode tool to request that the user switch to another mode to implement the solution. + Rules: # Rules from .clinerules-architect: Mock mode-specific rules @@ -3906,7 +3911,7 @@ Mock generic rules" `; exports[`addCustomInstructions should generate correct prompt for ask mode 1`] = ` -"You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. You can analyze code, explain concepts, and access external resources. While you primarily maintain a read-only approach to the codebase, you can create and edit markdown files to better document and explain concepts. Make sure to answer the user's questions and don't rush to switch to implementing code. +"You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. ==== @@ -4182,7 +4187,7 @@ RULES SYSTEM INFORMATION Operating System: Linux -Default Shell: /bin/bash +Default Shell: /bin/zsh Home Directory: /home/user Current Working Directory: /test/path @@ -4207,6 +4212,9 @@ USER'S CUSTOM INSTRUCTIONS The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines. +Mode-specific Instructions: +You can analyze code, explain concepts, and access external resources. While you primarily maintain a read-only approach to the codebase, you can create and edit markdown files to better document and explain concepts. Make sure to answer the user's questions and don't rush to switch to implementing code. + Rules: # Rules from .clinerules-ask: Mock mode-specific rules @@ -4376,7 +4384,7 @@ Example: Requesting to write to frontend-config.json ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +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: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Usage: @@ -4948,7 +4956,7 @@ RULES SYSTEM INFORMATION Operating System: Linux -Default Shell: /bin/bash +Default Shell: /bin/zsh Home Directory: /home/user Current Working Directory: /test/path diff --git a/src/core/prompts/__tests__/system.test.ts b/src/core/prompts/__tests__/system.test.ts index 9ee0f87086..2adfa927eb 100644 --- a/src/core/prompts/__tests__/system.test.ts +++ b/src/core/prompts/__tests__/system.test.ts @@ -65,10 +65,14 @@ jest.mock("os", () => ({ homedir: () => "/home/user", })) -jest.mock("default-shell", () => "/bin/bash") +jest.mock("default-shell", () => "/bin/zsh") jest.mock("os-name", () => () => "Linux") +jest.mock("../../../utils/shell", () => ({ + getShell: () => "/bin/zsh", +})) + // Create a mock ExtensionContext const mockContext = { extensionPath: "/mock/extension/path", diff --git a/src/core/prompts/sections/system-info.ts b/src/core/prompts/sections/system-info.ts index 2e04a6fb00..c5a5ec1c28 100644 --- a/src/core/prompts/sections/system-info.ts +++ b/src/core/prompts/sections/system-info.ts @@ -2,6 +2,7 @@ import defaultShell from "default-shell" import os from "os" import osName from "os-name" import { Mode, ModeConfig, getModeBySlug, defaultModeSlug, isToolAllowedForMode } from "../../../shared/modes" +import { getShell } from "../../../utils/shell" export function getSystemInfoSection(cwd: string, currentMode: Mode, customModes?: ModeConfig[]): string { const findModeBySlug = (slug: string, modes?: ModeConfig[]) => modes?.find((m) => m.slug === slug) @@ -14,7 +15,7 @@ export function getSystemInfoSection(cwd: string, currentMode: Mode, customModes SYSTEM INFORMATION Operating System: ${osName()} -Default Shell: ${defaultShell} +Default Shell: ${getShell()} Home Directory: ${os.homedir().toPosix()} Current Working Directory: ${cwd.toPosix()} diff --git a/src/core/prompts/tools/execute-command.ts b/src/core/prompts/tools/execute-command.ts index e773a2f72c..b0a88a858d 100644 --- a/src/core/prompts/tools/execute-command.ts +++ b/src/core/prompts/tools/execute-command.ts @@ -2,7 +2,7 @@ import { ToolArgs } from "./types" export function getExecuteCommandDescription(args: ToolArgs): string | undefined { return `## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${args.cwd} +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: ${args.cwd} Parameters: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. Usage: diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index c4bdfc98c3..1b9b9a43d9 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -66,7 +66,7 @@ export function getToolDescriptionsForMode( const groupName = getGroupName(groupEntry) const toolGroup = TOOL_GROUPS[groupName] if (toolGroup) { - toolGroup.forEach((tool) => { + toolGroup.tools.forEach((tool) => { if (isToolAllowedForMode(tool as ToolName, mode, customModes ?? [], experiments ?? {})) { tools.add(tool) } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index aed9ee2f29..96526491ed 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -256,11 +256,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await visibleProvider.initClineWithTask(prompt) } - resolveWebviewView( - webviewView: vscode.WebviewView | vscode.WebviewPanel, - //context: vscode.WebviewViewResolveContext, used to recreate a deallocated webview, but we don't need this since we use retainContextWhenHidden - //token: vscode.CancellationToken - ): void | Thenable { + async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) { this.outputChannel.appendLine("Resolving webview view") this.view = webviewView @@ -277,7 +273,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { webviewView.webview.html = this.context.extensionMode === vscode.ExtensionMode.Development - ? this.getHMRHtmlContent(webviewView.webview) + ? await this.getHMRHtmlContent(webviewView.webview) : this.getHtmlContent(webviewView.webview) // Sets up an event listener to listen for messages passed from the webview view context @@ -402,9 +398,22 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.view?.webview.postMessage(message) } - private getHMRHtmlContent(webview: vscode.Webview): string { - const nonce = getNonce() + private async getHMRHtmlContent(webview: vscode.Webview): Promise { + const localPort = "5173" + const localServerUrl = `localhost:${localPort}` + // Check if local dev server is running. + try { + await axios.get(`http://${localServerUrl}`) + } catch (error) { + vscode.window.showErrorMessage( + "Local development server is not running, HMR will not work. Please run 'npm run dev' before launching the extension to enable HMR.", + ) + + return this.getHtmlContent(webview) + } + + const nonce = getNonce() const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"]) const codiconsUri = getUri(webview, this.context.extensionUri, [ "node_modules", @@ -415,8 +424,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { ]) const file = "src/index.tsx" - const localPort = "5173" - const localServerUrl = `localhost:${localPort}` const scriptUri = `http://${localServerUrl}/${file}` const reactRefresh = /*html*/ ` diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 664b7b5f0c..8ca919b27e 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -1,13 +1,17 @@ -import { ClineProvider } from "../ClineProvider" +// npx jest src/core/webview/__tests__/ClineProvider.test.ts + import * as vscode from "vscode" +import axios from "axios" + +import { ClineProvider } from "../ClineProvider" import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage" import { setSoundEnabled } from "../../../utils/sound" -import { defaultModeSlug, modes } from "../../../shared/modes" -import { addCustomInstructions } from "../../prompts/sections/custom-instructions" -import { experimentDefault, experiments } from "../../../shared/experiments" +import { defaultModeSlug } from "../../../shared/modes" +import { experimentDefault } from "../../../shared/experiments" // Mock custom-instructions module const mockAddCustomInstructions = jest.fn() + jest.mock("../../prompts/sections/custom-instructions", () => ({ addCustomInstructions: mockAddCustomInstructions, })) @@ -202,7 +206,6 @@ describe("ClineProvider", () => { let mockOutputChannel: vscode.OutputChannel let mockWebviewView: vscode.WebviewView let mockPostMessage: jest.Mock - let visibilityChangeCallback: (e?: unknown) => void beforeEach(() => { // Reset mocks @@ -270,13 +273,13 @@ describe("ClineProvider", () => { return { dispose: jest.fn() } }), onDidChangeVisibility: jest.fn().mockImplementation((callback) => { - visibilityChangeCallback = callback return { dispose: jest.fn() } }), } as unknown as vscode.WebviewView provider = new ClineProvider(mockContext, mockOutputChannel) - // @ts-ignore - accessing private property for testing + + // @ts-ignore - Accessing private property for testing. provider.customModesManager = mockCustomModesManager }) @@ -288,18 +291,36 @@ describe("ClineProvider", () => { expect(ClineProvider.getVisibleInstance()).toBe(provider) }) - test("resolveWebviewView sets up webview correctly", () => { - provider.resolveWebviewView(mockWebviewView) + test("resolveWebviewView sets up webview correctly", async () => { + await provider.resolveWebviewView(mockWebviewView) expect(mockWebviewView.webview.options).toEqual({ enableScripts: true, localResourceRoots: [mockContext.extensionUri], }) + + expect(mockWebviewView.webview.html).toContain("") + }) + + test("resolveWebviewView sets up webview correctly in development mode even if local server is not running", async () => { + provider = new ClineProvider( + { ...mockContext, extensionMode: vscode.ExtensionMode.Development }, + mockOutputChannel, + ) + ;(axios.get as jest.Mock).mockRejectedValueOnce(new Error("Network error")) + + await provider.resolveWebviewView(mockWebviewView) + + expect(mockWebviewView.webview.options).toEqual({ + enableScripts: true, + localResourceRoots: [mockContext.extensionUri], + }) + expect(mockWebviewView.webview.html).toContain("") }) test("postMessageToWebview sends message to webview", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const mockState: ExtensionState = { version: "1.0.0", @@ -341,7 +362,7 @@ describe("ClineProvider", () => { }) test("handles webviewDidLaunch message", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) // Get the message handler from onDidReceiveMessage const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] @@ -420,7 +441,7 @@ describe("ClineProvider", () => { }) test("handles writeDelayMs message", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] await messageHandler({ type: "writeDelayMs", value: 2000 }) @@ -430,7 +451,7 @@ describe("ClineProvider", () => { }) test("updates sound utility when sound setting changes", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) // Get the message handler from onDidReceiveMessage const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] @@ -470,7 +491,7 @@ describe("ClineProvider", () => { }) test("loads saved API config when switching modes", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] // Mock ConfigManager methods @@ -491,7 +512,7 @@ describe("ClineProvider", () => { }) test("saves current config when switching to mode without config", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] // Mock ConfigManager methods @@ -519,7 +540,7 @@ describe("ClineProvider", () => { }) test("saves config as default for current mode when loading config", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] provider.configManager = { @@ -540,7 +561,7 @@ describe("ClineProvider", () => { }) test("handles request delay settings messages", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] // Test alwaysApproveResubmit @@ -555,7 +576,7 @@ describe("ClineProvider", () => { }) test("handles updatePrompt message correctly", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] // Mock existing prompts @@ -650,7 +671,7 @@ describe("ClineProvider", () => { ) }) test("handles mode-specific custom instructions updates", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] // Mock existing prompts @@ -707,7 +728,7 @@ describe("ClineProvider", () => { // Create new provider with updated mock context provider = new ClineProvider(mockContext, mockOutputChannel) - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] provider.configManager = { @@ -732,10 +753,10 @@ describe("ClineProvider", () => { }) describe("deleteMessage", () => { - beforeEach(() => { + beforeEach(async () => { // Mock window.showInformationMessage ;(vscode.window.showInformationMessage as jest.Mock) = jest.fn() - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) }) test('handles "Just this message" deletion correctly', async () => { @@ -861,9 +882,9 @@ describe("ClineProvider", () => { }) describe("getSystemPrompt", () => { - beforeEach(() => { + beforeEach(async () => { mockPostMessage.mockClear() - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) // Reset and setup mock mockAddCustomInstructions.mockClear() mockAddCustomInstructions.mockImplementation( @@ -1111,7 +1132,7 @@ describe("ClineProvider", () => { }) // Resolve webview and trigger getSystemPrompt - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const architectHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] await architectHandler({ type: "getSystemPrompt" }) @@ -1125,9 +1146,9 @@ describe("ClineProvider", () => { }) describe("handleModeSwitch", () => { - beforeEach(() => { + beforeEach(async () => { // Set up webview for each test - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) }) test("loads saved API config when switching modes", async () => { @@ -1188,7 +1209,7 @@ describe("ClineProvider", () => { describe("updateCustomMode", () => { test("updates both file and state when updating custom mode", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] // Mock CustomModesManager methods @@ -1252,4 +1273,129 @@ describe("ClineProvider", () => { ) }) }) + + describe("upsertApiConfiguration", () => { + test("handles error in upsertApiConfiguration gracefully", async () => { + provider.resolveWebviewView(mockWebviewView) + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] + + // Mock ConfigManager methods to simulate error + provider.configManager = { + setModeConfig: jest.fn().mockRejectedValue(new Error("Failed to update mode config")), + listConfig: jest + .fn() + .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]), + } as any + + // Mock getState to provide necessary data + jest.spyOn(provider, "getState").mockResolvedValue({ + mode: "code", + currentApiConfigName: "test-config", + } as any) + + // Trigger updateApiConfiguration + await messageHandler({ + type: "upsertApiConfiguration", + text: "test-config", + apiConfiguration: { + apiProvider: "anthropic", + apiKey: "test-key", + }, + }) + + // Verify error was logged and user was notified + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + expect.stringContaining("Error create new api configuration"), + ) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to create api configuration") + }) + + test("handles successful upsertApiConfiguration", async () => { + provider.resolveWebviewView(mockWebviewView) + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] + + // Mock ConfigManager methods + provider.configManager = { + saveConfig: jest.fn().mockResolvedValue(undefined), + listConfig: jest + .fn() + .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]), + } as any + + const testApiConfig = { + apiProvider: "anthropic" as const, + apiKey: "test-key", + } + + // Trigger upsertApiConfiguration + await messageHandler({ + type: "upsertApiConfiguration", + text: "test-config", + apiConfiguration: testApiConfig, + }) + + // Verify config was saved + expect(provider.configManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig) + + // Verify state updates + expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [ + { name: "test-config", id: "test-id", apiProvider: "anthropic" }, + ]) + expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config") + + // Verify state was posted to webview + expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" })) + }) + + test("handles buildApiHandler error in updateApiConfiguration", async () => { + provider.resolveWebviewView(mockWebviewView) + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] + + // Mock buildApiHandler to throw an error + const { buildApiHandler } = require("../../../api") + ;(buildApiHandler as jest.Mock).mockImplementationOnce(() => { + throw new Error("API handler error") + }) + + // Mock ConfigManager methods + provider.configManager = { + saveConfig: jest.fn().mockResolvedValue(undefined), + listConfig: jest + .fn() + .mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]), + } as any + + // Setup mock Cline instance + const mockCline = { + api: undefined, + abortTask: jest.fn(), + } + // @ts-ignore - accessing private property for testing + provider.cline = mockCline + + const testApiConfig = { + apiProvider: "anthropic" as const, + apiKey: "test-key", + } + + // Trigger upsertApiConfiguration + await messageHandler({ + type: "upsertApiConfiguration", + text: "test-config", + apiConfiguration: testApiConfig, + }) + + // Verify error handling + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + expect.stringContaining("Error create new api configuration"), + ) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to create api configuration") + + // Verify state was still updated + expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [ + { name: "test-config", id: "test-id", apiProvider: "anthropic" }, + ]) + expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config") + }) + }) }) diff --git a/src/shared/modes.ts b/src/shared/modes.ts index abbeea72b7..f1b899cc5c 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -59,7 +59,8 @@ export function getToolsForMode(groups: readonly GroupEntry[]): string[] { // Add tools from each group groups.forEach((group) => { const groupName = getGroupName(group) - TOOL_GROUPS[groupName].forEach((tool) => tools.add(tool)) + const groupConfig = TOOL_GROUPS[groupName] + groupConfig.tools.forEach((tool: string) => tools.add(tool)) }) // Always add required tools @@ -81,15 +82,19 @@ export const modes: readonly ModeConfig[] = [ slug: "architect", name: "Architect", roleDefinition: - "You are Roo, a software architecture expert specializing in analyzing codebases, identifying patterns, and providing high-level technical guidance. You excel at understanding complex systems, evaluating architectural decisions, and suggesting improvements. You can edit markdown documentation files to help document architectural decisions and patterns.", + "You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution.", groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"], + customInstructions: + "Depending on the user's request, you may need to do some information gathering (for example using read_file or search_files) to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. (You can write the plan to a markdown file if it seems appropriate.)\n\nThen you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it. Finally once it seems like you've reached a good plan, use the switch_mode tool to request that the user switch to another mode to implement the solution.", }, { slug: "ask", name: "Ask", roleDefinition: - "You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. You can analyze code, explain concepts, and access external resources. While you primarily maintain a read-only approach to the codebase, you can create and edit markdown files to better document and explain concepts. Make sure to answer the user's questions and don't rush to switch to implementing code.", + "You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics.", groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"], + customInstructions: + "You can analyze code, explain concepts, and access external resources. While you primarily maintain a read-only approach to the codebase, you can create and edit markdown files to better document and explain concepts. Make sure to answer the user's questions and don't rush to switch to implementing code.", }, ] as const @@ -190,8 +195,10 @@ export function isToolAllowedForMode( const groupName = getGroupName(group) const options = getGroupOptions(group) - // If the tool isn't in this group, continue to next group - if (!TOOL_GROUPS[groupName].includes(tool)) { + const groupConfig = TOOL_GROUPS[groupName] + + // If the tool isn't in this group's tools, continue to next group + if (!groupConfig.tools.includes(tool)) { continue } @@ -220,7 +227,15 @@ export function isToolAllowedForMode( // Create the mode-specific default prompts export const defaultPrompts: Readonly = Object.freeze( - Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])), + Object.fromEntries( + modes.map((mode) => [ + mode.slug, + { + roleDefinition: mode.roleDefinition, + customInstructions: mode.customInstructions, + }, + ]), + ), ) // Helper function to safely get role definition @@ -232,3 +247,13 @@ export function getRoleDefinition(modeSlug: string, customModes?: ModeConfig[]): } return mode.roleDefinition } + +// Helper function to safely get custom instructions +export function getCustomInstructions(modeSlug: string, customModes?: ModeConfig[]): string { + const mode = getModeBySlug(modeSlug, customModes) + if (!mode) { + console.warn(`No mode found for slug: ${modeSlug}`) + return "" + } + return mode.customInstructions ?? "" +} diff --git a/src/shared/tool-groups.ts b/src/shared/tool-groups.ts index 055fb11660..2728d42319 100644 --- a/src/shared/tool-groups.ts +++ b/src/shared/tool-groups.ts @@ -1,5 +1,8 @@ -// Define tool group values -export type ToolGroupValues = readonly string[] +// Define tool group configuration +export type ToolGroupConfig = { + tools: readonly string[] + alwaysAvailable?: boolean // Whether this group is always available and shouldn't show in prompts view +} // Map of tool slugs to their display names export const TOOL_DISPLAY_NAMES = { @@ -20,13 +23,26 @@ export const TOOL_DISPLAY_NAMES = { } as const // Define available tool groups -export const TOOL_GROUPS: Record = { - read: ["read_file", "search_files", "list_files", "list_code_definition_names"], - edit: ["write_to_file", "apply_diff", "insert_content", "search_and_replace"], - browser: ["browser_action"], - command: ["execute_command"], - mcp: ["use_mcp_tool", "access_mcp_resource"], - modes: ["switch_mode", "new_task"], +export const TOOL_GROUPS: Record = { + read: { + tools: ["read_file", "search_files", "list_files", "list_code_definition_names"], + }, + edit: { + tools: ["write_to_file", "apply_diff", "insert_content", "search_and_replace"], + }, + browser: { + tools: ["browser_action"], + }, + command: { + tools: ["execute_command"], + }, + mcp: { + tools: ["use_mcp_tool", "access_mcp_resource"], + }, + modes: { + tools: ["switch_mode", "new_task"], + alwaysAvailable: true, + }, } export type ToolGroup = keyof typeof TOOL_GROUPS diff --git a/src/test/task.test.ts b/src/test/task.test.ts index cda766c713..63f4d4777a 100644 --- a/src/test/task.test.ts +++ b/src/test/task.test.ts @@ -31,7 +31,7 @@ suite("Roo Code Task", () => { try { // Initialize provider with panel. - provider.resolveWebviewView(panel) + await provider.resolveWebviewView(panel) // Wait for webview to launch. let startTime = Date.now() diff --git a/src/utils/__tests__/shell.test.ts b/src/utils/__tests__/shell.test.ts new file mode 100644 index 0000000000..9c2b23aaa5 --- /dev/null +++ b/src/utils/__tests__/shell.test.ts @@ -0,0 +1,228 @@ +import * as vscode from "vscode" +import { userInfo } from "os" +import { getShell } from "../shell" + +describe("Shell Detection Tests", () => { + let originalPlatform: string + let originalEnv: NodeJS.ProcessEnv + let originalGetConfig: any + let originalUserInfo: any + + // Helper to mock VS Code configuration + function mockVsCodeConfig(platformKey: string, defaultProfileName: string | null, profiles: Record) { + vscode.workspace.getConfiguration = () => + ({ + get: (key: string) => { + if (key === `defaultProfile.${platformKey}`) { + return defaultProfileName + } + if (key === `profiles.${platformKey}`) { + return profiles + } + return undefined + }, + }) as any + } + + beforeEach(() => { + // Store original references + originalPlatform = process.platform + originalEnv = { ...process.env } + originalGetConfig = vscode.workspace.getConfiguration + originalUserInfo = userInfo + + // Clear environment variables for a clean test + delete process.env.SHELL + delete process.env.COMSPEC + + // Default userInfo() mock + ;(userInfo as any) = () => ({ shell: null }) + }) + + afterEach(() => { + // Restore everything + Object.defineProperty(process, "platform", { value: originalPlatform }) + process.env = originalEnv + vscode.workspace.getConfiguration = originalGetConfig + ;(userInfo as any) = originalUserInfo + }) + + // -------------------------------------------------------------------------- + // Windows Shell Detection + // -------------------------------------------------------------------------- + describe("Windows Shell Detection", () => { + beforeEach(() => { + Object.defineProperty(process, "platform", { value: "win32" }) + }) + + it("uses explicit PowerShell 7 path from VS Code config (profile path)", () => { + mockVsCodeConfig("windows", "PowerShell", { + PowerShell: { path: "C:\\Program Files\\PowerShell\\7\\pwsh.exe" }, + }) + expect(getShell()).toBe("C:\\Program Files\\PowerShell\\7\\pwsh.exe") + }) + + it("uses PowerShell 7 path if source is 'PowerShell' but no explicit path", () => { + mockVsCodeConfig("windows", "PowerShell", { + PowerShell: { source: "PowerShell" }, + }) + expect(getShell()).toBe("C:\\Program Files\\PowerShell\\7\\pwsh.exe") + }) + + it("falls back to legacy PowerShell if profile includes 'powershell' but no path/source", () => { + mockVsCodeConfig("windows", "PowerShell", { + PowerShell: {}, + }) + expect(getShell()).toBe("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe") + }) + + it("uses WSL bash when profile indicates WSL source", () => { + mockVsCodeConfig("windows", "WSL", { + WSL: { source: "WSL" }, + }) + expect(getShell()).toBe("/bin/bash") + }) + + it("uses WSL bash when profile name includes 'wsl'", () => { + mockVsCodeConfig("windows", "Ubuntu WSL", { + "Ubuntu WSL": {}, + }) + expect(getShell()).toBe("/bin/bash") + }) + + it("defaults to cmd.exe if no special profile is matched", () => { + mockVsCodeConfig("windows", "CommandPrompt", { + CommandPrompt: {}, + }) + expect(getShell()).toBe("C:\\Windows\\System32\\cmd.exe") + }) + + it("handles undefined profile gracefully", () => { + // Mock a case where defaultProfileName exists but the profile doesn't + mockVsCodeConfig("windows", "NonexistentProfile", {}) + expect(getShell()).toBe("C:\\Windows\\System32\\cmd.exe") + }) + + it("respects userInfo() if no VS Code config is available", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + ;(userInfo as any) = () => ({ shell: "C:\\Custom\\PowerShell.exe" }) + + expect(getShell()).toBe("C:\\Custom\\PowerShell.exe") + }) + + it("respects an odd COMSPEC if no userInfo shell is available", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + process.env.COMSPEC = "D:\\CustomCmd\\cmd.exe" + + expect(getShell()).toBe("D:\\CustomCmd\\cmd.exe") + }) + }) + + // -------------------------------------------------------------------------- + // macOS Shell Detection + // -------------------------------------------------------------------------- + describe("macOS Shell Detection", () => { + beforeEach(() => { + Object.defineProperty(process, "platform", { value: "darwin" }) + }) + + it("uses VS Code profile path if available", () => { + mockVsCodeConfig("osx", "MyCustomShell", { + MyCustomShell: { path: "/usr/local/bin/fish" }, + }) + expect(getShell()).toBe("/usr/local/bin/fish") + }) + + it("falls back to userInfo().shell if no VS Code config is available", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + ;(userInfo as any) = () => ({ shell: "/opt/homebrew/bin/zsh" }) + expect(getShell()).toBe("/opt/homebrew/bin/zsh") + }) + + it("falls back to SHELL env var if no userInfo shell is found", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + process.env.SHELL = "/usr/local/bin/zsh" + expect(getShell()).toBe("/usr/local/bin/zsh") + }) + + it("falls back to /bin/zsh if no config, userInfo, or env variable is set", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + expect(getShell()).toBe("/bin/zsh") + }) + }) + + // -------------------------------------------------------------------------- + // Linux Shell Detection + // -------------------------------------------------------------------------- + describe("Linux Shell Detection", () => { + beforeEach(() => { + Object.defineProperty(process, "platform", { value: "linux" }) + }) + + it("uses VS Code profile path if available", () => { + mockVsCodeConfig("linux", "CustomProfile", { + CustomProfile: { path: "/usr/bin/fish" }, + }) + expect(getShell()).toBe("/usr/bin/fish") + }) + + it("falls back to userInfo().shell if no VS Code config is available", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + ;(userInfo as any) = () => ({ shell: "/usr/bin/zsh" }) + expect(getShell()).toBe("/usr/bin/zsh") + }) + + it("falls back to SHELL env var if no userInfo shell is found", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + process.env.SHELL = "/usr/bin/fish" + expect(getShell()).toBe("/usr/bin/fish") + }) + + it("falls back to /bin/bash if nothing is set", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + expect(getShell()).toBe("/bin/bash") + }) + }) + + // -------------------------------------------------------------------------- + // Unknown Platform & Error Handling + // -------------------------------------------------------------------------- + describe("Unknown Platform / Error Handling", () => { + it("falls back to /bin/sh for unknown platforms", () => { + Object.defineProperty(process, "platform", { value: "sunos" }) + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + expect(getShell()).toBe("/bin/sh") + }) + + it("handles VS Code config errors gracefully, falling back to userInfo shell if present", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + vscode.workspace.getConfiguration = () => { + throw new Error("Configuration error") + } + ;(userInfo as any) = () => ({ shell: "/bin/bash" }) + expect(getShell()).toBe("/bin/bash") + }) + + it("handles userInfo errors gracefully, falling back to environment variable if present", () => { + Object.defineProperty(process, "platform", { value: "darwin" }) + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + ;(userInfo as any) = () => { + throw new Error("userInfo error") + } + process.env.SHELL = "/bin/zsh" + expect(getShell()).toBe("/bin/zsh") + }) + + it("falls back fully to default shell paths if everything fails", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + vscode.workspace.getConfiguration = () => { + throw new Error("Configuration error") + } + ;(userInfo as any) = () => { + throw new Error("userInfo error") + } + delete process.env.SHELL + expect(getShell()).toBe("/bin/bash") + }) + }) +}) diff --git a/src/utils/shell.ts b/src/utils/shell.ts new file mode 100644 index 0000000000..2f7ffb3a88 --- /dev/null +++ b/src/utils/shell.ts @@ -0,0 +1,227 @@ +import * as vscode from "vscode" +import { userInfo } from "os" + +const SHELL_PATHS = { + // Windows paths + POWERSHELL_7: "C:\\Program Files\\PowerShell\\7\\pwsh.exe", + POWERSHELL_LEGACY: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + CMD: "C:\\Windows\\System32\\cmd.exe", + WSL_BASH: "/bin/bash", + // Unix paths + MAC_DEFAULT: "/bin/zsh", + LINUX_DEFAULT: "/bin/bash", + CSH: "/bin/csh", + BASH: "/bin/bash", + KSH: "/bin/ksh", + SH: "/bin/sh", + ZSH: "/bin/zsh", + DASH: "/bin/dash", + TCSH: "/bin/tcsh", + FALLBACK: "/bin/sh", +} as const + +interface MacTerminalProfile { + path?: string +} + +type MacTerminalProfiles = Record + +interface WindowsTerminalProfile { + path?: string + source?: "PowerShell" | "WSL" +} + +type WindowsTerminalProfiles = Record + +interface LinuxTerminalProfile { + path?: string +} + +type LinuxTerminalProfiles = Record + +// ----------------------------------------------------- +// 1) VS Code Terminal Configuration Helpers +// ----------------------------------------------------- + +function getWindowsTerminalConfig() { + try { + const config = vscode.workspace.getConfiguration("terminal.integrated") + const defaultProfileName = config.get("defaultProfile.windows") + const profiles = config.get("profiles.windows") || {} + return { defaultProfileName, profiles } + } catch { + return { defaultProfileName: null, profiles: {} as WindowsTerminalProfiles } + } +} + +function getMacTerminalConfig() { + try { + const config = vscode.workspace.getConfiguration("terminal.integrated") + const defaultProfileName = config.get("defaultProfile.osx") + const profiles = config.get("profiles.osx") || {} + return { defaultProfileName, profiles } + } catch { + return { defaultProfileName: null, profiles: {} as MacTerminalProfiles } + } +} + +function getLinuxTerminalConfig() { + try { + const config = vscode.workspace.getConfiguration("terminal.integrated") + const defaultProfileName = config.get("defaultProfile.linux") + const profiles = config.get("profiles.linux") || {} + return { defaultProfileName, profiles } + } catch { + return { defaultProfileName: null, profiles: {} as LinuxTerminalProfiles } + } +} + +// ----------------------------------------------------- +// 2) Platform-Specific VS Code Shell Retrieval +// ----------------------------------------------------- + +/** Attempts to retrieve a shell path from VS Code config on Windows. */ +function getWindowsShellFromVSCode(): string | null { + const { defaultProfileName, profiles } = getWindowsTerminalConfig() + if (!defaultProfileName) { + return null + } + + const profile = profiles[defaultProfileName] + + // If the profile name indicates PowerShell, do version-based detection. + // In testing it was found these typically do not have a path, and this + // implementation manages to deductively get the corect version of PowerShell + if (defaultProfileName.toLowerCase().includes("powershell")) { + if (profile?.path) { + // If there's an explicit PowerShell path, return that + return profile.path + } else if (profile?.source === "PowerShell") { + // If the profile is sourced from PowerShell, assume the newest + return SHELL_PATHS.POWERSHELL_7 + } + // Otherwise, assume legacy Windows PowerShell + return SHELL_PATHS.POWERSHELL_LEGACY + } + + // If there's a specific path, return that immediately + if (profile?.path) { + return profile.path + } + + // If the profile indicates WSL + if (profile?.source === "WSL" || defaultProfileName.toLowerCase().includes("wsl")) { + return SHELL_PATHS.WSL_BASH + } + + // If nothing special detected, we assume cmd + return SHELL_PATHS.CMD +} + +/** Attempts to retrieve a shell path from VS Code config on macOS. */ +function getMacShellFromVSCode(): string | null { + const { defaultProfileName, profiles } = getMacTerminalConfig() + if (!defaultProfileName) { + return null + } + + const profile = profiles[defaultProfileName] + return profile?.path || null +} + +/** Attempts to retrieve a shell path from VS Code config on Linux. */ +function getLinuxShellFromVSCode(): string | null { + const { defaultProfileName, profiles } = getLinuxTerminalConfig() + if (!defaultProfileName) { + return null + } + + const profile = profiles[defaultProfileName] + return profile?.path || null +} + +// ----------------------------------------------------- +// 3) General Fallback Helpers +// ----------------------------------------------------- + +/** + * Tries to get a user’s shell from os.userInfo() (works on Unix if the + * underlying system call is supported). Returns null on error or if not found. + */ +function getShellFromUserInfo(): string | null { + try { + const { shell } = userInfo() + return shell || null + } catch { + return null + } +} + +/** Returns the environment-based shell variable, or null if not set. */ +function getShellFromEnv(): string | null { + const { env } = process + + if (process.platform === "win32") { + // On Windows, COMSPEC typically holds cmd.exe + return env.COMSPEC || "C:\\Windows\\System32\\cmd.exe" + } + + if (process.platform === "darwin") { + // On macOS/Linux, SHELL is commonly the environment variable + return env.SHELL || "/bin/zsh" + } + + if (process.platform === "linux") { + // On Linux, SHELL is commonly the environment variable + return env.SHELL || "/bin/bash" + } + return null +} + +// ----------------------------------------------------- +// 4) Publicly Exposed Shell Getter +// ----------------------------------------------------- + +export function getShell(): string { + // 1. Check VS Code config first. + if (process.platform === "win32") { + // Special logic for Windows + const windowsShell = getWindowsShellFromVSCode() + if (windowsShell) { + return windowsShell + } + } else if (process.platform === "darwin") { + // macOS from VS Code + const macShell = getMacShellFromVSCode() + if (macShell) { + return macShell + } + } else if (process.platform === "linux") { + // Linux from VS Code + const linuxShell = getLinuxShellFromVSCode() + if (linuxShell) { + return linuxShell + } + } + + // 2. If no shell from VS Code, try userInfo() + const userInfoShell = getShellFromUserInfo() + if (userInfoShell) { + return userInfoShell + } + + // 3. If still nothing, try environment variable + const envShell = getShellFromEnv() + if (envShell) { + return envShell + } + + // 4. Finally, fall back to a default + if (process.platform === "win32") { + // On Windows, if we got here, we have no config, no COMSPEC, and one very messed up operating system. + // Use CMD as a last resort + return SHELL_PATHS.CMD + } + // On macOS/Linux, fallback to a POSIX shell - This is the behavior of our old shell detection method. + return SHELL_PATHS.FALLBACK +} diff --git a/webview-ui/.storybook/main.ts b/webview-ui/.storybook/main.ts index 8c6e2eb51d..48ece9def6 100644 --- a/webview-ui/.storybook/main.ts +++ b/webview-ui/.storybook/main.ts @@ -2,12 +2,7 @@ import type { StorybookConfig } from "@storybook/react-vite" const config: StorybookConfig = { stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], - addons: [ - "@storybook/addon-onboarding", - "@storybook/addon-essentials", - "@chromatic-com/storybook", - "@storybook/addon-interactions", - ], + addons: ["@storybook/addon-essentials", "@storybook/addon-interactions"], framework: { name: "@storybook/react-vite", options: {}, diff --git a/webview-ui/.storybook/vscode.css b/webview-ui/.storybook/vscode.css index 07e261846a..8b1e23e890 100644 --- a/webview-ui/.storybook/vscode.css +++ b/webview-ui/.storybook/vscode.css @@ -14,7 +14,7 @@ --vscode-button-foreground: #ffffff; /* "button.foreground" */ --vscode-button-secondaryBackground: #313131; /* "button.secondaryBackground" */ --vscode-button-secondaryForeground: #cccccc; /* "button.secondaryForeground" */ - --vscode-disabledForeground: red; /* "disabledForeground" */ + --vscode-disabledForeground: #313131; /* "disabledForeground" */ --vscode-descriptionForeground: #9d9d9d; /* "descriptionForeground" */ --vscode-focusBorder: #0078d4; /* "focusBorder" */ --vscode-errorForeground: #f85149; /* "errorForeground" */ diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 0334c86798..b336da2b8e 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -8,6 +8,8 @@ "name": "webview-ui", "version": "0.1.0", "dependencies": { + "@radix-ui/react-dropdown-menu": "^2.1.5", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-slot": "^1.1.1", "@tailwindcss/vite": "^4.0.0", "@vscode/webview-ui-toolkit": "^1.4.0", @@ -31,10 +33,8 @@ "vscrui": "^0.2.0" }, "devDependencies": { - "@chromatic-com/storybook": "^3.2.4", "@storybook/addon-essentials": "^8.5.2", "@storybook/addon-interactions": "^8.5.2", - "@storybook/addon-onboarding": "^8.5.2", "@storybook/blocks": "^8.5.2", "@storybook/react": "^8.5.2", "@storybook/react-vite": "^8.5.2", @@ -601,56 +601,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@chromatic-com/storybook": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@chromatic-com/storybook/-/storybook-3.2.4.tgz", - "integrity": "sha512-5/bOOYxfwZ2BktXeqcCpOVAoR6UCoeART5t9FVy22hoo8F291zOuX4y3SDgm10B1GVU/ZTtJWPT2X9wZFlxYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chromatic": "^11.15.0", - "filesize": "^10.0.12", - "jsonfile": "^6.1.0", - "react-confetti": "^6.1.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=16.0.0", - "yarn": ">=1.22.18" - }, - "peerDependencies": { - "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" - } - }, - "node_modules/@chromatic-com/storybook/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@chromatic-com/storybook/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", @@ -1164,6 +1114,44 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -1800,6 +1788,61 @@ "node": ">= 8" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", + "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", + "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", @@ -1815,6 +1858,333 @@ } } }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.4.tgz", + "integrity": "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.5.tgz", + "integrity": "sha512-50ZmEFL1kOuLalPKHrLWvPFMons2fGx9TqQCWlPwDVpbAnaUJ1g4XNcKqFNMQymYU0kKWR4MDDi+9vUQBGFgcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.5", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", + "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.5.tgz", + "integrity": "sha512-uH+3w5heoMJtqVCgYOtYVMECk1TOrkUn0OG0p5MqXC0W2ppcuVeESbou8PTHoqAjbdTEK19AGXBWcEtR5WpEQg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.4", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", + "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", + "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", + "integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", @@ -1833,6 +2203,114 @@ } } }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "license": "MIT" + }, "node_modules/@rollup/pluginutils": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", @@ -2300,20 +2778,6 @@ "storybook": "^8.5.2" } }, - "node_modules/@storybook/addon-onboarding": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-8.5.2.tgz", - "integrity": "sha512-IViKQdBTuF2KSOrhyyq2soT0Je90AZbAAM5SLrVF7Q4H/Pc2lbf1JX8WwAOW2RKH2o7/U2Mvl0SXqNNcwLZC1A==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^8.5.2" - } - }, "node_modules/@storybook/addon-outline": { "version": "8.5.2", "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.5.2.tgz", @@ -3344,7 +3808,7 @@ "version": "18.3.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -3997,6 +4461,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -4623,30 +5099,6 @@ "node": ">= 16" } }, - "node_modules/chromatic": { - "version": "11.25.2", - "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.25.2.tgz", - "integrity": "sha512-/9eQWn6BU1iFsop86t8Au21IksTRxwXAl7if8YHD05L2AbuMjClLWZo5cZojqrJHGKDhTqfrC2X2xE4uSm0iKw==", - "dev": true, - "license": "MIT", - "bin": { - "chroma": "dist/bin.js", - "chromatic": "dist/bin.js", - "chromatic-cli": "dist/bin.js" - }, - "peerDependencies": { - "@chromatic-com/cypress": "^0.*.* || ^1.0.0", - "@chromatic-com/playwright": "^0.*.* || ^1.0.0" - }, - "peerDependenciesMeta": { - "@chromatic-com/cypress": { - "optional": true - }, - "@chromatic-com/playwright": { - "optional": true - } - } - }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -5143,6 +5595,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -6204,16 +6662,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/filesize": { - "version": "10.1.6", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", - "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 10.4.0" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -6410,6 +6858,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -8412,29 +8869,6 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonfile/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -9930,22 +10364,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-confetti": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.2.2.tgz", - "integrity": "sha512-K+kTyOPgX+ZujMZ+Rmb7pZdHBvg+DzinG/w4Eh52WOB8/pfO38efnnrtEZNJmjTvLxc16RBYO+tPM68Fg8viBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tween-functions": "^1.2.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "react": "^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/react-docgen": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.1.1.tgz", @@ -10046,6 +10464,75 @@ "react": ">=16.8" } }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-textarea-autosize": { "version": "8.5.7", "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.7.tgz", @@ -11515,13 +12002,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tween-functions": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", - "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==", - "dev": true, - "license": "BSD" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -11957,6 +12437,27 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-composed-ref": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", @@ -12002,6 +12503,28 @@ } } }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", diff --git a/webview-ui/package.json b/webview-ui/package.json index 7616446955..8229a6aa34 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -13,6 +13,8 @@ "build-storybook": "storybook build" }, "dependencies": { + "@radix-ui/react-dropdown-menu": "^2.1.5", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-slot": "^1.1.1", "@tailwindcss/vite": "^4.0.0", "@vscode/webview-ui-toolkit": "^1.4.0", @@ -36,10 +38,8 @@ "vscrui": "^0.2.0" }, "devDependencies": { - "@chromatic-com/storybook": "^3.2.4", "@storybook/addon-essentials": "^8.5.2", "@storybook/addon-interactions": "^8.5.2", - "@storybook/addon-onboarding": "^8.5.2", "@storybook/blocks": "^8.5.2", "@storybook/react": "^8.5.2", "@storybook/react-vite": "^8.5.2", diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 1ec109f930..15b2666280 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -89,7 +89,7 @@ export const ChatRowContent = ({ } }, [isLast, message.say]) const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => { - if (message.text != null && message.say === "api_req_started") { + if (message.text !== null && message.text !== undefined && message.say === "api_req_started") { const info: ClineApiReqInfo = JSON.parse(message.text) return [info.cost, info.cancelReason, info.streamingFailedMessage] } @@ -183,26 +183,26 @@ export const ChatRowContent = ({ ) return [ - apiReqCancelReason != null ? ( + apiReqCancelReason !== null && apiReqCancelReason !== undefined ? ( apiReqCancelReason === "user_cancelled" ? ( getIconSpan("error", cancelledColor) ) : ( getIconSpan("error", errorColor) ) - ) : cost != null ? ( + ) : cost !== null && cost !== undefined ? ( getIconSpan("check", successColor) ) : apiRequestFailedMessage ? ( getIconSpan("error", errorColor) ) : ( ), - apiReqCancelReason != null ? ( + apiReqCancelReason !== null && apiReqCancelReason !== undefined ? ( apiReqCancelReason === "user_cancelled" ? ( API Request Cancelled ) : ( API Streaming Failed ) - ) : cost != null ? ( + ) : cost !== null && cost !== undefined ? ( API Request ) : apiRequestFailedMessage ? ( API Request Failed @@ -510,7 +510,8 @@ export const ChatRowContent = ({ style={{ ...headerStyle, marginBottom: - (cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage + ((cost === null || cost === undefined) && apiRequestFailedMessage) || + apiReqStreamingFailedMessage ? 10 : 0, justifyContent: "space-between", @@ -524,13 +525,15 @@ export const ChatRowContent = ({
{icon} {title} - 0 ? 1 : 0 }}> + 0 ? 1 : 0 }}> ${Number(cost || 0)?.toFixed(4)}
- {((cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && ( + {(((cost === null || cost === undefined) && apiRequestFailedMessage) || + apiReqStreamingFailedMessage) && ( <>

{apiRequestFailedMessage || apiReqStreamingFailedMessage} diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index a20922db54..ae1b342dfe 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -179,6 +179,18 @@ const ChatTextArea = forwardRef( return } + if (type === ContextMenuOptionType.Mode && value) { + // Handle mode selection + setMode(value) + setInputValue("") + setShowContextMenu(false) + vscode.postMessage({ + type: "mode", + text: value, + }) + return + } + if ( type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder || @@ -242,7 +254,12 @@ const ChatTextArea = forwardRef( event.preventDefault() setSelectedMenuIndex((prevIndex) => { const direction = event.key === "ArrowUp" ? -1 : 1 - const options = getContextMenuOptions(searchQuery, selectedType, queryItems) + const options = getContextMenuOptions( + searchQuery, + selectedType, + queryItems, + getAllModes(customModes), + ) const optionsLength = options.length if (optionsLength === 0) return prevIndex @@ -272,9 +289,12 @@ const ChatTextArea = forwardRef( } if ((event.key === "Enter" || event.key === "Tab") && selectedMenuIndex !== -1) { event.preventDefault() - const selectedOption = getContextMenuOptions(searchQuery, selectedType, queryItems)[ - selectedMenuIndex - ] + const selectedOption = getContextMenuOptions( + searchQuery, + selectedType, + queryItems, + getAllModes(customModes), + )[selectedMenuIndex] if ( selectedOption && selectedOption.type !== ContextMenuOptionType.URL && @@ -340,6 +360,7 @@ const ChatTextArea = forwardRef( setInputValue, justDeletedSpaceAfterMention, queryItems, + customModes, ], ) @@ -360,13 +381,21 @@ const ChatTextArea = forwardRef( setShowContextMenu(showMenu) if (showMenu) { - const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1) - const query = newValue.slice(lastAtIndex + 1, newCursorPosition) - setSearchQuery(query) - if (query.length > 0) { + if (newValue.startsWith("/")) { + // Handle slash command + const query = newValue + setSearchQuery(query) setSelectedMenuIndex(0) } else { - setSelectedMenuIndex(3) // Set to "File" option by default + // Existing @ mention handling + const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1) + const query = newValue.slice(lastAtIndex + 1, newCursorPosition) + setSearchQuery(query) + if (query.length > 0) { + setSelectedMenuIndex(0) + } else { + setSelectedMenuIndex(3) // Set to "File" option by default + } } } else { setSearchQuery("") @@ -614,6 +643,7 @@ const ChatTextArea = forwardRef( setSelectedIndex={setSelectedMenuIndex} selectedType={selectedType} queryItems={queryItems} + modes={getAllModes(customModes)} /> )} diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 519b7e5efb..a102ca1fc2 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -275,7 +275,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie return true } else { const lastApiReqStarted = findLast(modifiedMessages, (message) => message.say === "api_req_started") - if (lastApiReqStarted && lastApiReqStarted.text != null && lastApiReqStarted.say === "api_req_started") { + if ( + lastApiReqStarted && + lastApiReqStarted.text !== null && + lastApiReqStarted.text !== undefined && + lastApiReqStarted.say === "api_req_started" + ) { const cost = JSON.parse(lastApiReqStarted.text).cost if (cost === undefined) { // api request has not finished yet @@ -718,9 +723,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie if (message.say === "api_req_started") { // get last api_req_started in currentGroup to check if it's cancelled. If it is then this api req is not part of the current browser session const lastApiReqStarted = [...currentGroup].reverse().find((m) => m.say === "api_req_started") - if (lastApiReqStarted?.text != null) { + if (lastApiReqStarted?.text !== null && lastApiReqStarted?.text !== undefined) { const info = JSON.parse(lastApiReqStarted.text) - const isCancelled = info.cancelReason != null + const isCancelled = info.cancelReason !== null && info.cancelReason !== undefined if (isCancelled) { endBrowserSession() result.push(message) @@ -873,7 +878,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie const placeholderText = useMemo(() => { const baseText = task ? "Type a message..." : "Type your task here..." - const contextText = "(@ to add context" + 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 @@ -994,7 +999,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie }}> {showAnnouncement && }

-

What can I do for you?

+

What can Roo do for you?

Thanks to the latest breakthroughs in agentic coding capabilities, I can handle complex software development tasks step-by-step. With tools that let me create & edit files, explore diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx index 85ec865ccd..bd631a98d5 100644 --- a/webview-ui/src/components/chat/ContextMenu.tsx +++ b/webview-ui/src/components/chat/ContextMenu.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useMemo, useRef } from "react" import { ContextMenuOptionType, ContextMenuQueryItem, getContextMenuOptions } from "../../utils/context-mentions" import { removeLeadingNonAlphanumeric } from "../common/CodeAccordian" +import { ModeConfig } from "../../../../src/shared/modes" interface ContextMenuProps { onSelect: (type: ContextMenuOptionType, value?: string) => void @@ -10,6 +11,7 @@ interface ContextMenuProps { setSelectedIndex: (index: number) => void selectedType: ContextMenuOptionType | null queryItems: ContextMenuQueryItem[] + modes?: ModeConfig[] } const ContextMenu: React.FC = ({ @@ -20,12 +22,13 @@ const ContextMenu: React.FC = ({ setSelectedIndex, selectedType, queryItems, + modes, }) => { const menuRef = useRef(null) const filteredOptions = useMemo( - () => getContextMenuOptions(searchQuery, selectedType, queryItems), - [searchQuery, selectedType, queryItems], + () => getContextMenuOptions(searchQuery, selectedType, queryItems, modes), + [searchQuery, selectedType, queryItems, modes], ) useEffect(() => { @@ -46,6 +49,25 @@ const ContextMenu: React.FC = ({ const renderOptionContent = (option: ContextMenuQueryItem) => { switch (option.type) { + case ContextMenuOptionType.Mode: + return ( +

+ {option.label} + {option.description && ( + + {option.description} + + )} +
+ ) case ContextMenuOptionType.Problems: return Problems case ContextMenuOptionType.URL: @@ -101,6 +123,8 @@ const ContextMenu: React.FC = ({ const getIconForOption = (option: ContextMenuQueryItem): string => { switch (option.type) { + case ContextMenuOptionType.Mode: + return "symbol-misc" case ContextMenuOptionType.OpenedFile: return "window" case ContextMenuOptionType.File: @@ -174,15 +198,17 @@ const ContextMenu: React.FC = ({ overflow: "hidden", paddingTop: 0, }}> - + {option.type !== ContextMenuOptionType.Mode && getIconForOption(option) && ( + + )} {renderOptionContent(option)}
{(option.type === ContextMenuOptionType.File || diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index 68278785ff..caef1cf94e 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -12,6 +12,7 @@ import { Mode, PromptComponent, getRoleDefinition, + getCustomInstructions, getAllModes, ModeConfig, GroupEntry, @@ -26,8 +27,8 @@ import { import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups" import { vscode } from "../../utils/vscode" -// Get all available groups from GROUP_DISPLAY_NAMES -const availableGroups = Object.keys(TOOL_GROUPS) as ToolGroup[] +// Get all available groups that should show in prompts view +const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable) type PromptsViewProps = { onDone: () => void @@ -65,6 +66,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { const [isToolsEditMode, setIsToolsEditMode] = useState(false) const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false) const [activeSupportTab, setActiveSupportTab] = useState("ENHANCE") + const [selectedModeTab, setSelectedModeTab] = useState(mode) // Direct update functions const updateAgentPrompt = useCallback( @@ -110,26 +112,23 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { text: slug, }) }, []) - - // Handle mode switching with explicit state initialization + // Handle mode tab selection without actually switching modes const handleModeSwitch = useCallback( (modeConfig: ModeConfig) => { - if (modeConfig.slug === mode) return // Prevent unnecessary updates + if (modeConfig.slug === selectedModeTab) return // Prevent unnecessary updates - // First switch the mode - switchMode(modeConfig.slug) - - // Exit tools edit mode when switching modes + // Update selected tab and reset tools edit mode + setSelectedModeTab(modeConfig.slug) setIsToolsEditMode(false) }, - [mode, switchMode, setIsToolsEditMode], + [selectedModeTab, setIsToolsEditMode], ) // Helper function to get current mode's config const getCurrentMode = useCallback((): ModeConfig | undefined => { - const findMode = (m: ModeConfig): boolean => m.slug === mode + const findMode = (m: ModeConfig): boolean => m.slug === selectedModeTab return customModes?.find(findMode) || modes.find(findMode) - }, [mode, customModes, modes]) + }, [selectedModeTab, customModes, modes]) // Helper function to safely access mode properties const getModeProperty = ( @@ -155,6 +154,11 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { } }, [isCreateModeDialogOpen]) + // Keep selected tab in sync with actual mode + useEffect(() => { + setSelectedModeTab(mode) + }, [mode]) + // Helper function to generate a unique slug from a name const generateSlug = useCallback((name: string, attempt = 0): string => { const baseSlug = name @@ -184,22 +188,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { groups: newModeGroups, } updateCustomMode(newModeSlug, newMode) - switchMode(newModeSlug) setIsCreateModeDialogOpen(false) setNewModeName("") setNewModeSlug("") setNewModeRoleDefinition("") setNewModeCustomInstructions("") setNewModeGroups(availableGroups) - }, [ - newModeName, - newModeSlug, - newModeRoleDefinition, - newModeCustomInstructions, - newModeGroups, - updateCustomMode, - switchMode, - ]) + }, [newModeName, newModeSlug, newModeRoleDefinition, newModeCustomInstructions, newModeGroups, updateCustomMode]) const isNameOrSlugTaken = useCallback( (name: string, slug: string) => { @@ -278,12 +273,16 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { }) } - const handleAgentReset = (modeSlug: string) => { - // Only reset role definition for built-in modes + const handleAgentReset = (modeSlug: string, type: "roleDefinition" | "customInstructions") => { + // Only reset for built-in modes const existingPrompt = customModePrompts?.[modeSlug] as PromptComponent - updateAgentPrompt(modeSlug, { - ...existingPrompt, - roleDefinition: undefined, + const updatedPrompt = { ...existingPrompt } + delete updatedPrompt[type] // Remove the field entirely to ensure it reloads from defaults + + vscode.postMessage({ + type: "updatePrompt", + promptMode: modeSlug, + customPrompt: updatedPrompt, }) } @@ -472,16 +471,14 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
{modes.map((modeConfig) => { - const isActive = mode === modeConfig.slug + const isActive = selectedModeTab === modeConfig.slug return ( + + + Label + + + Item 1 + + Item 2⌘2 + + + + + + Submenu + + + Foo + + Bar + ⌘B + + + Baz + + + + + + + ), +} + +type DropdownMenuVariantProps = { + side?: "top" | "bottom" | "left" | "right" + align?: "start" | "center" | "end" + children?: React.ReactNode +} + +const DropdownMenuVariant = ({ side = "bottom", align = "center", children }: DropdownMenuVariantProps) => ( + + + + + + Foo + Bar + Baz + + +) + +export const Placements: Story = { + render: () => ( +
+ + + + + + + + + + + + +
+ ), +} + +export const Alignments: Story = { + render: () => ( +
+ + + + + + + + + +
+ ), +} diff --git a/webview-ui/src/stories/vscrui/Dropdown.stories.tsx b/webview-ui/src/stories/vscrui/Dropdown.stories.tsx new file mode 100644 index 0000000000..5509eb969f --- /dev/null +++ b/webview-ui/src/stories/vscrui/Dropdown.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Dropdown } from "vscrui" + +const meta = { + title: "@vscrui/Dropdown", + component: () => ( + + ), + parameters: { layout: "centered" }, + tags: ["autodocs"], + argTypes: {}, + args: {}, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: {}, + parameters: { + docs: { + source: { + code: ` +`, + language: "tsx", + }, + }, + }, +} diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts index 5cce936b28..0fb57071ab 100644 --- a/webview-ui/src/utils/context-mentions.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -1,11 +1,20 @@ import { mentionRegex } from "../../../src/shared/context-mentions" import { Fzf } from "fzf" +import { ModeConfig } from "../../../src/shared/modes" export function insertMention( text: string, position: number, value: string, ): { newValue: string; mentionIndex: number } { + // Handle slash command + if (text.startsWith("/")) { + return { + newValue: value, + mentionIndex: 0, + } + } + const beforeCursor = text.slice(0, position) const afterCursor = text.slice(position) @@ -55,6 +64,7 @@ export enum ContextMenuOptionType { URL = "url", Git = "git", NoResults = "noResults", + Mode = "mode", // Add mode type } export interface ContextMenuQueryItem { @@ -69,7 +79,42 @@ export function getContextMenuOptions( query: string, selectedType: ContextMenuOptionType | null = null, queryItems: ContextMenuQueryItem[], + modes?: ModeConfig[], ): ContextMenuQueryItem[] { + // Handle slash commands for modes + if (query.startsWith("/")) { + const modeQuery = query.slice(1) + if (!modes?.length) return [{ type: ContextMenuOptionType.NoResults }] + + // Create searchable strings array for fzf + const searchableItems = modes.map((mode) => ({ + original: mode, + searchStr: mode.name, + })) + + // Initialize fzf instance for fuzzy search + const fzf = new Fzf(searchableItems, { + selector: (item) => item.searchStr, + }) + + // Get fuzzy matching items + const matchingModes = modeQuery + ? fzf.find(modeQuery).map((result) => ({ + type: ContextMenuOptionType.Mode, + value: result.item.original.slug, + label: result.item.original.name, + description: result.item.original.roleDefinition.split("\n")[0], + })) + : modes.map((mode) => ({ + type: ContextMenuOptionType.Mode, + value: mode.slug, + label: mode.name, + description: mode.roleDefinition.split("\n")[0], + })) + + return matchingModes.length > 0 ? matchingModes : [{ type: ContextMenuOptionType.NoResults }] + } + const workingChanges: ContextMenuQueryItem = { type: ContextMenuOptionType.Git, value: "git-changes", @@ -203,6 +248,11 @@ export function getContextMenuOptions( } export function shouldShowContextMenu(text: string, position: number): boolean { + // Handle slash command + if (text.startsWith("/")) { + return position <= text.length && !text.includes(" ") + } + const beforeCursor = text.slice(0, position) const atIndex = beforeCursor.lastIndexOf("@")