Merge branch 'main' into WEB-1756

This commit is contained in:
pugazhendhi-m 2025-02-07 13:02:46 +05:30 committed by GitHub
commit 26baaf89a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 4812 additions and 800 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

@ -1,5 +0,0 @@
---
"roo-cline": patch
---
Visual cleanup to the list of modes on the prompts tab

View file

@ -17,8 +17,7 @@
"@typescript-eslint/semi": "off",
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": "off",
"react-hooks/exhaustive-deps": "off"
"semi": "off"
},
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
}

View file

@ -39,6 +39,13 @@ jobs:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
run: |
current_package_version=$(node -p "require('./package.json').version")
npm run vsix
package=$(unzip -l bin/roo-cline-${current_package_version}.vsix)
echo "$package"
echo "$package" | grep -q "dist/extension.js" || exit 1
echo "$package" | grep -q "extension/webview-ui/build/assets/index.js" || exit 1
echo "$package" | grep -q "extension/node_modules/@vscode/codicons/dist/codicon.ttf" || exit 1
npm run publish:marketplace
echo "Successfully published version $current_package_version to VS Code Marketplace"

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

2
.gitignore vendored
View file

@ -1,6 +1,6 @@
dist
out
out-integration
out-*
node_modules
coverage/

View file

@ -6,7 +6,3 @@ if [ "$branch" = "main" ]; then
fi
npx lint-staged
npm run compile
npm run lint
npm run check-types

View file

@ -7,11 +7,12 @@ fi
npm run compile
# Check for new changesets
# Check for new changesets.
NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
echo "Changeset files: $NEW_CHANGESETS"
if [ "$NEW_CHANGESETS" == "0" ]; then
echo "-------------------------------------------------------------------------------------"
echo "Changes detected. Please run 'npm run changeset' to create a changeset if applicable."
echo "-------------------------------------------------------------------------------------"
fi
fi

2
.vscode/launch.json vendored
View file

@ -13,7 +13,7 @@
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "debug-mode",
"preLaunchTask": "${defaultBuildTask}",
"env": {
"NODE_ENV": "development",
"VSCODE_DEBUG_MODE": "true"

51
.vscode/tasks.json vendored
View file

@ -3,42 +3,15 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "compile",
"type": "npm",
"script": "compile",
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "silent",
"panel": "shared"
},
"problemMatcher": ["$tsc", "$eslint-stylish"]
},
{
"label": "watch",
"dependsOn": ["npm: build:webview", "npm: watch:tsc", "npm: watch:esbuild"],
"dependsOn": ["npm: dev", "npm: watch:tsc", "npm: watch:esbuild"],
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": false
}
},
{
"label": "debug-mode",
"dependsOn": ["compile", "npm: dev"],
"group": {
"kind": "build",
"isDefault": false
},
"dependsOrder": "parallel",
"presentation": {
"reveal": "always",
"panel": "new"
"isDefault": true
}
},
{
@ -59,20 +32,8 @@
},
"isBackground": true,
"presentation": {
"group": "watch",
"reveal": "never"
}
},
{
"label": "npm: build:webview",
"type": "npm",
"script": "build:webview",
"group": "build",
"problemMatcher": [],
"isBackground": true,
"presentation": {
"group": "watch",
"reveal": "never"
"group": "webview-ui",
"reveal": "always"
}
},
{
@ -84,7 +45,7 @@
"isBackground": true,
"presentation": {
"group": "watch",
"reveal": "never"
"reveal": "always"
}
},
{
@ -96,7 +57,7 @@
"isBackground": true,
"presentation": {
"group": "watch",
"reveal": "never"
"reveal": "always"
}
}
]

View file

@ -1,4 +1,6 @@
# Default
.github/**
.husky/**
.vscode/**
.vscode-test/**
out/**

View file

@ -1,5 +1,41 @@
# Roo Code Changelog
## [3.3.14]
- Should have skipped floor 13 like an elevator. This fixes the broken 3.3.13 release by reverting some changes to the deployment scripts.
## [3.3.13]
- Ensure the DeepSeek r1 model works with Ollama (thanks @sammcj!)
- Enable context menu commands in the terminal (thanks @samhvw8!)
- Improve sliding window truncation strategy for models that do not support prompt caching (thanks @nissa-seru!)
- First step of a more fundamental fix to the bugs around switching API profiles. If you've been having issues with this please try again and let us know if works any better! More to come soon, including fixing the laggy text entry in provider settings.
## [3.3.12]
- Bug fix to changing a mode's API configuration on the prompts tab
- Add new Gemini models
## [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,24 +1,21 @@
# Roo Code (prev. Roo Cline)
<div align="center">
<h2>Join the Roo Code Community</h2>
<p>Connect with developers, contribute ideas, and stay ahead with the latest AI-powered coding tools.</p>
<a href="https://discord.gg/roocode" target="_blank"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join Discord" height="60"></a>
<a href="https://www.reddit.com/r/RooCode/" target="_blank"><img src="https://img.shields.io/badge/Join%20Reddit-FF4500?style=for-the-badge&logo=reddit&logoColor=white" alt="Join Reddit" height="60"></a>
</div>
<br>
<br>
<table>
<tbody>
<td align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline" target="_blank"><strong>Download on VS Marketplace</strong></a>
</td>
<td align="center">
<a href="https://discord.gg/roocode" target="_blank"><strong>Discord</strong></a>
</td>
<td align="center">
<a href="https://www.reddit.com/r/RooCode/" target="_blank"><strong>r/RooCode</strong></a>
</td>
<td align="center">
<a href="https://github.com/RooVetGit/Roo-Code/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
</td>
<td align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline&ssr=false#review-details" target="_blank"><strong>Rate & Review</strong></a>
</td>
</tbody>
</table>
<div align="center">
<h1>Roo Code (prev. Roo Cline)</h1>
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline" target="_blank"><img src="https://img.shields.io/badge/Download%20on%20VS%20Marketplace-blue?style=for-the-badge&logo=visualstudiocode&logoColor=white" alt="Download on VS Marketplace"></a>
<a href="https://github.com/RooVetGit/Roo-Code/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><img src="https://img.shields.io/badge/Feature%20Requests-yellow?style=for-the-badge" alt="Feature Requests"></a>
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline&ssr=false#review-details" target="_blank"><img src="https://img.shields.io/badge/Rate%20%26%20Review-green?style=for-the-badge" alt="Rate & Review"></a>
</div>
**Roo Code** is an AI-powered **autonomous coding agent** that lives in your editor. It can:

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.

22
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "roo-cline",
"version": "3.3.9",
"version": "3.3.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
"version": "3.3.9",
"version": "3.3.14",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0",
@ -56,6 +56,7 @@
"@changesets/cli": "^2.27.10",
"@changesets/types": "^6.0.0",
"@dotenvx/dotenvx": "^1.34.0",
"@types/debug": "^4.1.12",
"@types/diff": "^5.2.1",
"@types/diff-match-patch": "^1.0.36",
"@types/jest": "^29.5.14",
@ -5895,6 +5896,16 @@
"resolved": "https://registry.npmjs.org/@types/clone-deep/-/clone-deep-4.0.4.tgz",
"integrity": "sha512-vXh6JuuaAha6sqEbJueYdh5zNBPPgG1OYumuz2UvLvriN6ABHDSW8ludREGWJb1MLIzbwZn4q4zUbUCerJTJfA=="
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/ms": "*"
}
},
"node_modules/@types/diff": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.3.tgz",
@ -5957,6 +5968,13 @@
"integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==",
"dev": true
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.17.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.9.tgz",

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.14",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
@ -123,6 +123,31 @@
"command": "roo-cline.addToContext",
"title": "Roo Code: Add To Context",
"category": "Roo Code"
},
{
"command": "roo-cline.terminalAddToContext",
"title": "Roo Code: Add Terminal Content to Context",
"category": "Terminal"
},
{
"command": "roo-cline.terminalFixCommand",
"title": "Roo Code: Fix This Command",
"category": "Terminal"
},
{
"command": "roo-cline.terminalExplainCommand",
"title": "Roo Code: Explain This Command",
"category": "Terminal"
},
{
"command": "roo-cline.terminalFixCommandInCurrentTask",
"title": "Roo Code: Fix This Command (Current Task)",
"category": "Terminal"
},
{
"command": "roo-cline.terminalExplainCommandInCurrentTask",
"title": "Roo Code: Explain This Command (Current Task)",
"category": "Terminal"
}
],
"menus": {
@ -148,6 +173,28 @@
"group": "Roo Code@4"
}
],
"terminal/context": [
{
"command": "roo-cline.terminalAddToContext",
"group": "Roo Code@1"
},
{
"command": "roo-cline.terminalFixCommand",
"group": "Roo Code@2"
},
{
"command": "roo-cline.terminalExplainCommand",
"group": "Roo Code@3"
},
{
"command": "roo-cline.terminalFixCommandInCurrentTask",
"group": "Roo Code@5"
},
{
"command": "roo-cline.terminalExplainCommandInCurrentTask",
"group": "Roo Code@6"
}
],
"view/title": [
{
"command": "roo-cline.plusButtonClicked",
@ -242,31 +289,6 @@
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"watch-tests": "tsc -p . -w --outDir out"
},
"devDependencies": {
"@changesets/cli": "^2.27.10",
"@changesets/types": "^6.0.0",
"@dotenvx/dotenvx": "^1.34.0",
"@types/diff": "^5.2.1",
"@types/diff-match-patch": "^1.0.36",
"@types/jest": "^29.5.14",
"@types/mocha": "^10.0.7",
"@types/node": "20.x",
"@types/string-similarity": "^4.0.2",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"esbuild": "^0.24.0",
"eslint": "^8.57.0",
"husky": "^9.1.7",
"jest": "^29.7.0",
"jest-simple-dot-reporter": "^1.0.5",
"lint-staged": "^15.2.11",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"ts-jest": "^29.2.5",
"typescript": "^5.4.5"
},
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0",
@ -312,13 +334,41 @@
"web-tree-sitter": "^0.22.6",
"zod": "^3.23.8"
},
"devDependencies": {
"@changesets/cli": "^2.27.10",
"@changesets/types": "^6.0.0",
"@dotenvx/dotenvx": "^1.34.0",
"@types/debug": "^4.1.12",
"@types/diff": "^5.2.1",
"@types/diff-match-patch": "^1.0.36",
"@types/jest": "^29.5.14",
"@types/mocha": "^10.0.7",
"@types/node": "20.x",
"@types/string-similarity": "^4.0.2",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"esbuild": "^0.24.0",
"eslint": "^8.57.0",
"husky": "^9.1.7",
"jest": "^29.7.0",
"jest-simple-dot-reporter": "^1.0.5",
"lint-staged": "^15.2.11",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"ts-jest": "^29.2.5",
"typescript": "^5.4.5"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,md}": [
"prettier --write"
],
"src/**/*.{ts,tsx}": [
"prettier --write",
"npx eslint -c .eslintrc.json --fix"
"npx eslint -c .eslintrc.json --max-warnings=0 --fix"
],
"webview-ui/**/*.{ts,tsx}": [
"npx eslint -c webview-ui/.eslintrc.json --max-warnings=0 --fix"
]
}
}

View file

@ -1,3 +1,4 @@
export { handleUri } from "./handleUri"
export { registerCommands } from "./registerCommands"
export { registerCodeActions } from "./registerCodeActions"
export { registerTerminalActions } from "./registerTerminalActions"

View file

@ -0,0 +1,81 @@
import * as vscode from "vscode"
import { ClineProvider } from "../core/webview/ClineProvider"
import { TerminalManager } from "../integrations/terminal/TerminalManager"
const TERMINAL_COMMAND_IDS = {
ADD_TO_CONTEXT: "roo-cline.terminalAddToContext",
FIX: "roo-cline.terminalFixCommand",
FIX_IN_CURRENT_TASK: "roo-cline.terminalFixCommandInCurrentTask",
EXPLAIN: "roo-cline.terminalExplainCommand",
EXPLAIN_IN_CURRENT_TASK: "roo-cline.terminalExplainCommandInCurrentTask",
} as const
export const registerTerminalActions = (context: vscode.ExtensionContext) => {
const terminalManager = new TerminalManager()
registerTerminalAction(context, terminalManager, TERMINAL_COMMAND_IDS.ADD_TO_CONTEXT, "TERMINAL_ADD_TO_CONTEXT")
registerTerminalActionPair(
context,
terminalManager,
TERMINAL_COMMAND_IDS.FIX,
"TERMINAL_FIX",
"What would you like Roo to fix?",
)
registerTerminalActionPair(
context,
terminalManager,
TERMINAL_COMMAND_IDS.EXPLAIN,
"TERMINAL_EXPLAIN",
"What would you like Roo to explain?",
)
}
const registerTerminalAction = (
context: vscode.ExtensionContext,
terminalManager: TerminalManager,
command: string,
promptType: "TERMINAL_ADD_TO_CONTEXT" | "TERMINAL_FIX" | "TERMINAL_EXPLAIN",
inputPrompt?: string,
) => {
context.subscriptions.push(
vscode.commands.registerCommand(command, async (args: any) => {
let content = args.selection
if (!content || content === "") {
content = await terminalManager.getTerminalContents(promptType === "TERMINAL_ADD_TO_CONTEXT" ? -1 : 1)
}
if (!content) {
vscode.window.showWarningMessage("No terminal content selected")
return
}
const params: Record<string, any> = {
terminalContent: content,
}
if (inputPrompt) {
params.userInput =
(await vscode.window.showInputBox({
prompt: inputPrompt,
})) ?? ""
}
await ClineProvider.handleTerminalAction(command, promptType, params)
}),
)
}
const registerTerminalActionPair = (
context: vscode.ExtensionContext,
terminalManager: TerminalManager,
baseCommand: string,
promptType: "TERMINAL_ADD_TO_CONTEXT" | "TERMINAL_FIX" | "TERMINAL_EXPLAIN",
inputPrompt?: string,
) => {
// Register new task version
registerTerminalAction(context, terminalManager, baseCommand, promptType, inputPrompt)
// Register current task version
registerTerminalAction(context, terminalManager, `${baseCommand}InCurrentTask`, promptType, inputPrompt)
}

View file

@ -204,7 +204,7 @@ describe("GeminiHandler", () => {
geminiApiKey: "test-key",
})
const modelInfo = invalidHandler.getModel()
expect(modelInfo.id).toBe("gemini-2.0-flash-thinking-exp-01-21") // Default model
expect(modelInfo.id).toBe("gemini-2.0-flash-001") // Default model
})
})
})

View file

@ -3,6 +3,7 @@ import OpenAI from "openai"
import { ApiHandler, SingleCompletionHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToR1Format } from "../transform/r1-format"
import { ApiStream } from "../transform/stream"
export class OllamaHandler implements ApiHandler, SingleCompletionHandler {
@ -18,9 +19,11 @@ export class OllamaHandler implements ApiHandler, SingleCompletionHandler {
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.getModel().id
const useR1Format = modelId.toLowerCase().includes('deepseek-r1')
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
...(useR1Format ? convertToR1Format(messages) : convertToOpenAiMessages(messages)),
]
const stream = await this.client.chat.completions.create({
@ -49,9 +52,11 @@ export class OllamaHandler implements ApiHandler, SingleCompletionHandler {
async completePrompt(prompt: string): Promise<string> {
try {
const modelId = this.getModel().id
const useR1Format = modelId.toLowerCase().includes('deepseek-r1')
const response = await this.client.chat.completions.create({
model: this.getModel().id,
messages: [{ role: "user", content: prompt }],
messages: useR1Format ? convertToR1Format([{ role: "user", content: prompt }]) : [{ role: "user", content: prompt }],
temperature: 0,
stream: false,
})

View file

@ -18,10 +18,20 @@ export class OpenAiHandler implements ApiHandler, SingleCompletionHandler {
constructor(options: ApiHandlerOptions) {
this.options = options
// Azure API shape slightly differs from the core API shape:
// https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
const urlHost = new URL(this.options.openAiBaseUrl ?? "").host
let urlHost: string
try {
urlHost = new URL(this.options.openAiBaseUrl ?? "").host
} catch (error) {
// Likely an invalid `openAiBaseUrl`; we're still working on
// proper settings validation.
urlHost = ""
}
if (urlHost === "azure.com" || urlHost.endsWith(".azure.com") || options.openAiUseAzure) {
// Azure API shape slightly differs from the core API shape:
// https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,

View file

@ -52,8 +52,8 @@ import { parseMentions } from "./mentions"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
import { formatResponse } from "./prompts/responses"
import { SYSTEM_PROMPT } from "./prompts/system"
import { modes, defaultModeSlug, getModeBySlug, parseSlashCommand } from "../shared/modes"
import { truncateHalfConversation } from "./sliding-window"
import { modes, defaultModeSlug, getModeBySlug } from "../shared/modes"
import { truncateConversationIfNeeded } from "./sliding-window"
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
import { detectCodeOmission } from "../integrations/editor/detect-omission"
import { BrowserSession } from "../services/browser/BrowserSession"
@ -77,29 +77,6 @@ export class Cline {
private terminalManager: TerminalManager
private urlContentFetcher: UrlContentFetcher
private browserSession: BrowserSession
/**
* Processes a message for slash commands and handles mode switching if needed.
* @param message The message to process
* @returns The processed message with slash command removed if one was present
*/
private async handleSlashCommand(message: string): Promise<string> {
if (!message) return message
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
const slashCommand = parseSlashCommand(message, customModes)
if (slashCommand) {
// Switch mode before processing the remaining message
const provider = this.providerRef.deref()
if (provider) {
await provider.handleModeSwitch(slashCommand.modeSlug)
return slashCommand.remainingMessage
}
}
return message
}
private didEditFile: boolean = false
customInstructions?: string
diffStrategy?: DiffStrategy
@ -378,11 +355,6 @@ export class Cline {
}
async handleWebviewAskResponse(askResponse: ClineAskResponse, text?: string, images?: string[]) {
// Process slash command if present
if (text) {
text = await this.handleSlashCommand(text)
}
this.askResponse = askResponse
this.askResponseText = text
this.askResponseImages = images
@ -465,22 +437,6 @@ export class Cline {
this.apiConversationHistory = []
await this.providerRef.deref()?.postStateToWebview()
// Check for slash command if task is provided
if (task) {
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
const slashCommand = parseSlashCommand(task, customModes)
if (slashCommand) {
// Switch mode before processing the remaining message
const provider = this.providerRef.deref()
if (provider) {
await provider.handleModeSwitch(slashCommand.modeSlug)
// Update task to be just the remaining message
task = slashCommand.remainingMessage
}
}
}
await this.say("text", task, images)
let imageBlocks: Anthropic.ImageBlockParam[] = formatResponse.imageBlocks(images)
@ -920,18 +876,25 @@ export class Cline {
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
if (previousApiReqIndex >= 0) {
const previousRequest = this.clineMessages[previousApiReqIndex]
if (previousRequest && previousRequest.text) {
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(
previousRequest.text,
)
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
const contextWindow = this.api.getModel().info.contextWindow || 128_000
const maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8)
if (totalTokens >= maxAllowedSize) {
const truncatedMessages = truncateHalfConversation(this.apiConversationHistory)
await this.overwriteApiConversationHistory(truncatedMessages)
}
const previousRequest = this.clineMessages[previousApiReqIndex]?.text
if (!previousRequest) return
const {
tokensIn = 0,
tokensOut = 0,
cacheWrites = 0,
cacheReads = 0,
}: ClineApiReqInfo = JSON.parse(previousRequest)
const totalTokens = tokensIn + tokensOut + cacheWrites + cacheReads
const trimmedMessages = truncateConversationIfNeeded(
this.apiConversationHistory,
totalTokens,
this.api.getModel().info,
)
if (trimmedMessages !== this.apiConversationHistory) {
await this.overwriteApiConversationHistory(trimmedMessages)
}
}

View file

@ -1,26 +1,97 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ModelInfo } from "../../shared/api"
/*
We can't implement a dynamically updating sliding window as it would break prompt cache
every time. To maintain the benefits of caching, we need to keep conversation history
static. This operation should be performed as infrequently as possible. If a user reaches
a 200k context, we can assume that the first half is likely irrelevant to their current task.
Therefore, this function should only be called when absolutely necessary to fit within
context limits, not as a continuous process.
*/
export function truncateHalfConversation(
/**
* Truncates a conversation by removing a fraction of the messages.
*
* The first message is always retained, and a specified fraction (rounded to an even number)
* of messages from the beginning (excluding the first) is removed.
*
* @param {Anthropic.Messages.MessageParam[]} messages - The conversation messages.
* @param {number} fracToRemove - The fraction (between 0 and 1) of messages (excluding the first) to remove.
* @returns {Anthropic.Messages.MessageParam[]} The truncated conversation messages.
*/
export function truncateConversation(
messages: Anthropic.Messages.MessageParam[],
fracToRemove: number,
): Anthropic.Messages.MessageParam[] {
// API expects messages to be in user-assistant order, and tool use messages must be followed by tool results. We need to maintain this structure while truncating.
// Always keep the first Task message (this includes the project's file structure in environment_details)
const truncatedMessages = [messages[0]]
// Remove half of user-assistant pairs
const messagesToRemove = Math.floor(messages.length / 4) * 2 // has to be even number
const remainingMessages = messages.slice(messagesToRemove + 1) // has to start with assistant message since tool result cannot follow assistant message with no tool use
const rawMessagesToRemove = Math.floor((messages.length - 1) * fracToRemove)
const messagesToRemove = rawMessagesToRemove - (rawMessagesToRemove % 2)
const remainingMessages = messages.slice(messagesToRemove + 1)
truncatedMessages.push(...remainingMessages)
return truncatedMessages
}
/**
* Conditionally truncates the conversation messages if the total token count exceeds the model's limit.
*
* Depending on whether the model supports prompt caching, different maximum token thresholds
* and truncation fractions are used. If the current total tokens exceed the threshold,
* the conversation is truncated using the appropriate fraction.
*
* @param {Anthropic.Messages.MessageParam[]} messages - The conversation messages.
* @param {number} totalTokens - The total number of tokens in the conversation.
* @param {ModelInfo} modelInfo - Model metadata including context window size and prompt cache support.
* @returns {Anthropic.Messages.MessageParam[]} The original or truncated conversation messages.
*/
export function truncateConversationIfNeeded(
messages: Anthropic.Messages.MessageParam[],
totalTokens: number,
modelInfo: ModelInfo,
): Anthropic.Messages.MessageParam[] {
if (modelInfo.supportsPromptCache) {
return totalTokens < getMaxTokensForPromptCachingModels(modelInfo)
? messages
: truncateConversation(messages, getTruncFractionForPromptCachingModels(modelInfo))
} else {
return totalTokens < getMaxTokensForNonPromptCachingModels(modelInfo)
? messages
: truncateConversation(messages, getTruncFractionForNonPromptCachingModels(modelInfo))
}
}
/**
* Calculates the maximum allowed tokens for models that support prompt caching.
*
* The maximum is computed as the greater of (contextWindow - 40000) and 80% of the contextWindow.
*
* @param {ModelInfo} modelInfo - The model information containing the context window size.
* @returns {number} The maximum number of tokens allowed for prompt caching models.
*/
function getMaxTokensForPromptCachingModels(modelInfo: ModelInfo): number {
return Math.max(modelInfo.contextWindow - 40_000, modelInfo.contextWindow * 0.8)
}
/**
* Provides the fraction of messages to remove for models that support prompt caching.
*
* @param {ModelInfo} modelInfo - The model information (unused in current implementation).
* @returns {number} The truncation fraction for prompt caching models (fixed at 0.5).
*/
function getTruncFractionForPromptCachingModels(modelInfo: ModelInfo): number {
return 0.5
}
/**
* Calculates the maximum allowed tokens for models that do not support prompt caching.
*
* The maximum is computed as the greater of (contextWindow - 40000) and 80% of the contextWindow.
*
* @param {ModelInfo} modelInfo - The model information containing the context window size.
* @returns {number} The maximum number of tokens allowed for non-prompt caching models.
*/
function getMaxTokensForNonPromptCachingModels(modelInfo: ModelInfo): number {
return Math.max(modelInfo.contextWindow - 40_000, modelInfo.contextWindow * 0.8)
}
/**
* Provides the fraction of messages to remove for models that do not support prompt caching.
*
* @param {ModelInfo} modelInfo - The model information.
* @returns {number} The truncation fraction for non-prompt caching models (fixed at 0.1).
*/
function getTruncFractionForNonPromptCachingModels(modelInfo: ModelInfo): number {
return Math.min(40_000 / modelInfo.contextWindow, 0.2)
}

View file

@ -258,6 +258,41 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await visibleProvider.initClineWithTask(prompt)
}
public static async handleTerminalAction(
command: string,
promptType: "TERMINAL_ADD_TO_CONTEXT" | "TERMINAL_FIX" | "TERMINAL_EXPLAIN",
params: Record<string, string | any[]>,
): Promise<void> {
const visibleProvider = await ClineProvider.getInstance()
if (!visibleProvider) {
return
}
const { customSupportPrompts } = await visibleProvider.getState()
const prompt = supportPrompt.create(promptType, params, customSupportPrompts)
if (command.endsWith("AddToContext")) {
await visibleProvider.postMessageToWebview({
type: "invoke",
invoke: "setChatBoxMessage",
text: prompt,
})
return
}
if (visibleProvider.cline && command.endsWith("InCurrentTask")) {
await visibleProvider.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: prompt,
})
return
}
await visibleProvider.initClineWithTask(prompt)
}
async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
this.outputChannel.appendLine("Resolving webview view")
this.view = webviewView

View file

@ -5,7 +5,7 @@ import { createClineAPI } from "./exports"
import "./utils/path" // Necessary to have access to String.prototype.toPosix.
import { CodeActionProvider } from "./core/CodeActionProvider"
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
import { handleUri, registerCommands, registerCodeActions } from "./activate"
import { handleUri, registerCommands, registerCodeActions, registerTerminalActions } from "./activate"
/**
* Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@ -78,6 +78,7 @@ export function activate(context: vscode.ExtensionContext) {
)
registerCodeActions(context)
registerTerminalActions(context)
return createClineAPI(outputChannel, sidebarProvider)
}

View file

@ -224,4 +224,57 @@ export class TerminalManager {
this.disposables.forEach((disposable) => disposable.dispose())
this.disposables = []
}
/**
* Gets the terminal contents based on the number of commands to include
* @param commands Number of previous commands to include (-1 for all)
* @returns The selected terminal contents
*/
public async getTerminalContents(commands = -1): Promise<string> {
// Save current clipboard content
const tempCopyBuffer = await vscode.env.clipboard.readText()
try {
// Select terminal content
if (commands < 0) {
await vscode.commands.executeCommand("workbench.action.terminal.selectAll")
} else {
for (let i = 0; i < commands; i++) {
await vscode.commands.executeCommand("workbench.action.terminal.selectToPreviousCommand")
}
}
// Copy selection and clear it
await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
await vscode.commands.executeCommand("workbench.action.terminal.clearSelection")
// Get copied content
let terminalContents = (await vscode.env.clipboard.readText()).trim()
// Restore original clipboard content
await vscode.env.clipboard.writeText(tempCopyBuffer)
if (tempCopyBuffer === terminalContents) {
// No terminal content was copied
return ""
}
// Process multi-line content
const lines = terminalContents.split("\n")
const lastLine = lines.pop()?.trim()
if (lastLine) {
let i = lines.length - 1
while (i >= 0 && !lines[i].trim().startsWith(lastLine)) {
i--
}
terminalContents = lines.slice(Math.max(i, 0)).join("\n")
}
return terminalContents
} catch (error) {
// Ensure clipboard is restored even if an error occurs
await vscode.env.clipboard.writeText(tempCopyBuffer)
throw error
}
}
}

View file

@ -1,4 +1,4 @@
import { isToolAllowedForMode, FileRestrictionError, ModeConfig, parseSlashCommand } from "../modes"
import { isToolAllowedForMode, FileRestrictionError, ModeConfig } from "../modes"
describe("isToolAllowedForMode", () => {
const customModes: ModeConfig[] = [
@ -332,65 +332,3 @@ describe("FileRestrictionError", () => {
expect(error.name).toBe("FileRestrictionError")
})
})
describe("parseSlashCommand", () => {
const customModes: ModeConfig[] = [
{
slug: "custom-mode",
name: "Custom Mode",
roleDefinition: "Custom role",
groups: ["read"],
},
]
it("returns null for non-slash messages", () => {
expect(parseSlashCommand("hello world")).toBeNull()
expect(parseSlashCommand("code help me")).toBeNull()
})
it("returns null for incomplete commands", () => {
expect(parseSlashCommand("/")).toBeNull()
expect(parseSlashCommand("/code")).toBeNull()
expect(parseSlashCommand("/code ")).toBeNull()
})
it("returns null for invalid mode slugs", () => {
expect(parseSlashCommand("/invalid help me")).toBeNull()
expect(parseSlashCommand("/nonexistent do something")).toBeNull()
})
it("successfully parses valid commands", () => {
expect(parseSlashCommand("/code help me write tests")).toEqual({
modeSlug: "code",
remainingMessage: "help me write tests",
})
expect(parseSlashCommand("/ask what is typescript?")).toEqual({
modeSlug: "ask",
remainingMessage: "what is typescript?",
})
expect(parseSlashCommand("/architect plan this feature")).toEqual({
modeSlug: "architect",
remainingMessage: "plan this feature",
})
})
it("preserves whitespace in remaining message", () => {
expect(parseSlashCommand("/code help me write tests ")).toEqual({
modeSlug: "code",
remainingMessage: "help me write tests",
})
})
it("handles custom modes", () => {
expect(parseSlashCommand("/custom-mode do something", customModes)).toEqual({
modeSlug: "custom-mode",
remainingMessage: "do something",
})
})
it("returns null for invalid custom mode slugs", () => {
expect(parseSlashCommand("/invalid-custom do something", customModes)).toBeNull()
})
})

View file

@ -430,8 +430,32 @@ export const openAiModelInfoSaneDefaults: ModelInfo = {
// Gemini
// https://ai.google.dev/gemini-api/docs/models/gemini
export type GeminiModelId = keyof typeof geminiModels
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-thinking-exp-01-21"
export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-001"
export const geminiModels = {
"gemini-2.0-flash-001": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-flash-lite-preview-02-05": {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-pro-exp-02-05": {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"gemini-2.0-flash-thinking-exp-01-21": {
maxTokens: 65_536,
contextWindow: 1_048_576,

View file

@ -257,36 +257,3 @@ export function getCustomInstructions(modeSlug: string, customModes?: ModeConfig
}
return mode.customInstructions ?? ""
}
// Slash command parsing types and functions
export type SlashCommandResult = {
modeSlug: string
remainingMessage: string
} | null
export function parseSlashCommand(message: string, customModes?: ModeConfig[]): SlashCommandResult {
// Check if message starts with a slash
if (!message.startsWith("/")) {
return null
}
// Extract command (everything between / and first space)
const parts = message.trim().split(/\s+/)
if (parts.length < 2) {
return null // Need both command and message
}
const command = parts[0].substring(1) // Remove leading slash
const remainingMessage = parts.slice(1).join(" ")
// Validate command is a valid mode slug
const mode = getModeBySlug(command, customModes)
if (!mode) {
return null
}
return {
modeSlug: command,
remainingMessage,
}
}

View file

@ -101,6 +101,46 @@ Provide the improved code along with explanations for each enhancement.`,
\${selectedText}
\`\`\``,
},
TERMINAL_ADD_TO_CONTEXT: {
label: "Add Terminal Content to Context",
description:
"Add terminal output to your current task or conversation. Useful for providing command outputs or logs. Available in the terminal context menu (right-click on selected terminal content).",
template: `\${userInput}
Terminal output:
\`\`\`
\${terminalContent}
\`\`\``,
},
TERMINAL_FIX: {
label: "Fix Terminal Command",
description:
"Get help fixing terminal commands that failed or need improvement. Available in the terminal context menu (right-click on selected terminal content).",
template: `\${userInput}
Fix this terminal command:
\`\`\`
\${terminalContent}
\`\`\`
Please:
1. Identify any issues in the command
2. Provide the corrected command
3. Explain what was fixed and why`,
},
TERMINAL_EXPLAIN: {
label: "Explain Terminal Command",
description:
"Get detailed explanations of terminal commands and their outputs. Available in the terminal context menu (right-click on selected terminal content).",
template: `\${userInput}
Explain this terminal command:
\`\`\`
\${terminalContent}
\`\`\`
Please provide:
1. What the command does
2. Explanation of each part/flag
3. Expected output and behavior`,
},
} as const
type SupportPromptType = keyof typeof supportPromptConfigs

View file

@ -97,6 +97,12 @@ describe("Shell Detection Tests", () => {
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" })

View file

@ -105,7 +105,7 @@ function getWindowsShellFromVSCode(): string | null {
}
// If there's a specific path, return that immediately
if (profile.path) {
if (profile?.path) {
return profile.path
}

View file

@ -0,0 +1,3 @@
{
"extends": "react-app"
}

View file

@ -0,0 +1,22 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "jsdom",
injectGlobals: true,
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
transform: { "^.+\\.(ts|tsx)$": ["ts-jest", { tsconfig: { jsx: "react-jsx" } }] },
testMatch: ["<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}", "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"],
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts", "@testing-library/jest-dom/extend-expect"],
moduleNameMapper: {
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
"^vscrui$": "<rootDir>/src/__mocks__/vscrui.ts",
"^@vscode/webview-ui-toolkit/react$": "<rootDir>/src/__mocks__/@vscode/webview-ui-toolkit/react.ts",
"^@/(.*)$": "<rootDir>/src/$1",
},
reporters: [["jest-simple-dot-reporter", {}]],
transformIgnorePatterns: [
"/node_modules/(?!(rehype-highlight|react-remark|unist-util-visit|unist-util-find-after|vfile|unified|bail|is-plain-obj|trough|vfile-message|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|longest-streak|escape-string-regexp|unist-util-is|hast-util-to-text|@vscode/webview-ui-toolkit|@microsoft/fast-react-wrapper|@microsoft/fast-element|@microsoft/fast-foundation|@microsoft/fast-web-utilities|exenv-es6|vscrui)/)",
],
roots: ["<rootDir>/src"],
moduleDirectories: ["node_modules", "src"],
}

File diff suppressed because it is too large Load diff

View file

@ -4,22 +4,26 @@
"private": true,
"type": "module",
"scripts": {
"lint": "eslint src --ext ts,tsx",
"check-types": "tsc --noEmit",
"test": "jest",
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint src --ext ts,tsx",
"test": "jest",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-slot": "^1.1.1",
"@tailwindcss/vite": "^4.0.0",
"@vscode/webview-ui-toolkit": "^1.4.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"debounce": "^2.1.1",
"fast-deep-equal": "^3.1.3",
"fzf": "^0.5.2",
@ -58,6 +62,7 @@
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-storybook": "^0.11.2",
@ -69,44 +74,5 @@
"ts-jest": "^27.1.5",
"typescript": "^4.9.5",
"vite": "6.0.11"
},
"jest": {
"testEnvironment": "jsdom",
"setupFilesAfterEnv": [
"@testing-library/jest-dom/extend-expect"
],
"preset": "ts-jest",
"reporters": [
[
"jest-simple-dot-reporter",
{}
]
],
"moduleNameMapper": {
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
"^vscrui$": "<rootDir>/src/__mocks__/vscrui.ts",
"^@vscode/webview-ui-toolkit/react$": "<rootDir>/src/__mocks__/@vscode/webview-ui-toolkit/react.ts"
},
"transformIgnorePatterns": [
"/node_modules/(?!(rehype-highlight|react-remark|unist-util-visit|unist-util-find-after|vfile|unified|bail|is-plain-obj|trough|vfile-message|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|longest-streak|escape-string-regexp|unist-util-is|hast-util-to-text|@vscode/webview-ui-toolkit|@microsoft/fast-react-wrapper|@microsoft/fast-element|@microsoft/fast-foundation|@microsoft/fast-web-utilities|exenv-es6|vscrui)/)"
],
"transform": {
"^.+\\.(ts|tsx)$": [
"ts-jest",
{
"tsconfig": {
"jsx": "react-jsx"
}
}
]
},
"moduleDirectories": [
"node_modules",
"src"
],
"testMatch": [
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
]
}
}

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 ||
@ -226,6 +238,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}, 0)
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[setInputValue, cursorPosition],
)
@ -242,7 +255,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 +290,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 +361,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
setInputValue,
justDeletedSpaceAfterMention,
queryItems,
customModes,
],
)
@ -360,13 +382,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 +644,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
setSelectedIndex={setSelectedMenuIndex}
selectedType={selectedType}
queryItems={queryItems}
modes={getAllModes(customModes)}
/>
</div>
)}

View file

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

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

@ -1,5 +1,4 @@
import { render, fireEvent, screen } from "@testing-library/react"
import "@testing-library/jest-dom"
import ChatTextArea from "../ChatTextArea"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import { vscode } from "../../../utils/vscode"

View file

@ -66,7 +66,6 @@ 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(
@ -112,23 +111,26 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
text: slug,
})
}, [])
// Handle mode tab selection without actually switching modes
// Handle mode switching with explicit state initialization
const handleModeSwitch = useCallback(
(modeConfig: ModeConfig) => {
if (modeConfig.slug === selectedModeTab) return // Prevent unnecessary updates
if (modeConfig.slug === mode) return // Prevent unnecessary updates
// Update selected tab and reset tools edit mode
setSelectedModeTab(modeConfig.slug)
// First switch the mode
switchMode(modeConfig.slug)
// Exit tools edit mode when switching modes
setIsToolsEditMode(false)
},
[selectedModeTab, setIsToolsEditMode],
[mode, switchMode, setIsToolsEditMode],
)
// Helper function to get current mode's config
const getCurrentMode = useCallback((): ModeConfig | undefined => {
const findMode = (m: ModeConfig): boolean => m.slug === selectedModeTab
const findMode = (m: ModeConfig): boolean => m.slug === mode
return customModes?.find(findMode) || modes.find(findMode)
}, [selectedModeTab, customModes, modes])
}, [mode, customModes, modes])
// Helper function to safely access mode properties
const getModeProperty = <T extends keyof ModeConfig>(
@ -154,11 +156,6 @@ 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
@ -188,12 +185,14 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
groups: newModeGroups,
}
updateCustomMode(newModeSlug, newMode)
switchMode(newModeSlug)
setIsCreateModeDialogOpen(false)
setNewModeName("")
setNewModeSlug("")
setNewModeRoleDefinition("")
setNewModeCustomInstructions("")
setNewModeGroups(availableGroups)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newModeName, newModeSlug, newModeRoleDefinition, newModeCustomInstructions, newModeGroups, updateCustomMode])
const isNameOrSlugTaken = useCallback(
@ -478,7 +477,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
padding: "4px 0",
}}>
{modes.map((modeConfig) => {
const isActive = selectedModeTab === modeConfig.slug
const isActive = mode === modeConfig.slug
return (
<button
key={modeConfig.slug}
@ -506,22 +505,20 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
<div style={{ marginBottom: "20px" }}>
{/* Only show name and delete for custom modes */}
{selectedModeTab && findModeBySlug(selectedModeTab, customModes) && (
{mode && findModeBySlug(mode, customModes) && (
<div style={{ display: "flex", gap: "12px", marginBottom: "16px" }}>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Name</div>
<div style={{ display: "flex", gap: "8px" }}>
<VSCodeTextField
value={
getModeProperty(findModeBySlug(selectedModeTab, customModes), "name") ?? ""
}
value={getModeProperty(findModeBySlug(mode, customModes), "name") ?? ""}
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
const target =
(e as CustomEvent)?.detail?.target ||
((e as any).target as HTMLInputElement)
const customMode = findModeBySlug(selectedModeTab, customModes)
const customMode = findModeBySlug(mode, customModes)
if (customMode) {
updateCustomMode(selectedModeTab, {
updateCustomMode(mode, {
...customMode,
name: target.value,
})
@ -535,7 +532,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
onClick={() => {
vscode.postMessage({
type: "deleteCustomMode",
slug: selectedModeTab,
slug: mode,
})
}}>
<span className="codicon codicon-trash"></span>
@ -553,7 +550,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
marginBottom: "4px",
}}>
<div style={{ fontWeight: "bold" }}>Role Definition</div>
{!findModeBySlug(selectedModeTab, customModes) && (
{!findModeBySlug(mode, customModes) && (
<VSCodeButton
appearance="icon"
onClick={() => {
@ -579,28 +576,24 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
<VSCodeTextArea
value={(() => {
const customMode = findModeBySlug(selectedModeTab, customModes)
const prompt = customModePrompts?.[selectedModeTab] as PromptComponent
return (
customMode?.roleDefinition ??
prompt?.roleDefinition ??
getRoleDefinition(selectedModeTab)
)
const customMode = findModeBySlug(mode, customModes)
const prompt = customModePrompts?.[mode] as PromptComponent
return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode)
})()}
onChange={(e) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
const customMode = findModeBySlug(selectedModeTab, customModes)
const customMode = findModeBySlug(mode, customModes)
if (customMode) {
// For custom modes, update the JSON file
updateCustomMode(selectedModeTab, {
updateCustomMode(mode, {
...customMode,
roleDefinition: value.trim() || "",
})
} else {
// For built-in modes, update the prompts
updateAgentPrompt(selectedModeTab, {
updateAgentPrompt(mode, {
roleDefinition: value.trim() || undefined,
})
}
@ -762,7 +755,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
marginBottom: "4px",
}}>
<div style={{ fontWeight: "bold" }}>Mode-specific Custom Instructions</div>
{!findModeBySlug(selectedModeTab, customModes) && (
{!findModeBySlug(mode, customModes) && (
<VSCodeButton
appearance="icon"
onClick={() => {
@ -787,29 +780,29 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
<VSCodeTextArea
value={(() => {
const customMode = findModeBySlug(selectedModeTab, customModes)
const prompt = customModePrompts?.[selectedModeTab] as PromptComponent
const customMode = findModeBySlug(mode, customModes)
const prompt = customModePrompts?.[mode] as PromptComponent
return (
customMode?.customInstructions ??
prompt?.customInstructions ??
getCustomInstructions(selectedModeTab, customModes)
getCustomInstructions(mode, customModes)
)
})()}
onChange={(e) => {
const value =
(e as CustomEvent)?.detail?.target?.value ||
((e as any).target as HTMLTextAreaElement).value
const customMode = findModeBySlug(selectedModeTab, customModes)
const customMode = findModeBySlug(mode, customModes)
if (customMode) {
// For custom modes, update the JSON file
updateCustomMode(selectedModeTab, {
updateCustomMode(mode, {
...customMode,
customInstructions: value.trim() || undefined,
})
} else {
// For built-in modes, update the prompts
const existingPrompt = customModePrompts?.[selectedModeTab] as PromptComponent
updateAgentPrompt(selectedModeTab, {
const existingPrompt = customModePrompts?.[mode] as PromptComponent
updateAgentPrompt(mode, {
...existingPrompt,
customInstructions: value.trim() || undefined,
})

View file

@ -1,5 +1,4 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import "@testing-library/jest-dom"
import PromptsView from "../PromptsView"
import { ExtensionStateContext } from "../../../context/ExtensionStateContext"
import { vscode } from "../../../utils/vscode"

View file

@ -1,5 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import "@testing-library/jest-dom"
import ApiConfigManager from "../ApiConfigManager"
// Mock VSCode components

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-0 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,131 @@
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { MagnifyingGlassIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />)
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className,
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View file

@ -0,0 +1,96 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View file

@ -53,9 +53,11 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & {
container?: HTMLElement
}
>(({ className, sideOffset = 4, container, ...props }, ref) => (
<DropdownMenuPrimitive.Portal container={container}>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
@ -79,7 +81,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default 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",
"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,
)}

View file

@ -1,2 +1,5 @@
export * from "./button"
export * from "./command"
export * from "./dialog"
export * from "./dropdown-menu"
export * from "./popover"

View file

@ -0,0 +1,31 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View file

@ -22,6 +22,11 @@
@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);
@ -65,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);
@ -284,3 +289,11 @@ vscode-dropdown::part(listbox) {
.vscrui-checkbox__listbox > ul {
max-height: unset !important;
}
/**
* @shadcn/ui Overrides / Hacks
*/
input[cmdk-input]:focus {
outline: none;
}

View file

@ -0,0 +1,99 @@
import { useState } from "react"
import type { Meta, StoryObj } from "@storybook/react"
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
import {
Button,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui"
const meta = {
title: "@shadcn/Combobox",
component: Combobox,
parameters: { layout: "centered" },
tags: ["autodocs"],
} satisfies Meta<typeof Combobox>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
name: "Combobox",
render: () => <Combobox />,
}
const frameworks = [
{
value: "next.js",
label: "Next.js",
},
{
value: "sveltekit",
label: "SvelteKit",
},
{
value: "nuxt.js",
label: "Nuxt.js",
},
{
value: "remix",
label: "Remix",
},
{
value: "astro",
label: "Astro",
},
]
function Combobox() {
const [open, setOpen] = useState(false)
const [value, setValue] = useState("")
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="secondary" role="combobox" aria-expanded={open} className="w-[200px] justify-between">
{value ? frameworks.find((framework) => framework.value === value)?.label : "Select framework..."}
<CaretSortIcon className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search framework..." className="h-9" />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{frameworks.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue)
setOpen(false)
}}>
{framework.label}
<CheckIcon
className={cn(
"ml-auto",
value === framework.value ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}

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