Merge remote-tracking branch 'origin/main' into fix-o3-formatting

This commit is contained in:
Matt Rubens
2025-02-02 00:53:15 -05:00
62 changed files with 2724 additions and 895 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

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

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

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

@@ -0,0 +1,22 @@
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
uses: Ilshidur/action-discord@master
with:
args: |
🚀 **New Pull Request Opened!**
📝 **Title:** ${{ github.event.pull_request.title }}
🔗 <${{ github.event.pull_request.html_url }}>
👤 **Author:** ${{ github.event.pull_request.user.login }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}

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

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

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

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",
@@ -221,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 && 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",
"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",
@@ -245,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",
@@ -255,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() {}
@@ -57,6 +73,17 @@ const vscode = {
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"),
}
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

@@ -313,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,16 +72,19 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
maxTokens = 8_192
}
const { data: completion, response } = await this.client.chat.completions
.create(
{
const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = {
model: this.getModel().id,
max_tokens: maxTokens,
temperature: 0,
messages: openAiMessages,
stream: true,
},
{
}
if (this.supportsTemperature()) {
requestOptions.temperature = 0
}
const { data: completion, response } = await this.client.chat.completions
.create(requestOptions, {
headers: {
"X-Glama-Metadata": JSON.stringify({
labels: [
@@ -92,8 +95,7 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
],
}),
},
},
)
})
.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

@@ -96,6 +96,7 @@ export class Cline {
didFinishAborting = false
abandoned = false
private diffViewProvider: DiffViewProvider
private lastApiRequestTime?: number
// streaming
private currentStreamingContentIndex = 0
@@ -796,9 +797,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 +842,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 +870,7 @@ export class Cline {
preferredLanguage,
this.diffEnabled,
experiments,
enableMcpServerCreation,
)
})()

View File

