mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Merged with main branch
This commit is contained in:
commit
32f91b6291
114 changed files with 8101 additions and 1604 deletions
5
.changeset/blue-masks-camp.md
Normal file
5
.changeset/blue-masks-camp.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Add shortcuts to the currently open tabs in the "Add File" section of @-mentions (thanks @olup!)
|
||||
5
.changeset/breezy-badgers-refuse.md
Normal file
5
.changeset/breezy-badgers-refuse.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Visual cleanup to the list of modes on the prompts tab
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Use an exponential backoff for API retries
|
||||
1
.env.integration.example
Normal file
1
.env.integration.example
Normal file
|
|
@ -0,0 +1 @@
|
|||
OPENROUTER_API_KEY=sk-or-v1-...
|
||||
|
|
@ -1,56 +1,24 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2021,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"@typescript-eslint/naming-convention": ["warn"],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_"
|
||||
"selector": "import",
|
||||
"format": ["camelCase", "PascalCase"]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
"warn",
|
||||
{
|
||||
"allowExpressions": true,
|
||||
"allowTypedFunctionExpressions": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"warn",
|
||||
{
|
||||
"accessibility": "explicit"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"@typescript-eslint/semi": "off",
|
||||
"eqeqeq": "warn",
|
||||
"no-throw-literal": "warn",
|
||||
"semi": ["off", "always"],
|
||||
"quotes": ["warn", "double", { "avoidEscape": true }],
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/no-var-requires": "warn",
|
||||
"no-extra-semi": "warn",
|
||||
"prefer-const": "warn",
|
||||
"no-mixed-spaces-and-tabs": "warn",
|
||||
"no-case-declarations": "warn",
|
||||
"no-useless-escape": "warn",
|
||||
"require-yield": "warn",
|
||||
"no-empty": "warn",
|
||||
"no-control-regex": "warn",
|
||||
"@typescript-eslint/ban-ts-comment": "warn"
|
||||
"semi": "off",
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2021": true
|
||||
},
|
||||
"ignorePatterns": ["dist/**", "out/**", "webview-ui/**", "**/*.js"]
|
||||
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
|
||||
}
|
||||
|
|
|
|||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
|
@ -1,2 +1,2 @@
|
|||
# These owners will be the default owners for everything in the repo
|
||||
* @stea9499 @ColemanRoo @mrubens
|
||||
* @stea9499 @ColemanRoo @mrubens @cte
|
||||
|
|
|
|||
49
.github/workflows/code-qa.yml
vendored
49
.github/workflows/code-qa.yml
vendored
|
|
@ -1,6 +1,7 @@
|
|||
name: Code QA Roo Code
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
|
@ -13,33 +14,65 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm run install:all
|
||||
|
||||
- name: Compile TypeScript
|
||||
- name: Compile
|
||||
run: npm run compile
|
||||
- name: Check types
|
||||
run: npm run check-types
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
unit-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm run install:all
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm test
|
||||
run: npm test
|
||||
|
||||
check-openrouter-api-key:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
exists: ${{ steps.openrouter-api-key-check.outputs.defined }}
|
||||
steps:
|
||||
- name: Check if OpenRouter API key exists
|
||||
id: openrouter-api-key-check
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ secrets.OPENROUTER_API_KEY }}" != '' ]; then
|
||||
echo "defined=true" >> $GITHUB_OUTPUT;
|
||||
else
|
||||
echo "defined=false" >> $GITHUB_OUTPUT;
|
||||
fi
|
||||
|
||||
integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-openrouter-api-key]
|
||||
if: needs.check-openrouter-api-key.outputs.exists == 'true'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
- name: Create env.integration file
|
||||
run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.integration
|
||||
- name: Install dependencies
|
||||
run: npm run install:all
|
||||
- name: Run integration tests
|
||||
run: xvfb-run -a npm run test:integration
|
||||
|
|
|
|||
26
.github/workflows/discord-pr-notify.yml
vendored
Normal file
26
.github/workflows/discord-pr-notify.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
name: Discord PR Notifier
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.head_ref != 'changeset-release/main'
|
||||
steps:
|
||||
- name: Send Discord Notification
|
||||
run: |
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg title "${{ github.event.pull_request.title }}" \
|
||||
--arg url "${{ github.event.pull_request.html_url }}" \
|
||||
--arg author "${{ github.event.pull_request.user.login }}" \
|
||||
'{
|
||||
content: ("🚀 **New PR:** " + $title + "\n🔗 <" + $url + ">\n👤 **Author:** " + $author),
|
||||
thread_name: ($title + " by " + $author)
|
||||
}')
|
||||
|
||||
curl -X POST "${{ secrets.DISCORD_WEBHOOK }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD"
|
||||
44
.github/workflows/pages.yml
vendored
Normal file
44
.github/workflows/pages.yml
vendored
Normal 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
9
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,4 +6,7 @@ if [ "$branch" = "main" ]; then
|
|||
fi
|
||||
|
||||
npx lint-staged
|
||||
|
||||
npm run compile
|
||||
npm run lint
|
||||
npm run check-types
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
|
|
@ -4,6 +4,9 @@
|
|||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"connor4312.esbuild-problem-matchers",
|
||||
"ms-vscode.extension-test-runner"
|
||||
"ms-vscode.extension-test-runner",
|
||||
"csstools.postcss",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"tobermory.es6-string-html"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
18
.vscode/launch.json
vendored
18
.vscode/launch.json
vendored
|
|
@ -10,20 +10,20 @@
|
|||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
],
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"sourceMaps": true,
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "compile",
|
||||
"preLaunchTask": "debug-mode",
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"VSCODE_DEBUG_MODE": "true"
|
||||
},
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/**",
|
||||
"!**/node_modules/**"
|
||||
]
|
||||
},
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
"presentation": {
|
||||
"hidden": false,
|
||||
"group": "tasks",
|
||||
"order": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
58
.vscode/tasks.json
vendored
58
.vscode/tasks.json
vendored
|
|
@ -7,7 +7,6 @@
|
|||
"label": "compile",
|
||||
"type": "npm",
|
||||
"script": "compile",
|
||||
"dependsOn": ["npm: build:webview"],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
|
|
@ -30,56 +29,75 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"label": "debug-mode",
|
||||
"dependsOn": ["compile", "npm: dev"],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": false
|
||||
},
|
||||
"dependsOrder": "parallel",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "npm: dev",
|
||||
"type": "npm",
|
||||
"script": "dev",
|
||||
"group": "build",
|
||||
"problemMatcher": {
|
||||
"owner": "vite",
|
||||
"pattern": {
|
||||
"regexp": "^$"
|
||||
},
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": ".*VITE.*",
|
||||
"endsPattern": ".*Local:.*"
|
||||
}
|
||||
},
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "npm: build:webview",
|
||||
"type": "npm",
|
||||
"script": "build:webview",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
"label": "npm: build:webview",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "npm: watch:esbuild",
|
||||
"type": "npm",
|
||||
"script": "watch:esbuild",
|
||||
"group": "build",
|
||||
"problemMatcher": "$esbuild-watch",
|
||||
"isBackground": true,
|
||||
"label": "npm: watch:esbuild",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "npm: watch:tsc",
|
||||
"type": "npm",
|
||||
"script": "watch:tsc",
|
||||
"group": "build",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"label": "npm: watch:tsc",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch-tests",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
"group": "watchers"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "tasks: watch-tests",
|
||||
"dependsOn": ["npm: watch", "npm: watch-tests"],
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
21
CHANGELOG.md
21
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -255,9 +255,15 @@ Roo Code is available on:
|
|||
```bash
|
||||
code --install-extension bin/roo-code-4.0.0.vsix
|
||||
```
|
||||
5. **Debug**:
|
||||
5. **Start the webview (Vite/React app with HMR)**:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
6. **Debug**:
|
||||
- Press `F5` (or **Run** → **Start Debugging**) in VSCode to open a new session with Roo Code loaded.
|
||||
|
||||
Changes to the webview will appear immediately. Changes to the core extension will require a restart of the extension host.
|
||||
|
||||
We use [changesets](https://github.com/changesets/changesets) for versioning and publishing. Check our `CHANGELOG.md` for release notes.
|
||||
|
||||
---
|
||||
|
|
|
|||
2
docs/Gemfile
Normal file
2
docs/Gemfile
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
source 'https://rubygems.org'
|
||||
gem 'github-pages', group: :jekyll_plugins
|
||||
308
docs/Gemfile.lock
Normal file
308
docs/Gemfile.lock
Normal 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
15
docs/_config.yml
Normal 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
|
||||
10
docs/getting-started/index.md
Normal file
10
docs/getting-started/index.md
Normal 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
9
docs/index.md
Normal 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
27
flake.lock
generated
Normal 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
33
flake.nix
Normal 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;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ module.exports = {
|
|||
transformIgnorePatterns: [
|
||||
"node_modules/(?!(@modelcontextprotocol|delay|p-wait-for|globby|serialize-error|strip-ansi|default-shell|os-name)/)",
|
||||
],
|
||||
roots: ["<rootDir>/src", "<rootDir>/webview-ui/src"],
|
||||
modulePathIgnorePatterns: [".vscode-test"],
|
||||
reporters: [["jest-simple-dot-reporter", {}]],
|
||||
setupFiles: [],
|
||||
|
|
|
|||
180
package-lock.json
generated
180
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
26
package.json
26
package.json
|
|
@ -3,7 +3,7 @@
|
|||
"displayName": "Roo Code (prev. Roo Cline)",
|
||||
"description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.",
|
||||
"publisher": "RooVeterinaryInc",
|
||||
"version": "3.3.6",
|
||||
"version": "3.3.9",
|
||||
"icon": "assets/icons/rocket.png",
|
||||
"galleryBanner": {
|
||||
"color": "#617A91",
|
||||
|
|
@ -118,6 +118,11 @@
|
|||
"command": "roo-cline.improveCode",
|
||||
"title": "Roo Code: Improve Code",
|
||||
"category": "Roo Code"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.addToContext",
|
||||
"title": "Roo Code: Add To Context",
|
||||
"category": "Roo Code"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
|
|
@ -136,6 +141,11 @@
|
|||
"command": "roo-cline.improveCode",
|
||||
"when": "editorHasSelection",
|
||||
"group": "Roo Code@3"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.addToContext",
|
||||
"when": "editorHasSelection",
|
||||
"group": "Roo Code@4"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
|
|
@ -211,16 +221,16 @@
|
|||
"build:webview": "cd webview-ui && npm run build",
|
||||
"changeset": "changeset",
|
||||
"check-types": "tsc --noEmit",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||
"compile-tests": "tsc -p . --outDir out",
|
||||
"compile": "tsc -p . --outDir out && node esbuild.js",
|
||||
"compile:integration": "tsc -p tsconfig.integration.json",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"lint": "eslint src --ext ts --quiet && npm run lint --prefix webview-ui",
|
||||
"lint": "eslint src --ext ts && npm run lint --prefix webview-ui",
|
||||
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
"start:webview": "cd webview-ui && npm run start",
|
||||
"pretest": "npm run compile && npm run compile:integration",
|
||||
"dev": "cd webview-ui && npm run dev",
|
||||
"test": "jest && npm run test:webview",
|
||||
"test:webview": "cd webview-ui && npm run test",
|
||||
"test:extension": "vscode-test",
|
||||
"test:integration": "npm run build && npm run compile:integration && npx dotenvx run -f .env.integration -- vscode-test",
|
||||
"prepare": "husky",
|
||||
"publish:marketplace": "vsce publish && ovsx publish",
|
||||
"publish": "npm run build && changeset publish && npm install --package-lock-only",
|
||||
|
|
@ -235,6 +245,7 @@
|
|||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.10",
|
||||
"@changesets/types": "^6.0.0",
|
||||
"@dotenvx/dotenvx": "^1.34.0",
|
||||
"@types/diff": "^5.2.1",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/jest": "^29.5.14",
|
||||
|
|
@ -245,7 +256,6 @@
|
|||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"esbuild": "^0.24.0",
|
||||
"eslint": "^8.57.0",
|
||||
"husky": "^9.1.7",
|
||||
|
|
|
|||
|
|
@ -5,9 +5,25 @@ const vscode = {
|
|||
createTextEditorDecorationType: jest.fn().mockReturnValue({
|
||||
dispose: jest.fn(),
|
||||
}),
|
||||
tabGroups: {
|
||||
onDidChangeTabs: jest.fn(() => {
|
||||
return {
|
||||
dispose: jest.fn(),
|
||||
}
|
||||
}),
|
||||
all: [],
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
onDidSaveTextDocument: jest.fn(),
|
||||
createFileSystemWatcher: jest.fn().mockReturnValue({
|
||||
onDidCreate: jest.fn().mockReturnValue({ dispose: jest.fn() }),
|
||||
onDidDelete: jest.fn().mockReturnValue({ dispose: jest.fn() }),
|
||||
dispose: jest.fn(),
|
||||
}),
|
||||
fs: {
|
||||
stat: jest.fn(),
|
||||
},
|
||||
},
|
||||
Disposable: class {
|
||||
dispose() {}
|
||||
|
|
@ -52,6 +68,22 @@ const vscode = {
|
|||
this.id = id
|
||||
}
|
||||
},
|
||||
ExtensionMode: {
|
||||
Production: 1,
|
||||
Development: 2,
|
||||
Test: 3,
|
||||
},
|
||||
FileType: {
|
||||
Unknown: 0,
|
||||
File: 1,
|
||||
Directory: 2,
|
||||
SymbolicLink: 64,
|
||||
},
|
||||
TabInputText: class {
|
||||
constructor(uri) {
|
||||
this.uri = uri
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = vscode
|
||||
|
|
|
|||
32
src/activate/handleUri.ts
Normal file
32
src/activate/handleUri.ts
Normal 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
3
src/activate/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { handleUri } from "./handleUri"
|
||||
export { registerCommands } from "./registerCommands"
|
||||
export { registerCodeActions } from "./registerCodeActions"
|
||||
91
src/activate/registerCodeActions.ts
Normal file
91
src/activate/registerCodeActions.ts
Normal 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)
|
||||
}
|
||||
83
src/activate/registerCommands.ts
Normal file
83
src/activate/registerCommands.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import * as vscode from "vscode"
|
||||
import delay from "delay"
|
||||
|
||||
import { ClineProvider } from "../core/webview/ClineProvider"
|
||||
|
||||
export type RegisterCommandOptions = {
|
||||
context: vscode.ExtensionContext
|
||||
outputChannel: vscode.OutputChannel
|
||||
provider: ClineProvider
|
||||
}
|
||||
|
||||
export const registerCommands = (options: RegisterCommandOptions) => {
|
||||
const { context, outputChannel } = options
|
||||
|
||||
for (const [command, callback] of Object.entries(getCommandsMap(options))) {
|
||||
context.subscriptions.push(vscode.commands.registerCommand(command, callback))
|
||||
}
|
||||
}
|
||||
|
||||
const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions) => {
|
||||
return {
|
||||
"roo-cline.plusButtonClicked": async () => {
|
||||
await provider.clearTask()
|
||||
await provider.postStateToWebview()
|
||||
await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
|
||||
},
|
||||
"roo-cline.mcpButtonClicked": () => {
|
||||
provider.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
|
||||
},
|
||||
"roo-cline.promptsButtonClicked": () => {
|
||||
provider.postMessageToWebview({ type: "action", action: "promptsButtonClicked" })
|
||||
},
|
||||
"roo-cline.popoutButtonClicked": () => openClineInNewTab({ context, outputChannel }),
|
||||
"roo-cline.openInNewTab": () => openClineInNewTab({ context, outputChannel }),
|
||||
"roo-cline.settingsButtonClicked": () => {
|
||||
provider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" })
|
||||
},
|
||||
"roo-cline.historyButtonClicked": () => {
|
||||
provider.postMessageToWebview({ type: "action", action: "historyButtonClicked" })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {
|
||||
outputChannel.appendLine("Opening Roo Code in new tab")
|
||||
|
||||
// (This example uses webviewProvider activation event which is necessary to
|
||||
// deserialize cached webview, but since we use retainContextWhenHidden, we
|
||||
// don't need to use that event).
|
||||
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
|
||||
const tabProvider = new ClineProvider(context, outputChannel)
|
||||
// const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined
|
||||
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
|
||||
|
||||
// Check if there are any visible text editors, otherwise open a new group
|
||||
// to the right.
|
||||
const hasVisibleEditors = vscode.window.visibleTextEditors.length > 0
|
||||
|
||||
if (!hasVisibleEditors) {
|
||||
await vscode.commands.executeCommand("workbench.action.newGroupRight")
|
||||
}
|
||||
|
||||
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
|
||||
|
||||
const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, {
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [context.extensionUri],
|
||||
})
|
||||
|
||||
// TODO: use better svg icon with light and dark variants (see
|
||||
// https://stackoverflow.com/questions/58365687/vscode-extension-iconpath).
|
||||
panel.iconPath = {
|
||||
light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
|
||||
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
|
||||
}
|
||||
|
||||
await tabProvider.resolveWebviewView(panel)
|
||||
|
||||
// Lock the editor group so clicking on files doesn't open them over the panel
|
||||
await delay(100)
|
||||
await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
|
||||
}
|
||||
|
|
@ -153,11 +153,35 @@ describe("OpenAiNativeHandler", () => {
|
|||
expect(mockCreate).toHaveBeenCalledWith({
|
||||
model: "o1",
|
||||
messages: [
|
||||
{ role: "developer", content: systemPrompt },
|
||||
{ role: "developer", content: "Formatting re-enabled\n" + systemPrompt },
|
||||
{ role: "user", content: "Hello!" },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle o3-mini model family correctly", async () => {
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "o3-mini",
|
||||
})
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
const chunks: any[] = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith({
|
||||
model: "o3-mini",
|
||||
messages: [
|
||||
{ role: "developer", content: "Formatting re-enabled\n" + systemPrompt },
|
||||
{ role: "user", content: "Hello!" },
|
||||
],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
reasoning_effort: "medium",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("streaming models", () => {
|
||||
|
|
@ -289,6 +313,21 @@ describe("OpenAiNativeHandler", () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("should complete prompt successfully with o3-mini model", async () => {
|
||||
handler = new OpenAiNativeHandler({
|
||||
apiModelId: "o3-mini",
|
||||
openAiNativeApiKey: "test-api-key",
|
||||
})
|
||||
|
||||
const result = await handler.completePrompt("Test prompt")
|
||||
expect(result).toBe("Test response")
|
||||
expect(mockCreate).toHaveBeenCalledWith({
|
||||
model: "o3-mini",
|
||||
messages: [{ role: "user", content: "Test prompt" }],
|
||||
reasoning_effort: "medium",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle API errors", async () => {
|
||||
mockCreate.mockRejectedValueOnce(new Error("API Error"))
|
||||
await expect(handler.completePrompt("Test prompt")).rejects.toThrow(
|
||||
|
|
|
|||
|
|
@ -72,28 +72,30 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
|
|||
maxTokens = 8_192
|
||||
}
|
||||
|
||||
const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = {
|
||||
model: this.getModel().id,
|
||||
max_tokens: maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
}
|
||||
|
||||
if (this.supportsTemperature()) {
|
||||
requestOptions.temperature = 0
|
||||
}
|
||||
|
||||
const { data: completion, response } = await this.client.chat.completions
|
||||
.create(
|
||||
{
|
||||
model: this.getModel().id,
|
||||
max_tokens: maxTokens,
|
||||
temperature: 0,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
.create(requestOptions, {
|
||||
headers: {
|
||||
"X-Glama-Metadata": JSON.stringify({
|
||||
labels: [
|
||||
{
|
||||
key: "app",
|
||||
value: "vscode.rooveterinaryinc.roo-cline",
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"X-Glama-Metadata": JSON.stringify({
|
||||
labels: [
|
||||
{
|
||||
key: "app",
|
||||
value: "vscode.rooveterinaryinc.roo-cline",
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
.withResponse()
|
||||
|
||||
const completionRequestId = response.headers.get("x-completion-request-id")
|
||||
|
|
@ -148,6 +150,10 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private supportsTemperature(): boolean {
|
||||
return !this.getModel().id.startsWith("openai/o3-mini")
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.glamaModelId
|
||||
const modelInfo = this.options.glamaModelInfo
|
||||
|
|
@ -164,7 +170,10 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
|
|||
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
|
||||
model: this.getModel().id,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
temperature: 0,
|
||||
}
|
||||
|
||||
if (this.supportsTemperature()) {
|
||||
requestOptions.temperature = 0
|
||||
}
|
||||
|
||||
if (this.getModel().id.startsWith("anthropic/")) {
|
||||
|
|
|
|||
|
|
@ -24,57 +24,107 @@ export class OpenAiNativeHandler implements ApiHandler, SingleCompletionHandler
|
|||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const modelId = this.getModel().id
|
||||
switch (modelId) {
|
||||
case "o1":
|
||||
case "o1-preview":
|
||||
case "o1-mini": {
|
||||
// o1-preview and o1-mini don't support streaming, non-1 temp, or system prompt
|
||||
// o1 doesnt support streaming or non-1 temp but does support a developer prompt
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: modelId,
|
||||
messages: [
|
||||
{ role: modelId === "o1" ? "developer" : "user", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
],
|
||||
})
|
||||
|
||||
if (modelId.startsWith("o1")) {
|
||||
yield* this.handleO1FamilyMessage(modelId, systemPrompt, messages)
|
||||
return
|
||||
}
|
||||
|
||||
if (modelId.startsWith("o3-mini")) {
|
||||
yield* this.handleO3FamilyMessage(modelId, systemPrompt, messages)
|
||||
return
|
||||
}
|
||||
|
||||
yield* this.handleDefaultModelMessage(modelId, systemPrompt, messages)
|
||||
}
|
||||
|
||||
private async *handleO1FamilyMessage(
|
||||
modelId: string,
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
): ApiStream {
|
||||
// o1 supports developer prompt with formatting
|
||||
// o1-preview and o1-mini only support user messages
|
||||
const isOriginalO1 = modelId === "o1"
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: modelId,
|
||||
messages: [
|
||||
{
|
||||
role: isOriginalO1 ? "developer" : "user",
|
||||
content: isOriginalO1 ? `Formatting re-enabled\n${systemPrompt}` : systemPrompt,
|
||||
},
|
||||
...convertToOpenAiMessages(messages),
|
||||
],
|
||||
})
|
||||
|
||||
yield* this.yieldResponseData(response)
|
||||
}
|
||||
|
||||
private async *handleO3FamilyMessage(
|
||||
modelId: string,
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
): ApiStream {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: "o3-mini",
|
||||
messages: [
|
||||
{
|
||||
role: "developer",
|
||||
content: `Formatting re-enabled\n${systemPrompt}`,
|
||||
},
|
||||
...convertToOpenAiMessages(messages),
|
||||
],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
reasoning_effort: this.getModel().info.reasoningEffort,
|
||||
})
|
||||
|
||||
yield* this.handleStreamResponse(stream)
|
||||
}
|
||||
|
||||
private async *handleDefaultModelMessage(
|
||||
modelId: string,
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
): ApiStream {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelId,
|
||||
temperature: 0,
|
||||
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
|
||||
yield* this.handleStreamResponse(stream)
|
||||
}
|
||||
|
||||
private async *yieldResponseData(response: OpenAI.Chat.Completions.ChatCompletion): ApiStream {
|
||||
yield {
|
||||
type: "text",
|
||||
text: response.choices[0]?.message.content || "",
|
||||
}
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: response.usage?.prompt_tokens || 0,
|
||||
outputTokens: response.usage?.completion_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
private async *handleStreamResponse(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): ApiStream {
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: response.choices[0]?.message.content || "",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: response.usage?.prompt_tokens || 0,
|
||||
outputTokens: response.usage?.completion_tokens || 0,
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: this.getModel().id,
|
||||
// max_completion_tokens: this.getModel().info.maxTokens,
|
||||
temperature: 0,
|
||||
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
// contains a null value except for the last chunk which contains the token usage statistics for the entire request
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
}
|
||||
}
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -94,22 +144,12 @@ export class OpenAiNativeHandler implements ApiHandler, SingleCompletionHandler
|
|||
const modelId = this.getModel().id
|
||||
let requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming
|
||||
|
||||
switch (modelId) {
|
||||
case "o1":
|
||||
case "o1-preview":
|
||||
case "o1-mini":
|
||||
// o1 doesn't support non-1 temp
|
||||
requestOptions = {
|
||||
model: modelId,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
}
|
||||
break
|
||||
default:
|
||||
requestOptions = {
|
||||
model: modelId,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
temperature: 0,
|
||||
}
|
||||
if (modelId.startsWith("o1")) {
|
||||
requestOptions = this.getO1CompletionOptions(modelId, prompt)
|
||||
} else if (modelId.startsWith("o3-mini")) {
|
||||
requestOptions = this.getO3CompletionOptions(modelId, prompt)
|
||||
} else {
|
||||
requestOptions = this.getDefaultCompletionOptions(modelId, prompt)
|
||||
}
|
||||
|
||||
const response = await this.client.chat.completions.create(requestOptions)
|
||||
|
|
@ -121,4 +161,36 @@ export class OpenAiNativeHandler implements ApiHandler, SingleCompletionHandler
|
|||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private getO1CompletionOptions(
|
||||
modelId: string,
|
||||
prompt: string,
|
||||
): OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming {
|
||||
return {
|
||||
model: modelId,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
}
|
||||
}
|
||||
|
||||
private getO3CompletionOptions(
|
||||
modelId: string,
|
||||
prompt: string,
|
||||
): OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming {
|
||||
return {
|
||||
model: "o3-mini",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
reasoning_effort: this.getModel().info.reasoningEffort,
|
||||
}
|
||||
}
|
||||
|
||||
private getDefaultCompletionOptions(
|
||||
modelId: string,
|
||||
prompt: string,
|
||||
): OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming {
|
||||
return {
|
||||
model: modelId,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
temperature: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,8 +118,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
|
|||
|
||||
// Handle models based on deepseek-r1
|
||||
if (
|
||||
this.getModel().id === "deepseek/deepseek-r1" ||
|
||||
this.getModel().id.startsWith("deepseek/deepseek-r1:") ||
|
||||
this.getModel().id.startsWith("deepseek/deepseek-r1") ||
|
||||
this.getModel().id === "perplexity/sonar-reasoning"
|
||||
) {
|
||||
// Recommended temperature for DeepSeek reasoning models
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ import { parseMentions } from "./mentions"
|
|||
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
|
||||
import { formatResponse } from "./prompts/responses"
|
||||
import { SYSTEM_PROMPT } from "./prompts/system"
|
||||
import { modes, defaultModeSlug, getModeBySlug } from "../shared/modes"
|
||||
import { modes, defaultModeSlug, getModeBySlug, parseSlashCommand } from "../shared/modes"
|
||||
import { truncateHalfConversation } from "./sliding-window"
|
||||
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
|
||||
import { detectCodeOmission } from "../integrations/editor/detect-omission"
|
||||
|
|
@ -77,6 +77,29 @@ export class Cline {
|
|||
private terminalManager: TerminalManager
|
||||
private urlContentFetcher: UrlContentFetcher
|
||||
private browserSession: BrowserSession
|
||||
|
||||
/**
|
||||
* Processes a message for slash commands and handles mode switching if needed.
|
||||
* @param message The message to process
|
||||
* @returns The processed message with slash command removed if one was present
|
||||
*/
|
||||
private async handleSlashCommand(message: string): Promise<string> {
|
||||
if (!message) return message
|
||||
|
||||
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
const slashCommand = parseSlashCommand(message, customModes)
|
||||
|
||||
if (slashCommand) {
|
||||
// Switch mode before processing the remaining message
|
||||
const provider = this.providerRef.deref()
|
||||
if (provider) {
|
||||
await provider.handleModeSwitch(slashCommand.modeSlug)
|
||||
return slashCommand.remainingMessage
|
||||
}
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
private didEditFile: boolean = false
|
||||
customInstructions?: string
|
||||
diffStrategy?: DiffStrategy
|
||||
|
|
@ -96,6 +119,7 @@ export class Cline {
|
|||
didFinishAborting = false
|
||||
abandoned = false
|
||||
private diffViewProvider: DiffViewProvider
|
||||
private lastApiRequestTime?: number
|
||||
|
||||
// streaming
|
||||
private currentStreamingContentIndex = 0
|
||||
|
|
@ -354,6 +378,11 @@ export class Cline {
|
|||
}
|
||||
|
||||
async handleWebviewAskResponse(askResponse: ClineAskResponse, text?: string, images?: string[]) {
|
||||
// Process slash command if present
|
||||
if (text) {
|
||||
text = await this.handleSlashCommand(text)
|
||||
}
|
||||
|
||||
this.askResponse = askResponse
|
||||
this.askResponseText = text
|
||||
this.askResponseImages = images
|
||||
|
|
@ -436,6 +465,22 @@ export class Cline {
|
|||
this.apiConversationHistory = []
|
||||
await this.providerRef.deref()?.postStateToWebview()
|
||||
|
||||
// Check for slash command if task is provided
|
||||
if (task) {
|
||||
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
const slashCommand = parseSlashCommand(task, customModes)
|
||||
|
||||
if (slashCommand) {
|
||||
// Switch mode before processing the remaining message
|
||||
const provider = this.providerRef.deref()
|
||||
if (provider) {
|
||||
await provider.handleModeSwitch(slashCommand.modeSlug)
|
||||
// Update task to be just the remaining message
|
||||
task = slashCommand.remainingMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.say("text", task, images)
|
||||
|
||||
let imageBlocks: Anthropic.ImageBlockParam[] = formatResponse.imageBlocks(images)
|
||||
|
|
@ -796,9 +841,40 @@ export class Cline {
|
|||
async *attemptApiRequest(previousApiReqIndex: number, retryAttempt: number = 0): ApiStream {
|
||||
let mcpHub: McpHub | undefined
|
||||
|
||||
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } =
|
||||
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds, rateLimitSeconds } =
|
||||
(await this.providerRef.deref()?.getState()) ?? {}
|
||||
|
||||
let finalDelay = 0
|
||||
|
||||
// Only apply rate limiting if this isn't the first request
|
||||
if (this.lastApiRequestTime) {
|
||||
const now = Date.now()
|
||||
const timeSinceLastRequest = now - this.lastApiRequestTime
|
||||
const rateLimit = rateLimitSeconds || 0
|
||||
const rateLimitDelay = Math.max(0, rateLimit * 1000 - timeSinceLastRequest)
|
||||
finalDelay = rateLimitDelay
|
||||
}
|
||||
|
||||
// Add exponential backoff delay for retries
|
||||
if (retryAttempt > 0) {
|
||||
const baseDelay = requestDelaySeconds || 5
|
||||
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt)) * 1000
|
||||
finalDelay = Math.max(finalDelay, exponentialDelay)
|
||||
}
|
||||
|
||||
if (finalDelay > 0) {
|
||||
// Show countdown timer
|
||||
for (let i = Math.ceil(finalDelay / 1000); i > 0; i--) {
|
||||
const delayMessage =
|
||||
retryAttempt > 0 ? `Retrying in ${i} seconds...` : `Rate limiting for ${i} seconds...`
|
||||
await this.say("api_req_retry_delayed", delayMessage, undefined, true)
|
||||
await delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
// Update last request time before making the request
|
||||
this.lastApiRequestTime = Date.now()
|
||||
|
||||
if (mcpEnabled ?? true) {
|
||||
mcpHub = this.providerRef.deref()?.mcpHub
|
||||
if (!mcpHub) {
|
||||
|
|
@ -810,8 +886,14 @@ export class Cline {
|
|||
})
|
||||
}
|
||||
|
||||
const { browserViewportSize, mode, customModePrompts, preferredLanguage, experiments } =
|
||||
(await this.providerRef.deref()?.getState()) ?? {}
|
||||
const {
|
||||
browserViewportSize,
|
||||
mode,
|
||||
customModePrompts,
|
||||
preferredLanguage,
|
||||
experiments,
|
||||
enableMcpServerCreation,
|
||||
} = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
const systemPrompt = await (async () => {
|
||||
const provider = this.providerRef.deref()
|
||||
|
|
@ -832,6 +914,7 @@ export class Cline {
|
|||
preferredLanguage,
|
||||
this.diffEnabled,
|
||||
experiments,
|
||||
enableMcpServerCreation,
|
||||
)
|
||||
})()
|
||||
|
||||
|
|
@ -1093,35 +1176,23 @@ export class Cline {
|
|||
const askApproval = async (type: ClineAsk, partialMessage?: string) => {
|
||||
const { response, text, images } = await this.ask(type, partialMessage, false)
|
||||
if (response !== "yesButtonClicked") {
|
||||
if (response === "messageResponse") {
|
||||
// Handle both messageResponse and noButtonClicked with text
|
||||
if (text) {
|
||||
await this.say("user_feedback", text, images)
|
||||
pushToolResult(
|
||||
formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images),
|
||||
)
|
||||
// this.userMessageContent.push({
|
||||
// type: "text",
|
||||
// text: `${toolDescription()}`,
|
||||
// })
|
||||
// this.toolResults.push({
|
||||
// type: "tool_result",
|
||||
// tool_use_id: toolUseId,
|
||||
// content: this.formatToolResponseWithImages(
|
||||
// await this.formatToolDeniedFeedback(text),
|
||||
// images
|
||||
// ),
|
||||
// })
|
||||
this.didRejectTool = true
|
||||
return false
|
||||
} else {
|
||||
pushToolResult(formatResponse.toolDenied())
|
||||
}
|
||||
pushToolResult(formatResponse.toolDenied())
|
||||
// this.toolResults.push({
|
||||
// type: "tool_result",
|
||||
// tool_use_id: toolUseId,
|
||||
// content: await this.formatToolDenied(),
|
||||
// })
|
||||
this.didRejectTool = true
|
||||
return false
|
||||
}
|
||||
// Handle yesButtonClicked with text
|
||||
if (text) {
|
||||
await this.say("user_feedback", text, images)
|
||||
pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,113 +1,27 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { ClineProvider } from "./webview/ClineProvider"
|
||||
import { EditorUtils } from "./EditorUtils"
|
||||
|
||||
export const ACTION_NAMES = {
|
||||
EXPLAIN: "Roo Code: Explain Code",
|
||||
FIX: "Roo Code: Fix Code",
|
||||
FIX_LOGIC: "Roo Code: Fix Logic",
|
||||
IMPROVE: "Roo Code: Improve Code",
|
||||
ADD_TO_CONTEXT: "Roo Code: Add to Context",
|
||||
} as const
|
||||
|
||||
const COMMAND_IDS = {
|
||||
export const COMMAND_IDS = {
|
||||
EXPLAIN: "roo-cline.explainCode",
|
||||
FIX: "roo-cline.fixCode",
|
||||
IMPROVE: "roo-cline.improveCode",
|
||||
ADD_TO_CONTEXT: "roo-cline.addToContext",
|
||||
} as const
|
||||
|
||||
interface DiagnosticData {
|
||||
message: string
|
||||
severity: vscode.DiagnosticSeverity
|
||||
code?: string | number | { value: string | number; target: vscode.Uri }
|
||||
source?: string
|
||||
range: vscode.Range
|
||||
}
|
||||
|
||||
interface EffectiveRange {
|
||||
range: vscode.Range
|
||||
text: string
|
||||
}
|
||||
|
||||
export class CodeActionProvider implements vscode.CodeActionProvider {
|
||||
public static readonly providedCodeActionKinds = [
|
||||
vscode.CodeActionKind.QuickFix,
|
||||
vscode.CodeActionKind.RefactorRewrite,
|
||||
]
|
||||
|
||||
// Cache file paths for performance
|
||||
private readonly filePathCache = new WeakMap<vscode.TextDocument, string>()
|
||||
|
||||
private getEffectiveRange(
|
||||
document: vscode.TextDocument,
|
||||
range: vscode.Range | vscode.Selection,
|
||||
): EffectiveRange | null {
|
||||
try {
|
||||
const selectedText = document.getText(range)
|
||||
if (selectedText) {
|
||||
return { range, text: selectedText }
|
||||
}
|
||||
|
||||
const currentLine = document.lineAt(range.start.line)
|
||||
if (!currentLine.text.trim()) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Optimize range creation by checking bounds first
|
||||
const startLine = Math.max(0, currentLine.lineNumber - 1)
|
||||
const endLine = Math.min(document.lineCount - 1, currentLine.lineNumber + 1)
|
||||
|
||||
// Only create new positions if needed
|
||||
const effectiveRange = new vscode.Range(
|
||||
startLine === currentLine.lineNumber ? range.start : new vscode.Position(startLine, 0),
|
||||
endLine === currentLine.lineNumber
|
||||
? range.end
|
||||
: new vscode.Position(endLine, document.lineAt(endLine).text.length),
|
||||
)
|
||||
|
||||
return {
|
||||
range: effectiveRange,
|
||||
text: document.getText(effectiveRange),
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting effective range:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private getFilePath(document: vscode.TextDocument): string {
|
||||
// Check cache first
|
||||
let filePath = this.filePathCache.get(document)
|
||||
if (filePath) {
|
||||
return filePath
|
||||
}
|
||||
|
||||
try {
|
||||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri)
|
||||
if (!workspaceFolder) {
|
||||
filePath = document.uri.fsPath
|
||||
} else {
|
||||
const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath)
|
||||
filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
this.filePathCache.set(document, filePath)
|
||||
return filePath
|
||||
} catch (error) {
|
||||
console.error("Error getting file path:", error)
|
||||
return document.uri.fsPath
|
||||
}
|
||||
}
|
||||
|
||||
private createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData {
|
||||
return {
|
||||
message: diagnostic.message,
|
||||
severity: diagnostic.severity,
|
||||
code: diagnostic.code,
|
||||
source: diagnostic.source,
|
||||
range: diagnostic.range, // Reuse the range object
|
||||
}
|
||||
}
|
||||
|
||||
private createAction(title: string, kind: vscode.CodeActionKind, command: string, args: any[]): vscode.CodeAction {
|
||||
const action = new vscode.CodeAction(title, kind)
|
||||
action.command = { command, title, arguments: args }
|
||||
|
|
@ -126,32 +40,20 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
|
|||
]
|
||||
}
|
||||
|
||||
private hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean {
|
||||
// Optimize range intersection check
|
||||
return !(
|
||||
range2.end.line < range1.start.line ||
|
||||
range2.start.line > range1.end.line ||
|
||||
(range2.end.line === range1.start.line && range2.end.character < range1.start.character) ||
|
||||
(range2.start.line === range1.end.line && range2.start.character > range1.end.character)
|
||||
)
|
||||
}
|
||||
|
||||
public provideCodeActions(
|
||||
document: vscode.TextDocument,
|
||||
range: vscode.Range | vscode.Selection,
|
||||
context: vscode.CodeActionContext,
|
||||
): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> {
|
||||
try {
|
||||
const effectiveRange = this.getEffectiveRange(document, range)
|
||||
const effectiveRange = EditorUtils.getEffectiveRange(document, range)
|
||||
if (!effectiveRange) {
|
||||
return []
|
||||
}
|
||||
|
||||
const filePath = this.getFilePath(document)
|
||||
const filePath = EditorUtils.getFilePath(document)
|
||||
const actions: vscode.CodeAction[] = []
|
||||
|
||||
// Create actions using helper method
|
||||
// Add explain actions
|
||||
actions.push(
|
||||
...this.createActionPair(ACTION_NAMES.EXPLAIN, vscode.CodeActionKind.QuickFix, COMMAND_IDS.EXPLAIN, [
|
||||
filePath,
|
||||
|
|
@ -159,14 +61,13 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
|
|||
]),
|
||||
)
|
||||
|
||||
// Only process diagnostics if they exist
|
||||
if (context.diagnostics.length > 0) {
|
||||
const relevantDiagnostics = context.diagnostics.filter((d) =>
|
||||
this.hasIntersectingRange(effectiveRange.range, d.range),
|
||||
EditorUtils.hasIntersectingRange(effectiveRange.range, d.range),
|
||||
)
|
||||
|
||||
if (relevantDiagnostics.length > 0) {
|
||||
const diagnosticMessages = relevantDiagnostics.map(this.createDiagnosticData)
|
||||
const diagnosticMessages = relevantDiagnostics.map(EditorUtils.createDiagnosticData)
|
||||
actions.push(
|
||||
...this.createActionPair(ACTION_NAMES.FIX, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [
|
||||
filePath,
|
||||
|
|
@ -175,9 +76,15 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
|
|||
]),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
actions.push(
|
||||
...this.createActionPair(ACTION_NAMES.FIX_LOGIC, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [
|
||||
filePath,
|
||||
effectiveRange.text,
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
// Add improve actions
|
||||
actions.push(
|
||||
...this.createActionPair(
|
||||
ACTION_NAMES.IMPROVE,
|
||||
|
|
@ -187,6 +94,15 @@ export class CodeActionProvider implements vscode.CodeActionProvider {
|
|||
),
|
||||
)
|
||||
|
||||
actions.push(
|
||||
this.createAction(
|
||||
ACTION_NAMES.ADD_TO_CONTEXT,
|
||||
vscode.CodeActionKind.QuickFix,
|
||||
COMMAND_IDS.ADD_TO_CONTEXT,
|
||||
[filePath, effectiveRange.text],
|
||||
),
|
||||
)
|
||||
|
||||
return actions
|
||||
} catch (error) {
|
||||
console.error("Error providing code actions:", error)
|
||||
|
|
|
|||
204
src/core/EditorUtils.ts
Normal file
204
src/core/EditorUtils.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
|
||||
/**
|
||||
* Represents an effective range in a document along with the corresponding text.
|
||||
*/
|
||||
export interface EffectiveRange {
|
||||
/** The range within the document. */
|
||||
range: vscode.Range
|
||||
/** The text within the specified range. */
|
||||
text: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents diagnostic information extracted from a VSCode diagnostic.
|
||||
*/
|
||||
export interface DiagnosticData {
|
||||
/** The diagnostic message. */
|
||||
message: string
|
||||
/** The severity level of the diagnostic. */
|
||||
severity: vscode.DiagnosticSeverity
|
||||
/**
|
||||
* Optional diagnostic code.
|
||||
* Can be a string, number, or an object with value and target.
|
||||
*/
|
||||
code?: string | number | { value: string | number; target: vscode.Uri }
|
||||
/** Optional source identifier for the diagnostic (e.g., the extension name). */
|
||||
source?: string
|
||||
/** The range within the document where the diagnostic applies. */
|
||||
range: vscode.Range
|
||||
}
|
||||
|
||||
/**
|
||||
* Contextual information for a VSCode text editor.
|
||||
*/
|
||||
export interface EditorContext {
|
||||
/** The file path of the current document. */
|
||||
filePath: string
|
||||
/** The effective text selected or derived from the document. */
|
||||
selectedText: string
|
||||
/** Optional list of diagnostics associated with the effective range. */
|
||||
diagnostics?: DiagnosticData[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class providing helper methods for working with VSCode editors and documents.
|
||||
*/
|
||||
export class EditorUtils {
|
||||
/** Cache mapping text documents to their computed file paths. */
|
||||
private static readonly filePathCache = new WeakMap<vscode.TextDocument, string>()
|
||||
|
||||
/**
|
||||
* Computes the effective range of text from the given document based on the user's selection.
|
||||
* If the selection is non-empty, returns that directly.
|
||||
* Otherwise, if the current line is non-empty, expands the range to include the adjacent lines.
|
||||
*
|
||||
* @param document - The text document to extract text from.
|
||||
* @param range - The user selected range or selection.
|
||||
* @returns An EffectiveRange object containing the effective range and its text, or null if no valid text is found.
|
||||
*/
|
||||
static getEffectiveRange(
|
||||
document: vscode.TextDocument,
|
||||
range: vscode.Range | vscode.Selection,
|
||||
): EffectiveRange | null {
|
||||
try {
|
||||
const selectedText = document.getText(range)
|
||||
if (selectedText) {
|
||||
return { range, text: selectedText }
|
||||
}
|
||||
|
||||
const currentLine = document.lineAt(range.start.line)
|
||||
if (!currentLine.text.trim()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const startLineIndex = Math.max(0, currentLine.lineNumber - 1)
|
||||
const endLineIndex = Math.min(document.lineCount - 1, currentLine.lineNumber + 1)
|
||||
|
||||
const effectiveRange = new vscode.Range(
|
||||
new vscode.Position(startLineIndex, 0),
|
||||
new vscode.Position(endLineIndex, document.lineAt(endLineIndex).text.length),
|
||||
)
|
||||
|
||||
return {
|
||||
range: effectiveRange,
|
||||
text: document.getText(effectiveRange),
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting effective range:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the file path of a given text document.
|
||||
* Utilizes an internal cache to avoid redundant computations.
|
||||
* If the document belongs to a workspace, attempts to compute a relative path; otherwise, returns the absolute fsPath.
|
||||
*
|
||||
* @param document - The text document for which to retrieve the file path.
|
||||
* @returns The file path as a string.
|
||||
*/
|
||||
static getFilePath(document: vscode.TextDocument): string {
|
||||
let filePath = this.filePathCache.get(document)
|
||||
if (filePath) {
|
||||
return filePath
|
||||
}
|
||||
|
||||
try {
|
||||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri)
|
||||
if (!workspaceFolder) {
|
||||
filePath = document.uri.fsPath
|
||||
} else {
|
||||
const relativePath = path.relative(workspaceFolder.uri.fsPath, document.uri.fsPath)
|
||||
filePath = !relativePath || relativePath.startsWith("..") ? document.uri.fsPath : relativePath
|
||||
}
|
||||
|
||||
this.filePathCache.set(document, filePath)
|
||||
return filePath
|
||||
} catch (error) {
|
||||
console.error("Error getting file path:", error)
|
||||
return document.uri.fsPath
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a VSCode Diagnostic object to a local DiagnosticData instance.
|
||||
*
|
||||
* @param diagnostic - The VSCode diagnostic to convert.
|
||||
* @returns The corresponding DiagnosticData object.
|
||||
*/
|
||||
static createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData {
|
||||
return {
|
||||
message: diagnostic.message,
|
||||
severity: diagnostic.severity,
|
||||
code: diagnostic.code,
|
||||
source: diagnostic.source,
|
||||
range: diagnostic.range,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether two VSCode ranges intersect.
|
||||
*
|
||||
* @param range1 - The first range.
|
||||
* @param range2 - The second range.
|
||||
* @returns True if the ranges intersect; otherwise, false.
|
||||
*/
|
||||
static hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean {
|
||||
if (
|
||||
range1.end.line < range2.start.line ||
|
||||
(range1.end.line === range2.start.line && range1.end.character <= range2.start.character)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
range2.end.line < range1.start.line ||
|
||||
(range2.end.line === range1.start.line && range2.end.character <= range1.start.character)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the editor context from the provided text editor or from the active text editor.
|
||||
* The context includes file path, effective selected text, and any diagnostics that intersect with the effective range.
|
||||
*
|
||||
* @param editor - (Optional) A specific text editor instance. If not provided, the active text editor is used.
|
||||
* @returns An EditorContext object if successful; otherwise, null.
|
||||
*/
|
||||
static getEditorContext(editor?: vscode.TextEditor): EditorContext | null {
|
||||
try {
|
||||
if (!editor) {
|
||||
editor = vscode.window.activeTextEditor
|
||||
}
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
const document = editor.document
|
||||
const selection = editor.selection
|
||||
const effectiveRange = this.getEffectiveRange(document, selection)
|
||||
|
||||
if (!effectiveRange) {
|
||||
return null
|
||||
}
|
||||
|
||||
const filePath = this.getFilePath(document)
|
||||
const diagnostics = vscode.languages
|
||||
.getDiagnostics(document.uri)
|
||||
.filter((d) => this.hasIntersectingRange(effectiveRange.range, d.range))
|
||||
.map(this.createDiagnosticData)
|
||||
|
||||
return {
|
||||
filePath,
|
||||
selectedText: effectiveRange.text,
|
||||
...(diagnostics.length > 0 ? { diagnostics } : {}),
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting editor context:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import * as vscode from "vscode"
|
||||
import { CodeActionProvider, ACTION_NAMES } from "../CodeActionProvider"
|
||||
import { EditorUtils } from "../EditorUtils"
|
||||
|
||||
// Mock VSCode API
|
||||
jest.mock("vscode", () => ({
|
||||
|
|
@ -16,13 +17,6 @@ jest.mock("vscode", () => ({
|
|||
start: { line: startLine, character: startChar },
|
||||
end: { line: endLine, character: endChar },
|
||||
})),
|
||||
Position: jest.fn().mockImplementation((line, character) => ({
|
||||
line,
|
||||
character,
|
||||
})),
|
||||
workspace: {
|
||||
getWorkspaceFolder: jest.fn(),
|
||||
},
|
||||
DiagnosticSeverity: {
|
||||
Error: 0,
|
||||
Warning: 1,
|
||||
|
|
@ -31,6 +25,16 @@ jest.mock("vscode", () => ({
|
|||
},
|
||||
}))
|
||||
|
||||
// Mock EditorUtils
|
||||
jest.mock("../EditorUtils", () => ({
|
||||
EditorUtils: {
|
||||
getEffectiveRange: jest.fn(),
|
||||
getFilePath: jest.fn(),
|
||||
hasIntersectingRange: jest.fn(),
|
||||
createDiagnosticData: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe("CodeActionProvider", () => {
|
||||
let provider: CodeActionProvider
|
||||
let mockDocument: any
|
||||
|
|
@ -55,68 +59,32 @@ describe("CodeActionProvider", () => {
|
|||
mockContext = {
|
||||
diagnostics: [],
|
||||
}
|
||||
})
|
||||
|
||||
describe("getEffectiveRange", () => {
|
||||
it("should return selected text when available", () => {
|
||||
mockDocument.getText.mockReturnValue("selected text")
|
||||
|
||||
const result = (provider as any).getEffectiveRange(mockDocument, mockRange)
|
||||
|
||||
expect(result).toEqual({
|
||||
range: mockRange,
|
||||
text: "selected text",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return null for empty line", () => {
|
||||
mockDocument.getText.mockReturnValue("")
|
||||
mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 })
|
||||
|
||||
const result = (provider as any).getEffectiveRange(mockDocument, mockRange)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getFilePath", () => {
|
||||
it("should return relative path when in workspace", () => {
|
||||
const mockWorkspaceFolder = {
|
||||
uri: { fsPath: "/test" },
|
||||
}
|
||||
;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder)
|
||||
|
||||
const result = (provider as any).getFilePath(mockDocument)
|
||||
|
||||
expect(result).toBe("file.ts")
|
||||
})
|
||||
|
||||
it("should return absolute path when not in workspace", () => {
|
||||
;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null)
|
||||
|
||||
const result = (provider as any).getFilePath(mockDocument)
|
||||
|
||||
expect(result).toBe("/test/file.ts")
|
||||
// Setup default EditorUtils mocks
|
||||
;(EditorUtils.getEffectiveRange as jest.Mock).mockReturnValue({
|
||||
range: mockRange,
|
||||
text: "test code",
|
||||
})
|
||||
;(EditorUtils.getFilePath as jest.Mock).mockReturnValue("/test/file.ts")
|
||||
;(EditorUtils.hasIntersectingRange as jest.Mock).mockReturnValue(true)
|
||||
;(EditorUtils.createDiagnosticData as jest.Mock).mockImplementation((d) => d)
|
||||
})
|
||||
|
||||
describe("provideCodeActions", () => {
|
||||
beforeEach(() => {
|
||||
mockDocument.getText.mockReturnValue("test code")
|
||||
mockDocument.lineAt.mockReturnValue({ text: "test code", lineNumber: 0 })
|
||||
})
|
||||
|
||||
it("should provide explain and improve actions by default", () => {
|
||||
it("should provide explain, improve, fix logic, and add to context actions by default", () => {
|
||||
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
|
||||
|
||||
expect(actions).toHaveLength(4)
|
||||
expect(actions).toHaveLength(7) // 2 explain + 2 fix logic + 2 improve + 1 add to context
|
||||
expect((actions as any)[0].title).toBe(`${ACTION_NAMES.EXPLAIN} in New Task`)
|
||||
expect((actions as any)[1].title).toBe(`${ACTION_NAMES.EXPLAIN} in Current Task`)
|
||||
expect((actions as any)[2].title).toBe(`${ACTION_NAMES.IMPROVE} in New Task`)
|
||||
expect((actions as any)[3].title).toBe(`${ACTION_NAMES.IMPROVE} in Current Task`)
|
||||
expect((actions as any)[2].title).toBe(`${ACTION_NAMES.FIX_LOGIC} in New Task`)
|
||||
expect((actions as any)[3].title).toBe(`${ACTION_NAMES.FIX_LOGIC} in Current Task`)
|
||||
expect((actions as any)[4].title).toBe(`${ACTION_NAMES.IMPROVE} in New Task`)
|
||||
expect((actions as any)[5].title).toBe(`${ACTION_NAMES.IMPROVE} in Current Task`)
|
||||
expect((actions as any)[6].title).toBe(ACTION_NAMES.ADD_TO_CONTEXT)
|
||||
})
|
||||
|
||||
it("should provide fix action when diagnostics exist", () => {
|
||||
it("should provide fix action instead of fix logic when diagnostics exist", () => {
|
||||
mockContext.diagnostics = [
|
||||
{
|
||||
message: "test error",
|
||||
|
|
@ -127,22 +95,33 @@ describe("CodeActionProvider", () => {
|
|||
|
||||
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
|
||||
|
||||
expect(actions).toHaveLength(6)
|
||||
expect(actions).toHaveLength(7) // 2 explain + 2 fix + 2 improve + 1 add to context
|
||||
expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX} in New Task`)).toBe(true)
|
||||
expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX} in Current Task`)).toBe(true)
|
||||
expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX_LOGIC} in New Task`)).toBe(false)
|
||||
expect((actions as any).some((a: any) => a.title === `${ACTION_NAMES.FIX_LOGIC} in Current Task`)).toBe(
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle errors gracefully", () => {
|
||||
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
|
||||
mockDocument.getText.mockImplementation(() => {
|
||||
throw new Error("Test error")
|
||||
})
|
||||
mockDocument.lineAt.mockReturnValue({ text: "test", lineNumber: 0 })
|
||||
it("should return empty array when no effective range", () => {
|
||||
;(EditorUtils.getEffectiveRange as jest.Mock).mockReturnValue(null)
|
||||
|
||||
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
|
||||
|
||||
expect(actions).toEqual([])
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Error getting effective range:", expect.any(Error))
|
||||
})
|
||||
|
||||
it("should handle errors gracefully", () => {
|
||||
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
|
||||
;(EditorUtils.getEffectiveRange as jest.Mock).mockImplementation(() => {
|
||||
throw new Error("Test error")
|
||||
})
|
||||
|
||||
const actions = provider.provideCodeActions(mockDocument, mockRange, mockContext)
|
||||
|
||||
expect(actions).toEqual([])
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Error providing code actions:", expect.any(Error))
|
||||
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
|
|
|||
142
src/core/__tests__/EditorUtils.test.ts
Normal file
142
src/core/__tests__/EditorUtils.test.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import * as vscode from "vscode"
|
||||
import { EditorUtils } from "../EditorUtils"
|
||||
|
||||
// Use simple classes to simulate VSCode's Range and Position behavior.
|
||||
jest.mock("vscode", () => {
|
||||
class MockPosition {
|
||||
constructor(
|
||||
public line: number,
|
||||
public character: number,
|
||||
) {}
|
||||
}
|
||||
class MockRange {
|
||||
start: MockPosition
|
||||
end: MockPosition
|
||||
constructor(start: MockPosition, end: MockPosition) {
|
||||
this.start = start
|
||||
this.end = end
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Range: MockRange,
|
||||
Position: MockPosition,
|
||||
workspace: {
|
||||
getWorkspaceFolder: jest.fn(),
|
||||
},
|
||||
window: { activeTextEditor: undefined },
|
||||
languages: {
|
||||
getDiagnostics: jest.fn(() => []),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe("EditorUtils", () => {
|
||||
let mockDocument: any
|
||||
|
||||
beforeEach(() => {
|
||||
mockDocument = {
|
||||
getText: jest.fn(),
|
||||
lineAt: jest.fn(),
|
||||
lineCount: 10,
|
||||
uri: { fsPath: "/test/file.ts" },
|
||||
}
|
||||
})
|
||||
|
||||
describe("getEffectiveRange", () => {
|
||||
it("should return selected text when available", () => {
|
||||
const mockRange = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10))
|
||||
mockDocument.getText.mockReturnValue("selected text")
|
||||
|
||||
const result = EditorUtils.getEffectiveRange(mockDocument, mockRange)
|
||||
|
||||
expect(result).toEqual({
|
||||
range: mockRange,
|
||||
text: "selected text",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return null for empty line", () => {
|
||||
const mockRange = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10))
|
||||
mockDocument.getText.mockReturnValue("")
|
||||
mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 })
|
||||
|
||||
const result = EditorUtils.getEffectiveRange(mockDocument, mockRange)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("should expand empty selection to full lines", () => {
|
||||
// Simulate a caret (empty selection) on line 2 at character 5.
|
||||
const initialRange = new vscode.Range(new vscode.Position(2, 5), new vscode.Position(2, 5))
|
||||
// Return non-empty text for any line with text (lines 1, 2, and 3).
|
||||
mockDocument.lineAt.mockImplementation((line: number) => {
|
||||
return { text: `Line ${line} text`, lineNumber: line }
|
||||
})
|
||||
mockDocument.getText.mockImplementation((range: any) => {
|
||||
// If the range is exactly the empty initial selection, return an empty string.
|
||||
if (
|
||||
range.start.line === initialRange.start.line &&
|
||||
range.start.character === initialRange.start.character &&
|
||||
range.end.line === initialRange.end.line &&
|
||||
range.end.character === initialRange.end.character
|
||||
) {
|
||||
return ""
|
||||
}
|
||||
return "expanded text"
|
||||
})
|
||||
|
||||
const result = EditorUtils.getEffectiveRange(mockDocument, initialRange)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
// Expected effective range: from the beginning of line 1 to the end of line 3.
|
||||
expect(result?.range.start).toEqual({ line: 1, character: 0 })
|
||||
expect(result?.range.end).toEqual({ line: 3, character: 11 })
|
||||
expect(result?.text).toBe("expanded text")
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasIntersectingRange", () => {
|
||||
it("should return false for ranges that only touch boundaries", () => {
|
||||
// Range1: [0, 0) - [0, 10) and Range2: [0, 10) - [0, 20)
|
||||
const range1 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10))
|
||||
const range2 = new vscode.Range(new vscode.Position(0, 10), new vscode.Position(0, 20))
|
||||
expect(EditorUtils.hasIntersectingRange(range1, range2)).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true for overlapping ranges", () => {
|
||||
// Range1: [0, 0) - [0, 15) and Range2: [0, 10) - [0, 20)
|
||||
const range1 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 15))
|
||||
const range2 = new vscode.Range(new vscode.Position(0, 10), new vscode.Position(0, 20))
|
||||
expect(EditorUtils.hasIntersectingRange(range1, range2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for non-overlapping ranges", () => {
|
||||
// Range1: [0, 0) - [0, 10) and Range2: [1, 0) - [1, 5)
|
||||
const range1 = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10))
|
||||
const range2 = new vscode.Range(new vscode.Position(1, 0), new vscode.Position(1, 5))
|
||||
expect(EditorUtils.hasIntersectingRange(range1, range2)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getFilePath", () => {
|
||||
it("should return relative path when in workspace", () => {
|
||||
const mockWorkspaceFolder = {
|
||||
uri: { fsPath: "/test" },
|
||||
}
|
||||
;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(mockWorkspaceFolder)
|
||||
|
||||
const result = EditorUtils.getFilePath(mockDocument)
|
||||
|
||||
expect(result).toBe("file.ts")
|
||||
})
|
||||
|
||||
it("should return absolute path when not in workspace", () => {
|
||||
;(vscode.workspace.getWorkspaceFolder as jest.Mock).mockReturnValue(null)
|
||||
|
||||
const result = EditorUtils.getFilePath(mockDocument)
|
||||
|
||||
expect(result).toBe("/test/file.ts")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -9,8 +9,8 @@ describe("mode-validator", () => {
|
|||
it("allows all code mode tools", () => {
|
||||
const mode = getModeConfig(codeMode)
|
||||
// Code mode has all groups
|
||||
Object.entries(TOOL_GROUPS).forEach(([_, tools]) => {
|
||||
tools.forEach((tool) => {
|
||||
Object.entries(TOOL_GROUPS).forEach(([_, config]) => {
|
||||
config.tools.forEach((tool: string) => {
|
||||
expect(isToolAllowedForMode(tool, codeMode, [])).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -25,7 +25,11 @@ describe("mode-validator", () => {
|
|||
it("allows configured tools", () => {
|
||||
const mode = getModeConfig(architectMode)
|
||||
// Architect mode has read, browser, and mcp groups
|
||||
const architectTools = [...TOOL_GROUPS.read, ...TOOL_GROUPS.browser, ...TOOL_GROUPS.mcp]
|
||||
const architectTools = [
|
||||
...TOOL_GROUPS.read.tools,
|
||||
...TOOL_GROUPS.browser.tools,
|
||||
...TOOL_GROUPS.mcp.tools,
|
||||
]
|
||||
architectTools.forEach((tool) => {
|
||||
expect(isToolAllowedForMode(tool, architectMode, [])).toBe(true)
|
||||
})
|
||||
|
|
@ -36,7 +40,7 @@ describe("mode-validator", () => {
|
|||
it("allows configured tools", () => {
|
||||
const mode = getModeConfig(askMode)
|
||||
// Ask mode has read, browser, and mcp groups
|
||||
const askTools = [...TOOL_GROUPS.read, ...TOOL_GROUPS.browser, ...TOOL_GROUPS.mcp]
|
||||
const askTools = [...TOOL_GROUPS.read.tools, ...TOOL_GROUPS.browser.tools, ...TOOL_GROUPS.mcp.tools]
|
||||
askTools.forEach((tool) => {
|
||||
expect(isToolAllowedForMode(tool, askMode, [])).toBe(true)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -65,10 +65,14 @@ jest.mock("os", () => ({
|
|||
homedir: () => "/home/user",
|
||||
}))
|
||||
|
||||
jest.mock("default-shell", () => "/bin/bash")
|
||||
jest.mock("default-shell", () => "/bin/zsh")
|
||||
|
||||
jest.mock("os-name", () => () => "Linux")
|
||||
|
||||
jest.mock("../../../utils/shell", () => ({
|
||||
getShell: () => "/bin/zsh",
|
||||
}))
|
||||
|
||||
// Create a mock ExtensionContext
|
||||
const mockContext = {
|
||||
extensionPath: "/mock/extension/path",
|
||||
|
|
@ -174,6 +178,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
|
|
@ -194,6 +199,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
|
|
@ -216,6 +222,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
|
|
@ -236,6 +243,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
|
|
@ -256,6 +264,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
|
|
@ -276,6 +285,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined, // preferredLanguage
|
||||
true, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toContain("apply_diff")
|
||||
|
|
@ -297,6 +307,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined, // preferredLanguage
|
||||
false, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).not.toContain("apply_diff")
|
||||
|
|
@ -318,6 +329,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).not.toContain("apply_diff")
|
||||
|
|
@ -339,6 +351,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
"Spanish", // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toContain("Language Preference:")
|
||||
|
|
@ -371,6 +384,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Role definition should be at the top
|
||||
|
|
@ -406,6 +420,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Role definition from promptComponent should be at the top
|
||||
|
|
@ -436,6 +451,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Should use the default mode's role definition
|
||||
|
|
@ -458,6 +474,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments, // experiments - undefined should disable all experimental tools
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Verify experimental tools are not included in the prompt
|
||||
|
|
@ -485,6 +502,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Verify experimental tools are included in the prompt when enabled
|
||||
|
|
@ -512,6 +530,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Verify only enabled experimental tools are included
|
||||
|
|
@ -539,6 +558,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined,
|
||||
true, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Verify base instruction lists all available tools
|
||||
|
|
@ -568,6 +588,7 @@ describe("SYSTEM_PROMPT", () => {
|
|||
undefined,
|
||||
true,
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Verify detailed instructions for each tool
|
||||
|
|
@ -623,6 +644,7 @@ describe("addCustomInstructions", () => {
|
|||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
|
|
@ -643,11 +665,60 @@ describe("addCustomInstructions", () => {
|
|||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should include MCP server creation info when enabled", async () => {
|
||||
const mockMcpHub = createMockMcpHub()
|
||||
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false, // supportsComputerUse
|
||||
mockMcpHub, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes,
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toContain("Creating an MCP Server")
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should exclude MCP server creation info when disabled", async () => {
|
||||
const mockMcpHub = createMockMcpHub()
|
||||
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false, // supportsComputerUse
|
||||
mockMcpHub, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes,
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
false, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).not.toContain("Creating an MCP Server")
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should prioritize mode-specific rules for code mode", async () => {
|
||||
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ export const formatResponse = {
|
|||
toolDeniedWithFeedback: (feedback?: string) =>
|
||||
`The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
|
||||
|
||||
toolApprovedWithFeedback: (feedback?: string) =>
|
||||
`The user approved this operation and provided the following context:\n<feedback>\n${feedback}\n</feedback>`,
|
||||
|
||||
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
|
||||
|
||||
noToolsUsed: () =>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
import { McpHub } from "../../../services/mcp/McpHub"
|
||||
|
||||
export async function getMcpServersSection(mcpHub?: McpHub, diffStrategy?: DiffStrategy): Promise<string> {
|
||||
export async function getMcpServersSection(
|
||||
mcpHub?: McpHub,
|
||||
diffStrategy?: DiffStrategy,
|
||||
enableMcpServerCreation?: boolean,
|
||||
): Promise<string> {
|
||||
if (!mcpHub) {
|
||||
return ""
|
||||
}
|
||||
|
|
@ -43,7 +47,7 @@ export async function getMcpServersSection(mcpHub?: McpHub, diffStrategy?: DiffS
|
|||
.join("\n\n")}`
|
||||
: "(No MCP servers currently connected)"
|
||||
|
||||
return `MCP SERVERS
|
||||
const baseSection = `MCP SERVERS
|
||||
|
||||
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
|
||||
|
||||
|
|
@ -51,7 +55,15 @@ The Model Context Protocol (MCP) enables communication between the system and lo
|
|||
|
||||
When a server is connected, you can use the server's tools via the \`use_mcp_tool\` tool, and access the server's resources via the \`access_mcp_resource\` tool.
|
||||
|
||||
${connectedServers}
|
||||
${connectedServers}`
|
||||
|
||||
if (!enableMcpServerCreation) {
|
||||
return baseSection
|
||||
}
|
||||
|
||||
return (
|
||||
baseSection +
|
||||
`
|
||||
|
||||
## Creating an MCP Server
|
||||
|
||||
|
|
@ -398,11 +410,11 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de
|
|||
## Editing MCP Servers
|
||||
|
||||
The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' above: ${
|
||||
mcpHub
|
||||
.getServers()
|
||||
.map((server) => server.name)
|
||||
.join(", ") || "(None running currently)"
|
||||
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file${diffStrategy ? " or apply_diff" : ""} to make changes to the files.
|
||||
mcpHub
|
||||
.getServers()
|
||||
.map((server) => server.name)
|
||||
.join(", ") || "(None running currently)"
|
||||
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use write_to_file${diffStrategy ? " or apply_diff" : ""} to make changes to the files.
|
||||
|
||||
However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server.
|
||||
|
||||
|
|
@ -411,4 +423,5 @@ However some MCP servers may be running from installed packages rather than a lo
|
|||
The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that...").
|
||||
|
||||
Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.`
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,13 +16,17 @@ MODES
|
|||
${modes.map((mode: ModeConfig) => ` * "${mode.name}" mode - ${mode.roleDefinition.split(".")[0]}`).join("\n")}
|
||||
Custom modes will be referred to by their configured name property.
|
||||
|
||||
- Custom modes can be configured by creating or editing the custom modes file at '${customModesPath}'. The following fields are required and must not be empty:
|
||||
- Custom modes can be configured by editing the custom modes file at '${customModesPath}'. The file gets created automatically on startup and should always exist. Make sure to read the latest contents before writing to it to avoid overwriting existing modes.
|
||||
|
||||
- The following fields are required and must not be empty:
|
||||
* slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better.
|
||||
* name: The display name for the mode
|
||||
* roleDefinition: A detailed description of the mode's role and capabilities
|
||||
* groups: Array of allowed tool groups (can be empty). Each group can be specified either as a string (e.g., "edit" to allow editing any file) or with file restrictions (e.g., ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }] to only allow editing markdown files)
|
||||
|
||||
The customInstructions field is optional.
|
||||
- The customInstructions field is optional.
|
||||
|
||||
- For multi-line text, include newline characters in the string like "This is the first line.\nThis is the next line.\n\nThis is a double line break."
|
||||
|
||||
The file should follow this structure:
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import defaultShell from "default-shell"
|
|||
import os from "os"
|
||||
import osName from "os-name"
|
||||
import { Mode, ModeConfig, getModeBySlug, defaultModeSlug, isToolAllowedForMode } from "../../../shared/modes"
|
||||
import { getShell } from "../../../utils/shell"
|
||||
|
||||
export function getSystemInfoSection(cwd: string, currentMode: Mode, customModes?: ModeConfig[]): string {
|
||||
const findModeBySlug = (slug: string, modes?: ModeConfig[]) => modes?.find((m) => m.slug === slug)
|
||||
|
|
@ -14,7 +15,7 @@ export function getSystemInfoSection(cwd: string, currentMode: Mode, customModes
|
|||
SYSTEM INFORMATION
|
||||
|
||||
Operating System: ${osName()}
|
||||
Default Shell: ${defaultShell}
|
||||
Default Shell: ${getShell()}
|
||||
Home Directory: ${os.homedir().toPosix()}
|
||||
Current Working Directory: ${cwd.toPosix()}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { ToolArgs } from "./types"
|
|||
|
||||
export function getExecuteCommandDescription(args: ToolArgs): string | undefined {
|
||||
return `## execute_command
|
||||
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${args.cwd}
|
||||
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${args.cwd}
|
||||
Parameters:
|
||||
- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
|
||||
Usage:
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export function getToolDescriptionsForMode(
|
|||
const groupName = getGroupName(groupEntry)
|
||||
const toolGroup = TOOL_GROUPS[groupName]
|
||||
if (toolGroup) {
|
||||
toolGroup.forEach((tool) => {
|
||||
toolGroup.tools.forEach((tool) => {
|
||||
if (isToolAllowedForMode(tool as ToolName, mode, customModes ?? [], experiments ?? {})) {
|
||||
tools.add(tool)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,15 +19,7 @@ import { findLast } from "../../shared/array"
|
|||
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
import { WebviewMessage } from "../../shared/WebviewMessage"
|
||||
import {
|
||||
Mode,
|
||||
modes,
|
||||
CustomModePrompts,
|
||||
PromptComponent,
|
||||
ModeConfig,
|
||||
defaultModeSlug,
|
||||
getModeBySlug,
|
||||
} from "../../shared/modes"
|
||||
import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug } from "../../shared/modes"
|
||||
import { SYSTEM_PROMPT } from "../prompts/system"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { Cline } from "../Cline"
|
||||
|
|
@ -37,7 +29,7 @@ import { getUri } from "./getUri"
|
|||
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
|
||||
import { checkExistKey } from "../../shared/checkExistApiConfig"
|
||||
import { singleCompletionHandler } from "../../utils/single-completion-handler"
|
||||
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
|
||||
import { searchCommits } from "../../utils/git"
|
||||
import { ConfigManager } from "../config/ConfigManager"
|
||||
import { CustomModesManager } from "../config/CustomModesManager"
|
||||
import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
|
||||
|
|
@ -110,8 +102,10 @@ type GlobalStateKey =
|
|||
| "writeDelayMs"
|
||||
| "terminalOutputLineLimit"
|
||||
| "mcpEnabled"
|
||||
| "enableMcpServerCreation"
|
||||
| "alwaysApproveResubmit"
|
||||
| "requestDelaySeconds"
|
||||
| "rateLimitSeconds"
|
||||
| "currentApiConfigName"
|
||||
| "listApiConfigMeta"
|
||||
| "vsCodeLmModelSelector"
|
||||
|
|
@ -141,6 +135,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
private static activeInstances: Set<ClineProvider> = new Set()
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private view?: vscode.WebviewView | vscode.WebviewPanel
|
||||
private isViewLaunched = false
|
||||
private cline?: Cline
|
||||
private workspaceTracker?: WorkspaceTracker
|
||||
mcpHub?: McpHub
|
||||
|
|
@ -240,6 +235,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
|
||||
const prompt = supportPrompt.create(promptType, params, customSupportPrompts)
|
||||
|
||||
if (command.endsWith("addToContext")) {
|
||||
await visibleProvider.postMessageToWebview({
|
||||
type: "invoke",
|
||||
invoke: "setChatBoxMessage",
|
||||
text: prompt,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (visibleProvider.cline && command.endsWith("InCurrentTask")) {
|
||||
await visibleProvider.postMessageToWebview({
|
||||
type: "invoke",
|
||||
|
|
@ -253,11 +258,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
await visibleProvider.initClineWithTask(prompt)
|
||||
}
|
||||
|
||||
resolveWebviewView(
|
||||
webviewView: vscode.WebviewView | vscode.WebviewPanel,
|
||||
//context: vscode.WebviewViewResolveContext<unknown>, used to recreate a deallocated webview, but we don't need this since we use retainContextWhenHidden
|
||||
//token: vscode.CancellationToken
|
||||
): void | Thenable<void> {
|
||||
async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
|
||||
this.outputChannel.appendLine("Resolving webview view")
|
||||
this.view = webviewView
|
||||
|
||||
|
|
@ -271,7 +272,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
enableScripts: true,
|
||||
localResourceRoots: [this.context.extensionUri],
|
||||
}
|
||||
webviewView.webview.html = this.getHtmlContent(webviewView.webview)
|
||||
|
||||
webviewView.webview.html =
|
||||
this.context.extensionMode === vscode.ExtensionMode.Development
|
||||
? await this.getHMRHtmlContent(webviewView.webview)
|
||||
: this.getHtmlContent(webviewView.webview)
|
||||
|
||||
// Sets up an event listener to listen for messages passed from the webview view context
|
||||
// and executes code based on the message that is recieved
|
||||
|
|
@ -395,6 +400,73 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
await this.view?.webview.postMessage(message)
|
||||
}
|
||||
|
||||
private async getHMRHtmlContent(webview: vscode.Webview): Promise<string> {
|
||||
const localPort = "5173"
|
||||
const localServerUrl = `localhost:${localPort}`
|
||||
|
||||
// Check if local dev server is running.
|
||||
try {
|
||||
await axios.get(`http://${localServerUrl}`)
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
"Local development server is not running, HMR will not work. Please run 'npm run dev' before launching the extension to enable HMR.",
|
||||
)
|
||||
|
||||
return this.getHtmlContent(webview)
|
||||
}
|
||||
|
||||
const nonce = getNonce()
|
||||
const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
|
||||
const codiconsUri = getUri(webview, this.context.extensionUri, [
|
||||
"node_modules",
|
||||
"@vscode",
|
||||
"codicons",
|
||||
"dist",
|
||||
"codicon.css",
|
||||
])
|
||||
|
||||
const file = "src/index.tsx"
|
||||
const scriptUri = `http://${localServerUrl}/${file}`
|
||||
|
||||
const reactRefresh = /*html*/ `
|
||||
<script nonce="${nonce}" type="module">
|
||||
import RefreshRuntime from "http://localhost:${localPort}/@react-refresh"
|
||||
RefreshRuntime.injectIntoGlobalHook(window)
|
||||
window.$RefreshReg$ = () => {}
|
||||
window.$RefreshSig$ = () => (type) => type
|
||||
window.__vite_plugin_react_preamble_installed__ = true
|
||||
</script>
|
||||
`
|
||||
|
||||
const csp = [
|
||||
"default-src 'none'",
|
||||
`font-src ${webview.cspSource}`,
|
||||
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
|
||||
`img-src ${webview.cspSource} data:`,
|
||||
`script-src 'unsafe-eval' https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
|
||||
`connect-src https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
|
||||
]
|
||||
|
||||
return /*html*/ `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
|
||||
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
|
||||
<link rel="stylesheet" type="text/css" href="${stylesUri}">
|
||||
<link href="${codiconsUri}" rel="stylesheet" />
|
||||
<title>Roo Code</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
${reactRefresh}
|
||||
<script type="module" src="${scriptUri}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines and returns the HTML that should be rendered within the webview panel.
|
||||
*
|
||||
|
|
@ -601,6 +673,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
),
|
||||
)
|
||||
|
||||
this.isViewLaunched = true
|
||||
break
|
||||
case "newTask":
|
||||
// Code that should run in response to the hello message command
|
||||
|
|
@ -799,6 +872,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
await this.updateGlobalState("mcpEnabled", mcpEnabled)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "enableMcpServerCreation":
|
||||
await this.updateGlobalState("enableMcpServerCreation", message.bool ?? true)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "playSound":
|
||||
if (message.audioType) {
|
||||
const soundPath = path.join(this.context.extensionPath, "audio", `${message.audioType}.wav`)
|
||||
|
|
@ -839,6 +916,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
await this.updateGlobalState("requestDelaySeconds", message.value ?? 5)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "rateLimitSeconds":
|
||||
await this.updateGlobalState("rateLimitSeconds", message.value ?? 0)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "preferredLanguage":
|
||||
await this.updateGlobalState("preferredLanguage", message.text)
|
||||
await this.postStateToWebview()
|
||||
|
|
@ -1083,6 +1164,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
mcpEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
experiments,
|
||||
enableMcpServerCreation,
|
||||
} = await this.getState()
|
||||
|
||||
// Create diffStrategy based on current model and settings
|
||||
|
|
@ -1111,6 +1193,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
preferredLanguage,
|
||||
diffEnabled,
|
||||
experiments,
|
||||
enableMcpServerCreation,
|
||||
)
|
||||
|
||||
await this.postMessageToWebview({
|
||||
|
|
@ -1169,6 +1252,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
try {
|
||||
const { oldName, newName } = message.values
|
||||
|
||||
if (oldName === newName) {
|
||||
break
|
||||
}
|
||||
|
||||
await this.configManager.saveConfig(newName, message.apiConfiguration)
|
||||
await this.configManager.deleteConfig(oldName)
|
||||
|
||||
|
|
@ -1996,8 +2083,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
terminalOutputLineLimit,
|
||||
fuzzyMatchThreshold,
|
||||
mcpEnabled,
|
||||
enableMcpServerCreation,
|
||||
alwaysApproveResubmit,
|
||||
requestDelaySeconds,
|
||||
rateLimitSeconds,
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
mode,
|
||||
|
|
@ -2037,8 +2126,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
|
||||
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
|
||||
mcpEnabled: mcpEnabled ?? true,
|
||||
enableMcpServerCreation: enableMcpServerCreation ?? true,
|
||||
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
|
||||
requestDelaySeconds: requestDelaySeconds ?? 10,
|
||||
rateLimitSeconds: rateLimitSeconds ?? 0,
|
||||
currentApiConfigName: currentApiConfigName ?? "default",
|
||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||
mode: mode ?? defaultModeSlug,
|
||||
|
|
@ -2160,8 +2251,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
screenshotQuality,
|
||||
terminalOutputLineLimit,
|
||||
mcpEnabled,
|
||||
enableMcpServerCreation,
|
||||
alwaysApproveResubmit,
|
||||
requestDelaySeconds,
|
||||
rateLimitSeconds,
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
vsCodeLmModelSelector,
|
||||
|
|
@ -2233,8 +2326,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
this.getGlobalState("screenshotQuality") as Promise<number | undefined>,
|
||||
this.getGlobalState("terminalOutputLineLimit") as Promise<number | undefined>,
|
||||
this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("enableMcpServerCreation") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
|
||||
this.getGlobalState("rateLimitSeconds") as Promise<number | undefined>,
|
||||
this.getGlobalState("currentApiConfigName") as Promise<string | undefined>,
|
||||
this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
|
||||
this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
|
||||
|
|
@ -2357,8 +2452,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
return langMap[vscodeLang.split("-")[0]] ?? "English"
|
||||
})(),
|
||||
mcpEnabled: mcpEnabled ?? true,
|
||||
enableMcpServerCreation: enableMcpServerCreation ?? true,
|
||||
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
|
||||
requestDelaySeconds: Math.max(5, requestDelaySeconds ?? 10),
|
||||
rateLimitSeconds: rateLimitSeconds ?? 0,
|
||||
currentApiConfigName: currentApiConfigName ?? "default",
|
||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||
modeApiConfigs: modeApiConfigs ?? ({} as Record<Mode, string>),
|
||||
|
|
@ -2416,7 +2513,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
|
||||
// secrets
|
||||
|
||||
private async storeSecret(key: SecretKey, value?: string) {
|
||||
public async storeSecret(key: SecretKey, value?: string) {
|
||||
if (value) {
|
||||
await this.context.secrets.store(key, value)
|
||||
} else {
|
||||
|
|
@ -2470,4 +2567,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
await this.postStateToWebview()
|
||||
await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
|
||||
}
|
||||
|
||||
// integration tests
|
||||
|
||||
get viewLaunched() {
|
||||
return this.isViewLaunched
|
||||
}
|
||||
|
||||
get messages() {
|
||||
return this.cline?.clineMessages || []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import { ClineProvider } from "../ClineProvider"
|
||||
// npx jest src/core/webview/__tests__/ClineProvider.test.ts
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import axios from "axios"
|
||||
|
||||
import { ClineProvider } from "../ClineProvider"
|
||||
import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
|
||||
import { setSoundEnabled } from "../../../utils/sound"
|
||||
import { defaultModeSlug, modes } from "../../../shared/modes"
|
||||
import { addCustomInstructions } from "../../prompts/sections/custom-instructions"
|
||||
import { experimentDefault, experiments } from "../../../shared/experiments"
|
||||
import { defaultModeSlug } from "../../../shared/modes"
|
||||
import { experimentDefault } from "../../../shared/experiments"
|
||||
|
||||
// Mock custom-instructions module
|
||||
const mockAddCustomInstructions = jest.fn()
|
||||
|
||||
jest.mock("../../prompts/sections/custom-instructions", () => ({
|
||||
addCustomInstructions: mockAddCustomInstructions,
|
||||
}))
|
||||
|
|
@ -108,6 +112,11 @@ jest.mock("vscode", () => ({
|
|||
uriScheme: "vscode",
|
||||
language: "en",
|
||||
},
|
||||
ExtensionMode: {
|
||||
Production: 1,
|
||||
Development: 2,
|
||||
Test: 3,
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock sound utility
|
||||
|
|
@ -197,7 +206,6 @@ describe("ClineProvider", () => {
|
|||
let mockOutputChannel: vscode.OutputChannel
|
||||
let mockWebviewView: vscode.WebviewView
|
||||
let mockPostMessage: jest.Mock
|
||||
let visibilityChangeCallback: (e?: unknown) => void
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
|
|
@ -265,13 +273,13 @@ describe("ClineProvider", () => {
|
|||
return { dispose: jest.fn() }
|
||||
}),
|
||||
onDidChangeVisibility: jest.fn().mockImplementation((callback) => {
|
||||
visibilityChangeCallback = callback
|
||||
return { dispose: jest.fn() }
|
||||
}),
|
||||
} as unknown as vscode.WebviewView
|
||||
|
||||
provider = new ClineProvider(mockContext, mockOutputChannel)
|
||||
// @ts-ignore - accessing private property for testing
|
||||
|
||||
// @ts-ignore - Accessing private property for testing.
|
||||
provider.customModesManager = mockCustomModesManager
|
||||
})
|
||||
|
||||
|
|
@ -283,18 +291,36 @@ describe("ClineProvider", () => {
|
|||
expect(ClineProvider.getVisibleInstance()).toBe(provider)
|
||||
})
|
||||
|
||||
test("resolveWebviewView sets up webview correctly", () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
test("resolveWebviewView sets up webview correctly", async () => {
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
|
||||
expect(mockWebviewView.webview.options).toEqual({
|
||||
enableScripts: true,
|
||||
localResourceRoots: [mockContext.extensionUri],
|
||||
})
|
||||
|
||||
expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")
|
||||
})
|
||||
|
||||
test("resolveWebviewView sets up webview correctly in development mode even if local server is not running", async () => {
|
||||
provider = new ClineProvider(
|
||||
{ ...mockContext, extensionMode: vscode.ExtensionMode.Development },
|
||||
mockOutputChannel,
|
||||
)
|
||||
;(axios.get as jest.Mock).mockRejectedValueOnce(new Error("Network error"))
|
||||
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
|
||||
expect(mockWebviewView.webview.options).toEqual({
|
||||
enableScripts: true,
|
||||
localResourceRoots: [mockContext.extensionUri],
|
||||
})
|
||||
|
||||
expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")
|
||||
})
|
||||
|
||||
test("postMessageToWebview sends message to webview", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
|
||||
const mockState: ExtensionState = {
|
||||
version: "1.0.0",
|
||||
|
|
@ -318,7 +344,9 @@ describe("ClineProvider", () => {
|
|||
browserViewportSize: "900x600",
|
||||
fuzzyMatchThreshold: 1.0,
|
||||
mcpEnabled: true,
|
||||
enableMcpServerCreation: false,
|
||||
requestDelaySeconds: 5,
|
||||
rateLimitSeconds: 0,
|
||||
mode: defaultModeSlug,
|
||||
customModes: [],
|
||||
experiments: experimentDefault,
|
||||
|
|
@ -334,7 +362,7 @@ describe("ClineProvider", () => {
|
|||
})
|
||||
|
||||
test("handles webviewDidLaunch message", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
|
||||
// Get the message handler from onDidReceiveMessage
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
|
@ -413,7 +441,7 @@ describe("ClineProvider", () => {
|
|||
})
|
||||
|
||||
test("handles writeDelayMs message", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
await messageHandler({ type: "writeDelayMs", value: 2000 })
|
||||
|
|
@ -423,7 +451,7 @@ describe("ClineProvider", () => {
|
|||
})
|
||||
|
||||
test("updates sound utility when sound setting changes", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
|
||||
// Get the message handler from onDidReceiveMessage
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
|
@ -463,7 +491,7 @@ describe("ClineProvider", () => {
|
|||
})
|
||||
|
||||
test("loads saved API config when switching modes", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock ConfigManager methods
|
||||
|
|
@ -484,7 +512,7 @@ describe("ClineProvider", () => {
|
|||
})
|
||||
|
||||
test("saves current config when switching to mode without config", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock ConfigManager methods
|
||||
|
|
@ -512,7 +540,7 @@ describe("ClineProvider", () => {
|
|||
})
|
||||
|
||||
test("saves config as default for current mode when loading config", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
provider.configManager = {
|
||||
|
|
@ -533,7 +561,7 @@ describe("ClineProvider", () => {
|
|||
})
|
||||
|
||||
test("handles request delay settings messages", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Test alwaysApproveResubmit
|
||||
|
|
@ -548,7 +576,7 @@ describe("ClineProvider", () => {
|
|||
})
|
||||
|
||||
test("handles updatePrompt message correctly", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock existing prompts
|
||||
|
|
@ -643,7 +671,7 @@ describe("ClineProvider", () => {
|
|||
)
|
||||
})
|
||||
test("handles mode-specific custom instructions updates", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock existing prompts
|
||||
|
|
@ -700,7 +728,7 @@ describe("ClineProvider", () => {
|
|||
|
||||
// Create new provider with updated mock context
|
||||
provider = new ClineProvider(mockContext, mockOutputChannel)
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
provider.configManager = {
|
||||
|
|
@ -725,10 +753,10 @@ describe("ClineProvider", () => {
|
|||
})
|
||||
|
||||
describe("deleteMessage", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
// Mock window.showInformationMessage
|
||||
;(vscode.window.showInformationMessage as jest.Mock) = jest.fn()
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
})
|
||||
|
||||
test('handles "Just this message" deletion correctly', async () => {
|
||||
|
|
@ -854,9 +882,9 @@ describe("ClineProvider", () => {
|
|||
})
|
||||
|
||||
describe("getSystemPrompt", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
mockPostMessage.mockClear()
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
// Reset and setup mock
|
||||
mockAddCustomInstructions.mockClear()
|
||||
mockAddCustomInstructions.mockImplementation(
|
||||
|
|
@ -889,6 +917,7 @@ describe("ClineProvider", () => {
|
|||
},
|
||||
},
|
||||
mcpEnabled: true,
|
||||
enableMcpServerCreation: false,
|
||||
mode: "code" as const,
|
||||
experiments: experimentDefault,
|
||||
} as any)
|
||||
|
|
@ -921,6 +950,7 @@ describe("ClineProvider", () => {
|
|||
},
|
||||
},
|
||||
mcpEnabled: false,
|
||||
enableMcpServerCreation: false,
|
||||
mode: "code" as const,
|
||||
experiments: experimentDefault,
|
||||
} as any)
|
||||
|
|
@ -985,6 +1015,7 @@ describe("ClineProvider", () => {
|
|||
},
|
||||
customModePrompts: {},
|
||||
mode: "code",
|
||||
enableMcpServerCreation: true,
|
||||
mcpEnabled: false,
|
||||
browserViewportSize: "900x600",
|
||||
experimentalDiffStrategy: true,
|
||||
|
|
@ -1019,6 +1050,7 @@ describe("ClineProvider", () => {
|
|||
undefined, // preferredLanguage
|
||||
true, // diffEnabled
|
||||
experimentDefault,
|
||||
true,
|
||||
)
|
||||
|
||||
// Run the test again to verify it's consistent
|
||||
|
|
@ -1042,6 +1074,7 @@ describe("ClineProvider", () => {
|
|||
diffEnabled: false,
|
||||
fuzzyMatchThreshold: 0.8,
|
||||
experiments: experimentDefault,
|
||||
enableMcpServerCreation: true,
|
||||
} as any)
|
||||
|
||||
// Mock SYSTEM_PROMPT to verify diffEnabled is passed as false
|
||||
|
|
@ -1070,6 +1103,7 @@ describe("ClineProvider", () => {
|
|||
undefined, // preferredLanguage
|
||||
false, // diffEnabled
|
||||
experimentDefault,
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -1084,6 +1118,7 @@ describe("ClineProvider", () => {
|
|||
architect: { customInstructions: "Architect mode instructions" },
|
||||
},
|
||||
mode: "architect",
|
||||
enableMcpServerCreation: false,
|
||||
mcpEnabled: false,
|
||||
browserViewportSize: "900x600",
|
||||
experiments: experimentDefault,
|
||||
|
|
@ -1097,7 +1132,7 @@ describe("ClineProvider", () => {
|
|||
})
|
||||
|
||||
// Resolve webview and trigger getSystemPrompt
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
const architectHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
await architectHandler({ type: "getSystemPrompt" })
|
||||
|
||||
|
|
@ -1111,9 +1146,9 @@ describe("ClineProvider", () => {
|
|||
})
|
||||
|
||||
describe("handleModeSwitch", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
// Set up webview for each test
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
})
|
||||
|
||||
test("loads saved API config when switching modes", async () => {
|
||||
|
|
@ -1174,7 +1209,7 @@ describe("ClineProvider", () => {
|
|||
|
||||
describe("updateCustomMode", () => {
|
||||
test("updates both file and state when updating custom mode", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
await provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock CustomModesManager methods
|
||||
|
|
@ -1238,4 +1273,129 @@ describe("ClineProvider", () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("upsertApiConfiguration", () => {
|
||||
test("handles error in upsertApiConfiguration gracefully", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock ConfigManager methods to simulate error
|
||||
provider.configManager = {
|
||||
setModeConfig: jest.fn().mockRejectedValue(new Error("Failed to update mode config")),
|
||||
listConfig: jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
|
||||
} as any
|
||||
|
||||
// Mock getState to provide necessary data
|
||||
jest.spyOn(provider, "getState").mockResolvedValue({
|
||||
mode: "code",
|
||||
currentApiConfigName: "test-config",
|
||||
} as any)
|
||||
|
||||
// Trigger updateApiConfiguration
|
||||
await messageHandler({
|
||||
type: "upsertApiConfiguration",
|
||||
text: "test-config",
|
||||
apiConfiguration: {
|
||||
apiProvider: "anthropic",
|
||||
apiKey: "test-key",
|
||||
},
|
||||
})
|
||||
|
||||
// Verify error was logged and user was notified
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Error create new api configuration"),
|
||||
)
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to create api configuration")
|
||||
})
|
||||
|
||||
test("handles successful upsertApiConfiguration", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock ConfigManager methods
|
||||
provider.configManager = {
|
||||
saveConfig: jest.fn().mockResolvedValue(undefined),
|
||||
listConfig: jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
|
||||
} as any
|
||||
|
||||
const testApiConfig = {
|
||||
apiProvider: "anthropic" as const,
|
||||
apiKey: "test-key",
|
||||
}
|
||||
|
||||
// Trigger upsertApiConfiguration
|
||||
await messageHandler({
|
||||
type: "upsertApiConfiguration",
|
||||
text: "test-config",
|
||||
apiConfiguration: testApiConfig,
|
||||
})
|
||||
|
||||
// Verify config was saved
|
||||
expect(provider.configManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
|
||||
|
||||
// Verify state updates
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
|
||||
{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
|
||||
])
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
|
||||
|
||||
// Verify state was posted to webview
|
||||
expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
|
||||
})
|
||||
|
||||
test("handles buildApiHandler error in updateApiConfiguration", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock buildApiHandler to throw an error
|
||||
const { buildApiHandler } = require("../../../api")
|
||||
;(buildApiHandler as jest.Mock).mockImplementationOnce(() => {
|
||||
throw new Error("API handler error")
|
||||
})
|
||||
|
||||
// Mock ConfigManager methods
|
||||
provider.configManager = {
|
||||
saveConfig: jest.fn().mockResolvedValue(undefined),
|
||||
listConfig: jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
|
||||
} as any
|
||||
|
||||
// Setup mock Cline instance
|
||||
const mockCline = {
|
||||
api: undefined,
|
||||
abortTask: jest.fn(),
|
||||
}
|
||||
// @ts-ignore - accessing private property for testing
|
||||
provider.cline = mockCline
|
||||
|
||||
const testApiConfig = {
|
||||
apiProvider: "anthropic" as const,
|
||||
apiKey: "test-key",
|
||||
}
|
||||
|
||||
// Trigger upsertApiConfiguration
|
||||
await messageHandler({
|
||||
type: "upsertApiConfiguration",
|
||||
text: "test-config",
|
||||
apiConfiguration: testApiConfig,
|
||||
})
|
||||
|
||||
// Verify error handling
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Error create new api configuration"),
|
||||
)
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to create api configuration")
|
||||
|
||||
// Verify state was still updated
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
|
||||
{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
|
||||
])
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
234
src/extension.ts
234
src/extension.ts
|
|
@ -1,36 +1,33 @@
|
|||
// The module 'vscode' contains the VS Code extensibility API
|
||||
// Import the module and reference it with the alias vscode in your code below
|
||||
import delay from "delay"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
import { ClineProvider } from "./core/webview/ClineProvider"
|
||||
import { createClineAPI } from "./exports"
|
||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
import { ACTION_NAMES, CodeActionProvider } from "./core/CodeActionProvider"
|
||||
import "./utils/path" // Necessary to have access to String.prototype.toPosix.
|
||||
import { CodeActionProvider } from "./core/CodeActionProvider"
|
||||
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
|
||||
import { handleUri, registerCommands, registerCodeActions } from "./activate"
|
||||
|
||||
/*
|
||||
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
|
||||
Inspired by
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/default/weather-webview
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/frameworks/hello-world-react-cra
|
||||
|
||||
*/
|
||||
/**
|
||||
* Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
*
|
||||
* Inspired by:
|
||||
* - https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/default/weather-webview
|
||||
* - https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/frameworks/hello-world-react-cra
|
||||
*/
|
||||
|
||||
let outputChannel: vscode.OutputChannel
|
||||
|
||||
// This method is called when your extension is activated
|
||||
// Your extension is activated the very first time the command is executed
|
||||
// This method is called when your extension is activated.
|
||||
// Your extension is activated the very first time the command is executed.
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
outputChannel = vscode.window.createOutputChannel("Roo-Code")
|
||||
context.subscriptions.push(outputChannel)
|
||||
|
||||
outputChannel.appendLine("Roo-Code extension activated")
|
||||
|
||||
// Get default commands from configuration
|
||||
// Get default commands from configuration.
|
||||
const defaultCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
|
||||
|
||||
// Initialize global state if not already set
|
||||
// Initialize global state if not already set.
|
||||
if (!context.globalState.get("allowedCommands")) {
|
||||
context.globalState.update("allowedCommands", defaultCommands)
|
||||
}
|
||||
|
|
@ -43,208 +40,49 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("roo-cline.plusButtonClicked", async () => {
|
||||
outputChannel.appendLine("Plus button Clicked")
|
||||
await sidebarProvider.clearTask()
|
||||
await sidebarProvider.postStateToWebview()
|
||||
await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
|
||||
}),
|
||||
)
|
||||
registerCommands({ context, outputChannel, provider: sidebarProvider })
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("roo-cline.mcpButtonClicked", () => {
|
||||
sidebarProvider.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("roo-cline.promptsButtonClicked", () => {
|
||||
sidebarProvider.postMessageToWebview({ type: "action", action: "promptsButtonClicked" })
|
||||
}),
|
||||
)
|
||||
|
||||
const openClineInNewTab = async () => {
|
||||
outputChannel.appendLine("Opening Roo Code in new tab")
|
||||
// (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event)
|
||||
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
|
||||
const tabProvider = new ClineProvider(context, outputChannel)
|
||||
//const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined
|
||||
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
|
||||
|
||||
// Check if there are any visible text editors, otherwise open a new group to the right
|
||||
const hasVisibleEditors = vscode.window.visibleTextEditors.length > 0
|
||||
if (!hasVisibleEditors) {
|
||||
await vscode.commands.executeCommand("workbench.action.newGroupRight")
|
||||
}
|
||||
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
|
||||
|
||||
const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Roo Code", targetCol, {
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [context.extensionUri],
|
||||
})
|
||||
// TODO: use better svg icon with light and dark variants (see https://stackoverflow.com/questions/58365687/vscode-extension-iconpath)
|
||||
|
||||
panel.iconPath = {
|
||||
light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
|
||||
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
|
||||
}
|
||||
tabProvider.resolveWebviewView(panel)
|
||||
|
||||
// Lock the editor group so clicking on files doesn't open them over the panel
|
||||
await delay(100)
|
||||
await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
|
||||
}
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand("roo-cline.popoutButtonClicked", openClineInNewTab))
|
||||
context.subscriptions.push(vscode.commands.registerCommand("roo-cline.openInNewTab", openClineInNewTab))
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("roo-cline.settingsButtonClicked", () => {
|
||||
//vscode.window.showInformationMessage(message)
|
||||
sidebarProvider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" })
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("roo-cline.historyButtonClicked", () => {
|
||||
sidebarProvider.postMessageToWebview({ type: "action", action: "historyButtonClicked" })
|
||||
}),
|
||||
)
|
||||
|
||||
/*
|
||||
We use the text document content provider API to show the left side for diff view by creating a virtual document for the original content. This makes it readonly so users know to edit the right side if they want to keep their changes.
|
||||
|
||||
- This API allows you to create readonly documents in VSCode from arbitrary sources, and works by claiming an uri-scheme for which your provider then returns text contents. The scheme must be provided when registering a provider and cannot change afterwards.
|
||||
- Note how the provider doesn't create uris for virtual documents - its role is to provide contents given such an uri. In return, content providers are wired into the open document logic so that providers are always considered.
|
||||
https://code.visualstudio.com/api/extension-guides/virtual-documents
|
||||
*/
|
||||
/**
|
||||
* We use the text document content provider API to show the left side for diff
|
||||
* view by creating a virtual document for the original content. This makes it
|
||||
* readonly so users know to edit the right side if they want to keep their changes.
|
||||
*
|
||||
* This API allows you to create readonly documents in VSCode from arbitrary
|
||||
* sources, and works by claiming an uri-scheme for which your provider then
|
||||
* returns text contents. The scheme must be provided when registering a
|
||||
* provider and cannot change afterwards.
|
||||
*
|
||||
* Note how the provider doesn't create uris for virtual documents - its role
|
||||
* is to provide contents given such an uri. In return, content providers are
|
||||
* wired into the open document logic so that providers are always considered.
|
||||
*
|
||||
* https://code.visualstudio.com/api/extension-guides/virtual-documents
|
||||
*/
|
||||
const diffContentProvider = new (class implements vscode.TextDocumentContentProvider {
|
||||
provideTextDocumentContent(uri: vscode.Uri): string {
|
||||
return Buffer.from(uri.query, "base64").toString("utf-8")
|
||||
}
|
||||
})()
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.registerTextDocumentContentProvider(DIFF_VIEW_URI_SCHEME, diffContentProvider),
|
||||
)
|
||||
|
||||
// URI Handler
|
||||
const handleUri = async (uri: vscode.Uri) => {
|
||||
const path = uri.path
|
||||
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
|
||||
const visibleProvider = ClineProvider.getVisibleInstance()
|
||||
if (!visibleProvider) {
|
||||
return
|
||||
}
|
||||
switch (path) {
|
||||
case "/glama": {
|
||||
const code = query.get("code")
|
||||
if (code) {
|
||||
await visibleProvider.handleGlamaCallback(code)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "/openrouter": {
|
||||
const code = query.get("code")
|
||||
if (code) {
|
||||
await visibleProvider.handleOpenRouterCallback(code)
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
|
||||
|
||||
// Register code actions provider
|
||||
// Register code actions provider.
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeActionsProvider({ pattern: "**/*" }, new CodeActionProvider(), {
|
||||
providedCodeActionKinds: CodeActionProvider.providedCodeActionKinds,
|
||||
}),
|
||||
)
|
||||
|
||||
// Helper function to handle code actions
|
||||
const registerCodeAction = (
|
||||
context: vscode.ExtensionContext,
|
||||
command: string,
|
||||
promptType: keyof typeof ACTION_NAMES,
|
||||
inNewTask: boolean,
|
||||
inputPrompt?: string,
|
||||
inputPlaceholder?: string,
|
||||
) => {
|
||||
let userInput: string | undefined
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
command,
|
||||
async (filePath: string, selectedText: string, diagnostics?: any[]) => {
|
||||
if (inputPrompt) {
|
||||
userInput = await vscode.window.showInputBox({
|
||||
prompt: inputPrompt,
|
||||
placeHolder: inputPlaceholder,
|
||||
})
|
||||
}
|
||||
|
||||
const params = {
|
||||
filePath,
|
||||
selectedText,
|
||||
...(diagnostics ? { diagnostics } : {}),
|
||||
...(userInput ? { userInput } : {}),
|
||||
}
|
||||
|
||||
await ClineProvider.handleCodeAction(command, promptType, params)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to register both versions of a code action
|
||||
const registerCodeActionPair = (
|
||||
context: vscode.ExtensionContext,
|
||||
baseCommand: string,
|
||||
promptType: keyof typeof ACTION_NAMES,
|
||||
inputPrompt?: string,
|
||||
inputPlaceholder?: string,
|
||||
) => {
|
||||
// Register new task version
|
||||
registerCodeAction(context, baseCommand, promptType, true, inputPrompt, inputPlaceholder)
|
||||
|
||||
// Register current task version
|
||||
registerCodeAction(context, `${baseCommand}InCurrentTask`, promptType, false, inputPrompt, inputPlaceholder)
|
||||
}
|
||||
|
||||
// Register code action commands
|
||||
registerCodeActionPair(
|
||||
context,
|
||||
"roo-cline.explainCode",
|
||||
"EXPLAIN",
|
||||
"What would you like Roo to explain?",
|
||||
"E.g. How does the error handling work?",
|
||||
)
|
||||
|
||||
registerCodeActionPair(
|
||||
context,
|
||||
"roo-cline.fixCode",
|
||||
"FIX",
|
||||
"What would you like Roo to fix?",
|
||||
"E.g. Maintain backward compatibility",
|
||||
)
|
||||
|
||||
registerCodeActionPair(
|
||||
context,
|
||||
"roo-cline.improveCode",
|
||||
"IMPROVE",
|
||||
"What would you like Roo to improve?",
|
||||
"E.g. Focus on performance optimization",
|
||||
)
|
||||
registerCodeActions(context)
|
||||
|
||||
return createClineAPI(outputChannel, sidebarProvider)
|
||||
}
|
||||
|
||||
// This method is called when your extension is deactivated
|
||||
// This method is called when your extension is deactivated.
|
||||
export function deactivate() {
|
||||
outputChannel.appendLine("Roo-Code extension deactivated")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -52,13 +52,18 @@ export interface ExtensionMessage {
|
|||
| "historyButtonClicked"
|
||||
| "promptsButtonClicked"
|
||||
| "didBecomeVisible"
|
||||
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
||||
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage"
|
||||
state?: ExtensionState
|
||||
images?: string[]
|
||||
ollamaModels?: string[]
|
||||
lmStudioModels?: string[]
|
||||
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
|
||||
filePaths?: string[]
|
||||
openedTabs?: Array<{
|
||||
label: string
|
||||
isActive: boolean
|
||||
path?: string
|
||||
}>
|
||||
partialMessage?: ClineMessage
|
||||
glamaModels?: Record<string, ModelInfo>
|
||||
openRouterModels?: Record<string, ModelInfo>
|
||||
|
|
@ -97,6 +102,7 @@ export interface ExtensionState {
|
|||
alwaysApproveResubmit?: boolean
|
||||
alwaysAllowModeSwitch?: boolean
|
||||
requestDelaySeconds: number
|
||||
rateLimitSeconds: number // Minimum time between successive requests (0 = disabled)
|
||||
uriScheme?: string
|
||||
allowedCommands?: string[]
|
||||
soundEnabled?: boolean
|
||||
|
|
@ -109,6 +115,7 @@ export interface ExtensionState {
|
|||
writeDelayMs: number
|
||||
terminalOutputLineLimit?: number
|
||||
mcpEnabled: boolean
|
||||
enableMcpServerCreation: boolean
|
||||
mode: Mode
|
||||
modeApiConfigs?: Record<Mode, string>
|
||||
enhancementApiConfigId?: string
|
||||
|
|
|
|||
|
|
@ -63,10 +63,12 @@ export interface WebviewMessage {
|
|||
| "deleteMessage"
|
||||
| "terminalOutputLineLimit"
|
||||
| "mcpEnabled"
|
||||
| "enableMcpServerCreation"
|
||||
| "searchCommits"
|
||||
| "refreshGlamaModels"
|
||||
| "alwaysApproveResubmit"
|
||||
| "requestDelaySeconds"
|
||||
| "rateLimitSeconds"
|
||||
| "setApiConfigPassword"
|
||||
| "requestVsCodeLmModels"
|
||||
| "mode"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { isToolAllowedForMode, FileRestrictionError, ModeConfig } from "../modes"
|
||||
import { isToolAllowedForMode, FileRestrictionError, ModeConfig, parseSlashCommand } from "../modes"
|
||||
|
||||
describe("isToolAllowedForMode", () => {
|
||||
const customModes: ModeConfig[] = [
|
||||
|
|
@ -332,3 +332,65 @@ describe("FileRestrictionError", () => {
|
|||
expect(error.name).toBe("FileRestrictionError")
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseSlashCommand", () => {
|
||||
const customModes: ModeConfig[] = [
|
||||
{
|
||||
slug: "custom-mode",
|
||||
name: "Custom Mode",
|
||||
roleDefinition: "Custom role",
|
||||
groups: ["read"],
|
||||
},
|
||||
]
|
||||
|
||||
it("returns null for non-slash messages", () => {
|
||||
expect(parseSlashCommand("hello world")).toBeNull()
|
||||
expect(parseSlashCommand("code help me")).toBeNull()
|
||||
})
|
||||
|
||||
it("returns null for incomplete commands", () => {
|
||||
expect(parseSlashCommand("/")).toBeNull()
|
||||
expect(parseSlashCommand("/code")).toBeNull()
|
||||
expect(parseSlashCommand("/code ")).toBeNull()
|
||||
})
|
||||
|
||||
it("returns null for invalid mode slugs", () => {
|
||||
expect(parseSlashCommand("/invalid help me")).toBeNull()
|
||||
expect(parseSlashCommand("/nonexistent do something")).toBeNull()
|
||||
})
|
||||
|
||||
it("successfully parses valid commands", () => {
|
||||
expect(parseSlashCommand("/code help me write tests")).toEqual({
|
||||
modeSlug: "code",
|
||||
remainingMessage: "help me write tests",
|
||||
})
|
||||
|
||||
expect(parseSlashCommand("/ask what is typescript?")).toEqual({
|
||||
modeSlug: "ask",
|
||||
remainingMessage: "what is typescript?",
|
||||
})
|
||||
|
||||
expect(parseSlashCommand("/architect plan this feature")).toEqual({
|
||||
modeSlug: "architect",
|
||||
remainingMessage: "plan this feature",
|
||||
})
|
||||
})
|
||||
|
||||
it("preserves whitespace in remaining message", () => {
|
||||
expect(parseSlashCommand("/code help me write tests ")).toEqual({
|
||||
modeSlug: "code",
|
||||
remainingMessage: "help me write tests",
|
||||
})
|
||||
})
|
||||
|
||||
it("handles custom modes", () => {
|
||||
expect(parseSlashCommand("/custom-mode do something", customModes)).toEqual({
|
||||
modeSlug: "custom-mode",
|
||||
remainingMessage: "do something",
|
||||
})
|
||||
})
|
||||
|
||||
it("returns null for invalid custom mode slugs", () => {
|
||||
expect(parseSlashCommand("/invalid-custom do something", customModes)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ export interface ModelInfo {
|
|||
cacheWritesPrice?: number
|
||||
cacheReadsPrice?: number
|
||||
description?: string
|
||||
reasoningEffort?: "low" | "medium" | "high"
|
||||
}
|
||||
|
||||
// Anthropic
|
||||
|
|
@ -511,6 +512,33 @@ export type OpenAiNativeModelId = keyof typeof openAiNativeModels
|
|||
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4o"
|
||||
export const openAiNativeModels = {
|
||||
// don't support tool use yet
|
||||
"o3-mini": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.1,
|
||||
outputPrice: 4.4,
|
||||
reasoningEffort: "medium",
|
||||
},
|
||||
"o3-mini-high": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.1,
|
||||
outputPrice: 4.4,
|
||||
reasoningEffort: "high",
|
||||
},
|
||||
"o3-mini-low": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.1,
|
||||
outputPrice: 4.4,
|
||||
reasoningEffort: "low",
|
||||
},
|
||||
o1: {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
|
|
@ -532,8 +560,8 @@ export const openAiNativeModels = {
|
|||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3,
|
||||
outputPrice: 12,
|
||||
inputPrice: 1.1,
|
||||
outputPrice: 4.4,
|
||||
},
|
||||
"gpt-4o": {
|
||||
maxTokens: 4_096,
|
||||
|
|
@ -565,7 +593,8 @@ export const deepSeekModels = {
|
|||
supportsPromptCache: false,
|
||||
inputPrice: 0.014, // $0.014 per million tokens
|
||||
outputPrice: 0.28, // $0.28 per million tokens
|
||||
description: "DeepSeek-V3 achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally.",
|
||||
description:
|
||||
"DeepSeek-V3 achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally.",
|
||||
},
|
||||
"deepseek-reasoner": {
|
||||
maxTokens: 8192,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ interface ApiMetrics {
|
|||
totalCacheWrites?: number
|
||||
totalCacheReads?: number
|
||||
totalCost: number
|
||||
contextTokens: number // Total tokens in conversation (last message's tokensIn + tokensOut)
|
||||
contextTokens: number // Total tokens in conversation (last message's tokensIn + tokensOut + cacheWrites + cacheReads)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,7 +17,7 @@ interface ApiMetrics {
|
|||
* It extracts and sums up the tokensIn, tokensOut, cacheWrites, cacheReads, and cost from these messages.
|
||||
*
|
||||
* @param messages - An array of ClineMessage objects to process.
|
||||
* @returns An ApiMetrics object containing totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, and totalCost.
|
||||
* @returns An ApiMetrics object containing totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, totalCost, and contextTokens.
|
||||
*
|
||||
* @example
|
||||
* const messages = [
|
||||
|
|
@ -36,27 +36,30 @@ export function getApiMetrics(messages: ClineMessage[]): ApiMetrics {
|
|||
contextTokens: 0,
|
||||
}
|
||||
|
||||
// Find the last api_req_started message that has valid token information
|
||||
// Helper function to get total tokens from a message
|
||||
const getTotalTokensFromMessage = (message: ClineMessage): number => {
|
||||
if (!message.text) return 0
|
||||
try {
|
||||
const { tokensIn, tokensOut, cacheWrites, cacheReads } = JSON.parse(message.text)
|
||||
return (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Find the last api_req_started message that has any tokens
|
||||
const lastApiReq = [...messages].reverse().find((message) => {
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
try {
|
||||
const parsedData = JSON.parse(message.text)
|
||||
return typeof parsedData.tokensIn === "number" && typeof parsedData.tokensOut === "number"
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
if (message.type === "say" && message.say === "api_req_started") {
|
||||
return getTotalTokensFromMessage(message) > 0
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Keep track of the last valid context tokens
|
||||
let lastValidContextTokens = 0
|
||||
|
||||
// Calculate running totals
|
||||
messages.forEach((message) => {
|
||||
if (message.type === "say" && message.say === "api_req_started" && message.text) {
|
||||
try {
|
||||
const parsedData = JSON.parse(message.text)
|
||||
const { tokensIn, tokensOut, cacheWrites, cacheReads, cost } = parsedData
|
||||
const { tokensIn, tokensOut, cacheWrites, cacheReads, cost } = JSON.parse(message.text)
|
||||
|
||||
if (typeof tokensIn === "number") {
|
||||
result.totalTokensIn += tokensIn
|
||||
|
|
@ -74,15 +77,9 @@ export function getApiMetrics(messages: ClineMessage[]): ApiMetrics {
|
|||
result.totalCost += cost
|
||||
}
|
||||
|
||||
// Update last valid context tokens whenever we have valid input and output tokens
|
||||
if (tokensIn > 0 && tokensOut > 0) {
|
||||
lastValidContextTokens = tokensIn + tokensOut
|
||||
}
|
||||
|
||||
// If this is the last api request, use its tokens for context size
|
||||
// If this is the last api request with tokens, use its total for context size
|
||||
if (message === lastApiReq) {
|
||||
// Use the last valid context tokens if the current request doesn't have valid tokens
|
||||
result.contextTokens = tokensIn > 0 && tokensOut > 0 ? tokensIn + tokensOut : lastValidContextTokens
|
||||
result.contextTokens = getTotalTokensFromMessage(message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing JSON:", error)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ export function getToolsForMode(groups: readonly GroupEntry[]): string[] {
|
|||
// Add tools from each group
|
||||
groups.forEach((group) => {
|
||||
const groupName = getGroupName(group)
|
||||
TOOL_GROUPS[groupName].forEach((tool) => tools.add(tool))
|
||||
const groupConfig = TOOL_GROUPS[groupName]
|
||||
groupConfig.tools.forEach((tool: string) => tools.add(tool))
|
||||
})
|
||||
|
||||
// Always add required tools
|
||||
|
|
@ -81,15 +82,19 @@ export const modes: readonly ModeConfig[] = [
|
|||
slug: "architect",
|
||||
name: "Architect",
|
||||
roleDefinition:
|
||||
"You are Roo, a software architecture expert specializing in analyzing codebases, identifying patterns, and providing high-level technical guidance. You excel at understanding complex systems, evaluating architectural decisions, and suggesting improvements. You can edit markdown documentation files to help document architectural decisions and patterns.",
|
||||
"You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution.",
|
||||
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"],
|
||||
customInstructions:
|
||||
"Depending on the user's request, you may need to do some information gathering (for example using read_file or search_files) to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. (You can write the plan to a markdown file if it seems appropriate.)\n\nThen you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it. Finally once it seems like you've reached a good plan, use the switch_mode tool to request that the user switch to another mode to implement the solution.",
|
||||
},
|
||||
{
|
||||
slug: "ask",
|
||||
name: "Ask",
|
||||
roleDefinition:
|
||||
"You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics. You can analyze code, explain concepts, and access external resources. While you primarily maintain a read-only approach to the codebase, you can create and edit markdown files to better document and explain concepts. Make sure to answer the user's questions and don't rush to switch to implementing code.",
|
||||
"You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics.",
|
||||
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"],
|
||||
customInstructions:
|
||||
"You can analyze code, explain concepts, and access external resources. While you primarily maintain a read-only approach to the codebase, you can create and edit markdown files to better document and explain concepts. Make sure to answer the user's questions and don't rush to switch to implementing code.",
|
||||
},
|
||||
] as const
|
||||
|
||||
|
|
@ -190,8 +195,10 @@ export function isToolAllowedForMode(
|
|||
const groupName = getGroupName(group)
|
||||
const options = getGroupOptions(group)
|
||||
|
||||
// If the tool isn't in this group, continue to next group
|
||||
if (!TOOL_GROUPS[groupName].includes(tool)) {
|
||||
const groupConfig = TOOL_GROUPS[groupName]
|
||||
|
||||
// If the tool isn't in this group's tools, continue to next group
|
||||
if (!groupConfig.tools.includes(tool)) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +227,15 @@ export function isToolAllowedForMode(
|
|||
|
||||
// Create the mode-specific default prompts
|
||||
export const defaultPrompts: Readonly<CustomModePrompts> = Object.freeze(
|
||||
Object.fromEntries(modes.map((mode) => [mode.slug, { roleDefinition: mode.roleDefinition }])),
|
||||
Object.fromEntries(
|
||||
modes.map((mode) => [
|
||||
mode.slug,
|
||||
{
|
||||
roleDefinition: mode.roleDefinition,
|
||||
customInstructions: mode.customInstructions,
|
||||
},
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
// Helper function to safely get role definition
|
||||
|
|
@ -232,3 +247,46 @@ export function getRoleDefinition(modeSlug: string, customModes?: ModeConfig[]):
|
|||
}
|
||||
return mode.roleDefinition
|
||||
}
|
||||
|
||||
// Helper function to safely get custom instructions
|
||||
export function getCustomInstructions(modeSlug: string, customModes?: ModeConfig[]): string {
|
||||
const mode = getModeBySlug(modeSlug, customModes)
|
||||
if (!mode) {
|
||||
console.warn(`No mode found for slug: ${modeSlug}`)
|
||||
return ""
|
||||
}
|
||||
return mode.customInstructions ?? ""
|
||||
}
|
||||
|
||||
// Slash command parsing types and functions
|
||||
export type SlashCommandResult = {
|
||||
modeSlug: string
|
||||
remainingMessage: string
|
||||
} | null
|
||||
|
||||
export function parseSlashCommand(message: string, customModes?: ModeConfig[]): SlashCommandResult {
|
||||
// Check if message starts with a slash
|
||||
if (!message.startsWith("/")) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Extract command (everything between / and first space)
|
||||
const parts = message.trim().split(/\s+/)
|
||||
if (parts.length < 2) {
|
||||
return null // Need both command and message
|
||||
}
|
||||
|
||||
const command = parts[0].substring(1) // Remove leading slash
|
||||
const remainingMessage = parts.slice(1).join(" ")
|
||||
|
||||
// Validate command is a valid mode slug
|
||||
const mode = getModeBySlug(command, customModes)
|
||||
if (!mode) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
modeSlug: command,
|
||||
remainingMessage,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ export const createPrompt = (template: string, params: PromptParams): string =>
|
|||
}
|
||||
}
|
||||
|
||||
// Replace any remaining user_input placeholders with empty string
|
||||
result = result.replaceAll("${userInput}", "")
|
||||
// Replace any remaining placeholders with empty strings
|
||||
result = result.replaceAll(/\${[^}]*}/g, "")
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ const supportPromptConfigs: Record<string, SupportPromptConfig> = {
|
|||
EXPLAIN: {
|
||||
label: "Explain Code",
|
||||
description:
|
||||
"Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in the editor context menu (right-click on selected code).",
|
||||
"Get detailed explanations of code snippets, functions, or entire files. Useful for understanding complex code or learning new patterns. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).",
|
||||
template: `Explain the following code from file path @/\${filePath}:
|
||||
\${userInput}
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ Please provide a clear and concise explanation of what this code does, including
|
|||
FIX: {
|
||||
label: "Fix Issues",
|
||||
description:
|
||||
"Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in the editor context menu (right-click on selected code).",
|
||||
"Get help identifying and resolving bugs, errors, or code quality issues. Provides step-by-step guidance for fixing problems. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).",
|
||||
template: `Fix any issues in the following code from file path @/\${filePath}
|
||||
\${diagnosticText}
|
||||
\${userInput}
|
||||
|
|
@ -76,7 +76,7 @@ Please:
|
|||
IMPROVE: {
|
||||
label: "Improve Code",
|
||||
description:
|
||||
"Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in the editor context menu (right-click on selected code).",
|
||||
"Receive suggestions for code optimization, better practices, and architectural improvements while maintaining functionality. Available in code actions (lightbulb icon in the editor) and the editor context menu (right-click on selected code).",
|
||||
template: `Improve the following code from file path @/\${filePath}:
|
||||
\${userInput}
|
||||
|
||||
|
|
@ -92,6 +92,15 @@ Please suggest improvements for:
|
|||
|
||||
Provide the improved code along with explanations for each enhancement.`,
|
||||
},
|
||||
ADD_TO_CONTEXT: {
|
||||
label: "Add to Context",
|
||||
description:
|
||||
"Add context to your current task or conversation. Useful for providing additional information or clarifications. Available in code actions (lightbulb icon in the editor). and the editor context menu (right-click on selected code).",
|
||||
template: `@/\${filePath}:
|
||||
\`\`\`
|
||||
\${selectedText}
|
||||
\`\`\``,
|
||||
},
|
||||
} as const
|
||||
|
||||
type SupportPromptType = keyof typeof supportPromptConfigs
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
// Define tool group values
|
||||
export type ToolGroupValues = readonly string[]
|
||||
// Define tool group configuration
|
||||
export type ToolGroupConfig = {
|
||||
tools: readonly string[]
|
||||
alwaysAvailable?: boolean // Whether this group is always available and shouldn't show in prompts view
|
||||
}
|
||||
|
||||
// Map of tool slugs to their display names
|
||||
export const TOOL_DISPLAY_NAMES = {
|
||||
|
|
@ -20,13 +23,26 @@ export const TOOL_DISPLAY_NAMES = {
|
|||
} as const
|
||||
|
||||
// Define available tool groups
|
||||
export const TOOL_GROUPS: Record<string, ToolGroupValues> = {
|
||||
read: ["read_file", "search_files", "list_files", "list_code_definition_names"],
|
||||
edit: ["write_to_file", "apply_diff", "insert_content", "search_and_replace"],
|
||||
browser: ["browser_action"],
|
||||
command: ["execute_command"],
|
||||
mcp: ["use_mcp_tool", "access_mcp_resource"],
|
||||
modes: ["switch_mode", "new_task"],
|
||||
export const TOOL_GROUPS: Record<string, ToolGroupConfig> = {
|
||||
read: {
|
||||
tools: ["read_file", "search_files", "list_files", "list_code_definition_names"],
|
||||
},
|
||||
edit: {
|
||||
tools: ["write_to_file", "apply_diff", "insert_content", "search_and_replace"],
|
||||
},
|
||||
browser: {
|
||||
tools: ["browser_action"],
|
||||
},
|
||||
command: {
|
||||
tools: ["execute_command"],
|
||||
},
|
||||
mcp: {
|
||||
tools: ["use_mcp_tool", "access_mcp_resource"],
|
||||
},
|
||||
modes: {
|
||||
tools: ["switch_mode", "new_task"],
|
||||
alwaysAvailable: true,
|
||||
},
|
||||
}
|
||||
|
||||
export type ToolGroup = keyof typeof TOOL_GROUPS
|
||||
|
|
|
|||
|
|
@ -1,121 +1,18 @@
|
|||
const assert = require("assert")
|
||||
const vscode = require("vscode")
|
||||
const path = require("path")
|
||||
const fs = require("fs")
|
||||
const dotenv = require("dotenv")
|
||||
import * as assert from "assert"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
// Load test environment variables
|
||||
const testEnvPath = path.join(__dirname, ".test_env")
|
||||
dotenv.config({ path: testEnvPath })
|
||||
|
||||
suite("Roo Code Extension Test Suite", () => {
|
||||
vscode.window.showInformationMessage("Starting Roo Code extension tests.")
|
||||
|
||||
test("Extension should be present", () => {
|
||||
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
|
||||
assert.notStrictEqual(extension, undefined)
|
||||
})
|
||||
|
||||
test("Extension should activate", async () => {
|
||||
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
|
||||
if (!extension) {
|
||||
assert.fail("Extension not found")
|
||||
suite("Roo Code Extension", () => {
|
||||
test("OPENROUTER_API_KEY environment variable is set", () => {
|
||||
if (!process.env.OPENROUTER_API_KEY) {
|
||||
assert.fail("OPENROUTER_API_KEY environment variable is not set")
|
||||
}
|
||||
await extension.activate()
|
||||
assert.strictEqual(extension.isActive, true)
|
||||
})
|
||||
|
||||
test("OpenRouter API key and models should be configured correctly", function (done) {
|
||||
// @ts-ignore
|
||||
this.timeout(60000) // Increase timeout to 60s for network requests
|
||||
;(async () => {
|
||||
try {
|
||||
// Get extension instance
|
||||
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
|
||||
if (!extension) {
|
||||
done(new Error("Extension not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify API key is set and valid
|
||||
const apiKey = process.env.OPEN_ROUTER_API_KEY
|
||||
if (!apiKey) {
|
||||
done(new Error("OPEN_ROUTER_API_KEY environment variable is not set"))
|
||||
return
|
||||
}
|
||||
if (!apiKey.startsWith("sk-or-v1-")) {
|
||||
done(new Error("OpenRouter API key should have correct format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Activate extension and get provider
|
||||
const api = await extension.activate()
|
||||
if (!api) {
|
||||
done(new Error("Extension API not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get the provider from the extension's exports
|
||||
const provider = api.sidebarProvider
|
||||
if (!provider) {
|
||||
done(new Error("Provider not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set up the API configuration
|
||||
await provider.updateGlobalState("apiProvider", "openrouter")
|
||||
await provider.storeSecret("openRouterApiKey", apiKey)
|
||||
|
||||
// Set up timeout to fail test if models don't load
|
||||
const timeout = setTimeout(() => {
|
||||
done(new Error("Timeout waiting for models to load"))
|
||||
}, 30000)
|
||||
|
||||
// Wait for models to be loaded
|
||||
const checkModels = setInterval(async () => {
|
||||
try {
|
||||
const models = await provider.readOpenRouterModels()
|
||||
if (!models) {
|
||||
return
|
||||
}
|
||||
|
||||
clearInterval(checkModels)
|
||||
clearTimeout(timeout)
|
||||
|
||||
// Verify expected Claude models are available
|
||||
const expectedModels = [
|
||||
"anthropic/claude-3.5-sonnet:beta",
|
||||
"anthropic/claude-3-sonnet:beta",
|
||||
"anthropic/claude-3.5-sonnet",
|
||||
"anthropic/claude-3.5-sonnet-20240620",
|
||||
"anthropic/claude-3.5-sonnet-20240620:beta",
|
||||
"anthropic/claude-3.5-haiku:beta",
|
||||
]
|
||||
|
||||
for (const modelId of expectedModels) {
|
||||
assert.strictEqual(modelId in models, true, `Model ${modelId} should be available`)
|
||||
}
|
||||
|
||||
done()
|
||||
} catch (error) {
|
||||
clearInterval(checkModels)
|
||||
clearTimeout(timeout)
|
||||
done(error)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
// Trigger model loading
|
||||
await provider.refreshOpenRouterModels()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
})()
|
||||
})
|
||||
|
||||
test("Commands should be registered", async () => {
|
||||
const commands = await vscode.commands.getCommands(true)
|
||||
const timeout = 10 * 1_000
|
||||
const interval = 1_000
|
||||
const startTime = Date.now()
|
||||
|
||||
// Test core commands are registered
|
||||
const expectedCommands = [
|
||||
"roo-cline.plusButtonClicked",
|
||||
"roo-cline.mcpButtonClicked",
|
||||
|
|
@ -128,204 +25,39 @@ suite("Roo Code Extension Test Suite", () => {
|
|||
"roo-cline.improveCode",
|
||||
]
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
const commands = await vscode.commands.getCommands(true)
|
||||
const missingCommands = []
|
||||
|
||||
for (const cmd of expectedCommands) {
|
||||
if (!commands.includes(cmd)) {
|
||||
missingCommands.push(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
if (missingCommands.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
const commands = await vscode.commands.getCommands(true)
|
||||
|
||||
for (const cmd of expectedCommands) {
|
||||
assert.strictEqual(commands.includes(cmd), true, `Command ${cmd} should be registered`)
|
||||
assert.ok(commands.includes(cmd), `Command ${cmd} should be registered`)
|
||||
}
|
||||
})
|
||||
|
||||
test("Views should be registered", () => {
|
||||
test("Webview panel can be created", () => {
|
||||
const view = vscode.window.createWebviewPanel(
|
||||
"roo-cline.SidebarProvider",
|
||||
"Roo Code",
|
||||
vscode.ViewColumn.One,
|
||||
{},
|
||||
)
|
||||
assert.notStrictEqual(view, undefined)
|
||||
|
||||
assert.ok(view, "Failed to create webview panel")
|
||||
view.dispose()
|
||||
})
|
||||
|
||||
test("Should handle prompt and response correctly", async function () {
|
||||
// @ts-ignore
|
||||
this.timeout(60000) // Increase timeout for API request
|
||||
|
||||
const timeout = 30000
|
||||
const interval = 1000
|
||||
|
||||
// Get extension instance
|
||||
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
|
||||
if (!extension) {
|
||||
assert.fail("Extension not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Activate extension and get API
|
||||
const api = await extension.activate()
|
||||
if (!api) {
|
||||
assert.fail("Extension API not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Get provider
|
||||
const provider = api.sidebarProvider
|
||||
if (!provider) {
|
||||
assert.fail("Provider not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Set up API configuration
|
||||
await provider.updateGlobalState("apiProvider", "openrouter")
|
||||
await provider.updateGlobalState("openRouterModelId", "anthropic/claude-3.5-sonnet")
|
||||
const apiKey = process.env.OPEN_ROUTER_API_KEY
|
||||
if (!apiKey) {
|
||||
assert.fail("OPEN_ROUTER_API_KEY environment variable is not set")
|
||||
return
|
||||
}
|
||||
await provider.storeSecret("openRouterApiKey", apiKey)
|
||||
|
||||
// Create webview panel with development options
|
||||
const extensionUri = extension.extensionUri
|
||||
const panel = vscode.window.createWebviewPanel("roo-cline.SidebarProvider", "Roo Code", vscode.ViewColumn.One, {
|
||||
enableScripts: true,
|
||||
enableCommandUris: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [extensionUri],
|
||||
})
|
||||
|
||||
try {
|
||||
// Initialize webview with development context
|
||||
panel.webview.options = {
|
||||
enableScripts: true,
|
||||
enableCommandUris: true,
|
||||
localResourceRoots: [extensionUri],
|
||||
}
|
||||
|
||||
// Initialize provider with panel
|
||||
provider.resolveWebviewView(panel)
|
||||
|
||||
// Set up message tracking
|
||||
let webviewReady = false
|
||||
let messagesReceived = false
|
||||
const originalPostMessage = provider.postMessageToWebview.bind(provider)
|
||||
// @ts-ignore
|
||||
provider.postMessageToWebview = async (message) => {
|
||||
if (message.type === "state") {
|
||||
webviewReady = true
|
||||
console.log("Webview state received:", message)
|
||||
if (message.state?.clineMessages?.length > 0) {
|
||||
messagesReceived = true
|
||||
console.log("Messages in state:", message.state.clineMessages)
|
||||
}
|
||||
}
|
||||
await originalPostMessage(message)
|
||||
}
|
||||
|
||||
// Wait for webview to launch and receive initial state
|
||||
let startTime = Date.now()
|
||||
while (Date.now() - startTime < timeout) {
|
||||
if (webviewReady) {
|
||||
// Wait an additional second for webview to fully initialize
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
break
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
if (!webviewReady) {
|
||||
throw new Error("Timeout waiting for webview to be ready")
|
||||
}
|
||||
|
||||
// Send webviewDidLaunch to initialize chat
|
||||
await provider.postMessageToWebview({ type: "webviewDidLaunch" })
|
||||
console.log("Sent webviewDidLaunch")
|
||||
|
||||
// Wait for webview to fully initialize
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
// Restore original postMessage
|
||||
provider.postMessageToWebview = originalPostMessage
|
||||
|
||||
// Wait for OpenRouter models to be fully loaded
|
||||
startTime = Date.now()
|
||||
while (Date.now() - startTime < timeout) {
|
||||
const models = await provider.readOpenRouterModels()
|
||||
if (models && Object.keys(models).length > 0) {
|
||||
console.log("OpenRouter models loaded")
|
||||
break
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
// Send prompt
|
||||
const prompt = "Hello world, what is your name?"
|
||||
console.log("Sending prompt:", prompt)
|
||||
|
||||
// Start task
|
||||
try {
|
||||
await api.startNewTask(prompt)
|
||||
console.log("Task started")
|
||||
} catch (error) {
|
||||
console.error("Error starting task:", error)
|
||||
throw error
|
||||
}
|
||||
|
||||
// Wait for task to appear in history with tokens
|
||||
startTime = Date.now()
|
||||
while (Date.now() - startTime < timeout) {
|
||||
const state = await provider.getState()
|
||||
const task = state.taskHistory?.[0]
|
||||
if (task && task.tokensOut > 0) {
|
||||
console.log("Task completed with tokens:", task)
|
||||
break
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
// Wait for messages to be processed
|
||||
startTime = Date.now()
|
||||
let responseReceived = false
|
||||
while (Date.now() - startTime < timeout) {
|
||||
// Check provider.clineMessages
|
||||
const messages = provider.clineMessages
|
||||
if (messages && messages.length > 0) {
|
||||
console.log("Provider messages:", JSON.stringify(messages, null, 2))
|
||||
// @ts-ignore
|
||||
const hasResponse = messages.some(
|
||||
(m: { type: string; text: string }) =>
|
||||
m.type === "say" && m.text && m.text.toLowerCase().includes("cline"),
|
||||
)
|
||||
if (hasResponse) {
|
||||
console.log('Found response containing "Cline" in provider messages')
|
||||
responseReceived = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check provider.cline.clineMessages
|
||||
const clineMessages = provider.cline?.clineMessages
|
||||
if (clineMessages && clineMessages.length > 0) {
|
||||
console.log("Cline messages:", JSON.stringify(clineMessages, null, 2))
|
||||
// @ts-ignore
|
||||
const hasResponse = clineMessages.some(
|
||||
(m: { type: string; text: string }) =>
|
||||
m.type === "say" && m.text && m.text.toLowerCase().includes("cline"),
|
||||
)
|
||||
if (hasResponse) {
|
||||
console.log('Found response containing "Cline" in cline messages')
|
||||
responseReceived = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
if (!responseReceived) {
|
||||
console.log("Final provider state:", await provider.getState())
|
||||
console.log("Final cline messages:", provider.cline?.clineMessages)
|
||||
throw new Error('Did not receive expected response containing "Cline"')
|
||||
}
|
||||
} finally {
|
||||
panel.dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
77
src/test/task.test.ts
Normal file
77
src/test/task.test.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import * as assert from "assert"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
import { ClineAPI } from "../exports/cline"
|
||||
import { ClineProvider } from "../core/webview/ClineProvider"
|
||||
|
||||
suite("Roo Code Task", () => {
|
||||
test("Should handle prompt and response correctly", async function () {
|
||||
const timeout = 30000
|
||||
const interval = 1000
|
||||
|
||||
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
|
||||
|
||||
if (!extension) {
|
||||
assert.fail("Extension not found")
|
||||
}
|
||||
|
||||
const api: ClineAPI = await extension.activate()
|
||||
const provider = api.sidebarProvider as ClineProvider
|
||||
await provider.updateGlobalState("apiProvider", "openrouter")
|
||||
await provider.updateGlobalState("openRouterModelId", "anthropic/claude-3.5-sonnet")
|
||||
await provider.storeSecret("openRouterApiKey", process.env.OPENROUTER_API_KEY || "sk-or-v1-fake-api-key")
|
||||
|
||||
// Create webview panel with development options.
|
||||
const panel = vscode.window.createWebviewPanel("roo-cline.SidebarProvider", "Roo Code", vscode.ViewColumn.One, {
|
||||
enableScripts: true,
|
||||
enableCommandUris: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [extension.extensionUri],
|
||||
})
|
||||
|
||||
try {
|
||||
// Initialize provider with panel.
|
||||
await provider.resolveWebviewView(panel)
|
||||
|
||||
// Wait for webview to launch.
|
||||
let startTime = Date.now()
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
if (provider.viewLaunched) {
|
||||
break
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
await api.startNewTask("Hello world, what is your name? Respond with 'My name is ...'")
|
||||
|
||||
// Wait for task to appear in history with tokens.
|
||||
startTime = Date.now()
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
const state = await provider.getState()
|
||||
const task = state.taskHistory?.[0]
|
||||
|
||||
if (task && task.tokensOut > 0) {
|
||||
break
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
if (provider.messages.length === 0) {
|
||||
assert.fail("No messages received")
|
||||
}
|
||||
|
||||
// console.log("Provider messages:", JSON.stringify(provider.messages, null, 2))
|
||||
|
||||
assert.ok(
|
||||
provider.messages.some(({ type, text }) => type === "say" && text?.includes("My name is Roo")),
|
||||
"Did not receive expected response containing 'My name is Roo'",
|
||||
)
|
||||
} finally {
|
||||
panel.dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -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"]
|
||||
}
|
||||
222
src/utils/__tests__/shell.test.ts
Normal file
222
src/utils/__tests__/shell.test.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import * as vscode from "vscode"
|
||||
import { userInfo } from "os"
|
||||
import { getShell } from "../shell"
|
||||
|
||||
describe("Shell Detection Tests", () => {
|
||||
let originalPlatform: string
|
||||
let originalEnv: NodeJS.ProcessEnv
|
||||
let originalGetConfig: any
|
||||
let originalUserInfo: any
|
||||
|
||||
// Helper to mock VS Code configuration
|
||||
function mockVsCodeConfig(platformKey: string, defaultProfileName: string | null, profiles: Record<string, any>) {
|
||||
vscode.workspace.getConfiguration = () =>
|
||||
({
|
||||
get: (key: string) => {
|
||||
if (key === `defaultProfile.${platformKey}`) {
|
||||
return defaultProfileName
|
||||
}
|
||||
if (key === `profiles.${platformKey}`) {
|
||||
return profiles
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
}) as any
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// Store original references
|
||||
originalPlatform = process.platform
|
||||
originalEnv = { ...process.env }
|
||||
originalGetConfig = vscode.workspace.getConfiguration
|
||||
originalUserInfo = userInfo
|
||||
|
||||
// Clear environment variables for a clean test
|
||||
delete process.env.SHELL
|
||||
delete process.env.COMSPEC
|
||||
|
||||
// Default userInfo() mock
|
||||
;(userInfo as any) = () => ({ shell: null })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore everything
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform })
|
||||
process.env = originalEnv
|
||||
vscode.workspace.getConfiguration = originalGetConfig
|
||||
;(userInfo as any) = originalUserInfo
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Windows Shell Detection
|
||||
// --------------------------------------------------------------------------
|
||||
describe("Windows Shell Detection", () => {
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
})
|
||||
|
||||
it("uses explicit PowerShell 7 path from VS Code config (profile path)", () => {
|
||||
mockVsCodeConfig("windows", "PowerShell", {
|
||||
PowerShell: { path: "C:\\Program Files\\PowerShell\\7\\pwsh.exe" },
|
||||
})
|
||||
expect(getShell()).toBe("C:\\Program Files\\PowerShell\\7\\pwsh.exe")
|
||||
})
|
||||
|
||||
it("uses PowerShell 7 path if source is 'PowerShell' but no explicit path", () => {
|
||||
mockVsCodeConfig("windows", "PowerShell", {
|
||||
PowerShell: { source: "PowerShell" },
|
||||
})
|
||||
expect(getShell()).toBe("C:\\Program Files\\PowerShell\\7\\pwsh.exe")
|
||||
})
|
||||
|
||||
it("falls back to legacy PowerShell if profile includes 'powershell' but no path/source", () => {
|
||||
mockVsCodeConfig("windows", "PowerShell", {
|
||||
PowerShell: {},
|
||||
})
|
||||
expect(getShell()).toBe("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
|
||||
})
|
||||
|
||||
it("uses WSL bash when profile indicates WSL source", () => {
|
||||
mockVsCodeConfig("windows", "WSL", {
|
||||
WSL: { source: "WSL" },
|
||||
})
|
||||
expect(getShell()).toBe("/bin/bash")
|
||||
})
|
||||
|
||||
it("uses WSL bash when profile name includes 'wsl'", () => {
|
||||
mockVsCodeConfig("windows", "Ubuntu WSL", {
|
||||
"Ubuntu WSL": {},
|
||||
})
|
||||
expect(getShell()).toBe("/bin/bash")
|
||||
})
|
||||
|
||||
it("defaults to cmd.exe if no special profile is matched", () => {
|
||||
mockVsCodeConfig("windows", "CommandPrompt", {
|
||||
CommandPrompt: {},
|
||||
})
|
||||
expect(getShell()).toBe("C:\\Windows\\System32\\cmd.exe")
|
||||
})
|
||||
|
||||
it("respects userInfo() if no VS Code config is available", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
;(userInfo as any) = () => ({ shell: "C:\\Custom\\PowerShell.exe" })
|
||||
|
||||
expect(getShell()).toBe("C:\\Custom\\PowerShell.exe")
|
||||
})
|
||||
|
||||
it("respects an odd COMSPEC if no userInfo shell is available", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
process.env.COMSPEC = "D:\\CustomCmd\\cmd.exe"
|
||||
|
||||
expect(getShell()).toBe("D:\\CustomCmd\\cmd.exe")
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// macOS Shell Detection
|
||||
// --------------------------------------------------------------------------
|
||||
describe("macOS Shell Detection", () => {
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: "darwin" })
|
||||
})
|
||||
|
||||
it("uses VS Code profile path if available", () => {
|
||||
mockVsCodeConfig("osx", "MyCustomShell", {
|
||||
MyCustomShell: { path: "/usr/local/bin/fish" },
|
||||
})
|
||||
expect(getShell()).toBe("/usr/local/bin/fish")
|
||||
})
|
||||
|
||||
it("falls back to userInfo().shell if no VS Code config is available", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
;(userInfo as any) = () => ({ shell: "/opt/homebrew/bin/zsh" })
|
||||
expect(getShell()).toBe("/opt/homebrew/bin/zsh")
|
||||
})
|
||||
|
||||
it("falls back to SHELL env var if no userInfo shell is found", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
process.env.SHELL = "/usr/local/bin/zsh"
|
||||
expect(getShell()).toBe("/usr/local/bin/zsh")
|
||||
})
|
||||
|
||||
it("falls back to /bin/zsh if no config, userInfo, or env variable is set", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
expect(getShell()).toBe("/bin/zsh")
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Linux Shell Detection
|
||||
// --------------------------------------------------------------------------
|
||||
describe("Linux Shell Detection", () => {
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" })
|
||||
})
|
||||
|
||||
it("uses VS Code profile path if available", () => {
|
||||
mockVsCodeConfig("linux", "CustomProfile", {
|
||||
CustomProfile: { path: "/usr/bin/fish" },
|
||||
})
|
||||
expect(getShell()).toBe("/usr/bin/fish")
|
||||
})
|
||||
|
||||
it("falls back to userInfo().shell if no VS Code config is available", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
;(userInfo as any) = () => ({ shell: "/usr/bin/zsh" })
|
||||
expect(getShell()).toBe("/usr/bin/zsh")
|
||||
})
|
||||
|
||||
it("falls back to SHELL env var if no userInfo shell is found", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
process.env.SHELL = "/usr/bin/fish"
|
||||
expect(getShell()).toBe("/usr/bin/fish")
|
||||
})
|
||||
|
||||
it("falls back to /bin/bash if nothing is set", () => {
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
expect(getShell()).toBe("/bin/bash")
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Unknown Platform & Error Handling
|
||||
// --------------------------------------------------------------------------
|
||||
describe("Unknown Platform / Error Handling", () => {
|
||||
it("falls back to /bin/sh for unknown platforms", () => {
|
||||
Object.defineProperty(process, "platform", { value: "sunos" })
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
expect(getShell()).toBe("/bin/sh")
|
||||
})
|
||||
|
||||
it("handles VS Code config errors gracefully, falling back to userInfo shell if present", () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" })
|
||||
vscode.workspace.getConfiguration = () => {
|
||||
throw new Error("Configuration error")
|
||||
}
|
||||
;(userInfo as any) = () => ({ shell: "/bin/bash" })
|
||||
expect(getShell()).toBe("/bin/bash")
|
||||
})
|
||||
|
||||
it("handles userInfo errors gracefully, falling back to environment variable if present", () => {
|
||||
Object.defineProperty(process, "platform", { value: "darwin" })
|
||||
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
|
||||
;(userInfo as any) = () => {
|
||||
throw new Error("userInfo error")
|
||||
}
|
||||
process.env.SHELL = "/bin/zsh"
|
||||
expect(getShell()).toBe("/bin/zsh")
|
||||
})
|
||||
|
||||
it("falls back fully to default shell paths if everything fails", () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" })
|
||||
vscode.workspace.getConfiguration = () => {
|
||||
throw new Error("Configuration error")
|
||||
}
|
||||
;(userInfo as any) = () => {
|
||||
throw new Error("userInfo error")
|
||||
}
|
||||
delete process.env.SHELL
|
||||
expect(getShell()).toBe("/bin/bash")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -99,3 +99,8 @@ export function getReadablePath(cwd: string, relPath?: string): string {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const toRelativePath = (filePath: string, cwd: string) => {
|
||||
const relativePath = path.relative(cwd, filePath).toPosix()
|
||||
return filePath.endsWith("/") ? relativePath + "/" : relativePath
|
||||
}
|
||||
|
|
|
|||
227
src/utils/shell.ts
Normal file
227
src/utils/shell.ts
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
import * as vscode from "vscode"
|
||||
import { userInfo } from "os"
|
||||
|
||||
const SHELL_PATHS = {
|
||||
// Windows paths
|
||||
POWERSHELL_7: "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
|
||||
POWERSHELL_LEGACY: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
|
||||
CMD: "C:\\Windows\\System32\\cmd.exe",
|
||||
WSL_BASH: "/bin/bash",
|
||||
// Unix paths
|
||||
MAC_DEFAULT: "/bin/zsh",
|
||||
LINUX_DEFAULT: "/bin/bash",
|
||||
CSH: "/bin/csh",
|
||||
BASH: "/bin/bash",
|
||||
KSH: "/bin/ksh",
|
||||
SH: "/bin/sh",
|
||||
ZSH: "/bin/zsh",
|
||||
DASH: "/bin/dash",
|
||||
TCSH: "/bin/tcsh",
|
||||
FALLBACK: "/bin/sh",
|
||||
} as const
|
||||
|
||||
interface MacTerminalProfile {
|
||||
path?: string
|
||||
}
|
||||
|
||||
type MacTerminalProfiles = Record<string, MacTerminalProfile>
|
||||
|
||||
interface WindowsTerminalProfile {
|
||||
path?: string
|
||||
source?: "PowerShell" | "WSL"
|
||||
}
|
||||
|
||||
type WindowsTerminalProfiles = Record<string, WindowsTerminalProfile>
|
||||
|
||||
interface LinuxTerminalProfile {
|
||||
path?: string
|
||||
}
|
||||
|
||||
type LinuxTerminalProfiles = Record<string, LinuxTerminalProfile>
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 1) VS Code Terminal Configuration Helpers
|
||||
// -----------------------------------------------------
|
||||
|
||||
function getWindowsTerminalConfig() {
|
||||
try {
|
||||
const config = vscode.workspace.getConfiguration("terminal.integrated")
|
||||
const defaultProfileName = config.get<string>("defaultProfile.windows")
|
||||
const profiles = config.get<WindowsTerminalProfiles>("profiles.windows") || {}
|
||||
return { defaultProfileName, profiles }
|
||||
} catch {
|
||||
return { defaultProfileName: null, profiles: {} as WindowsTerminalProfiles }
|
||||
}
|
||||
}
|
||||
|
||||
function getMacTerminalConfig() {
|
||||
try {
|
||||
const config = vscode.workspace.getConfiguration("terminal.integrated")
|
||||
const defaultProfileName = config.get<string>("defaultProfile.osx")
|
||||
const profiles = config.get<MacTerminalProfiles>("profiles.osx") || {}
|
||||
return { defaultProfileName, profiles }
|
||||
} catch {
|
||||
return { defaultProfileName: null, profiles: {} as MacTerminalProfiles }
|
||||
}
|
||||
}
|
||||
|
||||
function getLinuxTerminalConfig() {
|
||||
try {
|
||||
const config = vscode.workspace.getConfiguration("terminal.integrated")
|
||||
const defaultProfileName = config.get<string>("defaultProfile.linux")
|
||||
const profiles = config.get<LinuxTerminalProfiles>("profiles.linux") || {}
|
||||
return { defaultProfileName, profiles }
|
||||
} catch {
|
||||
return { defaultProfileName: null, profiles: {} as LinuxTerminalProfiles }
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 2) Platform-Specific VS Code Shell Retrieval
|
||||
// -----------------------------------------------------
|
||||
|
||||
/** Attempts to retrieve a shell path from VS Code config on Windows. */
|
||||
function getWindowsShellFromVSCode(): string | null {
|
||||
const { defaultProfileName, profiles } = getWindowsTerminalConfig()
|
||||
if (!defaultProfileName) {
|
||||
return null
|
||||
}
|
||||
|
||||
const profile = profiles[defaultProfileName]
|
||||
|
||||
// If the profile name indicates PowerShell, do version-based detection.
|
||||
// In testing it was found these typically do not have a path, and this
|
||||
// implementation manages to deductively get the corect version of PowerShell
|
||||
if (defaultProfileName.toLowerCase().includes("powershell")) {
|
||||
if (profile?.path) {
|
||||
// If there's an explicit PowerShell path, return that
|
||||
return profile.path
|
||||
} else if (profile?.source === "PowerShell") {
|
||||
// If the profile is sourced from PowerShell, assume the newest
|
||||
return SHELL_PATHS.POWERSHELL_7
|
||||
}
|
||||
// Otherwise, assume legacy Windows PowerShell
|
||||
return SHELL_PATHS.POWERSHELL_LEGACY
|
||||
}
|
||||
|
||||
// If there's a specific path, return that immediately
|
||||
if (profile.path) {
|
||||
return profile.path
|
||||
}
|
||||
|
||||
// If the profile indicates WSL
|
||||
if (profile?.source === "WSL" || defaultProfileName.toLowerCase().includes("wsl")) {
|
||||
return SHELL_PATHS.WSL_BASH
|
||||
}
|
||||
|
||||
// If nothing special detected, we assume cmd
|
||||
return SHELL_PATHS.CMD
|
||||
}
|
||||
|
||||
/** Attempts to retrieve a shell path from VS Code config on macOS. */
|
||||
function getMacShellFromVSCode(): string | null {
|
||||
const { defaultProfileName, profiles } = getMacTerminalConfig()
|
||||
if (!defaultProfileName) {
|
||||
return null
|
||||
}
|
||||
|
||||
const profile = profiles[defaultProfileName]
|
||||
return profile?.path || null
|
||||
}
|
||||
|
||||
/** Attempts to retrieve a shell path from VS Code config on Linux. */
|
||||
function getLinuxShellFromVSCode(): string | null {
|
||||
const { defaultProfileName, profiles } = getLinuxTerminalConfig()
|
||||
if (!defaultProfileName) {
|
||||
return null
|
||||
}
|
||||
|
||||
const profile = profiles[defaultProfileName]
|
||||
return profile?.path || null
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 3) General Fallback Helpers
|
||||
// -----------------------------------------------------
|
||||
|
||||
/**
|
||||
* Tries to get a user’s shell from os.userInfo() (works on Unix if the
|
||||
* underlying system call is supported). Returns null on error or if not found.
|
||||
*/
|
||||
function getShellFromUserInfo(): string | null {
|
||||
try {
|
||||
const { shell } = userInfo()
|
||||
return shell || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the environment-based shell variable, or null if not set. */
|
||||
function getShellFromEnv(): string | null {
|
||||
const { env } = process
|
||||
|
||||
if (process.platform === "win32") {
|
||||
// On Windows, COMSPEC typically holds cmd.exe
|
||||
return env.COMSPEC || "C:\\Windows\\System32\\cmd.exe"
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
// On macOS/Linux, SHELL is commonly the environment variable
|
||||
return env.SHELL || "/bin/zsh"
|
||||
}
|
||||
|
||||
if (process.platform === "linux") {
|
||||
// On Linux, SHELL is commonly the environment variable
|
||||
return env.SHELL || "/bin/bash"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 4) Publicly Exposed Shell Getter
|
||||
// -----------------------------------------------------
|
||||
|
||||
export function getShell(): string {
|
||||
// 1. Check VS Code config first.
|
||||
if (process.platform === "win32") {
|
||||
// Special logic for Windows
|
||||
const windowsShell = getWindowsShellFromVSCode()
|
||||
if (windowsShell) {
|
||||
return windowsShell
|
||||
}
|
||||
} else if (process.platform === "darwin") {
|
||||
// macOS from VS Code
|
||||
const macShell = getMacShellFromVSCode()
|
||||
if (macShell) {
|
||||
return macShell
|
||||
}
|
||||
} else if (process.platform === "linux") {
|
||||
// Linux from VS Code
|
||||
const linuxShell = getLinuxShellFromVSCode()
|
||||
if (linuxShell) {
|
||||
return linuxShell
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If no shell from VS Code, try userInfo()
|
||||
const userInfoShell = getShellFromUserInfo()
|
||||
if (userInfoShell) {
|
||||
return userInfoShell
|
||||
}
|
||||
|
||||
// 3. If still nothing, try environment variable
|
||||
const envShell = getShellFromEnv()
|
||||
if (envShell) {
|
||||
return envShell
|
||||
}
|
||||
|
||||
// 4. Finally, fall back to a default
|
||||
if (process.platform === "win32") {
|
||||
// On Windows, if we got here, we have no config, no COMSPEC, and one very messed up operating system.
|
||||
// Use CMD as a last resort
|
||||
return SHELL_PATHS.CMD
|
||||
}
|
||||
// On macOS/Linux, fallback to a POSIX shell - This is the behavior of our old shell detection method.
|
||||
return SHELL_PATHS.FALLBACK
|
||||
}
|
||||
17
tsconfig.integration.json
Normal file
17
tsconfig.integration.json
Normal 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"]
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["react", "@typescript-eslint", "react-hooks"],
|
||||
"rules": {
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"react/display-name": "warn",
|
||||
"no-case-declarations": "warn",
|
||||
"react/no-unescaped-entities": "warn",
|
||||
"react/jsx-key": "warn",
|
||||
"no-extra-semi": "warn",
|
||||
"@typescript-eslint/no-var-requires": "warn",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
}
|
||||
}
|
||||
2
webview-ui/.gitignore
vendored
2
webview-ui/.gitignore
vendored
|
|
@ -21,3 +21,5 @@
|
|||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
*storybook.log
|
||||
|
|
|
|||
11
webview-ui/.storybook/main.ts
Normal file
11
webview-ui/.storybook/main.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import type { StorybookConfig } from "@storybook/react-vite"
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
addons: ["@storybook/addon-essentials", "@storybook/addon-interactions"],
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {},
|
||||
},
|
||||
}
|
||||
export default config
|
||||
17
webview-ui/.storybook/preview.ts
Normal file
17
webview-ui/.storybook/preview.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { Preview } from "@storybook/react"
|
||||
|
||||
import "../src/index.css"
|
||||
import "./vscode.css"
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default preview
|
||||
32
webview-ui/.storybook/vscode.css
Normal file
32
webview-ui/.storybook/vscode.css
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Use `Developer: Generate Color Theme From Current Settings` to generate themes
|
||||
* using your current VSCode settings.
|
||||
*
|
||||
* See: https://code.visualstudio.com/docs/getstarted/themes
|
||||
*/
|
||||
|
||||
:root {
|
||||
--vscode-editor-background: #1f1f1f; /* "editor.background" */
|
||||
--vscode-editor-foreground: #cccccc; /* "editor.foreground" */
|
||||
--vscode-menu-background: #1f1f1f; /* "menu.background" */
|
||||
--vscode-menu-foreground: #cccccc; /* "menu.foreground" */
|
||||
--vscode-button-background: #0078d4; /* "button.background" */
|
||||
--vscode-button-foreground: #ffffff; /* "button.foreground" */
|
||||
--vscode-button-secondaryBackground: #313131; /* "button.secondaryBackground" */
|
||||
--vscode-button-secondaryForeground: #cccccc; /* "button.secondaryForeground" */
|
||||
--vscode-disabledForeground: #313131; /* "disabledForeground" */
|
||||
--vscode-descriptionForeground: #9d9d9d; /* "descriptionForeground" */
|
||||
--vscode-focusBorder: #0078d4; /* "focusBorder" */
|
||||
--vscode-errorForeground: #f85149; /* "errorForeground" */
|
||||
--vscode-widget-border: #313131; /* "widget.border" */
|
||||
--vscode-input-background: #313131; /* "input.background" */
|
||||
--vscode-input-foreground: #cccccc; /* "input.foreground" */
|
||||
--vscode-input-border: #3c3c3c; /* "input.border" */
|
||||
|
||||
/* I can't find these in the output of `Developer: Generate Color Theme From Current Settings` */
|
||||
--vscode-charts-red: red;
|
||||
--vscode-charts-blue: blue;
|
||||
--vscode-charts-yellow: yellow;
|
||||
--vscode-charts-orange: orange;
|
||||
--vscode-charts-green: green;
|
||||
}
|
||||
20
webview-ui/components.json
Normal file
20
webview-ui/components.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
2605
webview-ui/package-lock.json
generated
2605
webview-ui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,14 +4,22 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src --ext ts,tsx",
|
||||
"test": "jest",
|
||||
"lint": "eslint src --ext ts,tsx --quiet"
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"debounce": "^2.1.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fzf": "^0.5.2",
|
||||
|
|
@ -24,36 +32,43 @@
|
|||
"rehype-highlight": "^7.0.0",
|
||||
"shell-quote": "^1.8.2",
|
||||
"styled-components": "^6.1.13",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vscrui": "^0.2.0",
|
||||
"@tailwindcss/vite": "^4.0.0"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vscrui": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^8.5.2",
|
||||
"@storybook/addon-interactions": "^8.5.2",
|
||||
"@storybook/blocks": "^8.5.2",
|
||||
"@storybook/react": "^8.5.2",
|
||||
"@storybook/react-vite": "^8.5.2",
|
||||
"@storybook/test": "^8.5.2",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/vscode-webview": "^1.57.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-storybook": "^0.11.2",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^27.5.1",
|
||||
"jest-environment-jsdom": "^27.5.1",
|
||||
"jest-simple-dot-reporter": "^1.0.5",
|
||||
"postcss": "^8.5.1",
|
||||
"storybook": "^8.5.2",
|
||||
"ts-jest": "^27.1.5",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^5.4.14"
|
||||
"vite": "6.0.11"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "jsdom",
|
||||
|
|
@ -93,20 +108,5 @@
|
|||
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
|
||||
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all",
|
||||
"last 2 chrome version",
|
||||
"last 2 firefox version",
|
||||
"last 2 safari version"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
0
webview-ui/public/.gitkeep
Normal file
0
webview-ui/public/.gitkeep
Normal file
|
|
@ -1,38 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created using create-react-app" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
--></body>
|
||||
</html>
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
|
@ -89,7 +89,7 @@ export const ChatRowContent = ({
|
|||
}
|
||||
}, [isLast, message.say])
|
||||
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
|
||||
if (message.text != null && message.say === "api_req_started") {
|
||||
if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
|
||||
const info: ClineApiReqInfo = JSON.parse(message.text)
|
||||
return [info.cost, info.cancelReason, info.streamingFailedMessage]
|
||||
}
|
||||
|
|
@ -183,26 +183,26 @@ export const ChatRowContent = ({
|
|||
</div>
|
||||
)
|
||||
return [
|
||||
apiReqCancelReason != null ? (
|
||||
apiReqCancelReason !== null && apiReqCancelReason !== undefined ? (
|
||||
apiReqCancelReason === "user_cancelled" ? (
|
||||
getIconSpan("error", cancelledColor)
|
||||
) : (
|
||||
getIconSpan("error", errorColor)
|
||||
)
|
||||
) : cost != null ? (
|
||||
) : cost !== null && cost !== undefined ? (
|
||||
getIconSpan("check", successColor)
|
||||
) : apiRequestFailedMessage ? (
|
||||
getIconSpan("error", errorColor)
|
||||
) : (
|
||||
<ProgressIndicator />
|
||||
),
|
||||
apiReqCancelReason != null ? (
|
||||
apiReqCancelReason !== null && apiReqCancelReason !== undefined ? (
|
||||
apiReqCancelReason === "user_cancelled" ? (
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Cancelled</span>
|
||||
) : (
|
||||
<span style={{ color: errorColor, fontWeight: "bold" }}>API Streaming Failed</span>
|
||||
)
|
||||
) : cost != null ? (
|
||||
) : cost !== null && cost !== undefined ? (
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
|
||||
) : apiRequestFailedMessage ? (
|
||||
<span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
|
||||
|
|
@ -510,7 +510,8 @@ export const ChatRowContent = ({
|
|||
style={{
|
||||
...headerStyle,
|
||||
marginBottom:
|
||||
(cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage
|
||||
((cost === null || cost === undefined) && apiRequestFailedMessage) ||
|
||||
apiReqStreamingFailedMessage
|
||||
? 10
|
||||
: 0,
|
||||
justifyContent: "space-between",
|
||||
|
|
@ -524,13 +525,15 @@ export const ChatRowContent = ({
|
|||
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
|
||||
{icon}
|
||||
{title}
|
||||
<VSCodeBadge style={{ opacity: cost != null && cost > 0 ? 1 : 0 }}>
|
||||
<VSCodeBadge
|
||||
style={{ opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0 }}>
|
||||
${Number(cost || 0)?.toFixed(4)}
|
||||
</VSCodeBadge>
|
||||
</div>
|
||||
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
|
||||
</div>
|
||||
{((cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && (
|
||||
{(((cost === null || cost === undefined) && apiRequestFailedMessage) ||
|
||||
apiReqStreamingFailedMessage) && (
|
||||
<>
|
||||
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
|
||||
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -275,7 +275,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
return true
|
||||
} else {
|
||||
const lastApiReqStarted = findLast(modifiedMessages, (message) => message.say === "api_req_started")
|
||||
if (lastApiReqStarted && lastApiReqStarted.text != null && lastApiReqStarted.say === "api_req_started") {
|
||||
if (
|
||||
lastApiReqStarted &&
|
||||
lastApiReqStarted.text !== null &&
|
||||
lastApiReqStarted.text !== undefined &&
|
||||
lastApiReqStarted.say === "api_req_started"
|
||||
) {
|
||||
const cost = JSON.parse(lastApiReqStarted.text).cost
|
||||
if (cost === undefined) {
|
||||
// api request has not finished yet
|
||||
|
|
@ -330,6 +335,20 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
[messages.length, clineAsk],
|
||||
)
|
||||
|
||||
const handleSetChatBoxMessage = useCallback(
|
||||
(text: string, images: string[]) => {
|
||||
// Avoid nested template literals by breaking down the logic
|
||||
let newValue = text
|
||||
if (inputValue !== "") {
|
||||
newValue = inputValue + " " + text
|
||||
}
|
||||
|
||||
setInputValue(newValue)
|
||||
setSelectedImages([...selectedImages, ...images])
|
||||
},
|
||||
[inputValue, selectedImages],
|
||||
)
|
||||
|
||||
const startNewTask = useCallback(() => {
|
||||
vscode.postMessage({ type: "clearTask" })
|
||||
}, [])
|
||||
|
|
@ -337,56 +356,96 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
/*
|
||||
This logic depends on the useEffect[messages] above to set clineAsk, after which buttons are shown and we then send an askResponse to the extension.
|
||||
*/
|
||||
const handlePrimaryButtonClick = useCallback(() => {
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "command":
|
||||
case "command_output":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
case "resume_task":
|
||||
case "mistake_limit_reached":
|
||||
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
|
||||
break
|
||||
case "completion_result":
|
||||
case "resume_completed_task":
|
||||
// extension waiting for feedback. but we can just present a new task button
|
||||
startNewTask()
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
disableAutoScrollRef.current = false
|
||||
}, [clineAsk, startNewTask])
|
||||
const handlePrimaryButtonClick = useCallback(
|
||||
(text?: string, images?: string[]) => {
|
||||
const trimmedInput = text?.trim()
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "command":
|
||||
case "command_output":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
case "resume_task":
|
||||
case "mistake_limit_reached":
|
||||
// Only send text/images if they exist
|
||||
if (trimmedInput || (images && images.length > 0)) {
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
text: trimmedInput,
|
||||
images: images,
|
||||
})
|
||||
} else {
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "yesButtonClicked",
|
||||
})
|
||||
}
|
||||
// Clear input state after sending
|
||||
setInputValue("")
|
||||
setSelectedImages([])
|
||||
break
|
||||
case "completion_result":
|
||||
case "resume_completed_task":
|
||||
// extension waiting for feedback. but we can just present a new task button
|
||||
startNewTask()
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
disableAutoScrollRef.current = false
|
||||
},
|
||||
[clineAsk, startNewTask],
|
||||
)
|
||||
|
||||
const handleSecondaryButtonClick = useCallback(() => {
|
||||
if (isStreaming) {
|
||||
vscode.postMessage({ type: "cancelTask" })
|
||||
setDidClickCancel(true)
|
||||
return
|
||||
}
|
||||
const handleSecondaryButtonClick = useCallback(
|
||||
(text?: string, images?: string[]) => {
|
||||
const trimmedInput = text?.trim()
|
||||
if (isStreaming) {
|
||||
vscode.postMessage({ type: "cancelTask" })
|
||||
setDidClickCancel(true)
|
||||
return
|
||||
}
|
||||
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "mistake_limit_reached":
|
||||
case "resume_task":
|
||||
startNewTask()
|
||||
break
|
||||
case "command":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
// responds to the API with a "This operation failed" and lets it try again
|
||||
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
disableAutoScrollRef.current = false
|
||||
}, [clineAsk, startNewTask, isStreaming])
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "mistake_limit_reached":
|
||||
case "resume_task":
|
||||
startNewTask()
|
||||
break
|
||||
case "command":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
// Only send text/images if they exist
|
||||
if (trimmedInput || (images && images.length > 0)) {
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "noButtonClicked",
|
||||
text: trimmedInput,
|
||||
images: images,
|
||||
})
|
||||
} else {
|
||||
// responds to the API with a "This operation failed" and lets it try again
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "noButtonClicked",
|
||||
})
|
||||
}
|
||||
// Clear input state after sending
|
||||
setInputValue("")
|
||||
setSelectedImages([])
|
||||
break
|
||||
}
|
||||
setTextAreaDisabled(true)
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
disableAutoScrollRef.current = false
|
||||
},
|
||||
[clineAsk, startNewTask, isStreaming],
|
||||
)
|
||||
|
||||
const handleTaskCloseButtonClick = useCallback(() => {
|
||||
startNewTask()
|
||||
|
|
@ -429,11 +488,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
case "sendMessage":
|
||||
handleSendMessage(message.text ?? "", message.images ?? [])
|
||||
break
|
||||
case "setChatBoxMessage":
|
||||
handleSetChatBoxMessage(message.text ?? "", message.images ?? [])
|
||||
break
|
||||
case "primaryButtonClick":
|
||||
handlePrimaryButtonClick()
|
||||
handlePrimaryButtonClick(message.text ?? "", message.images ?? [])
|
||||
break
|
||||
case "secondaryButtonClick":
|
||||
handleSecondaryButtonClick()
|
||||
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -444,6 +506,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
textAreaDisabled,
|
||||
enableButtons,
|
||||
handleSendMessage,
|
||||
handleSetChatBoxMessage,
|
||||
handlePrimaryButtonClick,
|
||||
handleSecondaryButtonClick,
|
||||
],
|
||||
|
|
@ -660,9 +723,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
if (message.say === "api_req_started") {
|
||||
// get last api_req_started in currentGroup to check if it's cancelled. If it is then this api req is not part of the current browser session
|
||||
const lastApiReqStarted = [...currentGroup].reverse().find((m) => m.say === "api_req_started")
|
||||
if (lastApiReqStarted?.text != null) {
|
||||
if (lastApiReqStarted?.text !== null && lastApiReqStarted?.text !== undefined) {
|
||||
const info = JSON.parse(lastApiReqStarted.text)
|
||||
const isCancelled = info.cancelReason != null
|
||||
const isCancelled = info.cancelReason !== null && info.cancelReason !== undefined
|
||||
if (isCancelled) {
|
||||
endBrowserSession()
|
||||
result.push(message)
|
||||
|
|
@ -936,7 +999,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
}}>
|
||||
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
|
||||
<div style={{ padding: "0 20px", flexShrink: 0 }}>
|
||||
<h2>What can I do for you?</h2>
|
||||
<h2>What can Roo do for you?</h2>
|
||||
<p>
|
||||
Thanks to the latest breakthroughs in agentic coding capabilities, I can handle complex
|
||||
software development tasks step-by-step. With tools that let me create & edit files, explore
|
||||
|
|
@ -1038,7 +1101,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
flex: secondaryButtonText ? 1 : 2,
|
||||
marginRight: secondaryButtonText ? "6px" : "0",
|
||||
}}
|
||||
onClick={handlePrimaryButtonClick}>
|
||||
onClick={(e) => handlePrimaryButtonClick(inputValue, selectedImages)}>
|
||||
{primaryButtonText}
|
||||
</VSCodeButton>
|
||||
)}
|
||||
|
|
@ -1050,7 +1113,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||
flex: isStreaming ? 2 : 1,
|
||||
marginLeft: isStreaming ? 0 : "6px",
|
||||
}}
|
||||
onClick={handleSecondaryButtonClick}>
|
||||
onClick={(e) => handleSecondaryButtonClick(inputValue, selectedImages)}>
|
||||
{isStreaming ? "Cancel" : secondaryButtonText}
|
||||
</VSCodeButton>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" }}>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
Mode,
|
||||
PromptComponent,
|
||||
getRoleDefinition,
|
||||
getCustomInstructions,
|
||||
getAllModes,
|
||||
ModeConfig,
|
||||
GroupEntry,
|
||||
|
|
@ -26,8 +27,8 @@ import {
|
|||
import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
|
||||
// Get all available groups from GROUP_DISPLAY_NAMES
|
||||
const availableGroups = Object.keys(TOOL_GROUPS) as ToolGroup[]
|
||||
// Get all available groups that should show in prompts view
|
||||
const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable)
|
||||
|
||||
type PromptsViewProps = {
|
||||
onDone: () => void
|
||||
|
|
@ -65,6 +66,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
const [isToolsEditMode, setIsToolsEditMode] = useState(false)
|
||||
const [isCreateModeDialogOpen, setIsCreateModeDialogOpen] = useState(false)
|
||||
const [activeSupportTab, setActiveSupportTab] = useState<SupportPromptType>("ENHANCE")
|
||||
const [selectedModeTab, setSelectedModeTab] = useState<string>(mode)
|
||||
|
||||
// Direct update functions
|
||||
const updateAgentPrompt = useCallback(
|
||||
|
|
@ -110,26 +112,23 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
text: slug,
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Handle mode switching with explicit state initialization
|
||||
// Handle mode tab selection without actually switching modes
|
||||
const handleModeSwitch = useCallback(
|
||||
(modeConfig: ModeConfig) => {
|
||||
if (modeConfig.slug === mode) return // Prevent unnecessary updates
|
||||
if (modeConfig.slug === selectedModeTab) return // Prevent unnecessary updates
|
||||
|
||||
// First switch the mode
|
||||
switchMode(modeConfig.slug)
|
||||
|
||||
// Exit tools edit mode when switching modes
|
||||
// Update selected tab and reset tools edit mode
|
||||
setSelectedModeTab(modeConfig.slug)
|
||||
setIsToolsEditMode(false)
|
||||
},
|
||||
[mode, switchMode, setIsToolsEditMode],
|
||||
[selectedModeTab, setIsToolsEditMode],
|
||||
)
|
||||
|
||||
// Helper function to get current mode's config
|
||||
const getCurrentMode = useCallback((): ModeConfig | undefined => {
|
||||
const findMode = (m: ModeConfig): boolean => m.slug === mode
|
||||
const findMode = (m: ModeConfig): boolean => m.slug === selectedModeTab
|
||||
return customModes?.find(findMode) || modes.find(findMode)
|
||||
}, [mode, customModes, modes])
|
||||
}, [selectedModeTab, customModes, modes])
|
||||
|
||||
// Helper function to safely access mode properties
|
||||
const getModeProperty = <T extends keyof ModeConfig>(
|
||||
|
|
@ -155,6 +154,11 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
}
|
||||
}, [isCreateModeDialogOpen])
|
||||
|
||||
// Keep selected tab in sync with actual mode
|
||||
useEffect(() => {
|
||||
setSelectedModeTab(mode)
|
||||
}, [mode])
|
||||
|
||||
// Helper function to generate a unique slug from a name
|
||||
const generateSlug = useCallback((name: string, attempt = 0): string => {
|
||||
const baseSlug = name
|
||||
|
|
@ -184,22 +188,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
groups: newModeGroups,
|
||||
}
|
||||
updateCustomMode(newModeSlug, newMode)
|
||||
switchMode(newModeSlug)
|
||||
setIsCreateModeDialogOpen(false)
|
||||
setNewModeName("")
|
||||
setNewModeSlug("")
|
||||
setNewModeRoleDefinition("")
|
||||
setNewModeCustomInstructions("")
|
||||
setNewModeGroups(availableGroups)
|
||||
}, [
|
||||
newModeName,
|
||||
newModeSlug,
|
||||
newModeRoleDefinition,
|
||||
newModeCustomInstructions,
|
||||
newModeGroups,
|
||||
updateCustomMode,
|
||||
switchMode,
|
||||
])
|
||||
}, [newModeName, newModeSlug, newModeRoleDefinition, newModeCustomInstructions, newModeGroups, updateCustomMode])
|
||||
|
||||
const isNameOrSlugTaken = useCallback(
|
||||
(name: string, slug: string) => {
|
||||
|
|
@ -278,12 +273,16 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
})
|
||||
}
|
||||
|
||||
const handleAgentReset = (modeSlug: string) => {
|
||||
// Only reset role definition for built-in modes
|
||||
const handleAgentReset = (modeSlug: string, type: "roleDefinition" | "customInstructions") => {
|
||||
// Only reset for built-in modes
|
||||
const existingPrompt = customModePrompts?.[modeSlug] as PromptComponent
|
||||
updateAgentPrompt(modeSlug, {
|
||||
...existingPrompt,
|
||||
roleDefinition: undefined,
|
||||
const updatedPrompt = { ...existingPrompt }
|
||||
delete updatedPrompt[type] // Remove the field entirely to ensure it reloads from defaults
|
||||
|
||||
vscode.postMessage({
|
||||
type: "updatePrompt",
|
||||
promptMode: modeSlug,
|
||||
customPrompt: updatedPrompt,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -472,16 +471,14 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "16px",
|
||||
gap: "8px",
|
||||
alignItems: "center",
|
||||
marginBottom: "12px",
|
||||
overflowX: "auto",
|
||||
flexWrap: "nowrap",
|
||||
paddingBottom: "4px",
|
||||
paddingRight: "20px",
|
||||
flexWrap: "wrap",
|
||||
padding: "4px 0",
|
||||
}}>
|
||||
{modes.map((modeConfig) => {
|
||||
const isActive = mode === modeConfig.slug
|
||||
const isActive = selectedModeTab === modeConfig.slug
|
||||
return (
|
||||
<button
|
||||
key={modeConfig.slug}
|
||||
|
|
@ -509,20 +506,22 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
|
||||
<div style={{ marginBottom: "20px" }}>
|
||||
{/* Only show name and delete for custom modes */}
|
||||
{mode && findModeBySlug(mode, customModes) && (
|
||||
{selectedModeTab && findModeBySlug(selectedModeTab, customModes) && (
|
||||
<div style={{ display: "flex", gap: "12px", marginBottom: "16px" }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Name</div>
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<VSCodeTextField
|
||||
value={getModeProperty(findModeBySlug(mode, customModes), "name") ?? ""}
|
||||
value={
|
||||
getModeProperty(findModeBySlug(selectedModeTab, customModes), "name") ?? ""
|
||||
}
|
||||
onChange={(e: Event | React.FormEvent<HTMLElement>) => {
|
||||
const target =
|
||||
(e as CustomEvent)?.detail?.target ||
|
||||
((e as any).target as HTMLInputElement)
|
||||
const customMode = findModeBySlug(mode, customModes)
|
||||
const customMode = findModeBySlug(selectedModeTab, customModes)
|
||||
if (customMode) {
|
||||
updateCustomMode(mode, {
|
||||
updateCustomMode(selectedModeTab, {
|
||||
...customMode,
|
||||
name: target.value,
|
||||
})
|
||||
|
|
@ -536,7 +535,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "deleteCustomMode",
|
||||
slug: mode,
|
||||
slug: selectedModeTab,
|
||||
})
|
||||
}}>
|
||||
<span className="codicon codicon-trash"></span>
|
||||
|
|
@ -554,13 +553,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
marginBottom: "4px",
|
||||
}}>
|
||||
<div style={{ fontWeight: "bold" }}>Role Definition</div>
|
||||
{!findModeBySlug(mode, customModes) && (
|
||||
{!findModeBySlug(selectedModeTab, customModes) && (
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => {
|
||||
const currentMode = getCurrentMode()
|
||||
if (currentMode?.slug) {
|
||||
handleAgentReset(currentMode.slug)
|
||||
handleAgentReset(currentMode.slug, "roleDefinition")
|
||||
}
|
||||
}}
|
||||
title="Reset to default"
|
||||
|
|
@ -580,24 +579,28 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
</div>
|
||||
<VSCodeTextArea
|
||||
value={(() => {
|
||||
const customMode = findModeBySlug(mode, customModes)
|
||||
const prompt = customModePrompts?.[mode] as PromptComponent
|
||||
return customMode?.roleDefinition ?? prompt?.roleDefinition ?? getRoleDefinition(mode)
|
||||
const customMode = findModeBySlug(selectedModeTab, customModes)
|
||||
const prompt = customModePrompts?.[selectedModeTab] as PromptComponent
|
||||
return (
|
||||
customMode?.roleDefinition ??
|
||||
prompt?.roleDefinition ??
|
||||
getRoleDefinition(selectedModeTab)
|
||||
)
|
||||
})()}
|
||||
onChange={(e) => {
|
||||
const value =
|
||||
(e as CustomEvent)?.detail?.target?.value ||
|
||||
((e as any).target as HTMLTextAreaElement).value
|
||||
const customMode = findModeBySlug(mode, customModes)
|
||||
const customMode = findModeBySlug(selectedModeTab, customModes)
|
||||
if (customMode) {
|
||||
// For custom modes, update the JSON file
|
||||
updateCustomMode(mode, {
|
||||
updateCustomMode(selectedModeTab, {
|
||||
...customMode,
|
||||
roleDefinition: value.trim() || "",
|
||||
})
|
||||
} else {
|
||||
// For built-in modes, update the prompts
|
||||
updateAgentPrompt(mode, {
|
||||
updateAgentPrompt(selectedModeTab, {
|
||||
roleDefinition: value.trim() || undefined,
|
||||
})
|
||||
}
|
||||
|
|
@ -751,7 +754,29 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
|
||||
{/* Role definition for both built-in and custom modes */}
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Mode-specific Custom Instructions</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "4px",
|
||||
}}>
|
||||
<div style={{ fontWeight: "bold" }}>Mode-specific Custom Instructions</div>
|
||||
{!findModeBySlug(selectedModeTab, customModes) && (
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => {
|
||||
const currentMode = getCurrentMode()
|
||||
if (currentMode?.slug) {
|
||||
handleAgentReset(currentMode.slug, "customInstructions")
|
||||
}
|
||||
}}
|
||||
title="Reset to default"
|
||||
data-testid="custom-instructions-reset">
|
||||
<span className="codicon codicon-discard"></span>
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
|
|
@ -762,25 +787,29 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
</div>
|
||||
<VSCodeTextArea
|
||||
value={(() => {
|
||||
const customMode = findModeBySlug(mode, customModes)
|
||||
const prompt = customModePrompts?.[mode] as PromptComponent
|
||||
return customMode?.customInstructions ?? prompt?.customInstructions ?? ""
|
||||
const customMode = findModeBySlug(selectedModeTab, customModes)
|
||||
const prompt = customModePrompts?.[selectedModeTab] as PromptComponent
|
||||
return (
|
||||
customMode?.customInstructions ??
|
||||
prompt?.customInstructions ??
|
||||
getCustomInstructions(selectedModeTab, customModes)
|
||||
)
|
||||
})()}
|
||||
onChange={(e) => {
|
||||
const value =
|
||||
(e as CustomEvent)?.detail?.target?.value ||
|
||||
((e as any).target as HTMLTextAreaElement).value
|
||||
const customMode = findModeBySlug(mode, customModes)
|
||||
const customMode = findModeBySlug(selectedModeTab, customModes)
|
||||
if (customMode) {
|
||||
// For custom modes, update the JSON file
|
||||
updateCustomMode(mode, {
|
||||
updateCustomMode(selectedModeTab, {
|
||||
...customMode,
|
||||
customInstructions: value.trim() || undefined,
|
||||
})
|
||||
} else {
|
||||
// For built-in modes, update the prompts
|
||||
const existingPrompt = customModePrompts?.[mode] as PromptComponent
|
||||
updateAgentPrompt(mode, {
|
||||
const existingPrompt = customModePrompts?.[selectedModeTab] as PromptComponent
|
||||
updateAgentPrompt(selectedModeTab, {
|
||||
...existingPrompt,
|
||||
customInstructions: value.trim() || undefined,
|
||||
})
|
||||
|
|
@ -859,13 +888,11 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
|
|||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "16px",
|
||||
gap: "8px",
|
||||
alignItems: "center",
|
||||
marginBottom: "12px",
|
||||
overflowX: "auto",
|
||||
flexWrap: "nowrap",
|
||||
paddingBottom: "4px",
|
||||
paddingRight: "20px",
|
||||
flexWrap: "wrap",
|
||||
padding: "4px 0",
|
||||
}}>
|
||||
{Object.keys(supportPrompt.default).map((type) => (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -132,7 +132,10 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
|||
id="api-provider"
|
||||
value={selectedProvider}
|
||||
onChange={(value: unknown) => {
|
||||
handleInputChange("apiProvider")({
|
||||
handleInputChange(
|
||||
"apiProvider",
|
||||
true,
|
||||
)({
|
||||
target: {
|
||||
value: (value as DropdownOption).value,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
47
webview-ui/src/components/ui/button.tsx
Normal file
47
webview-ui/src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline: "border border-input bg-foreground shadow-sm hover:bg-foreground/80",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
},
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
177
webview-ui/src/components/ui/dropdown-menu.tsx
Normal file
177
webview-ui/src/components/ui/dropdown-menu.tsx
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<DotFilledIcon className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
2
webview-ui/src/components/ui/index.ts
Normal file
2
webview-ui/src/components/ui/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./button"
|
||||
export * from "./dropdown-menu"
|
||||
|
|
@ -10,7 +10,7 @@ const WelcomeView = () => {
|
|||
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
|
||||
const disableLetsGoButton = apiErrorMessage != null
|
||||
const disableLetsGoButton = apiErrorMessage !== null && apiErrorMessage !== undefined
|
||||
|
||||
const handleSubmit = () => {
|
||||
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
openAiModels: string[]
|
||||
mcpServers: McpServer[]
|
||||
filePaths: string[]
|
||||
openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
|
||||
setApiConfiguration: (config: ApiConfiguration) => void
|
||||
setCustomInstructions: (value?: string) => void
|
||||
setAlwaysAllowReadOnly: (value: boolean) => void
|
||||
|
|
@ -54,10 +55,14 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setTerminalOutputLineLimit: (value: number) => void
|
||||
mcpEnabled: boolean
|
||||
setMcpEnabled: (value: boolean) => void
|
||||
enableMcpServerCreation: boolean
|
||||
setEnableMcpServerCreation: (value: boolean) => void
|
||||
alwaysApproveResubmit?: boolean
|
||||
setAlwaysApproveResubmit: (value: boolean) => void
|
||||
requestDelaySeconds: number
|
||||
setRequestDelaySeconds: (value: number) => void
|
||||
rateLimitSeconds: number
|
||||
setRateLimitSeconds: (value: number) => void
|
||||
setCurrentApiConfigName: (value: string) => void
|
||||
setListApiConfigMeta: (value: ApiConfigMeta[]) => void
|
||||
onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
|
||||
|
|
@ -69,7 +74,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setEnhancementApiConfigId: (value: string) => void
|
||||
setExperimentEnabled: (id: ExperimentId, enabled: boolean) => void
|
||||
setAutoApprovalEnabled: (value: boolean) => void
|
||||
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
|
||||
handleInputChange: (field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => void
|
||||
customModes: ModeConfig[]
|
||||
setCustomModes: (value: ModeConfig[]) => void
|
||||
}
|
||||
|
|
@ -93,8 +98,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
screenshotQuality: 75,
|
||||
terminalOutputLineLimit: 500,
|
||||
mcpEnabled: true,
|
||||
enableMcpServerCreation: true,
|
||||
alwaysApproveResubmit: false,
|
||||
requestDelaySeconds: 5,
|
||||
rateLimitSeconds: 0, // Minimum time between successive requests (0 = disabled)
|
||||
currentApiConfigName: "default",
|
||||
listApiConfigMeta: [],
|
||||
mode: defaultModeSlug,
|
||||
|
|
@ -113,6 +120,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
const [glamaModels, setGlamaModels] = useState<Record<string, ModelInfo>>({
|
||||
[glamaDefaultModelId]: glamaDefaultModelInfo,
|
||||
})
|
||||
const [openedTabs, setOpenedTabs] = useState<Array<{ label: string; isActive: boolean; path?: string }>>([])
|
||||
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
|
||||
})
|
||||
|
|
@ -140,14 +148,29 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
}, [])
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(field: keyof ApiConfiguration) => (event: any) => {
|
||||
// Returns a function that handles an input change event for a specific API configuration field.
|
||||
// The optional "softUpdate" flag determines whether to immediately update local state or send an external update.
|
||||
(field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => {
|
||||
// Use the functional form of setState to ensure the latest state is used in the update logic.
|
||||
setState((currentState) => {
|
||||
vscode.postMessage({
|
||||
type: "upsertApiConfiguration",
|
||||
text: currentState.currentApiConfigName,
|
||||
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
|
||||
})
|
||||
return currentState // No state update needed
|
||||
if (softUpdate) {
|
||||
// Return a new state object with the updated apiConfiguration.
|
||||
// This will trigger a re-render with the new configuration value.
|
||||
return {
|
||||
...currentState,
|
||||
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
|
||||
}
|
||||
} else {
|
||||
// For non-soft updates, send a message to the VS Code extension with the updated config.
|
||||
// This side effect communicates the change without updating local React state.
|
||||
vscode.postMessage({
|
||||
type: "upsertApiConfiguration",
|
||||
text: currentState.currentApiConfigName,
|
||||
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
|
||||
})
|
||||
// Return the unchanged state as no local state update is intended in this branch.
|
||||
return currentState
|
||||
}
|
||||
})
|
||||
},
|
||||
[],
|
||||
|
|
@ -176,7 +199,11 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
break
|
||||
}
|
||||
case "workspaceUpdated": {
|
||||
setFilePaths(message.filePaths ?? [])
|
||||
const paths = message.filePaths ?? []
|
||||
const tabs = message.openedTabs ?? []
|
||||
|
||||
setFilePaths(paths)
|
||||
setOpenedTabs(tabs)
|
||||
break
|
||||
}
|
||||
case "partialMessage": {
|
||||
|
|
@ -249,6 +276,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
unboundModels,
|
||||
mcpServers,
|
||||
filePaths,
|
||||
openedTabs,
|
||||
soundVolume: state.soundVolume,
|
||||
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
|
||||
writeDelayMs: state.writeDelayMs,
|
||||
|
|
@ -281,8 +309,11 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setTerminalOutputLineLimit: (value) =>
|
||||
setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
|
||||
setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
|
||||
setEnableMcpServerCreation: (value) =>
|
||||
setState((prevState) => ({ ...prevState, enableMcpServerCreation: value })),
|
||||
setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
|
||||
setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
|
||||
setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })),
|
||||
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
|
||||
setListApiConfigMeta,
|
||||
onUpdateApiConfig,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue