Merged with main branch

This commit is contained in:
Pugazhendhi 2025-02-06 12:44:22 +05:30
commit 32f91b6291
114 changed files with 8101 additions and 1604 deletions

View file

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

View file

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

View file

@ -1,5 +0,0 @@
---
"roo-cline": patch
---
Use an exponential backoff for API retries

1
.env.integration.example Normal file
View file

@ -0,0 +1 @@
OPENROUTER_API_KEY=sk-or-v1-...

View file

@ -1,56 +1,24 @@
{
"root": true,
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module",
"project": "./tsconfig.json"
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/naming-convention": ["warn"],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
"@typescript-eslint/naming-convention": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
"selector": "import",
"format": ["camelCase", "PascalCase"]
}
],
"@typescript-eslint/explicit-function-return-type": [
"warn",
{
"allowExpressions": true,
"allowTypedFunctionExpressions": true
}
],
"@typescript-eslint/explicit-member-accessibility": [
"warn",
{
"accessibility": "explicit"
}
],
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/semi": "off",
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": ["off", "always"],
"quotes": ["warn", "double", { "avoidEscape": true }],
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-var-requires": "warn",
"no-extra-semi": "warn",
"prefer-const": "warn",
"no-mixed-spaces-and-tabs": "warn",
"no-case-declarations": "warn",
"no-useless-escape": "warn",
"require-yield": "warn",
"no-empty": "warn",
"no-control-regex": "warn",
"@typescript-eslint/ban-ts-comment": "warn"
"semi": "off",
"react-hooks/exhaustive-deps": "off"
},
"env": {
"node": true,
"es2021": true
},
"ignorePatterns": ["dist/**", "out/**", "webview-ui/**", "**/*.js"]
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
}

2
.github/CODEOWNERS vendored
View file

@ -1,2 +1,2 @@
# These owners will be the default owners for everything in the repo
* @stea9499 @ColemanRoo @mrubens
* @stea9499 @ColemanRoo @mrubens @cte

View file

@ -1,6 +1,7 @@
name: Code QA Roo Code
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
@ -13,33 +14,65 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm run install:all
- name: Compile TypeScript
- name: Compile
run: npm run compile
- name: Check types
run: npm run check-types
- name: Lint
run: npm run lint
unit-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm run install:all
- name: Run unit tests
run: npm test
run: npm test
check-openrouter-api-key:
runs-on: ubuntu-latest
outputs:
exists: ${{ steps.openrouter-api-key-check.outputs.defined }}
steps:
- name: Check if OpenRouter API key exists
id: openrouter-api-key-check
shell: bash
run: |
if [ "${{ secrets.OPENROUTER_API_KEY }}" != '' ]; then
echo "defined=true" >> $GITHUB_OUTPUT;
else
echo "defined=false" >> $GITHUB_OUTPUT;
fi
integration-test:
runs-on: ubuntu-latest
needs: [check-openrouter-api-key]
if: needs.check-openrouter-api-key.outputs.exists == 'true'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Create env.integration file
run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.integration
- name: Install dependencies
run: npm run install:all
- name: Run integration tests
run: xvfb-run -a npm run test:integration

26
.github/workflows/discord-pr-notify.yml vendored Normal file
View file

@ -0,0 +1,26 @@
name: Discord PR Notifier
on:
workflow_dispatch:
pull_request_target:
types: [opened]
jobs:
notify:
runs-on: ubuntu-latest
if: github.head_ref != 'changeset-release/main'
steps:
- name: Send Discord Notification
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"

44
.github/workflows/pages.yml vendored Normal file
View file

@ -0,0 +1,44 @@
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

9
.gitignore vendored
View file

@ -1,5 +1,6 @@
out
dist
out
out-integration
node_modules
coverage/
@ -15,3 +16,9 @@ roo-cline-*.vsix
# Test environment
.test_env
.vscode-test/
# Docs
docs/_site/
# Dotenv
.env.integration

View file

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

View file

@ -1,11 +1,16 @@
/**
* See: https://code.visualstudio.com/api/working-with-extensions/testing-extension
*/
import { defineConfig } from '@vscode/test-cli';
export default defineConfig({
files: 'src/test/extension.test.ts',
label: 'integrationTest',
files: 'out-integration/test/**/*.test.js',
workspaceFolder: '.',
mocha: {
ui: 'tdd',
timeout: 60000,
ui: 'tdd'
},
launchArgs: [
'--enable-proposed-api=RooVeterinaryInc.roo-cline',

View file

@ -4,6 +4,9 @@
"recommendations": [
"dbaeumer.vscode-eslint",
"connor4312.esbuild-problem-matchers",
"ms-vscode.extension-test-runner"
"ms-vscode.extension-test-runner",
"csstools.postcss",
"bradlc.vscode-tailwindcss",
"tobermory.es6-string-html"
]
}

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,26 @@
# Roo Code Changelog
## [3.3.9]
- Add o3-mini-high and o3-mini-low
## [3.3.8]
- Fix o3-mini in the Glama provider (thanks @Punkpeye!)
- Add the option to omit instructions for creating MCP servers from the system prompt (thanks @samhvw8!)
- Fix a bug where renaming API profiles without actually changing the name would delete them (thanks @samhvw8!)
## [3.3.7]
- Support for o3-mini (thanks @shpigunov!)
- Code Action improvements to allow selecting code and adding it to context, plus bug fixes (thanks @samhvw8!)
- Ability to include a message when approving or rejecting tool use (thanks @napter!)
- Improvements to chat input box styling (thanks @psv2522!)
- Capture reasoning from more variants of DeepSeek R1 (thanks @Szpadel!)
- Use an exponential backoff for API retries (if delay after first error is 5s, delay after second consecutive error will be 10s, then 20s, etc)
- Add a slider in advanced settings to enable rate limiting requests to avoid overloading providers (i.e. wait at least 10 seconds between API requests)
- Prompt tweaks to make Roo better at creating new custom modes for you
## [3.3.6]
- Add a "new task" tool that allows Roo to start new tasks with an initial message and mode

View file

@ -255,9 +255,15 @@ Roo Code is available on:
```bash
code --install-extension bin/roo-code-4.0.0.vsix
```
5. **Debug**:
5. **Start the webview (Vite/React app with HMR)**:
```bash
npm run dev
```
6. **Debug**:
- Press `F5` (or **Run****Start Debugging**) in VSCode to open a new session with Roo Code loaded.
Changes to the webview will appear immediately. Changes to the core extension will require a restart of the extension host.
We use [changesets](https://github.com/changesets/changesets) for versioning and publishing. Check our `CHANGELOG.md` for release notes.
---

2
docs/Gemfile Normal file
View file

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

308
docs/Gemfile.lock Normal file
View file

@ -0,0 +1,308 @@
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

15
docs/_config.yml Normal file
View file

@ -0,0 +1,15 @@
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

@ -0,0 +1,10 @@
---
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.

9
docs/index.md Normal file
View file

@ -0,0 +1,9 @@
---
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.

27
flake.lock generated Normal file
View file

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1737569578,
"narHash": "sha256-6qY0pk2QmUtBT9Mywdvif0i/CLVgpCjMUn6g9vB+f3M=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "47addd76727f42d351590c905d9d1905ca895b82",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

33
flake.nix Normal file
View file

@ -0,0 +1,33 @@
{
description = "Roo Code development environment";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
};
outputs = { self, nixpkgs, ... }: let
systems = [ "aarch64-darwin" "x86_64-linux" ];
forAllSystems = nixpkgs.lib.genAttrs systems;
mkDevShell = system: let
pkgs = import nixpkgs { inherit system; };
in pkgs.mkShell {
name = "roo-code";
packages = with pkgs; [
zsh
nodejs_18
corepack_18
];
shellHook = ''
exec zsh
'';
};
in {
devShells = forAllSystems (system: {
default = mkDevShell system;
});
};
}

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: [],

180
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "roo-cline",
"version": "3.3.6",
"version": "3.3.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
"version": "3.3.6",
"version": "3.3.9",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0",
@ -55,6 +55,7 @@
"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",
@ -65,7 +66,6 @@
"@typescript-eslint/parser": "^7.11.0",
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"dotenv": "^16.4.7",
"esbuild": "^0.24.0",
"eslint": "^8.57.0",
"husky": "^9.1.7",
@ -3030,6 +3030,110 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/@dotenvx/dotenvx": {
"version": "1.34.0",
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.34.0.tgz",
"integrity": "sha512-+Dp/xaI3IZ4eKv+b2vg4V89VnqLKbmJ7UZ7unnZxMu9SNLOSc2jYaXey1YHCJM+67T0pOr2Gbej3TewnuoqTWQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"commander": "^11.1.0",
"dotenv": "^16.4.5",
"eciesjs": "^0.4.10",
"execa": "^5.1.1",
"fdir": "^6.2.0",
"ignore": "^5.3.0",
"object-treeify": "1.1.33",
"picomatch": "^4.0.2",
"which": "^4.0.0"
},
"bin": {
"dotenvx": "src/cli/dotenvx.js",
"git-dotenvx": "src/cli/dotenvx.js"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@dotenvx/dotenvx/node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/@dotenvx/dotenvx/node_modules/fdir": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
"integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/@dotenvx/dotenvx/node_modules/isexe": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16"
}
},
"node_modules/@dotenvx/dotenvx/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@dotenvx/dotenvx/node_modules/which": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^3.1.1"
},
"bin": {
"node-which": "bin/which.js"
},
"engines": {
"node": "^16.13.0 || >=18.0.0"
}
},
"node_modules/@ecies/ciphers": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.2.tgz",
"integrity": "sha512-ylfGR7PyTd+Rm2PqQowG08BCKA22QuX8NzrL+LxAAvazN10DMwdJ2fWwAzRj05FI/M8vNFGm3cv9Wq/GFWCBLg==",
"dev": true,
"license": "MIT",
"engines": {
"bun": ">=1",
"deno": ">=2",
"node": ">=16"
},
"peerDependencies": {
"@noble/ciphers": "^1.0.0"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz",
@ -3964,6 +4068,48 @@
"zod": "^3.23.8"
}
},
"node_modules/@noble/ciphers": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz",
"integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz",
"integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.7.1"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz",
"integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -7772,6 +7918,24 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/eciesjs": {
"version": "0.4.13",
"resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.13.tgz",
"integrity": "sha512-zBdtR4K+wbj10bWPpIOF9DW+eFYQu8miU5ypunh0t4Bvt83ZPlEWgT5Dq/0G6uwEXumZKjfb5BZxYUZQ2Hzn/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ecies/ciphers": "^0.2.2",
"@noble/ciphers": "^1.0.0",
"@noble/curves": "^1.6.0",
"@noble/hashes": "^1.5.0"
},
"engines": {
"bun": ">=1",
"deno": ">=2",
"node": ">=16"
}
},
"node_modules/eight-colors": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/eight-colors/-/eight-colors-1.3.1.tgz",
@ -12247,6 +12411,16 @@
"node": ">= 0.4"
}
},
"node_modules/object-treeify": {
"version": "1.1.33",
"resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz",
"integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/object.assign": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.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.6",
"version": "3.3.9",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
@ -118,6 +118,11 @@
"command": "roo-cline.improveCode",
"title": "Roo Code: Improve Code",
"category": "Roo Code"
},
{
"command": "roo-cline.addToContext",
"title": "Roo Code: Add To Context",
"category": "Roo Code"
}
],
"menus": {
@ -136,6 +141,11 @@
"command": "roo-cline.improveCode",
"when": "editorHasSelection",
"group": "Roo Code@3"
},
{
"command": "roo-cline.addToContext",
"when": "editorHasSelection",
"group": "Roo Code@4"
}
],
"view/title": [
@ -211,16 +221,16 @@
"build:webview": "cd webview-ui && npm run build",
"changeset": "changeset",
"check-types": "tsc --noEmit",
"compile": "npm run check-types && npm run lint && node esbuild.js",
"compile-tests": "tsc -p . --outDir out",
"compile": "tsc -p . --outDir out && node esbuild.js",
"compile:integration": "tsc -p tsconfig.integration.json",
"install:all": "npm install && cd webview-ui && npm install",
"lint": "eslint src --ext ts --quiet && npm run lint --prefix webview-ui",
"lint": "eslint src --ext ts && npm run lint --prefix webview-ui",
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"start:webview": "cd webview-ui && npm run start",
"pretest": "npm run compile && npm run compile:integration",
"dev": "cd webview-ui && npm run dev",
"test": "jest && npm run test:webview",
"test:webview": "cd webview-ui && npm run test",
"test:extension": "vscode-test",
"test:integration": "npm run build && npm run compile:integration && npx dotenvx run -f .env.integration -- vscode-test",
"prepare": "husky",
"publish:marketplace": "vsce publish && ovsx publish",
"publish": "npm run build && changeset publish && npm install --package-lock-only",
@ -235,6 +245,7 @@
"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",
@ -245,7 +256,6 @@
"@typescript-eslint/parser": "^7.11.0",
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"dotenv": "^16.4.7",
"esbuild": "^0.24.0",
"eslint": "^8.57.0",
"husky": "^9.1.7",

View file

@ -5,9 +5,25 @@ const vscode = {
createTextEditorDecorationType: jest.fn().mockReturnValue({
dispose: jest.fn(),
}),
tabGroups: {
onDidChangeTabs: jest.fn(() => {
return {
dispose: jest.fn(),
}
}),
all: [],
},
},
workspace: {
onDidSaveTextDocument: jest.fn(),
createFileSystemWatcher: jest.fn().mockReturnValue({
onDidCreate: jest.fn().mockReturnValue({ dispose: jest.fn() }),
onDidDelete: jest.fn().mockReturnValue({ dispose: jest.fn() }),
dispose: jest.fn(),
}),
fs: {
stat: jest.fn(),
},
},
Disposable: class {
dispose() {}
@ -52,6 +68,22 @@ const vscode = {
this.id = id
}
},
ExtensionMode: {
Production: 1,
Development: 2,
Test: 3,
},
FileType: {
Unknown: 0,
File: 1,
Directory: 2,
SymbolicLink: 64,
},
TabInputText: class {
constructor(uri) {
this.uri = uri
}
},
}
module.exports = vscode