@@ -9,7 +9,7 @@ export const ACTION_NAMES = {
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",

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

@@ -174,6 +174,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@@ -194,6 +195,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@@ -216,6 +218,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@@ -236,6 +239,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@@ -256,6 +260,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@@ -276,6 +281,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
true, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toContain("apply_diff")
@@ -297,6 +303,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
false, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).not.toContain("apply_diff")
@@ -318,6 +325,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).not.toContain("apply_diff")
@@ -339,6 +347,7 @@ describe("SYSTEM_PROMPT", () => {
"Spanish", // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toContain("Language Preference:")
@@ -371,6 +380,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
// Role definition should be at the top
@@ -406,6 +416,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
)
// Role definition from promptComponent should be at the top
@@ -436,6 +447,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
)
// Should use the default mode's role definition
@@ -458,6 +470,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 +498,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
// Verify experimental tools are included in the prompt when enabled
@@ -512,6 +526,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
// Verify only enabled experimental tools are included
@@ -539,6 +554,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
true, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
// Verify base instruction lists all available tools
@@ -568,6 +584,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
true,
experiments,
true, // enableMcpServerCreation
)
// Verify detailed instructions for each tool
@@ -623,6 +640,7 @@ describe("addCustomInstructions", () => {
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
)
expect(prompt).toMatchSnapshot()
@@ -643,11 +661,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

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

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

@@ -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"
@@ -139,6 +133,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
@@ -651,6 +646,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
),
)
this.isViewLaunched = true
break
case "newTask":
// Code that should run in response to the hello message command
@@ -846,6 +842,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`)
@@ -886,6 +886,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()
@@ -1130,6 +1134,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
mcpEnabled,
fuzzyMatchThreshold,
experiments,
enableMcpServerCreation,
} = await this.getState()
// Create diffStrategy based on current model and settings
@@ -1158,6 +1163,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
preferredLanguage,
diffEnabled,
experiments,
enableMcpServerCreation,
)
await this.postMessageToWebview({
@@ -1216,6 +1222,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)
@@ -1995,8 +2005,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
terminalOutputLineLimit,
fuzzyMatchThreshold,
mcpEnabled,
enableMcpServerCreation,
alwaysApproveResubmit,
requestDelaySeconds,
rateLimitSeconds,
currentApiConfigName,
listApiConfigMeta,
mode,
@@ -2036,8 +2048,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,
@@ -2159,8 +2173,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
screenshotQuality,
terminalOutputLineLimit,
mcpEnabled,
enableMcpServerCreation,
alwaysApproveResubmit,
requestDelaySeconds,
rateLimitSeconds,
currentApiConfigName,
listApiConfigMeta,
vsCodeLmModelSelector,
@@ -2231,8 +2247,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>,
@@ -2353,8 +2371,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>),
@@ -2412,7 +2432,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 {
@@ -2466,4 +2486,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

@@ -323,7 +323,9 @@ describe("ClineProvider", () => {
browserViewportSize: "900x600",
fuzzyMatchThreshold: 1.0,
mcpEnabled: true,
enableMcpServerCreation: false,
requestDelaySeconds: 5,
rateLimitSeconds: 0,
mode: defaultModeSlug,
customModes: [],
experiments: experimentDefault,
@@ -894,6 +896,7 @@ describe("ClineProvider", () => {
},
},
mcpEnabled: true,
enableMcpServerCreation: false,
mode: "code" as const,
experiments: experimentDefault,
} as any)
@@ -926,6 +929,7 @@ describe("ClineProvider", () => {
},
},
mcpEnabled: false,
enableMcpServerCreation: false,
mode: "code" as const,
experiments: experimentDefault,
} as any)
@@ -990,6 +994,7 @@ describe("ClineProvider", () => {
},
customModePrompts: {},
mode: "code",
enableMcpServerCreation: true,
mcpEnabled: false,
browserViewportSize: "900x600",
experimentalDiffStrategy: true,
@@ -1024,6 +1029,7 @@ describe("ClineProvider", () => {
undefined, // preferredLanguage
true, // diffEnabled
experimentDefault,
true,
)
// Run the test again to verify it's consistent
@@ -1047,6 +1053,7 @@ describe("ClineProvider", () => {
diffEnabled: false,
fuzzyMatchThreshold: 0.8,
experiments: experimentDefault,
enableMcpServerCreation: true,
} as any)
// Mock SYSTEM_PROMPT to verify diffEnabled is passed as false
@@ -1075,6 +1082,7 @@ describe("ClineProvider", () => {
undefined, // preferredLanguage
false, // diffEnabled
experimentDefault,
true,
)
})
@@ -1089,6 +1097,7 @@ describe("ClineProvider", () => {
architect: { customInstructions: "Architect mode instructions" },
},
mode: "architect",
enableMcpServerCreation: false,
mcpEnabled: false,
browserViewportSize: "900x600",
experiments: experimentDefault,

View File

@@ -1,37 +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 { EditorUtils } from "./core/EditorUtils"
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)
}
@@ -44,220 +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,
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)
}),
)
}
// 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, inputPrompt, inputPlaceholder)
// Register current task version
registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, 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",
)
registerCodeAction(context, "roo-cline.addToContext", "ADD_TO_CONTEXT")
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

@@ -57,6 +57,11 @@ export interface ExtensionMessage {
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>
@@ -94,6 +99,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
@@ -106,6 +112,7 @@ export interface ExtensionState {
writeDelayMs: number
terminalOutputLineLimit?: number
mcpEnabled: boolean
enableMcpServerCreation: boolean
mode: Mode
modeApiConfigs?: Record<Mode, string>
enhancementApiConfigId?: string

View File

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

View File

@@ -80,6 +80,7 @@ export interface ModelInfo {
cacheWritesPrice?: number
cacheReadsPrice?: number
description?: string
reasoningEffort?: "low" | "medium" | "high"
}
// Anthropic
@@ -510,6 +511,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,
@@ -531,8 +559,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,

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
const lastApiReq = [...messages].reverse().find((message) => {
if (message.type === "say" && message.say === "api_req_started" && message.text) {
// Helper function to get total tokens from a message
const getTotalTokensFromMessage = (message: ClineMessage): number => {
if (!message.text) return 0
try {
const parsedData = JSON.parse(message.text)
return typeof parsedData.tokensIn === "number" && typeof parsedData.tokensOut === "number"
const { tokensIn, tokensOut, cacheWrites, cacheReads } = JSON.parse(message.text)
return (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
} catch {
return false
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") {
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

@@ -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) {
assert.strictEqual(commands.includes(cmd), true, `Command ${cmd} should be registered`)
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.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.
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

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

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

@@ -89,7 +89,7 @@ export const ChatRowContent = ({
}
}, [isLast, message.say])
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
if (message.text && message.say === "api_req_started") {
if (message.text != null && message.say === "api_req_started") {
const info: ClineApiReqInfo = JSON.parse(message.text)
return [info.cost, info.cancelReason, info.streamingFailedMessage]
}
@@ -183,28 +183,26 @@ export const ChatRowContent = ({
</div>
)
return [
apiReqCancelReason ? (
apiReqCancelReason != null ? (
apiReqCancelReason === "user_cancelled" ? (
getIconSpan("error", cancelledColor)
) : (
getIconSpan("error", errorColor)
)
) : cost ? (
) : cost != null ? (
getIconSpan("check", successColor)
) : apiRequestFailedMessage ? (
getIconSpan("error", errorColor)
) : (
<ProgressIndicator />
),
apiReqCancelReason ? (
apiReqCancelReason != null ? (
apiReqCancelReason === "user_cancelled" ? (
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Cancelled</span>
) : (
<span style={{ color: errorColor, fontWeight: "bold" }}>
API Streaming Failed ({JSON.stringify(apiReqCancelReason)})
</span>
<span style={{ color: errorColor, fontWeight: "bold" }}>API Streaming Failed</span>
)
) : cost ? (
) : cost != null ? (
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
) : apiRequestFailedMessage ? (
<span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
@@ -512,7 +510,9 @@ export const ChatRowContent = ({
style={{
...headerStyle,
marginBottom:
(!cost && apiRequestFailedMessage) || apiReqStreamingFailedMessage ? 10 : 0,
(cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage
? 10
: 0,
justifyContent: "space-between",
cursor: "pointer",
userSelect: "none",
@@ -524,13 +524,13 @@ export const ChatRowContent = ({
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
{icon}
{title}
<VSCodeBadge style={{ opacity: cost ? 1 : 0 }}>
<VSCodeBadge style={{ opacity: cost != null && cost > 0 ? 1 : 0 }}>
${Number(cost || 0)?.toFixed(4)}
</VSCodeBadge>
</div>
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
</div>
{((!cost && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && (
{((cost == null && 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,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
return true
} else {
const lastApiReqStarted = findLast(modifiedMessages, (message) => message.say === "api_req_started")
if (lastApiReqStarted && lastApiReqStarted.text && lastApiReqStarted.say === "api_req_started") {
if (lastApiReqStarted && lastApiReqStarted.text != null && lastApiReqStarted.say === "api_req_started") {
const cost = JSON.parse(lastApiReqStarted.text).cost
if (cost === undefined) {
// api request has not finished yet
@@ -718,9 +718,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) {
if (lastApiReqStarted?.text != null) {
const info = JSON.parse(lastApiReqStarted.text)
const isCancelled = info.cancelReason !== null
const isCancelled = info.cancelReason != null
if (isCancelled) {
endBrowserSession()
result.push(message)

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

@@ -1,208 +0,0 @@
import { render, fireEvent, screen } from "@testing-library/react"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import AutoApproveMenu from "../AutoApproveMenu"
import { defaultModeSlug, defaultPrompts } from "../../../../../src/shared/modes"
import { experimentDefault } from "../../../../../src/shared/experiments"
// Mock the ExtensionStateContext hook
jest.mock("../../../context/ExtensionStateContext")
const mockUseExtensionState = useExtensionState as jest.MockedFunction<typeof useExtensionState>
describe("AutoApproveMenu", () => {
const defaultMockState = {
// Required state properties
version: "1.0.0",
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
allowedCommands: [],
soundEnabled: false,
soundVolume: 0.5,
diffEnabled: false,
fuzzyMatchThreshold: 1.0,
preferredLanguage: "English",
writeDelayMs: 1000,
browserViewportSize: "900x600",
screenshotQuality: 75,
terminalOutputLineLimit: 500,
mcpEnabled: true,
requestDelaySeconds: 5,
currentApiConfigName: "default",
listApiConfigMeta: [],
mode: defaultModeSlug,
customModePrompts: defaultPrompts,
customSupportPrompts: {},
enhancementApiConfigId: "",
didHydrateState: true,
showWelcome: false,
theme: {},
glamaModels: {},
openRouterModels: {},
openAiModels: [],
mcpServers: [],
filePaths: [],
experiments: experimentDefault,
customModes: [],
// Auto-approve specific properties
alwaysAllowReadOnly: false,
alwaysAllowWrite: false,
alwaysAllowExecute: false,
alwaysAllowBrowser: false,
alwaysAllowMcp: false,
alwaysApproveResubmit: false,
alwaysAllowModeSwitch: false,
autoApprovalEnabled: false,
// Required setter functions
setApiConfiguration: jest.fn(),
setCustomInstructions: jest.fn(),
setAlwaysAllowReadOnly: jest.fn(),
setAlwaysAllowWrite: jest.fn(),
setAlwaysAllowExecute: jest.fn(),
setAlwaysAllowBrowser: jest.fn(),
setAlwaysAllowMcp: jest.fn(),
setAlwaysAllowModeSwitch: jest.fn(),
setShowAnnouncement: jest.fn(),
setAllowedCommands: jest.fn(),
setSoundEnabled: jest.fn(),
setSoundVolume: jest.fn(),
setDiffEnabled: jest.fn(),
setBrowserViewportSize: jest.fn(),
setFuzzyMatchThreshold: jest.fn(),
setPreferredLanguage: jest.fn(),
setWriteDelayMs: jest.fn(),
setScreenshotQuality: jest.fn(),
setTerminalOutputLineLimit: jest.fn(),
setMcpEnabled: jest.fn(),
setAlwaysApproveResubmit: jest.fn(),
setRequestDelaySeconds: jest.fn(),
setCurrentApiConfigName: jest.fn(),
setListApiConfigMeta: jest.fn(),
onUpdateApiConfig: jest.fn(),
setMode: jest.fn(),
setCustomModePrompts: jest.fn(),
setCustomSupportPrompts: jest.fn(),
setEnhancementApiConfigId: jest.fn(),
setAutoApprovalEnabled: jest.fn(),
setExperimentEnabled: jest.fn(),
handleInputChange: jest.fn(),
setCustomModes: jest.fn(),
}
beforeEach(() => {
mockUseExtensionState.mockReturnValue(defaultMockState)
})
afterEach(() => {
jest.clearAllMocks()
})
it("renders with initial collapsed state", () => {
render(<AutoApproveMenu />)
// Check for main checkbox and label
expect(screen.getByText("Auto-approve:")).toBeInTheDocument()
expect(screen.getByText("None")).toBeInTheDocument()
// Verify the menu is collapsed (actions not visible)
expect(screen.queryByText("Read files and directories")).not.toBeInTheDocument()
})
it("expands menu when clicked", () => {
render(<AutoApproveMenu />)
// Click to expand
fireEvent.click(screen.getByText("Auto-approve:"))
// Verify menu items are visible
expect(screen.getByText("Read files and directories")).toBeInTheDocument()
expect(screen.getByText("Edit files")).toBeInTheDocument()
expect(screen.getByText("Execute approved commands")).toBeInTheDocument()
expect(screen.getByText("Use the browser")).toBeInTheDocument()
expect(screen.getByText("Use MCP servers")).toBeInTheDocument()
expect(screen.getByText("Retry failed requests")).toBeInTheDocument()
})
it("toggles main auto-approval checkbox", () => {
render(<AutoApproveMenu />)
const mainCheckbox = screen.getByRole("checkbox")
fireEvent.click(mainCheckbox)
expect(defaultMockState.setAutoApprovalEnabled).toHaveBeenCalledWith(true)
})
it("toggles individual permissions", () => {
render(<AutoApproveMenu />)
// Expand menu
fireEvent.click(screen.getByText("Auto-approve:"))
// Click read files checkbox
fireEvent.click(screen.getByText("Read files and directories"))
expect(defaultMockState.setAlwaysAllowReadOnly).toHaveBeenCalledWith(true)
// Click edit files checkbox
fireEvent.click(screen.getByText("Edit files"))
expect(defaultMockState.setAlwaysAllowWrite).toHaveBeenCalledWith(true)
// Click execute commands checkbox
fireEvent.click(screen.getByText("Execute approved commands"))
expect(defaultMockState.setAlwaysAllowExecute).toHaveBeenCalledWith(true)
})
it("displays enabled actions in summary", () => {
mockUseExtensionState.mockReturnValue({
...defaultMockState,
alwaysAllowReadOnly: true,
alwaysAllowWrite: true,
autoApprovalEnabled: true,
})
render(<AutoApproveMenu />)
// Check that enabled actions are shown in summary
expect(screen.getByText("Read, Edit")).toBeInTheDocument()
})
it("preserves checkbox states", () => {
// Mock state with some permissions enabled
const mockState = {
...defaultMockState,
alwaysAllowReadOnly: true,
alwaysAllowWrite: true,
}
// Update mock to return our state
mockUseExtensionState.mockReturnValue(mockState)
render(<AutoApproveMenu />)
// Expand menu
fireEvent.click(screen.getByText("Auto-approve:"))
// Verify read and edit checkboxes are checked
expect(screen.getByLabelText("Read files and directories")).toBeInTheDocument()
expect(screen.getByLabelText("Edit files")).toBeInTheDocument()
// Verify the setters haven't been called yet
expect(mockState.setAlwaysAllowReadOnly).not.toHaveBeenCalled()
expect(mockState.setAlwaysAllowWrite).not.toHaveBeenCalled()
// Collapse menu
fireEvent.click(screen.getByText("Auto-approve:"))
// Expand again
fireEvent.click(screen.getByText("Auto-approve:"))
// Verify checkboxes are still present
expect(screen.getByLabelText("Read files and directories")).toBeInTheDocument()
expect(screen.getByLabelText("Edit files")).toBeInTheDocument()
// Verify the setters still haven't been called
expect(mockState.setAlwaysAllowReadOnly).not.toHaveBeenCalled()
expect(mockState.setAlwaysAllowWrite).not.toHaveBeenCalled()
})
})

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

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

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

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

View File

@@ -27,6 +27,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
@@ -51,10 +52,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
@@ -90,8 +95,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,
@@ -110,6 +117,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,
})
@@ -170,7 +178,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": {
@@ -237,6 +249,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
openAiModels,
mcpServers,
filePaths,
openedTabs,
soundVolume: state.soundVolume,
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
writeDelayMs: state.writeDelayMs,
@@ -269,8 +282,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,

View File

@@ -264,3 +264,11 @@ vscode-dropdown::part(listbox) {
border-radius: 3px;
box-shadow: 0 0 0 0.5px color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
}
/**
* vscrui Overrides / Hacks
*/
.vscrui-checkbox__listbox > ul {
max-height: unset !important;
}

View File

@@ -48,6 +48,7 @@ export function removeMention(text: string, position: number): { newText: string
}
export enum ContextMenuOptionType {
OpenedFile = "openedFile",
File = "file",
Folder = "folder",
Problems = "problems",
@@ -80,8 +81,14 @@ export function getContextMenuOptions(
if (query === "") {
if (selectedType === ContextMenuOptionType.File) {
const files = queryItems
.filter((item) => item.type === ContextMenuOptionType.File)
.map((item) => ({ type: ContextMenuOptionType.File, value: item.value }))
.filter(
(item) =>
item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.OpenedFile,
)
.map((item) => ({
type: item.type,
value: item.value,
}))
return files.length > 0 ? files : [{ type: ContextMenuOptionType.NoResults }]
}
@@ -162,12 +169,16 @@ export function getContextMenuOptions(
// Separate matches by type
const fileMatches = matchingItems.filter(
(item) => item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.Folder,
(item) =>
item.type === ContextMenuOptionType.File ||
item.type === ContextMenuOptionType.OpenedFile ||
item.type === ContextMenuOptionType.Folder,
)
const gitMatches = matchingItems.filter((item) => item.type === ContextMenuOptionType.Git)
const otherMatches = matchingItems.filter(
(item) =>
item.type !== ContextMenuOptionType.File &&
item.type !== ContextMenuOptionType.OpenedFile &&
item.type !== ContextMenuOptionType.Folder &&
item.type !== ContextMenuOptionType.Git,
)