This commit is contained in:
Nissa Seru 2025-02-05 04:49:37 -05:00
commit 5e49421bad
51 changed files with 2744 additions and 858 deletions

View file

@ -1,5 +0,0 @@
---
"roo-cline": patch
---
Add shortcuts to the currently open tabs in the "Add File" section of @-mentions (thanks @olup!)

View file

@ -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"

View file

@ -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

18
.vscode/launch.json vendored
View file

@ -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
}
}
]
}

58
.vscode/tasks.json vendored
View file

@ -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": []
}
]
}

View file

@ -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

View file

@ -1,2 +0,0 @@
source 'https://rubygems.org'
gem 'github-pages', group: :jekyll_plugins

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -34,6 +34,7 @@ module.exports = {
transformIgnorePatterns: [
"node_modules/(?!(@modelcontextprotocol|delay|p-wait-for|globby|serialize-error|strip-ansi|default-shell|os-name)/)",
],
roots: ["<rootDir>/src", "<rootDir>/webview-ui/src"],
modulePathIgnorePatterns: [".vscode-test"],
reporters: [["jest-simple-dot-reporter", {}]],
setupFiles: [],

4
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -75,7 +75,7 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterComman
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
}
tabProvider.resolveWebviewView(panel)
await tabProvider.resolveWebviewView(panel)
// Lock the editor group so clicking on files doesn't open them over the panel
await delay(100)

View file