32
src/activate/handleUri.ts Normal file
View file

@ -0,0 +1,32 @@
import * as vscode from "vscode"
import { ClineProvider } from "../core/webview/ClineProvider"
export const handleUri = async (uri: vscode.Uri) => {
const path = uri.path
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
const visibleProvider = ClineProvider.getVisibleInstance()
if (!visibleProvider) {
return
}
switch (path) {
case "/glama": {
const code = query.get("code")
if (code) {
await visibleProvider.handleGlamaCallback(code)
}
break
}
case "/openrouter": {
const code = query.get("code")
if (code) {
await visibleProvider.handleOpenRouterCallback(code)
}
break
}
default:
break
}
}

3
src/activate/index.ts Normal file
View file

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

View file

@ -0,0 +1,91 @@
import * as vscode from "vscode"
import { ACTION_NAMES, COMMAND_IDS } from "../core/CodeActionProvider"
import { EditorUtils } from "../core/EditorUtils"
import { ClineProvider } from "../core/webview/ClineProvider"
export const registerCodeActions = (context: vscode.ExtensionContext) => {
registerCodeActionPair(
context,
COMMAND_IDS.EXPLAIN,
"EXPLAIN",
"What would you like Roo to explain?",
"E.g. How does the error handling work?",
)
registerCodeActionPair(
context,
COMMAND_IDS.FIX,
"FIX",
"What would you like Roo to fix?",
"E.g. Maintain backward compatibility",
)
registerCodeActionPair(
context,
COMMAND_IDS.IMPROVE,
"IMPROVE",
"What would you like Roo to improve?",
"E.g. Focus on performance optimization",
)
registerCodeAction(context, COMMAND_IDS.ADD_TO_CONTEXT, "ADD_TO_CONTEXT")
}
const registerCodeAction = (
context: vscode.ExtensionContext,
command: string,
promptType: keyof typeof ACTION_NAMES,
inputPrompt?: string,
inputPlaceholder?: string,
) => {
let userInput: string | undefined
context.subscriptions.push(
vscode.commands.registerCommand(command, async (...args: any[]) => {
if (inputPrompt) {
userInput = await vscode.window.showInputBox({
prompt: inputPrompt,
placeHolder: inputPlaceholder,
})
}
// Handle both code action and direct command cases.
let filePath: string
let selectedText: string
let diagnostics: any[] | undefined
if (args.length > 1) {
// Called from code action.
;[filePath, selectedText, diagnostics] = args
} else {
// Called directly from command palette.
const context = EditorUtils.getEditorContext()
if (!context) return
;({ filePath, selectedText, diagnostics } = context)
}
const params = {
...{ filePath, selectedText },
...(diagnostics ? { diagnostics } : {}),
...(userInput ? { userInput } : {}),
}
await ClineProvider.handleCodeAction(command, promptType, params)
}),
)
}
const registerCodeActionPair = (
context: vscode.ExtensionContext,
baseCommand: string,
promptType: keyof typeof ACTION_NAMES,
inputPrompt?: string,
inputPlaceholder?: string,
) => {
// Register new task version.
registerCodeAction(context, baseCommand, promptType, inputPrompt, inputPlaceholder)
// Register current task version.
registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, inputPrompt, inputPlaceholder)
}

View file

@ -0,0 +1,83 @@
import * as vscode from "vscode"
import delay from "delay"
import { ClineProvider } from "../core/webview/ClineProvider"
export type RegisterCommandOptions = {
context: vscode.ExtensionContext
outputChannel: vscode.OutputChannel
provider: ClineProvider
}
export const registerCommands = (options: RegisterCommandOptions) => {
const { context, outputChannel } = options
for (const [command, callback] of Object.entries(getCommandsMap(options))) {
context.subscriptions.push(vscode.commands.registerCommand(command, callback))
}
}
const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions) => {
return {
"roo-cline.plusButtonClicked": async () => {
await provider.clearTask()
await provider.postStateToWebview()
await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
},
"roo-cline.mcpButtonClicked": () => {
provider.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
},
"roo-cline.promptsButtonClicked": () => {
provider.postMessageToWebview({ type: "action", action: "promptsButtonClicked" })
},
"roo-cline.popoutButtonClicked": () => openClineInNewTab({ context, outputChannel }),
"roo-cline.openInNewTab": () => openClineInNewTab({ context, outputChannel }),
"roo-cline.settingsButtonClicked": () => {
provider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" })
},
"roo-cline.historyButtonClicked": () => {
provider.postMessageToWebview({ type: "action", action: "historyButtonClicked" })
},
}
}
const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {
outputChannel.appendLine("Opening Roo Code in new tab")
// (This example uses webviewProvider activation event which is necessary to
// deserialize cached webview, but since we use retainContextWhenHidden, we
// don't need to use that event).
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
const tabProvider = new ClineProvider(context, outputChannel)
// const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
// Check if there are any visible text editors, otherwise open a new group
// to the right.
const hasVisibleEditors = vscode.window.visibleTextEditors.length > 0
if (!hasVisibleEditors) {
await vscode.commands.executeCommand("workbench.action.newGroupRight")
}
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, {
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [context.extensionUri],
})
// TODO: use better svg icon with light and dark variants (see
// https://stackoverflow.com/questions/58365687/vscode-extension-iconpath).
panel.iconPath = {
light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
}
await tabProvider.resolveWebviewView(panel)
// Lock the editor group so clicking on files doesn't open them over the panel
await delay(100)
await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
}

View file

@ -153,11 +153,35 @@ describe("OpenAiNativeHandler", () => {
expect(mockCreate).toHaveBeenCalledWith({
model: "o1",
messages: [
{ role: "developer", content: systemPrompt },
{ role: "developer", content: "Formatting re-enabled\n" + systemPrompt },
{ role: "user", content: "Hello!" },
],
})
})
it("should handle o3-mini model family correctly", async () => {
handler = new OpenAiNativeHandler({
...mockOptions,
apiModelId: "o3-mini",
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
expect(mockCreate).toHaveBeenCalledWith({
model: "o3-mini",
messages: [
{ role: "developer", content: "Formatting re-enabled\n" + systemPrompt },
{ role: "user", content: "Hello!" },
],
stream: true,
stream_options: { include_usage: true },
reasoning_effort: "medium",
})
})
})
describe("streaming models", () => {
@ -289,6 +313,21 @@ describe("OpenAiNativeHandler", () => {
})
})
it("should complete prompt successfully with o3-mini model", async () => {
handler = new OpenAiNativeHandler({
apiModelId: "o3-mini",
openAiNativeApiKey: "test-api-key",
})
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("Test response")
expect(mockCreate).toHaveBeenCalledWith({
model: "o3-mini",
messages: [{ role: "user", content: "Test prompt" }],
reasoning_effort: "medium",
})
})
it("should handle API errors", async () => {
mockCreate.mockRejectedValueOnce(new Error("API Error"))
await expect(handler.completePrompt("Test prompt")).rejects.toThrow(

View file

@ -72,28 +72,30 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
maxTokens = 8_192
}
const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = {
model: this.getModel().id,
max_tokens: maxTokens,
messages: openAiMessages,
stream: true,
}
if (this.supportsTemperature()) {
requestOptions.temperature = 0
}
const { data: completion, response } = await this.client.chat.completions
.create(
{
model: this.getModel().id,
max_tokens: maxTokens,
temperature: 0,
messages: openAiMessages,
stream: true,
.create(requestOptions, {
headers: {
"X-Glama-Metadata": JSON.stringify({
labels: [
{
key: "app",
value: "vscode.rooveterinaryinc.roo-cline",
},
],
}),
},
{
headers: {
"X-Glama-Metadata": JSON.stringify({
labels: [
{
key: "app",
value: "vscode.rooveterinaryinc.roo-cline",
},
],
}),
},
},
)
})
.withResponse()
const completionRequestId = response.headers.get("x-completion-request-id")
@ -148,6 +150,10 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
}
}
private supportsTemperature(): boolean {
return !this.getModel().id.startsWith("openai/o3-mini")
}
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.glamaModelId
const modelInfo = this.options.glamaModelInfo
@ -164,7 +170,10 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
model: this.getModel().id,
messages: [{ role: "user", content: prompt }],
temperature: 0,
}
if (this.supportsTemperature()) {
requestOptions.temperature = 0
}
if (this.getModel().id.startsWith("anthropic/")) {

View file

@ -24,57 +24,107 @@ export class OpenAiNativeHandler implements ApiHandler, SingleCompletionHandler
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.getModel().id
switch (modelId) {
case "o1":
case "o1-preview":
case "o1-mini": {
// o1-preview and o1-mini don't support streaming, non-1 temp, or system prompt
// o1 doesnt support streaming or non-1 temp but does support a developer prompt
const response = await this.client.chat.completions.create({
model: modelId,
messages: [
{ role: modelId === "o1" ? "developer" : "user", content: systemPrompt },
...convertToOpenAiMessages(messages),
],
})
if (modelId.startsWith("o1")) {
yield* this.handleO1FamilyMessage(modelId, systemPrompt, messages)
return
}
if (modelId.startsWith("o3-mini")) {
yield* this.handleO3FamilyMessage(modelId, systemPrompt, messages)
return
}
yield* this.handleDefaultModelMessage(modelId, systemPrompt, messages)
}
private async *handleO1FamilyMessage(
modelId: string,
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream {
// o1 supports developer prompt with formatting
// o1-preview and o1-mini only support user messages
const isOriginalO1 = modelId === "o1"
const response = await this.client.chat.completions.create({
model: modelId,
messages: [
{
role: isOriginalO1 ? "developer" : "user",
content: isOriginalO1 ? `Formatting re-enabled\n${systemPrompt}` : systemPrompt,
},
...convertToOpenAiMessages(messages),
],
})
yield* this.yieldResponseData(response)
}
private async *handleO3FamilyMessage(
modelId: string,
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream {
const stream = await this.client.chat.completions.create({
model: "o3-mini",
messages: [
{
role: "developer",
content: `Formatting re-enabled\n${systemPrompt}`,
},
...convertToOpenAiMessages(messages),
],
stream: true,
stream_options: { include_usage: true },
reasoning_effort: this.getModel().info.reasoningEffort,
})
yield* this.handleStreamResponse(stream)
}
private async *handleDefaultModelMessage(
modelId: string,
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): ApiStream {
const stream = await this.client.chat.completions.create({
model: modelId,
temperature: 0,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
})
yield* this.handleStreamResponse(stream)
}
private async *yieldResponseData(response: OpenAI.Chat.Completions.ChatCompletion): ApiStream {
yield {
type: "text",
text: response.choices[0]?.message.content || "",
}
yield {
type: "usage",
inputTokens: response.usage?.prompt_tokens || 0,
outputTokens: response.usage?.completion_tokens || 0,
}
}
private async *handleStreamResponse(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): ApiStream {
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: response.choices[0]?.message.content || "",
text: delta.content,
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: response.usage?.prompt_tokens || 0,
outputTokens: response.usage?.completion_tokens || 0,
}
break
}
default: {
const stream = await this.client.chat.completions.create({
model: this.getModel().id,
// max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// contains a null value except for the last chunk which contains the token usage statistics for the entire request
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
@ -94,22 +144,12 @@ export class OpenAiNativeHandler implements ApiHandler, SingleCompletionHandler
const modelId = this.getModel().id
let requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming
switch (modelId) {
case "o1":
case "o1-preview":
case "o1-mini":
// o1 doesn't support non-1 temp
requestOptions = {
model: modelId,
messages: [{ role: "user", content: prompt }],
}
break
default:
requestOptions = {
model: modelId,
messages: [{ role: "user", content: prompt }],
temperature: 0,
}
if (modelId.startsWith("o1")) {
requestOptions = this.getO1CompletionOptions(modelId, prompt)
} else if (modelId.startsWith("o3-mini")) {
requestOptions = this.getO3CompletionOptions(modelId, prompt)
} else {
requestOptions = this.getDefaultCompletionOptions(modelId, prompt)
}
const response = await this.client.chat.completions.create(requestOptions)
@ -121,4 +161,36 @@ export class OpenAiNativeHandler implements ApiHandler, SingleCompletionHandler
throw error
}
}
private getO1CompletionOptions(
modelId: string,
prompt: string,
): OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming {
return {
model: modelId,
messages: [{ role: "user", content: prompt }],
}
}
private getO3CompletionOptions(
modelId: string,
prompt: string,
): OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming {
return {
model: "o3-mini",
messages: [{ role: "user", content: prompt }],
reasoning_effort: this.getModel().info.reasoningEffort,
}
}
private getDefaultCompletionOptions(
modelId: string,
prompt: string,
): OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming {
return {
model: modelId,
messages: [{ role: "user", content: prompt }],
temperature: 0,
}
}
}

View file

@ -118,8 +118,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
// Handle models based on deepseek-r1
if (
this.getModel().id === "deepseek/deepseek-r1" ||
this.getModel().id.startsWith("deepseek/deepseek-r1:") ||
this.getModel().id.startsWith("deepseek/deepseek-r1") ||
this.getModel().id === "perplexity/sonar-reasoning"
) {
// Recommended temperature for DeepSeek reasoning models

View file

@ -52,7 +52,7 @@ import { parseMentions } from "./mentions"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
import { formatResponse } from "./prompts/responses"
import { SYSTEM_PROMPT } from "./prompts/system"
import { modes, defaultModeSlug, getModeBySlug } from "../shared/modes"
import { modes, defaultModeSlug, getModeBySlug, parseSlashCommand } from "../shared/modes"
import { truncateHalfConversation } from "./sliding-window"
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
import { detectCodeOmission } from "../integrations/editor/detect-omission"
@ -77,6 +77,29 @@ 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
@ -96,6 +119,7 @@ export class Cline {
didFinishAborting = false
abandoned = false
private diffViewProvider: DiffViewProvider
private lastApiRequestTime?: number
// streaming
private currentStreamingContentIndex = 0
@ -354,6 +378,11 @@ 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
@ -436,6 +465,22 @@ 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)
@ -796,9 +841,40 @@ export class Cline {
async *attemptApiRequest(previousApiReqIndex: number, retryAttempt: number = 0): ApiStream {
let mcpHub: McpHub | undefined
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } =
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds, rateLimitSeconds } =
(await this.providerRef.deref()?.getState()) ?? {}
let finalDelay = 0
// Only apply rate limiting if this isn't the first request
if (this.lastApiRequestTime) {
const now = Date.now()
const timeSinceLastRequest = now - this.lastApiRequestTime
const rateLimit = rateLimitSeconds || 0
const rateLimitDelay = Math.max(0, rateLimit * 1000 - timeSinceLastRequest)
finalDelay = rateLimitDelay
}
// Add exponential backoff delay for retries
if (retryAttempt > 0) {
const baseDelay = requestDelaySeconds || 5
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt)) * 1000
finalDelay = Math.max(finalDelay, exponentialDelay)
}
if (finalDelay > 0) {
// Show countdown timer
for (let i = Math.ceil(finalDelay / 1000); i > 0; i--) {
const delayMessage =
retryAttempt > 0 ? `Retrying in ${i} seconds...` : `Rate limiting for ${i} seconds...`
await this.say("api_req_retry_delayed", delayMessage, undefined, true)
await delay(1000)
}
}
// Update last request time before making the request
this.lastApiRequestTime = Date.now()
if (mcpEnabled ?? true) {
mcpHub = this.providerRef.deref()?.mcpHub
if (!mcpHub) {
@ -810,8 +886,14 @@ export class Cline {
})
}
const { browserViewportSize, mode, customModePrompts, preferredLanguage, experiments } =
(await this.providerRef.deref()?.getState()) ?? {}
const {
browserViewportSize,
mode,
customModePrompts,
preferredLanguage,
experiments,
enableMcpServerCreation,
} = (await this.providerRef.deref()?.getState()) ?? {}
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
const systemPrompt = await (async () => {
const provider = this.providerRef.deref()
@ -832,6 +914,7 @@ export class Cline {
preferredLanguage,
this.diffEnabled,
experiments,
enableMcpServerCreation,
)
})()
@ -1093,35 +1176,23 @@ export class Cline {
const askApproval = async (type: ClineAsk, partialMessage?: string) => {
const { response, text, images } = await this.ask(type, partialMessage, false)
if (response !== "yesButtonClicked") {
if (response === "messageResponse") {
// Handle both messageResponse and noButtonClicked with text
if (text) {
await this.say("user_feedback", text, images)
pushToolResult(
formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images),
)
// this.userMessageContent.push({
// type: "text",
// text: `${toolDescription()}`,
// })
// this.toolResults.push({
// type: "tool_result",
// tool_use_id: toolUseId,
// content: this.formatToolResponseWithImages(
// await this.formatToolDeniedFeedback(text),
// images
// ),
// })
this.didRejectTool = true
return false
} else {
pushToolResult(formatResponse.toolDenied())
}
pushToolResult(formatResponse.toolDenied())
// this.toolResults.push({
// type: "tool_result",
// tool_use_id: toolUseId,
// content: await this.formatToolDenied(),
// })
this.didRejectTool = true
return false
}
// Handle yesButtonClicked with text
if (text) {
await this.say("user_feedback", text, images)
pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images))
}
return true
}

View file

@ -1,113 +1,27 @@
import * as vscode from "vscode"
import * as path from "path"
import { ClineProvider } from "./webview/ClineProvider"
import { EditorUtils } from "./EditorUtils"
export const ACTION_NAMES = {
EXPLAIN: "Roo Code: Explain Code",
FIX: "Roo Code: Fix Code",
FIX_LOGIC: "Roo Code: Fix Logic",
IMPROVE: "Roo Code: Improve Code",
ADD_TO_CONTEXT: "Roo Code: Add to Context",
} as const
const COMMAND_IDS = {
export const COMMAND_IDS = {
EXPLAIN: "roo-cline.explainCode",
FIX: "roo-cline.fixCode",
IMPROVE: "roo-cline.improveCode",
ADD_TO_CONTEXT: "roo-cline.addToContext",
} as const
interface DiagnosticData {
message: string
severity: vscode.DiagnosticSeverity
code?: string | number | { value: string | number; target: vscode.Uri }
source?: string
range: vscode.Range
}
interface EffectiveRange {
range: vscode.Range
text: string
}
export class CodeActionProvider implements vscode.CodeActionProvider {
public static readonly providedCodeActionKinds = [
vscode.CodeActionKind.QuickFix,
vscode.CodeActionKind.RefactorRewrite,
]
// Cache file paths for performance
private readonly filePathCache = new WeakMap<vscode.TextDocument, string>()
private getEffectiveRange(
document: vscode.TextDocument,
range: vscode.Range | vscode.Selection,
): EffectiveRange | null {
try {
const selectedText = document.getText(range)
if (selectedText) {
return { range, text: selectedText }
}
const currentLine = document.lineAt(range.start.line)
if (!currentLine.text.trim()) {
return null
}
// Optimize range creation by checking bounds first
const startLine = Math.max(0, currentLine.lineNumber - 1)
const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1)
// Only create new positions if needed
const effectiveRange = new vscode.Range(
startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0),
endLine === currentLine.lineNumber
? range.end
: new vscode.Position(endLine, document.lineAt(endLine).text.length),
)
return {
range: effectiveRange,
text: document.getText(effectiveRange),
}
} catch (error) {
console.error("Error getting effective range:", error)
return null
}
}
private getFilePath(document: vscode.TextDocument): string {
// Check cache first
let filePath = this.filePathCache.get(document)
if (filePath) {
return filePath
}
try {
const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri)
if (!workspaceFolder) {
filePath = document.uri.fsPath
} else {
const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath)
filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath
}
// Cache the result
this.filePathCache.set(document, filePath)
return filePath
} catch (error) {
console.error("Error getting file path:", error)
return document.uri.fsPath
}
}
private createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData {
return {
message: diagnostic.message,
severity: diagnostic.severity,
code: diagnostic.code,
source: diagnostic.source,
range: diagnostic.range, // Reuse the range object
}
}
private createAction(title: string, kind: vscode.CodeActionKind, command: string, args: any[]): vscode.CodeAction {
const action = new vscode.CodeAction(title, kind)
action.command = { command, title, arguments: args }
@ -126,32 +40,20 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
]
}
private hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean {
// Optimize range intersection check
return !(
range2.end.line < range1.start.line ||
range2.start.line > range1.end.line ||
(range2.end.line === range1.start.line && range2.end.character < range1.start.character) ||
(range2.start.line === range1.end.line && range2.start.character > range1.end.character)
)
}
public provideCodeActions(
document: vscode.TextDocument,
range: vscode.Range | vscode.Selection,
context: vscode.CodeActionContext,
): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> {
try {
const effectiveRange = this.getEffectiveRange(document, range)
const effectiveRange = EditorUtils.getEffectiveRange(document, range)
if (!effectiveRange) {
return []
}
const filePath = this.getFilePath(document)
const filePath = EditorUtils.getFilePath(document)
const actions: vscode.CodeAction[] = []
// Create actions using helper method
// Add explain actions
actions.push(
...this.createActionPair(ACTION_NAMES.EXPLAIN, vscode.CodeActionKind.QuickFix, COMMAND_IDS.EXPLAIN, [
filePath,
@ -159,14 +61,13 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
]),
)
// Only process diagnostics if they exist
if (context.diagnostics.length > 0) {
const relevantDiagnostics = context.diagnostics.filter((d) =>
this.hasIntersectingRange(effectiveRange.range, d.range),
EditorUtils.hasIntersectingRange(effectiveRange.range, d.range),
)
if (relevantDiagnostics.length > 0) {
const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData)
const diagnosticMessages = relevantDiagnostics.map(EditorUtils.createDiagnosticData)
actions.push(
...this.createActionPair(ACTION_NAMES.FIX, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [
filePath,
@ -175,9 +76,15 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
]),
)
}
} else {
actions.push(
...this.createActionPair(ACTION_NAMES.FIX_LOGIC, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [
filePath,
effectiveRange.text,
]),
)
}
// Add improve actions
actions.push(
...this.createActionPair(
ACTION_NAMES.IMPROVE,
@ -187,6 +94,15 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
),
)
actions.push(
this.createAction(
ACTION_NAMES.ADD_TO_CONTEXT,
vscode.CodeActionKind.QuickFix,
COMMAND_IDS.ADD_TO_CONTEXT,
[filePath, effectiveRange.text],
),
)
return actions
} catch (error) {
console.error("Error providing code actions:", error)

204
src/core/EditorUtils.ts Normal file
View file

@ -0,0 +1,204 @@
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 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,
): EffectiveRange | null {
try {
const selectedText = document.getText(range)
if (selectedText) {
return { range, text: selectedText }
}
const currentLine = document.lineAt(range.start.line)
if (!currentLine.text.trim()) {
return null
}
const startLineIndex = Math.max(0, currentLine.lineNumber - 1)
const endLineIndex = Math.min(document.lineCount - 1, currentLine.lineNumber + 1)
const effectiveRange = new vscode.Range(
new vscode.Position(startLineIndex, 0),
new vscode.Position(endLineIndex, document.lineAt(endLineIndex).text.length),
)
return {
range: effectiveRange,
text: document.getText(effectiveRange),
}
} catch (error) {
console.error("Error getting effective range:", error)
return null
}
}
/**
* 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 {
let filePath = this.filePathCache.get(document)
if (filePath) {
return filePath
}
try {
const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri)
if (!workspaceFolder) {
filePath = document.uri.fsPath
} else {
const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath)
filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath
}
this.filePathCache.set(document, filePath)
return filePath
} catch (error) {
console.error("Error getting file path:", error)
return document.uri.fsPath
}
}
/**
* 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,
severity: diagnostic.severity,
code: diagnostic.code,
source: diagnostic.source,
range: diagnostic.range,
}
}
/**
* 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 {
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.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) {
editor = vscode.window.activeTextEditor
}
if (!editor) {
return null
}
const document = editor.document
const selection = editor.selection
const effectiveRange = this.getEffectiveRange(document, selection)
if (!effectiveRange) {
return null
}
const filePath = this.getFilePath(document)
const diagnostics = vscode.languages
.getDiagnostics(document.uri)
.filter((d) => this.hasIntersectingRange(effectiveRange.range, d.range))
.map(this.createDiagnosticData)
return {
filePath,
selectedText: effectiveRange.text,
...(diagnostics.length > 0 ? { diagnostics } : {}),
}
} catch (error) {
console.error("Error getting editor context:", error)
return null
}
}
}

View file

@ -128,6 +128,7 @@ jest.mock("vscode", () => {
visibleTextEditors: [mockTextEditor],
tabGroups: {
all: [mockTabGroup],
onDidChangeTabs: jest.fn(() => ({ dispose: jest.fn() })),
},
},
workspace: {
@ -750,8 +751,11 @@ describe("Cline", () => {
false,
)
// Verify delay was called correctly
expect(mockDelay).toHaveBeenCalledTimes(baseDelay)
// Calculate expected delay calls based on exponential backoff
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, 1)) // retryAttempt = 1
const rateLimitDelay = baseDelay // Initial rate limit delay
const totalExpectedDelays = exponentialDelay + rateLimitDelay
expect(mockDelay).toHaveBeenCalledTimes(totalExpectedDelays)
expect(mockDelay).toHaveBeenCalledWith(1000)
// Verify error message content

View file

@ -1,5 +1,6 @@
import * as vscode from "vscode"
import { CodeActionProvider, ACTION_NAMES } from "../CodeActionProvider"
import { EditorUtils } from "../EditorUtils"
// Mock VSCode API
jest.mock("vscode", () => ({
@ -16,13 +17,6 @@ jest.mock("vscode", () => ({
start: { line: startLine, character: startChar },
end: { line: endLine, character: endChar },
})),
Position: jest.fn().mockImplementation((line, character) => ({
line,
character,
})),
workspace: {
getWorkspaceFolder: jest.fn(),
},
DiagnosticSeverity: {
Error: 0,
Warning: 1,
@ -31,6 +25,16 @@ jest.mock("vscode", () => ({
},
}))
// Mock EditorUtils
jest.mock("../EditorUtils", () => ({
EditorUtils: {
getEffectiveRange: jest.fn(),
getFilePath: jest.fn(),
hasIntersectingRange: jest.fn(),
createDiagnosticData: jest.fn(),
},
}))
describe("CodeActionProvider", () => {
let provider: CodeActionProvider
let mockDocument: any
@ -55,68 +59,32 @@ describe("CodeActionProvider", () => {
mockContext = {
diagnostics: [],
}
})
describe("getEffectiveRange", () => {
it("should return selected text when available", () => {
mockDocument.getText.mockReturnValue("selected text")
const result = (provider as any).getEffectiveRange(mockDocument, mockRange)
expect(result).toEqual({
range: mockRange,
text: "selected text",
})
})
it("should return null for empty line", () => {
mockDocument.getText.mockReturnValue("")
mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 })
const result = (provider as any).getEffectiveRange(mockDocument, mockRange)
expect(result).toBeNull()
})
})
describe("getFilePath", () => {
it("should return relative path when in workspace", () => {
const mockWorkspaceFolder = {
uri: { fsPath: "/test" },
}
;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder)
const result = (provider as any).getFilePath(mockDocument)
expect(result).toBe("file.ts")
})
it("should return absolute path when not in workspace", () => {
;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null)
const result = (provider as any).getFilePath(mockDocument)
expect(result).toBe("/test/file.ts")
// Setup default EditorUtils mocks
;(EditorUtils.getEffectiveRange as jest.Mock).mockReturnValue({
range: mockRange,
text: "test code",
})
;(EditorUtils.getFilePath as jest.Mock).mockReturnValue("/test/file.ts")
;(EditorUtils.hasIntersectingRange as jest.Mock).mockReturnValue(true)
;(EditorUtils.createDiagnosticData as jest.Mock).mockImplementation((d) => d)
})
describe("provideCodeActions", () => {
beforeEach(() => {
mockDocument.getText.mockReturnValue("test code")
mockDocument.lineAt.mockReturnValue({ text: "test code", lineNumber: 0 })
})
it("should provide explain and improve actions by default", () => {
it("should provide explain, improve, fix logic, and add to context actions by default", () => {
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
expect(actions).toHaveLength(4)
expect(actions).toHaveLength(7) // 2 explain + 2 fix logic + 2 improve + 1 add to context
expect((actions as any)[0].title).toBe(`${ACTION_NAMES.EXPLAIN} in New Task`)
expect((actions as any)[1].title).toBe(`${ACTION_NAMES.EXPLAIN} in Current Task`)
expect((actions as any)[2].title).toBe(`${ACTION_NAMES.IMPROVE} in New Task`)
expect((actions as any)[3].title).toBe(`${ACTION_NAMES.IMPROVE} in Current Task`)
expect((actions as any)[2].title).toBe(`${ACTION_NAMES.FIX_LOGIC} in New Task`)
expect((actions as any)[3].title).toBe(`${ACTION_NAMES.FIX_LOGIC} in Current Task`)
expect((actions as any)[4].title).toBe(`${ACTION_NAMES.IMPROVE} in New Task`)
expect((actions as any)[5].title).toBe(`${ACTION_NAMES.IMPROVE} in Current Task`)
expect((actions as any)[6].title).toBe(ACTION_NAMES.ADD_TO_CONTEXT)
})
it("should provide fix action when diagnostics exist", () => {
it("should provide fix action instead of fix logic when diagnostics exist", () => {
mockContext.diagnostics = [
{
message: "test error",
@ -127,22 +95,33 @@ describe("CodeActionProvider", () => {
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
expect(actions).toHaveLength(6)
expect(actions).toHaveLength(7) // 2 explain + 2 fix + 2 improve + 1 add to context
expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX} in New Task`)).toBe(true)
expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX} in Current Task`)).toBe(true)
expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX_LOGIC} in New Task`)).toBe(false)
expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX_LOGIC} in Current Task`)).toBe(
false,
)
})
it("should handle errors gracefully", () => {
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
mockDocument.getText.mockImplementation(() => {
throw new Error("Test error")
})
mockDocument.lineAt.mockReturnValue({ text: "test", lineNumber: 0 })
it("should return empty array when no effective range", () => {
;(EditorUtils.getEffectiveRange as jest.Mock).mockReturnValue(null)
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
expect(actions).toEqual([])
expect(consoleErrorSpy).toHaveBeenCalledWith("Error getting effective range:", expect.any(Error))
})
it("should handle errors gracefully", () => {
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
;(EditorUtils.getEffectiveRange as jest.Mock).mockImplementation(() => {
throw new Error("Test error")
})
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
expect(actions).toEqual([])
expect(consoleErrorSpy).toHaveBeenCalledWith("Error providing code actions:", expect.any(Error))
consoleErrorSpy.mockRestore()
})

View file

@ -0,0 +1,142 @@
import * as vscode from "vscode"
import { EditorUtils } from "../EditorUtils"
// 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
beforeEach(() => {
mockDocument = {
getText: jest.fn(),
lineAt: jest.fn(),
lineCount: 10,
uri: { fsPath: "/test/file.ts" },
}
})
describe("getEffectiveRange", () => {
it("should return selected text when available", () => {
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)
expect(result).toEqual({
range: mockRange,
text: "selected text",
})
})
it("should return null for empty line", () => {
const mockRange = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10))
mockDocument.getText.mockReturnValue("")
mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 })
const result = EditorUtils.getEffectiveRange(mockDocument, mockRange)
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", () => {
it("should return relative path when in workspace", () => {
const mockWorkspaceFolder = {
uri: { fsPath: "/test" },
}
;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder)
const result = EditorUtils.getFilePath(mockDocument)
expect(result).toBe("file.ts")
})
it("should return absolute path when not in workspace", () => {
;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null)
const result = EditorUtils.getFilePath(mockDocument)
expect(result).toBe("/test/file.ts")
})
})
})

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

@ -1,37 +1,9 @@
import { DiffStrategy, DiffResult } from "../types"
import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text"
import { distance } from "fastest-levenshtein"
const BUFFER_LINES = 20 // Number of extra context lines to show before and after matches
function levenshteinDistance(a: string, b: string): number {
const matrix: number[][] = []
// Initialize matrix
for (let i = 0; i <= a.length; i++) {
matrix[i] = [i]
}
for (let j = 0; j <= b.length; j++) {
matrix[0][j] = j
}
// Fill matrix
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
if (a[i - 1] === b[j - 1]) {
matrix[i][j] = matrix[i - 1][j - 1]
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1, // substitution
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j] + 1, // deletion
)
}
}
}
return matrix[a.length][b.length]
}
function getSimilarity(original: string, search: string): number {
if (search === "") {
return 1
@ -47,12 +19,12 @@ function getSimilarity(original: string, search: string): number {
return 1
}
// Calculate Levenshtein distance
const distance = levenshteinDistance(normalizedOriginal, normalizedSearch)
// Calculate Levenshtein distance using fastest-levenshtein's distance function
const dist = distance(normalizedOriginal, normalizedSearch)
// Calculate similarity ratio (0 to 1, where 1 is exact match)
// Calculate similarity ratio (0 to 1, where 1 is an exact match)
const maxLength = Math.max(normalizedOriginal.length, normalizedSearch.length)
return 1 - distance / maxLength
return 1 - dist / maxLength
}
export class SearchReplaceDiffStrategy implements DiffStrategy {

File diff suppressed because it is too large Load diff

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",
@ -174,6 +178,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@ -194,6 +199,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@ -216,6 +222,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@ -236,6 +243,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@ -256,6 +264,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@ -276,6 +285,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
true, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toContain("apply_diff")
@ -297,6 +307,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
false, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).not.toContain("apply_diff")
@ -318,6 +329,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).not.toContain("apply_diff")
@ -339,6 +351,7 @@ describe("SYSTEM_PROMPT", () => {
"Spanish", // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toContain("Language Preference:")
@ -371,6 +384,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
// Role definition should be at the top
@ -406,6 +420,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
)
// Role definition from promptComponent should be at the top
@ -436,6 +451,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
)
// Should use the default mode's role definition
@ -458,6 +474,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments, // experiments - undefined should disable all experimental tools
true, // enableMcpServerCreation
)
// Verify experimental tools are not included in the prompt
@ -485,6 +502,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
// Verify experimental tools are included in the prompt when enabled
@ -512,6 +530,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
// Verify only enabled experimental tools are included
@ -539,6 +558,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
true, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
// Verify base instruction lists all available tools
@ -568,6 +588,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
true,
experiments,
true, // enableMcpServerCreation
)
// Verify detailed instructions for each tool
@ -623,6 +644,7 @@ describe("addCustomInstructions", () => {
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@ -643,11 +665,60 @@ describe("addCustomInstructions", () => {
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
})
it("should include MCP server creation info when enabled", async () => {
const mockMcpHub = createMockMcpHub()
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
false, // supportsComputerUse
mockMcpHub, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toContain("Creating an MCP Server")
expect(prompt).toMatchSnapshot()
})
it("should exclude MCP server creation info when disabled", async () => {
const mockMcpHub = createMockMcpHub()
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
false, // supportsComputerUse
mockMcpHub, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
false, // enableMcpServerCreation
)
expect(prompt).not.toContain("Creating an MCP Server")
expect(prompt).toMatchSnapshot()
})
it("should prioritize mode-specific rules for code mode", async () => {
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
expect(instructions).toMatchSnapshot()

View file

@ -8,6 +8,9 @@ export const formatResponse = {
toolDeniedWithFeedback: (feedback?: string) =>
`The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
toolApprovedWithFeedback: (feedback?: string) =>
`The user approved this operation and provided the following context:\n<feedback>\n${feedback}\n</feedback>`,
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
noToolsUsed: () =>

View file

@ -1,7 +1,11 @@
import { DiffStrategy } from "../../diff/DiffStrategy"
import { McpHub } from "../../../services/mcp/McpHub"
export async function getMcpServersSection(mcpHub?: McpHub, diffStrategy?: DiffStrategy): Promise<string> {
export async function getMcpServersSection(
mcpHub?: McpHub,
diffStrategy?: DiffStrategy,
enableMcpServerCreation?: boolean,
): Promise<string> {
if (!mcpHub) {
return ""
}
@ -43,7 +47,7 @@ export async function getMcpServersSection(mcpHub?: McpHub, diffStrategy?: DiffS
.join("\n\n")}`
: "(No MCP servers currently connected)"
return `MCP SERVERS
const baseSection = `MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
@ -51,7 +55,15 @@ The Model Context Protocol (MCP) enables communication between the system and lo
When a server is connected, you can use the server's tools via the \`use_mcp_tool\` tool, and access the server's resources via the \`access_mcp_resource\` tool.
${connectedServers}
${connectedServers}`
if (!enableMcpServerCreation) {
return baseSection
}
return (
baseSection +
`
## Creating an MCP Server
@ -398,11 +410,11 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de
## Editing MCP Servers
The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' above: ${
mcpHub
.getServers()
.map((server) => server.name)
.join(", ") || "(None running currently)"
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file${diffStrategy ? " or apply_diff" : ""} to make changes to the files.
mcpHub
.getServers()
.map((server) => server.name)
.join(", ") || "(None running currently)"
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file${diffStrategy ? " or apply_diff" : ""} to make changes to the files.
However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server.
@ -411,4 +423,5 @@ However some MCP servers may be running from installed packages rather than a lo
The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that...").
Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.`
)
}

View file

@ -16,13 +16,17 @@ MODES
${modes.map((mode: ModeConfig) => ` * "${mode.name}" mode - ${mode.roleDefinition.split(".")[0]}`).join("\n")}
Custom modes will be referred to by their configured name property.
- Custom modes can be configured by creating or editing the custom modes file at '${customModesPath}'. The following fields are required and must not be empty:
- Custom modes can be configured by editing the custom modes file at '${customModesPath}'. The file gets created automatically on startup and should always exist. Make sure to read the latest contents before writing to it to avoid overwriting existing modes.
- The following fields are required and must not be empty:
* slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better.
* name: The display name for the mode
* roleDefinition: A detailed description of the mode's role and capabilities
* groups: Array of allowed tool groups (can be empty). Each group can be specified either as a string (e.g., "edit" to allow editing any file) or with file restrictions (e.g., ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }] to only allow editing markdown files)
The customInstructions field is optional.
- The customInstructions field is optional.
- For multi-line text, include newline characters in the string like "This is the first line.\nThis is the next line.\n\nThis is a double line break."
The file should follow this structure:
{

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

@ -40,6 +40,7 @@ async function generatePrompt(
preferredLanguage?: string,
diffEnabled?: boolean,
experiments?: Record<string, boolean>,
enableMcpServerCreation?: boolean,
): Promise<string> {
if (!context) {
throw new Error("Extension context is required for generating system prompt")
@ -49,7 +50,7 @@ async function generatePrompt(
const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined
const [mcpServersSection, modesSection] = await Promise.all([
getMcpServersSection(mcpHub, effectiveDiffStrategy),
getMcpServersSection(mcpHub, effectiveDiffStrategy, enableMcpServerCreation),
getModesSection(context),
])
@ -105,6 +106,7 @@ export const SYSTEM_PROMPT = async (
preferredLanguage?: string,
diffEnabled?: boolean,
experiments?: Record<string, boolean>,
enableMcpServerCreation?: boolean,
): Promise<string> => {
if (!context) {
throw new Error("Extension context is required for generating system prompt")
@ -139,5 +141,6 @@ export const SYSTEM_PROMPT = async (
preferredLanguage,
diffEnabled,
experiments,
enableMcpServerCreation,
)
}

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

@ -19,15 +19,7 @@ import { findLast } from "../../shared/array"
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
import { HistoryItem } from "../../shared/HistoryItem"
import { WebviewMessage } from "../../shared/WebviewMessage"
import {
Mode,
modes,
CustomModePrompts,
PromptComponent,
ModeConfig,
defaultModeSlug,
getModeBySlug,
} from "../../shared/modes"
import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug } from "../../shared/modes"
import { SYSTEM_PROMPT } from "../prompts/system"
import { fileExistsAtPath } from "../../utils/fs"
import { Cline } from "../Cline"
@ -37,7 +29,7 @@ import { getUri } from "./getUri"
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
import { checkExistKey } from "../../shared/checkExistApiConfig"
import { singleCompletionHandler } from "../../utils/single-completion-handler"
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
import { searchCommits } from "../../utils/git"
import { ConfigManager } from "../config/ConfigManager"
import { CustomModesManager } from "../config/CustomModesManager"
import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
@ -110,8 +102,10 @@ type GlobalStateKey =
| "writeDelayMs"
| "terminalOutputLineLimit"
| "mcpEnabled"
| "enableMcpServerCreation"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "rateLimitSeconds"
| "currentApiConfigName"
| "listApiConfigMeta"
| "vsCodeLmModelSelector"
@ -141,6 +135,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private static activeInstances: Set<ClineProvider> = new Set()
private disposables: vscode.Disposable[] = []
private view?: vscode.WebviewView | vscode.WebviewPanel
private isViewLaunched = false
private cline?: Cline
private workspaceTracker?: WorkspaceTracker
mcpHub?: McpHub
@ -240,6 +235,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {
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",
@ -253,11 +258,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
@ -271,7 +272,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
enableScripts: true,
localResourceRoots: [this.context.extensionUri],
}
webviewView.webview.html = this.getHtmlContent(webviewView.webview)
webviewView.webview.html =
this.context.extensionMode === vscode.ExtensionMode.Development
? await this.getHMRHtmlContent(webviewView.webview)
: this.getHtmlContent(webviewView.webview)
// Sets up an event listener to listen for messages passed from the webview view context
// and executes code based on the message that is recieved
@ -395,6 +400,73 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.view?.webview.postMessage(message)
}
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",
"@vscode",
"codicons",
"dist",
"codicon.css",
])
const file = "src/index.tsx"
const scriptUri = `http://${localServerUrl}/${file}`
const reactRefresh = /*html*/ `
<script nonce="${nonce}" type="module">
import RefreshRuntime from "http://localhost:${localPort}/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
`
const csp = [
"default-src 'none'",
`font-src ${webview.cspSource}`,
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
`img-src ${webview.cspSource} data:`,
`script-src 'unsafe-eval' https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
`connect-src https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
]
return /*html*/ `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<title>Roo Code</title>
</head>
<body>
<div id="root"></div>
${reactRefresh}
<script type="module" src="${scriptUri}"></script>
</body>
</html>
`
}
/**
* Defines and returns the HTML that should be rendered within the webview panel.
*
@ -601,6 +673,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
),
)
this.isViewLaunched = true
break
case "newTask":
// Code that should run in response to the hello message command
@ -799,6 +872,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("mcpEnabled", mcpEnabled)
await this.postStateToWebview()
break
case "enableMcpServerCreation":
await this.updateGlobalState("enableMcpServerCreation", message.bool ?? true)
await this.postStateToWebview()
break
case "playSound":
if (message.audioType) {
const soundPath = path.join(this.context.extensionPath, "audio", `${message.audioType}.wav`)
@ -839,6 +916,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("requestDelaySeconds", message.value ?? 5)
await this.postStateToWebview()
break
case "rateLimitSeconds":
await this.updateGlobalState("rateLimitSeconds", message.value ?? 0)
await this.postStateToWebview()
break
case "preferredLanguage":
await this.updateGlobalState("preferredLanguage", message.text)
await this.postStateToWebview()
@ -1083,6 +1164,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
mcpEnabled,
fuzzyMatchThreshold,
experiments,
enableMcpServerCreation,
} = await this.getState()
// Create diffStrategy based on current model and settings
@ -1111,6 +1193,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
preferredLanguage,
diffEnabled,
experiments,
enableMcpServerCreation,
)
await this.postMessageToWebview({
@ -1169,6 +1252,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
try {
const { oldName, newName } = message.values
if (oldName === newName) {
break
}
await this.configManager.saveConfig(newName, message.apiConfiguration)
await this.configManager.deleteConfig(oldName)
@ -1996,8 +2083,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
terminalOutputLineLimit,
fuzzyMatchThreshold,
mcpEnabled,
enableMcpServerCreation,
alwaysApproveResubmit,
requestDelaySeconds,
rateLimitSeconds,
currentApiConfigName,
listApiConfigMeta,
mode,
@ -2037,8 +2126,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
mcpEnabled: mcpEnabled ?? true,
enableMcpServerCreation: enableMcpServerCreation ?? true,
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
requestDelaySeconds: requestDelaySeconds ?? 10,
rateLimitSeconds: rateLimitSeconds ?? 0,
currentApiConfigName: currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [],
mode: mode ?? defaultModeSlug,
@ -2160,8 +2251,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
screenshotQuality,
terminalOutputLineLimit,
mcpEnabled,
enableMcpServerCreation,
alwaysApproveResubmit,
requestDelaySeconds,
rateLimitSeconds,
currentApiConfigName,
listApiConfigMeta,
vsCodeLmModelSelector,
@ -2233,8 +2326,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("screenshotQuality") as Promise<number | undefined>,
this.getGlobalState("terminalOutputLineLimit") as Promise<number | undefined>,
this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
this.getGlobalState("enableMcpServerCreation") as Promise<boolean | undefined>,
this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
this.getGlobalState("rateLimitSeconds") as Promise<number | undefined>,
this.getGlobalState("currentApiConfigName") as Promise<string | undefined>,
this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
@ -2357,8 +2452,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return langMap[vscodeLang.split("-")[0]] ?? "English"
})(),
mcpEnabled: mcpEnabled ?? true,
enableMcpServerCreation: enableMcpServerCreation ?? true,
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
requestDelaySeconds: Math.max(5, requestDelaySeconds ?? 10),
rateLimitSeconds: rateLimitSeconds ?? 0,
currentApiConfigName: currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [],
modeApiConfigs: modeApiConfigs ?? ({} as Record<Mode, string>),
@ -2416,7 +2513,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// secrets
private async storeSecret(key: SecretKey, value?: string) {
public async storeSecret(key: SecretKey, value?: string) {
if (value) {
await this.context.secrets.store(key, value)
} else {
@ -2470,4 +2567,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.postStateToWebview()
await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
}
// integration tests
get viewLaunched() {
return this.isViewLaunched
}
get messages() {
return this.cline?.clineMessages || []
}
}

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,
}))
@ -108,6 +112,11 @@ jest.mock("vscode", () => ({
uriScheme: "vscode",
language: "en",
},
ExtensionMode: {
Production: 1,
Development: 2,
Test: 3,
},
}))
// Mock sound utility
@ -197,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
@ -265,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
})
@ -283,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",
@ -318,7 +344,9 @@ describe("ClineProvider", () => {
browserViewportSize: "900x600",
fuzzyMatchThreshold: 1.0,
mcpEnabled: true,
enableMcpServerCreation: false,
requestDelaySeconds: 5,
rateLimitSeconds: 0,
mode: defaultModeSlug,
customModes: [],
experiments: experimentDefault,
@ -334,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]
@ -413,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 })
@ -423,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]
@ -463,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
@ -484,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
@ -512,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 = {
@ -533,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
@ -548,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
@ -643,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
@ -700,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 = {
@ -725,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 () => {
@ -854,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(
@ -889,6 +917,7 @@ describe("ClineProvider", () => {
},
},
mcpEnabled: true,
enableMcpServerCreation: false,
mode: "code" as const,
experiments: experimentDefault,
} as any)
@ -921,6 +950,7 @@ describe("ClineProvider", () => {
},
},
mcpEnabled: false,
enableMcpServerCreation: false,
mode: "code" as const,
experiments: experimentDefault,
} as any)
@ -985,6 +1015,7 @@ describe("ClineProvider", () => {
},
customModePrompts: {},
mode: "code",
enableMcpServerCreation: true,
mcpEnabled: false,
browserViewportSize: "900x600",
experimentalDiffStrategy: true,
@ -1019,6 +1050,7 @@ describe("ClineProvider", () => {
undefined, // preferredLanguage
true, // diffEnabled
experimentDefault,
true,
)
// Run the test again to verify it's consistent
@ -1042,6 +1074,7 @@ describe("ClineProvider", () => {
diffEnabled: false,
fuzzyMatchThreshold: 0.8,
experiments: experimentDefault,
enableMcpServerCreation: true,
} as any)
// Mock SYSTEM_PROMPT to verify diffEnabled is passed as false
@ -1070,6 +1103,7 @@ describe("ClineProvider", () => {
undefined, // preferredLanguage
false, // diffEnabled
experimentDefault,
true,
)
})
@ -1084,6 +1118,7 @@ describe("ClineProvider", () => {
architect: { customInstructions: "Architect mode instructions" },
},
mode: "architect",
enableMcpServerCreation: false,
mcpEnabled: false,
browserViewportSize: "900x600",
experiments: experimentDefault,
@ -1097,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" })
@ -1111,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 () => {
@ -1174,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
@ -1238,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

@ -1,36 +1,33 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import delay from "delay"
import * as vscode from "vscode"
import { ClineProvider } from "./core/webview/ClineProvider"
import { createClineAPI } from "./exports"
import "./utils/path" // necessary to have access to String.prototype.toPosix
import { ACTION_NAMES, CodeActionProvider } from "./core/CodeActionProvider"
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"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
Inspired by
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/default/weather-webview
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/frameworks/hello-world-react-cra
*/
/**
* Built using https://github.com/microsoft/vscode-webview-ui-toolkit
*
* Inspired by:
* - https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/default/weather-webview
* - https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/frameworks/hello-world-react-cra
*/
let outputChannel: vscode.OutputChannel
// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
// This method is called when your extension is activated.
// Your extension is activated the very first time the command is executed.
export function activate(context: vscode.ExtensionContext) {
outputChannel = vscode.window.createOutputChannel("Roo-Code")
context.subscriptions.push(outputChannel)
outputChannel.appendLine("Roo-Code extension activated")
// Get default commands from configuration
// Get default commands from configuration.
const defaultCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
// Initialize global state if not already set
// Initialize global state if not already set.
if (!context.globalState.get("allowedCommands")) {
context.globalState.update("allowedCommands", defaultCommands)
}
@ -43,208 +40,49 @@ export function activate(context: vscode.ExtensionContext) {
}),
)
context.subscriptions.push(
vscode.commands.registerCommand("roo-cline.plusButtonClicked", async () => {
outputChannel.appendLine("Plus button Clicked")
await sidebarProvider.clearTask()
await sidebarProvider.postStateToWebview()
await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
}),
)
registerCommands({ context, outputChannel, provider: sidebarProvider })
context.subscriptions.push(
vscode.commands.registerCommand("roo-cline.mcpButtonClicked", () => {
sidebarProvider.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
}),
)
context.subscriptions.push(
vscode.commands.registerCommand("roo-cline.promptsButtonClicked", () => {
sidebarProvider.postMessageToWebview({ type: "action", action: "promptsButtonClicked" })
}),
)
const openClineInNewTab = async () => {
outputChannel.appendLine("Opening Roo Code in new tab")
// (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event)
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
const tabProvider = new ClineProvider(context, outputChannel)
//const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
// Check if there are any visible text editors, otherwise open a new group to the right
const hasVisibleEditors = vscode.window.visibleTextEditors.length > 0
if (!hasVisibleEditors) {
await vscode.commands.executeCommand("workbench.action.newGroupRight")
}
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, {
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [context.extensionUri],
})
// TODO: use better svg icon with light and dark variants (see https://stackoverflow.com/questions/58365687/vscode-extension-iconpath)
panel.iconPath = {
light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
}
tabProvider.resolveWebviewView(panel)
// Lock the editor group so clicking on files doesn't open them over the panel
await delay(100)
await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
}
context.subscriptions.push(vscode.commands.registerCommand("roo-cline.popoutButtonClicked", openClineInNewTab))
context.subscriptions.push(vscode.commands.registerCommand("roo-cline.openInNewTab", openClineInNewTab))
context.subscriptions.push(
vscode.commands.registerCommand("roo-cline.settingsButtonClicked", () => {
//vscode.window.showInformationMessage(message)
sidebarProvider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" })
}),
)
context.subscriptions.push(
vscode.commands.registerCommand("roo-cline.historyButtonClicked", () => {
sidebarProvider.postMessageToWebview({ type: "action", action: "historyButtonClicked" })
}),
)
/*
We use the text document content provider API to show the left side for diff view by creating a virtual document for the original content. This makes it readonly so users know to edit the right side if they want to keep their changes.
- This API allows you to create readonly documents in VSCode from arbitrary sources, and works by claiming an uri-scheme for which your provider then returns text contents. The scheme must be provided when registering a provider and cannot change afterwards.
- Note how the provider doesn't create uris for virtual documents - its role is to provide contents given such an uri. In return, content providers are wired into the open document logic so that providers are always considered.
https://code.visualstudio.com/api/extension-guides/virtual-documents
*/
/**
* We use the text document content provider API to show the left side for diff
* view by creating a virtual document for the original content. This makes it
* readonly so users know to edit the right side if they want to keep their changes.
*
* This API allows you to create readonly documents in VSCode from arbitrary
* sources, and works by claiming an uri-scheme for which your provider then
* returns text contents. The scheme must be provided when registering a
* provider and cannot change afterwards.
*
* Note how the provider doesn't create uris for virtual documents - its role
* is to provide contents given such an uri. In return, content providers are
* wired into the open document logic so that providers are always considered.
*
* https://code.visualstudio.com/api/extension-guides/virtual-documents
*/
const diffContentProvider = new (class implements vscode.TextDocumentContentProvider {
provideTextDocumentContent(uri: vscode.Uri): string {
return Buffer.from(uri.query, "base64").toString("utf-8")
}
})()
context.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider(DIFF_VIEW_URI_SCHEME, diffContentProvider),
)
// URI Handler
const handleUri = async (uri: vscode.Uri) => {
const path = uri.path
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
const visibleProvider = ClineProvider.getVisibleInstance()
if (!visibleProvider) {
return
}
switch (path) {
case "/glama": {
const code = query.get("code")
if (code) {
await visibleProvider.handleGlamaCallback(code)
}
break
}
case "/openrouter": {
const code = query.get("code")
if (code) {
await visibleProvider.handleOpenRouterCallback(code)
}
break
}
default:
break
}
}
context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
// Register code actions provider
// Register code actions provider.
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider({ pattern: "**/*" }, new CodeActionProvider(), {
providedCodeActionKinds: CodeActionProvider.providedCodeActionKinds,
}),
)
// Helper function to handle code actions
const registerCodeAction = (
context: vscode.ExtensionContext,
command: string,
promptType: keyof typeof ACTION_NAMES,
inNewTask: boolean,
inputPrompt?: string,
inputPlaceholder?: string,
) => {
let userInput: string | undefined
context.subscriptions.push(
vscode.commands.registerCommand(
command,
async (filePath: string, selectedText: string, diagnostics?: any[]) => {
if (inputPrompt) {
userInput = await vscode.window.showInputBox({
prompt: inputPrompt,
placeHolder: inputPlaceholder,
})
}
const params = {
filePath,
selectedText,
...(diagnostics ? { diagnostics } : {}),
...(userInput ? { userInput } : {}),
}
await ClineProvider.handleCodeAction(command, promptType, params)
},
),
)
}
// Helper function to register both versions of a code action
const registerCodeActionPair = (
context: vscode.ExtensionContext,
baseCommand: string,
promptType: keyof typeof ACTION_NAMES,
inputPrompt?: string,
inputPlaceholder?: string,
) => {
// Register new task version
registerCodeAction(context, baseCommand, promptType, true, inputPrompt, inputPlaceholder)
// Register current task version
registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, false, inputPrompt, inputPlaceholder)
}
// Register code action commands
registerCodeActionPair(
context,
"roo-cline.explainCode",
"EXPLAIN",
"What would you like Roo to explain?",
"E.g. How does the error handling work?",
)
registerCodeActionPair(
context,
"roo-cline.fixCode",
"FIX",
"What would you like Roo to fix?",
"E.g. Maintain backward compatibility",
)
registerCodeActionPair(
context,
"roo-cline.improveCode",
"IMPROVE",
"What would you like Roo to improve?",
"E.g. Focus on performance optimization",
)
registerCodeActions(context)
return createClineAPI(outputChannel, sidebarProvider)
}
// This method is called when your extension is deactivated
// This method is called when your extension is deactivated.
export function deactivate() {
outputChannel.appendLine("Roo-Code extension deactivated")
}

View file

@ -2,6 +2,7 @@ import * as vscode from "vscode"
import * as path from "path"
import { listFiles } from "../../services/glob/list-files"
import { ClineProvider } from "../../core/webview/ClineProvider"
import { toRelativePath } from "../../utils/path"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
const MAX_INITIAL_FILES = 1_000
@ -48,6 +49,23 @@ class WorkspaceTracker {
)
this.disposables.push(watcher)
this.disposables.push(vscode.window.tabGroups.onDidChangeTabs(() => this.workspaceDidUpdate()))
}
private getOpenedTabsInfo() {
return vscode.window.tabGroups.all.flatMap((group) =>
group.tabs
.filter((tab) => tab.input instanceof vscode.TabInputText)
.map((tab) => {
const path = (tab.input as vscode.TabInputText).uri.fsPath
return {
label: tab.label,
isActive: tab.isActive,
path: toRelativePath(path, cwd || ""),
}
}),
)
}
private workspaceDidUpdate() {
@ -59,12 +77,12 @@ class WorkspaceTracker {
if (!cwd) {
return
}
const relativeFilePaths = Array.from(this.filePaths).map((file) => toRelativePath(file, cwd))
this.providerRef.deref()?.postMessageToWebview({
type: "workspaceUpdated",
filePaths: Array.from(this.filePaths).map((file) => {
const relativePath = path.relative(cwd, file).toPosix()
return file.endsWith("/") ? relativePath + "/" : relativePath
}),
filePaths: relativeFilePaths,
openedTabs: this.getOpenedTabsInfo(),
})
this.updateTimer = null
}, 300) // Debounce for 300ms