@ -1,29 +1,63 @@
import * as vscode from "vscode"
import * as path from "path"
/**
* Represents an effective range in a document along with the corresponding text.
*/
export interface EffectiveRange {
/** The range within the document. */
range: vscode.Range
/** The text within the specified range. */
text: string
}
/**
* Represents diagnostic information extracted from a VSCode diagnostic.
*/
export interface DiagnosticData {
/** The diagnostic message. */
message: string
/** The severity level of the diagnostic. */
severity: vscode.DiagnosticSeverity
/**
* Optional diagnostic code.
* Can be a string, number, or an object with value and target.
*/
code?: string | number | { value: string | number; target: vscode.Uri }
/** Optional source identifier for the diagnostic (e.g., the extension name). */
source?: string
/** The range within the document where the diagnostic applies. */
range: vscode.Range
}
/**
* Contextual information for a VSCode text editor.
*/
export interface EditorContext {
/** The file path of the current document. */
filePath: string
/** The effective text selected or derived from the document. */
selectedText: string
/** Optional list of diagnostics associated with the effective range. */
diagnostics?: DiagnosticData[]
}
/**
* Utility class providing helper methods for working with VSCode editors and documents.
*/
export class EditorUtils {
// Cache file paths for performance
/** Cache mapping text documents to their computed file paths. */
private static readonly filePathCache = new WeakMap<vscode.TextDocument, string>()
/**
* 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) {

View file

@ -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", () => {

View file

@ -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)
})

View file

@ -132,7 +132,7 @@ Example: Requesting to write to frontend-config.json
</write_to_file>
## 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
</write_to_file>
## 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
</write_to_file>
## 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
</browser_action>
## 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
</write_to_file>
## 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
</browser_action>
## 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
</apply_diff>
## 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
</write_to_file>
## 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
</write_to_file>
## 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
</write_to_file>
## 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

View file

@ -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",

View file

@ -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()}

View file

@ -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:

View file

@ -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)
}

View file

@ -256,11 +256,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await visibleProvider.initClineWithTask(prompt)
}
resolveWebviewView(
webviewView: vscode.WebviewView | vscode.WebviewPanel,
//context: vscode.WebviewViewResolveContext<unknown>, used to recreate a deallocated webview, but we don't need this since we use retainContextWhenHidden
//token: vscode.CancellationToken
): void | Thenable<void> {
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<string> {
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*/ `

View file

@ -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("<!DOCTYPE html>")
})
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("<!DOCTYPE html>")
})
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")
})
})
})

View file

@ -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<CustomModePrompts> = 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 ?? ""
}

View file

@ -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<string, ToolGroupValues> = {
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<string, ToolGroupConfig> = {
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

View file

@ -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()

View file

@ -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<string, any>) {
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")
})
})
})

227
src/utils/shell.ts Normal file
View file

@ -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<string, MacTerminalProfile>
interface WindowsTerminalProfile {
path?: string
source?: "PowerShell" | "WSL"
}
type WindowsTerminalProfiles = Record<string, WindowsTerminalProfile>
interface LinuxTerminalProfile {
path?: string
}
type LinuxTerminalProfiles = Record<string, LinuxTerminalProfile>
// -----------------------------------------------------
// 1) VS Code Terminal Configuration Helpers
// -----------------------------------------------------
function getWindowsTerminalConfig() {
try {
const config = vscode.workspace.getConfiguration("terminal.integrated")
const defaultProfileName = config.get<string>("defaultProfile.windows")
const profiles = config.get<WindowsTerminalProfiles>("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<string>("defaultProfile.osx")
const profiles = config.get<MacTerminalProfiles>("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<string>("defaultProfile.linux")
const profiles = config.get<LinuxTerminalProfiles>("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 users 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
}

View file

@ -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: {},

View file

@ -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" */

View file

@ -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",

View file

@ -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",

View file

@ -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 = ({
</div>
)
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)
) : (
<ProgressIndicator />
),
apiReqCancelReason != null ? (
apiReqCancelReason !== null && apiReqCancelReason !== undefined ? (
apiReqCancelReason === "user_cancelled" ? (
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Cancelled</span>
) : (
<span style={{ color: errorColor, fontWeight: "bold" }}>API Streaming Failed</span>
)
) : cost != null ? (
) : cost !== null && cost !== undefined ? (
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
) : apiRequestFailedMessage ? (
<span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
@ -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 = ({
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
{icon}
{title}
<VSCodeBadge style={{ opacity: cost != null && cost > 0 ? 1 : 0 }}>
<VSCodeBadge
style={{ opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0 }}>
${Number(cost || 0)?.toFixed(4)}
</VSCodeBadge>
</div>
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
</div>
{((cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && (
{(((cost === null || cost === undefined) && apiRequestFailedMessage) ||
apiReqStreamingFailedMessage) && (
<>
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
{apiRequestFailedMessage || apiReqStreamingFailedMessage}

View file

@ -179,6 +179,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
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<HTMLTextAreaElement, ChatTextAreaProps>(
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<HTMLTextAreaElement, ChatTextAreaProps>(
}
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<HTMLTextAreaElement, ChatTextAreaProps>(
setInputValue,
justDeletedSpaceAfterMention,
queryItems,
customModes,
],
)
@ -360,13 +381,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
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<HTMLTextAreaElement, ChatTextAreaProps>(
setSelectedIndex={setSelectedMenuIndex}
selectedType={selectedType}
queryItems={queryItems}
modes={getAllModes(customModes)}
/>
</div>
)}

View file

@ -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 && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px", flexShrink: 0 }}>
<h2>What can I do for you?</h2>
<h2>What can Roo do for you?</h2>
<p>
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

View file

@ -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<ContextMenuProps> = ({
@ -20,12 +22,13 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
setSelectedIndex,
selectedType,
queryItems,
modes,
}) => {
const menuRef = useRef<HTMLDivElement>(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<ContextMenuProps> = ({
const renderOptionContent = (option: ContextMenuQueryItem) => {
switch (option.type) {
case ContextMenuOptionType.Mode:
return (
<div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
{option.description && (
<span
style={{
opacity: 0.5,
fontSize: "0.9em",
lineHeight: "1.2",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}>
{option.description}
</span>
)}
</div>
)
case ContextMenuOptionType.Problems:
return <span>Problems</span>
case ContextMenuOptionType.URL:
@ -101,6 +123,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
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<ContextMenuProps> = ({
overflow: "hidden",
paddingTop: 0,
}}>
<i
className={`codicon codicon-${getIconForOption(option)}`}
style={{
marginRight: "6px",
flexShrink: 0,
fontSize: "14px",
marginTop: 0,
}}
/>
{option.type !== ContextMenuOptionType.Mode && getIconForOption(option) && (
<i
className={`codicon codicon-${getIconForOption(option)}`}
style={{
marginRight: "6px",
flexShrink: 0,
fontSize: "14px",
marginTop: 0,
}}
/>
)}
{renderOptionContent(option)}
</div>
{(option.type === ContextMenuOptionType.File ||

View file

@ -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<SupportPromptType>("ENHANCE")
const [selectedModeTab, setSelectedModeTab] = useState<string>(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 = <T extends keyof ModeConfig>(
@ -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) => {
<div
style={{
display: "flex",
gap: "16px",
gap: "8px",
alignItems: "center",
marginBottom: "12px",
overflowX: "auto",
flexWrap: "nowrap",
paddingBottom: "4px",
paddingRight: "20px",
flexWrap: "wrap",
padding: "4px 0",
}}>
{modes.map((modeConfig) => {
const isActive = mode === modeConfig.slug
const isActive = selectedModeTab === modeConfig.slug
return (
<button
key={modeConfig.slug}
@ -509,20 +506,22 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<div style={{ marginBottom: "20px" }}>
{/* Only show name and delete for custom modes */}
{mode && findModeBySlug(mode, customModes) && (
{selectedModeTab && findModeBySlug(selectedModeTab, customModes) && (
<div style={{ display: "flex", gap: "12px", marginBottom: "16px" }}>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Name</div>
<div style={{ display: "flex", gap: "8px" }}>
<VSCodeTextField
value={getModeProperty(findModeBySlug(mode, customModes), "name") ?? ""}
value={
getModeProperty(findModeBySlug(selectedModeTab, customModes), "name") ?? ""
}
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
const target =
(e as CustomEvent)?.detail?.target ||
((e as any).target as HTMLInputElement)
const customMode = findModeBySlug(mode, customModes)
const customMode = findModeBySlug(selectedModeTab, customModes)
if (customMode) {
updateCustomMode(mode, {
updateCustomMode(selectedModeTab, {
...customMode,
name: target.value,
})
@ -536,7 +535,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
onClick={() => {
vscode.postMessage({
type: "deleteCustomMode",
slug: mode,
slug: selectedModeTab,
})
}}>
<span className="codicon codicon-trash"></span>
@ -554,13 +553,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
marginBottom: "4px",
}}>
<div style={{ fontWeight: "bold" }}>Role Definition</div>
{!findModeBySlug(mode, customModes) && (
{!findModeBySlug(selectedModeTab, customModes) && (
<VSCodeButton
appearance="icon"
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode?.slug) {
handleAgentReset(currentMode.slug)
handleAgentReset(currentMode.slug, "roleDefinition")
}
}}
title="Reset to default"
@ -580,24 +579,28 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
<VSCodeTextArea
value={(() => {
const customMode = findModeBySlug(mode, customModes)
const prompt = customModePrompts?.[mode] as PromptComponent
return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode)
const customMode = findModeBySlug(selectedModeTab, customModes)
const prompt = customModePrompts?.[selectedModeTab] as PromptComponent
return (
customMode?.roleDefinition ??
prompt?.roleDefinition ??
getRoleDefinition(selectedModeTab)
)
})()}
onChange={(e) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
const customMode = findModeBySlug(mode, customModes)
const customMode = findModeBySlug(selectedModeTab, customModes)
if (customMode) {
// For custom modes, update the JSON file
updateCustomMode(mode, {
updateCustomMode(selectedModeTab, {
...customMode,
roleDefinition: value.trim() || "",
})
} else {
// For built-in modes, update the prompts
updateAgentPrompt(mode, {
updateAgentPrompt(selectedModeTab, {
roleDefinition: value.trim() || undefined,
})
}
@ -751,7 +754,29 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
{/* Role definition for both built-in and custom modes */}
<div style={{ marginBottom: "8px" }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Mode-specific Custom Instructions</div>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "4px",
}}>
<div style={{ fontWeight: "bold" }}>Mode-specific Custom Instructions</div>
{!findModeBySlug(selectedModeTab, customModes) && (
<VSCodeButton
appearance="icon"
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode?.slug) {
handleAgentReset(currentMode.slug, "customInstructions")
}
}}
title="Reset to default"
data-testid="custom-instructions-reset">
<span className="codicon codicon-discard"></span>
</VSCodeButton>
)}
</div>
<div
style={{
fontSize: "13px",
@ -762,25 +787,29 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
<VSCodeTextArea
value={(() => {
const customMode = findModeBySlug(mode, customModes)
const prompt = customModePrompts?.[mode] as PromptComponent
return customMode?.customInstructions ?? prompt?.customInstructions ?? ""
const customMode = findModeBySlug(selectedModeTab, customModes)
const prompt = customModePrompts?.[selectedModeTab] as PromptComponent
return (
customMode?.customInstructions ??
prompt?.customInstructions ??
getCustomInstructions(selectedModeTab, customModes)
)
})()}
onChange={(e) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
const customMode = findModeBySlug(mode, customModes)
const customMode = findModeBySlug(selectedModeTab, customModes)
if (customMode) {
// For custom modes, update the JSON file
updateCustomMode(mode, {
updateCustomMode(selectedModeTab, {
...customMode,
customInstructions: value.trim() || undefined,
})
} else {
// For built-in modes, update the prompts
const existingPrompt = customModePrompts?.[mode] as PromptComponent
updateAgentPrompt(mode, {
const existingPrompt = customModePrompts?.[selectedModeTab] as PromptComponent
updateAgentPrompt(selectedModeTab, {
...existingPrompt,
customInstructions: value.trim() || undefined,
})
@ -859,13 +888,11 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<div
style={{
display: "flex",
gap: "16px",
gap: "8px",
alignItems: "center",
marginBottom: "12px",
overflowX: "auto",
flexWrap: "nowrap",
paddingBottom: "4px",
paddingRight: "20px",
flexWrap: "wrap",
padding: "4px 0",
}}>
{Object.keys(supportPrompt.default).map((type) => (
<button

View file

@ -128,7 +128,10 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
id="api-provider"
value={selectedProvider}
onChange={(value: unknown) => {
handleInputChange("apiProvider")({
handleInputChange(
"apiProvider",
true,
)({
target: {
value: (value as DropdownOption).value,
},

View file

@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer active:opacity-90",
{
variants: {
variant: {
@ -17,10 +17,10 @@ const buttonVariants = cva(
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
default: "h-7 px-3",
sm: "h-6 px-2 text-sm",
lg: "h-8 px-4 text-lg",
icon: "h-7 w-7",
},
},
defaultVariants: {

View file

@ -0,0 +1,179 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className,
)}
{...props}>
{children}
<ChevronRightIcon className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
))
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & {
container?: HTMLElement
}
>(({ className, sideOffset = 4, container, ...props }, ref) => (
<DropdownMenuPrimitive.Portal container={container}>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0 active:opacity-90",
inset && "pl-8",
className,
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View file

@ -0,0 +1,2 @@
export * from "./button"
export * from "./dropdown-menu"

View file

@ -10,7 +10,7 @@ const WelcomeView = () => {
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const disableLetsGoButton = apiErrorMessage != null
const disableLetsGoButton = apiErrorMessage !== null && apiErrorMessage !== undefined
const handleSubmit = () => {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })

View file

@ -71,7 +71,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setEnhancementApiConfigId: (value: string) => void
setExperimentEnabled: (id: ExperimentId, enabled: boolean) => void
setAutoApprovalEnabled: (value: boolean) => void
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
handleInputChange: (field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => void
customModes: ModeConfig[]
setCustomModes: (value: ModeConfig[]) => void
}
@ -142,14 +142,29 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
}, [])
const handleInputChange = useCallback(
(field: keyof ApiConfiguration) => (event: any) => {
// Returns a function that handles an input change event for a specific API configuration field.
// The optional "softUpdate" flag determines whether to immediately update local state or send an external update.
(field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => {
// Use the functional form of setState to ensure the latest state is used in the update logic.
setState((currentState) => {
vscode.postMessage({
type: "upsertApiConfiguration",
text: currentState.currentApiConfigName,
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
})
return currentState // No state update needed
if (softUpdate) {
// Return a new state object with the updated apiConfiguration.
// This will trigger a re-render with the new configuration value.
return {
...currentState,
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
}
} else {
// For non-soft updates, send a message to the VS Code extension with the updated config.
// This side effect communicates the change without updating local React state.
vscode.postMessage({
type: "upsertApiConfiguration",
text: currentState.currentApiConfigName,
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
})
// Return the unchanged state as no local state update is intended in this branch.
return currentState
}
})
},
[],

View file

@ -1,15 +1,32 @@
/* @import "tailwindcss"; */
/**
* Normally we'd import tailwind with the following:
*
* @import "tailwindcss";
*
* However, we need to customize the preflight styles since the extension's
* current UI assumes there's no CSS resetting or normalization.
*
* We're excluding tailwind's default preflight and importing our own, which
* is based on the original:
* https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/preflight.css
*
* Reference: https://tailwindcss.com/docs/preflight
*/
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
/* https://tailwindcss.com/docs/preflight */
/* @import "tailwindcss/preflight.css" layer(base); */
@import "./preflight.css" layer(base);
@import "tailwindcss/utilities.css" layer(utilities);
@plugin "tailwindcss-animate";
@theme {
--font-display: var(--vscode-font-family);
--text-sm: calc(var(--vscode-font-size) * 0.9);
--text-base: var(--vscode-font-size);
--text-lg: calc(var(--vscode-font-size) * 1.1);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
@ -53,11 +70,11 @@
--secondary-foreground: var(--vscode-button-secondaryForeground);
--muted: var(--vscode-disabledForeground);
--muted-foreground: var(--vscode-descriptionForeground);
--accent: var(--vscode-input-border);
--accent: var(--vscode-list-hoverBackground);
--accent-foreground: var(--vscode-button-foreground);
--destructive: var(--vscode-errorForeground);
--destructive-foreground: var(--vscode-button-foreground);
--border: var(--vscode-widget-border);
--border: var(--vscode-input-border);
--input: var(--vscode-input-background);
--ring: var(--vscode-input-border);
--chart-1: var(--vscode-charts-red);

View file

@ -0,0 +1,383 @@
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Remove default margins and padding
3. Reset all borders.
*/
*,
::after,
::before,
::backdrop,
::file-selector-button {
box-sizing: border-box; /* 1 */
/* margin: 0; */ /* 2 */
padding: 0; /* 2 */
border: 0 solid; /* 3 */
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS.
*/
html,
:host {
line-height: 1.5; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
tab-size: 4; /* 3 */
font-family: var(
--default-font-family,
ui-sans-serif,
system-ui,
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji"
); /* 4 */
font-feature-settings: var(--default-font-feature-settings, normal); /* 5 */
font-variation-settings: var(--default-font-variation-settings, normal); /* 6 */
-webkit-tap-highlight-color: transparent; /* 7 */
}
/*
Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
line-height: inherit;
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Reset the default border style to a 1px solid border.
*/
hr {
height: 0; /* 1 */
color: inherit; /* 2 */
border-top-width: 1px; /* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
/* h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
} */
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
-webkit-text-decoration: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: var(
--default-mono-font-family,
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
"Liberation Mono",
"Courier New",
monospace
); /* 4 */
font-feature-settings: var(--default-mono-font-feature-settings, normal); /* 5 */
font-variation-settings: var(--default-mono-font-variation-settings, normal); /* 6 */
font-size: 1em; /* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0; /* 1 */
border-color: inherit; /* 2 */
border-collapse: collapse; /* 3 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Make lists unstyled by default.
*/
ol,
ul,
menu {
list-style: none;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block; /* 1 */
vertical-align: middle; /* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/*
1. Inherit font styles in all browsers.
2. Remove border radius in all browsers.
3. Remove background color in all browsers.
4. Ensure consistent opacity for disabled states in all browsers.
*/
button,
input,
select,
optgroup,
textarea,
::file-selector-button {
font: inherit; /* 1 */
font-feature-settings: inherit; /* 1 */
font-variation-settings: inherit; /* 1 */
letter-spacing: inherit; /* 1 */
color: inherit; /* 1 */
border-radius: 0; /* 2 */
background-color: transparent; /* 3 */
opacity: 1; /* 4 */
}
/*
Restore default font weight.
*/
:where(select:is([multiple], [size])) optgroup {
font-weight: bolder;
}
/*
Restore indentation.
*/
:where(select:is([multiple], [size])) optgroup option {
padding-inline-start: 20px;
}
/*
Restore space after button.
*/
::file-selector-button {
margin-inline-end: 4px;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to a semi-transparent version of the current text color.
*/
::placeholder {
opacity: 1; /* 1 */
color: color-mix(in oklab, currentColor 50%, transparent); /* 2 */
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Ensure date/time inputs have the same height when empty in iOS Safari.
2. Ensure text alignment can be changed on date/time inputs in iOS Safari.
*/
::-webkit-date-and-time-value {
min-height: 1lh; /* 1 */
text-align: inherit; /* 2 */
}
/*
Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`.
*/
::-webkit-datetime-edit {
display: inline-flex;
}
/*
Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers.
*/
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-datetime-edit,
::-webkit-datetime-edit-year-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-minute-field,
::-webkit-datetime-edit-second-field,
::-webkit-datetime-edit-millisecond-field,
::-webkit-datetime-edit-meridiem-field {
padding-block: 0;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Correct the inability to style the border radius in iOS Safari.
*/
button,
input:where([type="button"], [type="reset"], [type="submit"]),
::file-selector-button {
appearance: button;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
Make elements with the HTML hidden attribute stay hidden by default.
*/
[hidden]:where(:not([hidden="until-found"])) {
display: none !important;
}

View file

@ -1,15 +1,47 @@
import type { Meta, StoryObj } from "@storybook/react"
import { fn } from "@storybook/test"
import { Button } from "@/components/ui/button"
import { Button } from "@/components/ui"
const meta = {
title: "Example/Button",
title: "@shadcn/Button",
component: Button,
parameters: { layout: "centered" },
tags: ["autodocs"],
argTypes: {},
args: { onClick: fn(), children: "Button" },
argTypes: {
variant: {
control: "select",
options: ["default", "secondary", "outline", "ghost", "link", "destructive"],
type: "string",
table: {
defaultValue: {
summary: "default",
},
},
},
size: {
control: "select",
options: ["default", "sm", "lg", "icon"],
type: "string",
table: {
defaultValue: {
summary: "default",
},
},
},
children: {
table: {
disable: true,
},
},
asChild: {
table: {
disable: true,
},
},
},
args: {
children: "Button",
},
} satisfies Meta<typeof Button>
export default meta
@ -17,37 +49,5 @@ export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
variant: "default",
},
}
export const Secondary: Story = {
args: {
variant: "secondary",
},
}
export const Outline: Story = {
args: {
variant: "outline",
},
}
export const Ghost: Story = {
args: {
variant: "ghost",
},
}
export const Link: Story = {
args: {
variant: "link",
},
}
export const Destructive: Story = {
args: {
variant: "destructive",
},
name: "Button",
}

View file

@ -0,0 +1,134 @@
import type { Meta, StoryObj } from "@storybook/react"
import {
HamburgerMenuIcon,
BorderLeftIcon,
BorderRightIcon,
BorderBottomIcon,
BorderTopIcon,
TextAlignLeftIcon,
TextAlignCenterIcon,
TextAlignRightIcon,
} from "@radix-ui/react-icons"
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui"
const meta = {
title: "@shadcn/DropdownMenu",
component: DropdownMenu,
parameters: { layout: "centered" },
tags: ["autodocs"],
} satisfies Meta<typeof DropdownMenu>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
name: "DropdownMenu",
render: () => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<HamburgerMenuIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Label</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>Item 1</DropdownMenuItem>
<DropdownMenuItem>
Item 2<DropdownMenuShortcut>2</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Submenu</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>Foo</DropdownMenuItem>
<DropdownMenuItem>
Bar
<DropdownMenuShortcut>B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Baz</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
),
}
type DropdownMenuVariantProps = {
side?: "top" | "bottom" | "left" | "right"
align?: "start" | "center" | "end"
children?: React.ReactNode
}
const DropdownMenuVariant = ({ side = "bottom", align = "center", children }: DropdownMenuVariantProps) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
{children}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side={side} align={align}>
<DropdownMenuItem>Foo</DropdownMenuItem>
<DropdownMenuItem>Bar</DropdownMenuItem>
<DropdownMenuItem>Baz</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
export const Placements: Story = {
render: () => (
<div className="flex gap-2">
<DropdownMenuVariant side="top">
<BorderTopIcon />
</DropdownMenuVariant>
<DropdownMenuVariant side="bottom">
<BorderBottomIcon />
</DropdownMenuVariant>
<DropdownMenuVariant side="left">
<BorderLeftIcon />
</DropdownMenuVariant>
<DropdownMenuVariant side="right">
<BorderRightIcon />
</DropdownMenuVariant>
</div>
),
}
export const Alignments: Story = {
render: () => (
<div className="flex gap-2">
<DropdownMenuVariant align="center">
<TextAlignCenterIcon />
</DropdownMenuVariant>
<DropdownMenuVariant align="end">
<TextAlignRightIcon />
</DropdownMenuVariant>
<DropdownMenuVariant align="start">
<TextAlignLeftIcon />
</DropdownMenuVariant>
</div>
),
}

View file

@ -0,0 +1,47 @@
import type { Meta, StoryObj } from "@storybook/react"
import { Dropdown } from "vscrui"
const meta = {
title: "@vscrui/Dropdown",
component: () => (
<Dropdown
value="foo"
role="combobox"
options={[
{ value: "foo", label: "Foo" },
{ value: "bar", label: "Bar" },
{ value: "baz", label: "Baz" },
]}
/>
),
parameters: { layout: "centered" },
tags: ["autodocs"],
argTypes: {},
args: {},
} satisfies Meta<typeof Dropdown>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {},
parameters: {
docs: {
source: {
code: `
<Dropdown
value="foo"
role="combobox"
options={[
{ value: "foo", label: "Foo" },
{ value: "bar", label: "Bar" },
{ value: "baz", label: "Baz" }
]}
/>`,
language: "tsx",
},
},
},
}

View file

@ -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("@")