View file

@ -16,6 +16,12 @@ const mockWatcher = {
}
jest.mock("vscode", () => ({
window: {
tabGroups: {
onDidChangeTabs: jest.fn(() => ({ dispose: jest.fn() })),
all: [],
},
},
workspace: {
workspaceFolders: [
{
@ -61,6 +67,7 @@ describe("WorkspaceTracker", () => {
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "workspaceUpdated",
filePaths: expect.arrayContaining(["file1.ts", "file2.ts"]),
openedTabs: [],
})
expect((mockProvider.postMessageToWebview as jest.Mock).mock.calls[0][0].filePaths).toHaveLength(2)
})
@ -74,6 +81,7 @@ describe("WorkspaceTracker", () => {
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "workspaceUpdated",
filePaths: ["newfile.ts"],
openedTabs: [],
})
})
@ -92,6 +100,7 @@ describe("WorkspaceTracker", () => {
expect(mockProvider.postMessageToWebview).toHaveBeenLastCalledWith({
type: "workspaceUpdated",
filePaths: [],
openedTabs: [],
})
})
@ -106,6 +115,7 @@ describe("WorkspaceTracker", () => {
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "workspaceUpdated",
filePaths: expect.arrayContaining(["newdir"]),
openedTabs: [],
})
const lastCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
expect(lastCall[0].filePaths).toHaveLength(1)
@ -126,6 +136,7 @@ describe("WorkspaceTracker", () => {
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "workspaceUpdated",
filePaths: expect.arrayContaining(expectedFiles),
openedTabs: [],
})
expect(calls[0][0].filePaths).toHaveLength(1000)

View file

@ -52,13 +52,18 @@ export interface ExtensionMessage {
| "historyButtonClicked"
| "promptsButtonClicked"
| "didBecomeVisible"
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage"
state?: ExtensionState
images?: string[]
ollamaModels?: string[]
lmStudioModels?: string[]
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
filePaths?: string[]
openedTabs?: Array<{
label: string
isActive: boolean
path?: string
}>
partialMessage?: ClineMessage
glamaModels?: Record<string, ModelInfo>
openRouterModels?: Record<string, ModelInfo>
@ -97,6 +102,7 @@ export interface ExtensionState {
alwaysApproveResubmit?: boolean
alwaysAllowModeSwitch?: boolean
requestDelaySeconds: number
rateLimitSeconds: number // Minimum time between successive requests (0 = disabled)
uriScheme?: string
allowedCommands?: string[]
soundEnabled?: boolean
@ -109,6 +115,7 @@ export interface ExtensionState {
writeDelayMs: number
terminalOutputLineLimit?: number
mcpEnabled: boolean
enableMcpServerCreation: boolean
mode: Mode
modeApiConfigs?: Record<Mode, string>
enhancementApiConfigId?: string

View file

@ -63,10 +63,12 @@ export interface WebviewMessage {
| "deleteMessage"
| "terminalOutputLineLimit"
| "mcpEnabled"
| "enableMcpServerCreation"
| "searchCommits"
| "refreshGlamaModels"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "rateLimitSeconds"
| "setApiConfigPassword"
| "requestVsCodeLmModels"
| "mode"

View file

@ -1,4 +1,4 @@
import { isToolAllowedForMode, FileRestrictionError, ModeConfig } from "../modes"
import { isToolAllowedForMode, FileRestrictionError, ModeConfig, parseSlashCommand } from "../modes"
describe("isToolAllowedForMode", () => {
const customModes: ModeConfig[] = [
@ -332,3 +332,65 @@ 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

@ -81,6 +81,7 @@ export interface ModelInfo {
cacheWritesPrice?: number
cacheReadsPrice?: number
description?: string
reasoningEffort?: "low" | "medium" | "high"
}
// Anthropic
@ -511,6 +512,33 @@ export type OpenAiNativeModelId = keyof typeof openAiNativeModels
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4o"
export const openAiNativeModels = {
// don't support tool use yet
"o3-mini": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1.1,
outputPrice: 4.4,
reasoningEffort: "medium",
},
"o3-mini-high": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1.1,
outputPrice: 4.4,
reasoningEffort: "high",
},
"o3-mini-low": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1.1,
outputPrice: 4.4,
reasoningEffort: "low",
},
o1: {
maxTokens: 100_000,
contextWindow: 200_000,
@ -532,8 +560,8 @@ export const openAiNativeModels = {
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 3,
outputPrice: 12,
inputPrice: 1.1,
outputPrice: 4.4,
},
"gpt-4o": {
maxTokens: 4_096,
@ -565,7 +593,8 @@ export const deepSeekModels = {
supportsPromptCache: false,
inputPrice: 0.014, // $0.014 per million tokens
outputPrice: 0.28, // $0.28 per million tokens
description: "DeepSeek-V3 achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally.",
description:
"DeepSeek-V3 achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally.",
},
"deepseek-reasoner": {
maxTokens: 8192,

View file

@ -6,7 +6,7 @@ interface ApiMetrics {
totalCacheWrites?: number
totalCacheReads?: number
totalCost: number
contextTokens: number // Total tokens in conversation (last message's tokensIn + tokensOut)
contextTokens: number // Total tokens in conversation (last message's tokensIn + tokensOut + cacheWrites + cacheReads)
}
/**
@ -17,7 +17,7 @@ interface ApiMetrics {
* It extracts and sums up the tokensIn, tokensOut, cacheWrites, cacheReads, and cost from these messages.
*
* @param messages - An array of ClineMessage objects to process.
* @returns An ApiMetrics object containing totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, and totalCost.
* @returns An ApiMetrics object containing totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, totalCost, and contextTokens.
*
* @example
* const messages = [
@ -36,27 +36,30 @@ export function getApiMetrics(messages: ClineMessage[]): ApiMetrics {
contextTokens: 0,
}
// Find the last api_req_started message that has valid token information
// Helper function to get total tokens from a message
const getTotalTokensFromMessage = (message: ClineMessage): number => {
if (!message.text) return 0
try {
const { tokensIn, tokensOut, cacheWrites, cacheReads } = JSON.parse(message.text)
return (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
} catch {
return 0
}
}
// Find the last api_req_started message that has any tokens
const lastApiReq = [...messages].reverse().find((message) => {
if (message.type === "say" && message.say === "api_req_started" && message.text) {
try {
const parsedData = JSON.parse(message.text)
return typeof parsedData.tokensIn === "number" && typeof parsedData.tokensOut === "number"
} catch {
return false
}
if (message.type === "say" && message.say === "api_req_started") {
return getTotalTokensFromMessage(message) > 0
}
return false
})
// Keep track of the last valid context tokens
let lastValidContextTokens = 0
// Calculate running totals
messages.forEach((message) => {
if (message.type === "say" && message.say === "api_req_started" && message.text) {
try {
const parsedData = JSON.parse(message.text)
const { tokensIn, tokensOut, cacheWrites, cacheReads, cost } = parsedData
const { tokensIn, tokensOut, cacheWrites, cacheReads, cost } = JSON.parse(message.text)
if (typeof tokensIn === "number") {
result.totalTokensIn += tokensIn
@ -74,15 +77,9 @@ export function getApiMetrics(messages: ClineMessage[]): ApiMetrics {
result.totalCost += cost
}
// Update last valid context tokens whenever we have valid input and output tokens
if (tokensIn > 0 && tokensOut > 0) {
lastValidContextTokens = tokensIn + tokensOut
}
// If this is the last api request, use its tokens for context size
// If this is the last api request with tokens, use its total for context size
if (message === lastApiReq) {
// Use the last valid context tokens if the current request doesn't have valid tokens
result.contextTokens = tokensIn > 0 && tokensOut > 0 ? tokensIn + tokensOut : lastValidContextTokens
result.contextTokens = getTotalTokensFromMessage(message)
}
} catch (error) {
console.error("Error parsing JSON:", error)

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,46 @@ 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 ?? ""
}
// 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

@ -18,8 +18,8 @@ export const createPrompt = (template: string, params: PromptParams): string =>
}
}
// Replace any remaining user_input placeholders with empty string
result = result.replaceAll("${userInput}", "")
// Replace any remaining placeholders with empty strings
result = result.replaceAll(/\${[^}]*}/g, "")
return result
}
@ -42,7 +42,7 @@ const supportPromptConfigs: Record<string, SupportPromptConfig> = {
EXPLAIN: {
label: "Explain Code",
description:
"Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in the editor context menu (right-click on selected code).",
"Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).",
template: `Explain the following code from file path @/\${filePath}:
\${userInput}
@ -58,7 +58,7 @@ Please provide a clear and concise explanation of what this code does, including
FIX: {
label: "Fix Issues",
description:
"Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in the editor context menu (right-click on selected code).",
"Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).",
template: `Fix any issues in the following code from file path @/\${filePath}
\${diagnosticText}
\${userInput}
@ -76,7 +76,7 @@ Please:
IMPROVE: {
label: "Improve Code",
description:
"Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in the editor context menu (right-click on selected code).",
"Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).",
template: `Improve the following code from file path @/\${filePath}:
\${userInput}
@ -92,6 +92,15 @@ Please suggest improvements for:
Provide the improved code along with explanations for each enhancement.`,
},
ADD_TO_CONTEXT: {
label: "Add to Context",
description:
"Add context to your current task or conversation. Useful for providing additional information or clarifications. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).",
template: `@/\${filePath}:
\`\`\`
\${selectedText}
\`\`\``,
},
} as const
type SupportPromptType = keyof typeof supportPromptConfigs

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

@ -1,121 +1,18 @@
const assert = require("assert")
const vscode = require("vscode")
const path = require("path")
const fs = require("fs")
const dotenv = require("dotenv")
import * as assert from "assert"
import * as vscode from "vscode"
// Load test environment variables
const testEnvPath = path.join(__dirname, ".test_env")
dotenv.config({ path: testEnvPath })
suite("Roo Code Extension Test Suite", () => {
vscode.window.showInformationMessage("Starting Roo Code extension tests.")
test("Extension should be present", () => {
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
assert.notStrictEqual(extension, undefined)
})
test("Extension should activate", async () => {
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
if (!extension) {
assert.fail("Extension not found")
suite("Roo Code Extension", () => {
test("OPENROUTER_API_KEY environment variable is set", () => {
if (!process.env.OPENROUTER_API_KEY) {
assert.fail("OPENROUTER_API_KEY environment variable is not set")
}
await extension.activate()
assert.strictEqual(extension.isActive, true)
})
test("OpenRouter API key and models should be configured correctly", function (done) {
// @ts-ignore
this.timeout(60000) // Increase timeout to 60s for network requests
;(async () => {
try {
// Get extension instance
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
if (!extension) {
done(new Error("Extension not found"))
return
}
// Verify API key is set and valid
const apiKey = process.env.OPEN_ROUTER_API_KEY
if (!apiKey) {
done(new Error("OPEN_ROUTER_API_KEY environment variable is not set"))
return
}
if (!apiKey.startsWith("sk-or-v1-")) {
done(new Error("OpenRouter API key should have correct format"))
return
}
// Activate extension and get provider
const api = await extension.activate()
if (!api) {
done(new Error("Extension API not found"))
return
}
// Get the provider from the extension's exports
const provider = api.sidebarProvider
if (!provider) {
done(new Error("Provider not found"))
return
}
// Set up the API configuration
await provider.updateGlobalState("apiProvider", "openrouter")
await provider.storeSecret("openRouterApiKey", apiKey)
// Set up timeout to fail test if models don't load
const timeout = setTimeout(() => {
done(new Error("Timeout waiting for models to load"))
}, 30000)
// Wait for models to be loaded
const checkModels = setInterval(async () => {
try {
const models = await provider.readOpenRouterModels()
if (!models) {
return
}
clearInterval(checkModels)
clearTimeout(timeout)
// Verify expected Claude models are available
const expectedModels = [
"anthropic/claude-3.5-sonnet:beta",
"anthropic/claude-3-sonnet:beta",
"anthropic/claude-3.5-sonnet",
"anthropic/claude-3.5-sonnet-20240620",
"anthropic/claude-3.5-sonnet-20240620:beta",
"anthropic/claude-3.5-haiku:beta",
]
for (const modelId of expectedModels) {
assert.strictEqual(modelId in models, true, `Model ${modelId} should be available`)
}
done()
} catch (error) {
clearInterval(checkModels)
clearTimeout(timeout)
done(error)
}
}, 1000)
// Trigger model loading
await provider.refreshOpenRouterModels()
} catch (error) {
done(error)
}
})()
})
test("Commands should be registered", async () => {
const commands = await vscode.commands.getCommands(true)
const timeout = 10 * 1_000
const interval = 1_000
const startTime = Date.now()
// Test core commands are registered
const expectedCommands = [
"roo-cline.plusButtonClicked",
"roo-cline.mcpButtonClicked",
@ -128,204 +25,39 @@ suite("Roo Code Extension Test Suite", () => {
"roo-cline.improveCode",
]
while (Date.now() - startTime < timeout) {
const commands = await vscode.commands.getCommands(true)
const missingCommands = []
for (const cmd of expectedCommands) {
if (!commands.includes(cmd)) {
missingCommands.push(cmd)
}
}
if (missingCommands.length === 0) {
break
}
await new Promise((resolve) => setTimeout(resolve, interval))
}
const commands = await vscode.commands.getCommands(true)
for (const cmd of expectedCommands) {
assert.strictEqual(commands.includes(cmd), true, `Command ${cmd} should be registered`)
assert.ok(commands.includes(cmd), `Command ${cmd} should be registered`)
}
})
test("Views should be registered", () => {
test("Webview panel can be created", () => {
const view = vscode.window.createWebviewPanel(
"roo-cline.SidebarProvider",
"Roo Code",
vscode.ViewColumn.One,
{},
)
assert.notStrictEqual(view, undefined)
assert.ok(view, "Failed to create webview panel")
view.dispose()
})
test("Should handle prompt and response correctly", async function () {
// @ts-ignore
this.timeout(60000) // Increase timeout for API request
const timeout = 30000
const interval = 1000
// Get extension instance
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
if (!extension) {
assert.fail("Extension not found")
return
}
// Activate extension and get API
const api = await extension.activate()
if (!api) {
assert.fail("Extension API not found")
return
}
// Get provider
const provider = api.sidebarProvider
if (!provider) {
assert.fail("Provider not found")
return
}
// Set up API configuration
await provider.updateGlobalState("apiProvider", "openrouter")
await provider.updateGlobalState("openRouterModelId", "anthropic/claude-3.5-sonnet")
const apiKey = process.env.OPEN_ROUTER_API_KEY
if (!apiKey) {
assert.fail("OPEN_ROUTER_API_KEY environment variable is not set")
return
}
await provider.storeSecret("openRouterApiKey", apiKey)
// Create webview panel with development options
const extensionUri = extension.extensionUri
const panel = vscode.window.createWebviewPanel("roo-cline.SidebarProvider", "Roo Code", vscode.ViewColumn.One, {
enableScripts: true,
enableCommandUris: true,
retainContextWhenHidden: true,
localResourceRoots: [extensionUri],
})
try {
// Initialize webview with development context
panel.webview.options = {
enableScripts: true,
enableCommandUris: true,
localResourceRoots: [extensionUri],
}
// Initialize provider with panel
provider.resolveWebviewView(panel)
// Set up message tracking
let webviewReady = false
let messagesReceived = false
const originalPostMessage = provider.postMessageToWebview.bind(provider)
// @ts-ignore
provider.postMessageToWebview = async (message) => {
if (message.type === "state") {
webviewReady = true
console.log("Webview state received:", message)
if (message.state?.clineMessages?.length > 0) {
messagesReceived = true
console.log("Messages in state:", message.state.clineMessages)
}
}
await originalPostMessage(message)
}
// Wait for webview to launch and receive initial state
let startTime = Date.now()
while (Date.now() - startTime < timeout) {
if (webviewReady) {
// Wait an additional second for webview to fully initialize
await new Promise((resolve) => setTimeout(resolve, 1000))
break
}
await new Promise((resolve) => setTimeout(resolve, interval))
}
if (!webviewReady) {
throw new Error("Timeout waiting for webview to be ready")
}
// Send webviewDidLaunch to initialize chat
await provider.postMessageToWebview({ type: "webviewDidLaunch" })
console.log("Sent webviewDidLaunch")
// Wait for webview to fully initialize
await new Promise((resolve) => setTimeout(resolve, 2000))
// Restore original postMessage
provider.postMessageToWebview = originalPostMessage
// Wait for OpenRouter models to be fully loaded
startTime = Date.now()
while (Date.now() - startTime < timeout) {
const models = await provider.readOpenRouterModels()
if (models && Object.keys(models).length > 0) {
console.log("OpenRouter models loaded")
break
}
await new Promise((resolve) => setTimeout(resolve, interval))
}
// Send prompt
const prompt = "Hello world, what is your name?"
console.log("Sending prompt:", prompt)
// Start task
try {
await api.startNewTask(prompt)
console.log("Task started")
} catch (error) {
console.error("Error starting task:", error)
throw error
}
// Wait for task to appear in history with tokens
startTime = Date.now()
while (Date.now() - startTime < timeout) {
const state = await provider.getState()
const task = state.taskHistory?.[0]
if (task && task.tokensOut > 0) {
console.log("Task completed with tokens:", task)
break
}
await new Promise((resolve) => setTimeout(resolve, interval))
}
// Wait for messages to be processed
startTime = Date.now()
let responseReceived = false
while (Date.now() - startTime < timeout) {
// Check provider.clineMessages
const messages = provider.clineMessages
if (messages && messages.length > 0) {
console.log("Provider messages:", JSON.stringify(messages, null, 2))
// @ts-ignore
const hasResponse = messages.some(
(m: { type: string; text: string }) =>
m.type === "say" && m.text && m.text.toLowerCase().includes("cline"),
)
if (hasResponse) {
console.log('Found response containing "Cline" in provider messages')
responseReceived = true
break
}
}
// Check provider.cline.clineMessages
const clineMessages = provider.cline?.clineMessages
if (clineMessages && clineMessages.length > 0) {
console.log("Cline messages:", JSON.stringify(clineMessages, null, 2))
// @ts-ignore
const hasResponse = clineMessages.some(
(m: { type: string; text: string }) =>
m.type === "say" && m.text && m.text.toLowerCase().includes("cline"),
)
if (hasResponse) {
console.log('Found response containing "Cline" in cline messages')
responseReceived = true
break
}
}
await new Promise((resolve) => setTimeout(resolve, interval))
}
if (!responseReceived) {
console.log("Final provider state:", await provider.getState())
console.log("Final cline messages:", provider.cline?.clineMessages)
throw new Error('Did not receive expected response containing "Cline"')
}
} finally {
panel.dispose()
}
})
})

77
src/test/task.test.ts Normal file
View file

@ -0,0 +1,77 @@
import * as assert from "assert"
import * as vscode from "vscode"
import { ClineAPI } from "../exports/cline"
import { ClineProvider } from "../core/webview/ClineProvider"
suite("Roo Code Task", () => {
test("Should handle prompt and response correctly", async function () {
const timeout = 30000
const interval = 1000
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
if (!extension) {
assert.fail("Extension not found")
}
const api: ClineAPI = await extension.activate()
const provider = api.sidebarProvider as ClineProvider
await provider.updateGlobalState("apiProvider", "openrouter")
await provider.updateGlobalState("openRouterModelId", "anthropic/claude-3.5-sonnet")
await provider.storeSecret("openRouterApiKey", process.env.OPENROUTER_API_KEY || "sk-or-v1-fake-api-key")
// Create webview panel with development options.
const panel = vscode.window.createWebviewPanel("roo-cline.SidebarProvider", "Roo Code", vscode.ViewColumn.One, {
enableScripts: true,
enableCommandUris: true,
retainContextWhenHidden: true,
localResourceRoots: [extension.extensionUri],
})
try {
// Initialize provider with panel.
await provider.resolveWebviewView(panel)
// Wait for webview to launch.
let startTime = Date.now()
while (Date.now() - startTime < timeout) {
if (provider.viewLaunched) {
break
}
await new Promise((resolve) => setTimeout(resolve, interval))
}
await api.startNewTask("Hello world, what is your name? Respond with 'My name is ...'")
// Wait for task to appear in history with tokens.
startTime = Date.now()
while (Date.now() - startTime < timeout) {
const state = await provider.getState()
const task = state.taskHistory?.[0]
if (task && task.tokensOut > 0) {
break
}
await new Promise((resolve) => setTimeout(resolve, interval))
}
if (provider.messages.length === 0) {
assert.fail("No messages received")
}
// console.log("Provider messages:", JSON.stringify(provider.messages, null, 2))
assert.ok(
provider.messages.some(({ type, text }) => type === "say" && text?.includes("My name is Roo")),
"Did not receive expected response containing 'My name is Roo'",
)
} finally {
panel.dispose()
}
})
})

View file

@ -1,19 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"lib": ["ES2020"],
"sourceMap": true,
"rootDir": "../..",
"strict": false,
"noImplicitAny": false,
"noImplicitThis": false,
"alwaysStrict": false,
"skipLibCheck": true,
"baseUrl": "../..",
"paths": {
"*": ["*", "src/*"]
}
},
"exclude": ["node_modules", ".vscode-test"]
}

View file

@ -0,0 +1,222 @@
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("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")
})
})
})

View file

@ -99,3 +99,8 @@ export function getReadablePath(cwd: string, relPath?: string): string {
}
}
}
export const toRelativePath = (filePath: string, cwd: string) => {
const relativePath = path.relative(cwd, filePath).toPosix()
return filePath.endsWith("/") ? relativePath + "/" : relativePath
}

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
}

17
tsconfig.integration.json Normal file
View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node",
"esModuleInterop": true,
"target": "ES2022",
"lib": ["ES2022", "ESNext.Disposable", "DOM"],
"sourceMap": true,
"strict": true,
"skipLibCheck": true,
"useUnknownInCatchVariables": false,
"rootDir": "src",
"outDir": "out-integration"
},
"include": ["**/*.ts"],
"exclude": [".vscode-test", "benchmark", "dist", "**/node_modules/**", "out", "out-integration", "webview-ui"]
}

View file

@ -1,40 +0,0 @@
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": ["react", "@typescript-eslint", "react-hooks"],
"rules": {
"react/react-in-jsx-scope": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "warn",
"react/display-name": "warn",
"no-case-declarations": "warn",
"react/no-unescaped-entities": "warn",
"react/jsx-key": "warn",
"no-extra-semi": "warn",
"@typescript-eslint/no-var-requires": "warn",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
]
},
"settings": {
"react": {
"version": "detect"
}
},
"env": {
"browser": true,
"es2021": true,
"node": true
}
}

View file

@ -21,3 +21,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*storybook.log

View file

@ -0,0 +1,11 @@
import type { StorybookConfig } from "@storybook/react-vite"
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: ["@storybook/addon-essentials", "@storybook/addon-interactions"],
framework: {
name: "@storybook/react-vite",
options: {},
},
}
export default config

View file

@ -0,0 +1,17 @@
import type { Preview } from "@storybook/react"
import "../src/index.css"
import "./vscode.css"
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
}
export default preview

View file

@ -0,0 +1,32 @@
/**
* Use `Developer: Generate Color Theme From Current Settings` to generate themes
* using your current VSCode settings.
*
* See: https://code.visualstudio.com/docs/getstarted/themes
*/
:root {
--vscode-editor-background: #1f1f1f; /* "editor.background" */
--vscode-editor-foreground: #cccccc; /* "editor.foreground" */
--vscode-menu-background: #1f1f1f; /* "menu.background" */
--vscode-menu-foreground: #cccccc; /* "menu.foreground" */
--vscode-button-background: #0078d4; /* "button.background" */
--vscode-button-foreground: #ffffff; /* "button.foreground" */
--vscode-button-secondaryBackground: #313131; /* "button.secondaryBackground" */
--vscode-button-secondaryForeground: #cccccc; /* "button.secondaryForeground" */
--vscode-disabledForeground: #313131; /* "disabledForeground" */
--vscode-descriptionForeground: #9d9d9d; /* "descriptionForeground" */
--vscode-focusBorder: #0078d4; /* "focusBorder" */
--vscode-errorForeground: #f85149; /* "errorForeground" */
--vscode-widget-border: #313131; /* "widget.border" */
--vscode-input-background: #313131; /* "input.background" */
--vscode-input-foreground: #cccccc; /* "input.foreground" */
--vscode-input-border: #3c3c3c; /* "input.border" */
/* I can't find these in the output of `Developer: Generate Color Theme From Current Settings` */
--vscode-charts-red: red;
--vscode-charts-blue: blue;
--vscode-charts-yellow: yellow;
--vscode-charts-orange: orange;
--vscode-charts-green: green;
}

View file

@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

File diff suppressed because it is too large Load diff

View file

@ -4,14 +4,22 @@
"private": true,
"type": "module",
"scripts": {
"start": "vite",
"build": "tsc && vite build",
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint src --ext ts,tsx",
"test": "jest",
"lint": "eslint src --ext ts,tsx --quiet"
"storybook": "storybook dev -p 6006",
"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",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"debounce": "^2.1.1",
"fast-deep-equal": "^3.1.3",
"fzf": "^0.5.2",
@ -24,36 +32,43 @@
"rehype-highlight": "^7.0.0",
"shell-quote": "^1.8.2",
"styled-components": "^6.1.13",
"tailwind-merge": "^2.6.0",
"tailwindcss": "^4.0.0",
"vscrui": "^0.2.0",
"@tailwindcss/vite": "^4.0.0"
"tailwindcss-animate": "^1.0.7",
"vscrui": "^0.2.0"
},
"devDependencies": {
"@storybook/addon-essentials": "^8.5.2",
"@storybook/addon-interactions": "^8.5.2",
"@storybook/blocks": "^8.5.2",
"@storybook/react": "^8.5.2",
"@storybook/react-vite": "^8.5.2",
"@storybook/test": "^8.5.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^18.0.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/shell-quote": "^1.7.5",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/vscode-webview": "^1.57.5",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-storybook": "^0.11.2",
"identity-obj-proxy": "^3.0.0",
"jest": "^27.5.1",
"jest-environment-jsdom": "^27.5.1",
"jest-simple-dot-reporter": "^1.0.5",
"postcss": "^8.5.1",
"storybook": "^8.5.2",
"ts-jest": "^27.1.5",
"typescript": "^4.9.5",
"vite": "^5.4.14"
"vite": "6.0.11"
},
"jest": {
"testEnvironment": "jsdom",
@ -93,20 +108,5 @@
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all",
"last 2 chrome version",
"last 2 firefox version",
"last 2 safari version"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View file

View file

@ -1,38 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
--></body>
</html>

View file

@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View file

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

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

@ -50,7 +50,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
},
ref,
) => {
const { filePaths, currentApiConfigName, listApiConfigMeta, customModes } = useExtensionState()
const { filePaths, openedTabs, currentApiConfigName, listApiConfigMeta, customModes } = useExtensionState()
const [gitCommits, setGitCommits] = useState<any[]>([])
const [showDropdown, setShowDropdown] = useState(false)
@ -138,14 +138,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
return [
{ type: ContextMenuOptionType.Problems, value: "problems" },
...gitCommits,
...openedTabs
.filter((tab) => tab.path)
.map((tab) => ({
type: ContextMenuOptionType.OpenedFile,
value: "/" + tab.path,
})),
...filePaths
.map((file) => "/" + file)
.filter((path) => !openedTabs.some((tab) => tab.path && "/" + tab.path === path)) // Filter out paths that are already in openedTabs
.map((path) => ({
type: path.endsWith("/") ? ContextMenuOptionType.Folder : ContextMenuOptionType.File,
value: path,
})),
]
}, [filePaths, gitCommits])
}, [filePaths, gitCommits, openedTabs])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {

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
@ -330,6 +335,20 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
[messages.length, clineAsk],
)
const handleSetChatBoxMessage = useCallback(
(text: string, images: string[]) => {
// Avoid nested template literals by breaking down the logic
let newValue = text
if (inputValue !== "") {
newValue = inputValue + " " + text
}
setInputValue(newValue)
setSelectedImages([...selectedImages, ...images])
},
[inputValue, selectedImages],
)
const startNewTask = useCallback(() => {
vscode.postMessage({ type: "clearTask" })
}, [])
@ -337,56 +356,96 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
/*
This logic depends on the useEffect[messages] above to set clineAsk, after which buttons are shown and we then send an askResponse to the extension.
*/
const handlePrimaryButtonClick = useCallback(() => {
switch (clineAsk) {
case "api_req_failed":
case "command":
case "command_output":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
case "resume_task":
case "mistake_limit_reached":
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
break
case "completion_result":
case "resume_completed_task":
// extension waiting for feedback. but we can just present a new task button
startNewTask()
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
disableAutoScrollRef.current = false
}, [clineAsk, startNewTask])
const handlePrimaryButtonClick = useCallback(
(text?: string, images?: string[]) => {
const trimmedInput = text?.trim()
switch (clineAsk) {
case "api_req_failed":
case "command":
case "command_output":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
case "resume_task":
case "mistake_limit_reached":
// Only send text/images if they exist
if (trimmedInput || (images && images.length > 0)) {
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
text: trimmedInput,
images: images,
})
} else {
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
})
}
// Clear input state after sending
setInputValue("")
setSelectedImages([])
break
case "completion_result":
case "resume_completed_task":
// extension waiting for feedback. but we can just present a new task button
startNewTask()
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
disableAutoScrollRef.current = false
},
[clineAsk, startNewTask],
)
const handleSecondaryButtonClick = useCallback(() => {
if (isStreaming) {
vscode.postMessage({ type: "cancelTask" })
setDidClickCancel(true)
return
}
const handleSecondaryButtonClick = useCallback(
(text?: string, images?: string[]) => {
const trimmedInput = text?.trim()
if (isStreaming) {
vscode.postMessage({ type: "cancelTask" })
setDidClickCancel(true)
return
}
switch (clineAsk) {
case "api_req_failed":
case "mistake_limit_reached":
case "resume_task":
startNewTask()
break
case "command":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
// responds to the API with a "This operation failed" and lets it try again
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
disableAutoScrollRef.current = false
}, [clineAsk, startNewTask, isStreaming])
switch (clineAsk) {
case "api_req_failed":
case "mistake_limit_reached":
case "resume_task":
startNewTask()
break
case "command":
case "tool":
case "browser_action_launch":
case "use_mcp_server":
// Only send text/images if they exist
if (trimmedInput || (images && images.length > 0)) {
vscode.postMessage({
type: "askResponse",
askResponse: "noButtonClicked",
text: trimmedInput,
images: images,
})
} else {
// responds to the API with a "This operation failed" and lets it try again
vscode.postMessage({
type: "askResponse",
askResponse: "noButtonClicked",
})
}
// Clear input state after sending
setInputValue("")
setSelectedImages([])
break
}
setTextAreaDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
disableAutoScrollRef.current = false
},
[clineAsk, startNewTask, isStreaming],
)
const handleTaskCloseButtonClick = useCallback(() => {
startNewTask()
@ -429,11 +488,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "sendMessage":
handleSendMessage(message.text ?? "", message.images ?? [])
break
case "setChatBoxMessage":
handleSetChatBoxMessage(message.text ?? "", message.images ?? [])
break
case "primaryButtonClick":
handlePrimaryButtonClick()
handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
break
case "secondaryButtonClick":
handleSecondaryButtonClick()
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
break
}
}
@ -444,6 +506,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
textAreaDisabled,
enableButtons,
handleSendMessage,
handleSetChatBoxMessage,
handlePrimaryButtonClick,
handleSecondaryButtonClick,
],
@ -660,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)
@ -936,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
@ -1038,7 +1101,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: secondaryButtonText ? 1 : 2,
marginRight: secondaryButtonText ? "6px" : "0",
}}
onClick={handlePrimaryButtonClick}>
onClick={(e) => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText}
</VSCodeButton>
)}
@ -1050,7 +1113,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
flex: isStreaming ? 2 : 1,
marginLeft: isStreaming ? 0 : "6px",
}}
onClick={handleSecondaryButtonClick}>
onClick={(e) => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? "Cancel" : secondaryButtonText}
</VSCodeButton>
)}

View file

@ -74,6 +74,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
return <span>Git Commits</span>
}
case ContextMenuOptionType.File:
case ContextMenuOptionType.OpenedFile:
case ContextMenuOptionType.Folder:
if (option.value) {
return (
@ -100,6 +101,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
const getIconForOption = (option: ContextMenuQueryItem): string => {
switch (option.type) {
case ContextMenuOptionType.OpenedFile:
return "window"
case ContextMenuOptionType.File:
return "file"
case ContextMenuOptionType.Folder:
@ -194,6 +197,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
{(option.type === ContextMenuOptionType.Problems ||
((option.type === ContextMenuOptionType.File ||
option.type === ContextMenuOptionType.Folder ||
option.type === ContextMenuOptionType.OpenedFile ||
option.type === ContextMenuOptionType.Git) &&
option.value)) && (
<i

View file

@ -41,6 +41,7 @@ describe("ChatTextArea", () => {
// Default mock implementation for useExtensionState
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
apiConfiguration: {
apiProvider: "anthropic",
},
@ -51,6 +52,7 @@ describe("ChatTextArea", () => {
it("should be disabled when textAreaDisabled is true", () => {
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
})
render(<ChatTextArea {...defaultProps} textAreaDisabled={true} />)
@ -68,6 +70,7 @@ describe("ChatTextArea", () => {
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
apiConfiguration,
})
@ -85,6 +88,7 @@ describe("ChatTextArea", () => {
it("should not send message when input is empty", () => {
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
apiConfiguration: {
apiProvider: "openrouter",
},
@ -101,6 +105,7 @@ describe("ChatTextArea", () => {
it("should show loading state while enhancing", () => {
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
apiConfiguration: {
apiProvider: "openrouter",
},
@ -123,6 +128,7 @@ describe("ChatTextArea", () => {
// Update apiConfiguration
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
apiConfiguration: {
apiProvider: "openrouter",
newSetting: "test",

View file

@ -1,5 +1,6 @@
import {
VSCodeButton,
VSCodeCheckbox,
VSCodeLink,
VSCodePanels,
VSCodePanelTab,
@ -18,7 +19,13 @@ type McpViewProps = {
}
const McpView = ({ onDone }: McpViewProps) => {
const { mcpServers: servers, alwaysAllowMcp, mcpEnabled } = useExtensionState()
const {
mcpServers: servers,
alwaysAllowMcp,
mcpEnabled,
enableMcpServerCreation,
setEnableMcpServerCreation,
} = useExtensionState()
return (
<div
@ -67,6 +74,27 @@ const McpView = ({ onDone }: McpViewProps) => {
{mcpEnabled && (
<>
<div style={{ marginBottom: 15 }}>
<VSCodeCheckbox
checked={enableMcpServerCreation}
onChange={(e: any) => {
setEnableMcpServerCreation(e.target.checked)
vscode.postMessage({ type: "enableMcpServerCreation", bool: e.target.checked })
}}>
<span style={{ fontWeight: "500" }}>Enable MCP Server Creation</span>
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
When enabled, Roo can help you create new MCP servers via commands like "add a new tool
to...". If you don't need to create MCP servers you can disable this to reduce Roo's
token usage.
</p>
</div>
{/* Server List */}
{servers.length > 0 && (
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>

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

@ -60,6 +60,11 @@ const ApiConfigManager = ({
if (editState === "new") {
onUpsertConfig(trimmedValue)
} else if (editState === "rename" && currentApiConfigName) {
if (currentApiConfigName === trimmedValue) {
setEditState(null)
setInputValue("")
return
}
onRenameConfig(currentApiConfigName, trimmedValue)
}

View file

@ -132,7 +132,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

@ -53,6 +53,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setAlwaysApproveResubmit,
requestDelaySeconds,
setRequestDelaySeconds,
rateLimitSeconds,
setRateLimitSeconds,
currentApiConfigName,
listApiConfigMeta,
experiments,
@ -92,6 +94,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
vscode.postMessage({ type: "rateLimitSeconds", value: rateLimitSeconds })
vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName })
vscode.postMessage({
type: "upsertApiConfiguration",
@ -572,6 +575,26 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
<div style={{ marginBottom: 40 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Advanced Settings</h3>
<div style={{ marginBottom: 15 }}>
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
<span style={{ fontWeight: "500" }}>Rate limit</span>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<input
type="range"
min="0"
max="60"
step="1"
value={rateLimitSeconds}
onChange={(e) => setRateLimitSeconds(parseInt(e.target.value))}
style={{ ...sliderStyle }}
/>
<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds}s</span>
</div>
</div>
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
Minimum time between API requests.
</p>
</div>
<div style={{ marginBottom: 15 }}>
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
<span style={{ fontWeight: "500" }}>Terminal output limit</span>

View file

@ -0,0 +1,47 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
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",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-foreground shadow-sm hover:bg-foreground/80",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
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",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
},
)
Button.displayName = "Button"
export { Button, buttonVariants }

View file

@ -0,0 +1,177 @@
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>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<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-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",
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

@ -30,6 +30,7 @@ export interface ExtensionStateContextType extends ExtensionState {
openAiModels: string[]
mcpServers: McpServer[]
filePaths: string[]
openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
setApiConfiguration: (config: ApiConfiguration) => void
setCustomInstructions: (value?: string) => void
setAlwaysAllowReadOnly: (value: boolean) => void
@ -54,10 +55,14 @@ export interface ExtensionStateContextType extends ExtensionState {
setTerminalOutputLineLimit: (value: number) => void
mcpEnabled: boolean
setMcpEnabled: (value: boolean) => void
enableMcpServerCreation: boolean
setEnableMcpServerCreation: (value: boolean) => void
alwaysApproveResubmit?: boolean
setAlwaysApproveResubmit: (value: boolean) => void
requestDelaySeconds: number
setRequestDelaySeconds: (value: number) => void
rateLimitSeconds: number
setRateLimitSeconds: (value: number) => void
setCurrentApiConfigName: (value: string) => void
setListApiConfigMeta: (value: ApiConfigMeta[]) => void
onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
@ -69,7 +74,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
}
@ -93,8 +98,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
screenshotQuality: 75,
terminalOutputLineLimit: 500,
mcpEnabled: true,
enableMcpServerCreation: true,
alwaysApproveResubmit: false,
requestDelaySeconds: 5,
rateLimitSeconds: 0, // Minimum time between successive requests (0 = disabled)
currentApiConfigName: "default",
listApiConfigMeta: [],
mode: defaultModeSlug,
@ -113,6 +120,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
const [glamaModels, setGlamaModels] = useState<Record<string, ModelInfo>>({
[glamaDefaultModelId]: glamaDefaultModelInfo,
})
const [openedTabs, setOpenedTabs] = useState<Array<{ label: string; isActive: boolean; path?: string }>>([])
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
})
@ -140,14 +148,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
}
})
},
[],
@ -176,7 +199,11 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
break
}
case "workspaceUpdated": {
setFilePaths(message.filePaths ?? [])
const paths = message.filePaths ?? []
const tabs = message.openedTabs ?? []
setFilePaths(paths)
setOpenedTabs(tabs)
break
}
case "partialMessage": {
@ -249,6 +276,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
unboundModels,
mcpServers,
filePaths,
openedTabs,
soundVolume: state.soundVolume,
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
writeDelayMs: state.writeDelayMs,
@ -281,8 +309,11 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setTerminalOutputLineLimit: (value) =>
setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
setEnableMcpServerCreation: (value) =>
setState((prevState) => ({ ...prevState, enableMcpServerCreation: value })),
setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })),
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
setListApiConfigMeta,
onUpdateApiConfig,

Some files were not shown because too many files have changed in this diff Show more