mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Merge branch 'RooVetGit:main' into fix-context-calculation
This commit is contained in:
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!)
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Include keywords per new branding to Roo Code
|
||||
1
.env.integration.example
Normal file
1
.env.integration.example
Normal file
@@ -0,0 +1 @@
|
||||
OPENROUTER_API_KEY=sk-or-v1-...
|
||||
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
|
||||
|
||||
47
.github/workflows/code-qa.yml
vendored
47
.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
|
||||
|
||||
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
|
||||
|
||||
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"
|
||||
]
|
||||
}
|
||||
|
||||
29
CHANGELOG.md
29
CHANGELOG.md
@@ -1,5 +1,34 @@
|
||||
# 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
|
||||
- Fix a bug that was preventing the use of qwen-max and potentially other OpenAI-compatible providers (thanks @Szpadel!)
|
||||
- Add support for perplexity/sonar-reasoning (thanks @Szpadel!)
|
||||
- Visual fixes to dropdowns (thanks @psv2522!)
|
||||
- Add the [Unbound](https://getunbound.ai/) provider (thanks @vigneshsubbiah16!)
|
||||
|
||||
## [3.3.5]
|
||||
|
||||
- Make information about the conversation's context window usage visible in the task header for humans and in the environment for models (thanks @MuriloFP!)
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
}
|
||||
180
package-lock.json
generated
180
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "roo-cline",
|
||||
"version": "3.3.5",
|
||||
"version": "3.3.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "roo-cline",
|
||||
"version": "3.3.5",
|
||||
"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",
|
||||
|
||||
24
package.json
24
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.5",
|
||||
"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 && 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"),
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -289,6 +289,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,16 +72,19 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
|
||||
maxTokens = 8_192
|
||||
}
|
||||
|
||||
const { data: completion, response } = await this.client.chat.completions
|
||||
.create(
|
||||
{
|
||||
const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = {
|
||||
model: this.getModel().id,
|
||||
max_tokens: maxTokens,
|
||||
temperature: 0,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
},
|
||||
{
|
||||
}
|
||||
|
||||
if (this.supportsTemperature()) {
|
||||
requestOptions.temperature = 0
|
||||
}
|
||||
|
||||
const { data: completion, response } = await this.client.chat.completions
|
||||
.create(requestOptions, {
|
||||
headers: {
|
||||
"X-Glama-Metadata": JSON.stringify({
|
||||
labels: [
|
||||
@@ -92,8 +95,7 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
.withResponse()
|
||||
|
||||
const completionRequestId = response.headers.get("x-completion-request-id")
|
||||
@@ -148,6 +150,10 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private supportsTemperature(): boolean {
|
||||
return !this.getModel().id.startsWith("openai/o3-mini")
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.glamaModelId
|
||||
const modelInfo = this.options.glamaModelInfo
|
||||
@@ -164,7 +170,10 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
|
||||
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
|
||||
model: this.getModel().id,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
temperature: 0,
|
||||
}
|
||||
|
||||
if (this.supportsTemperature()) {
|
||||
requestOptions.temperature = 0
|
||||
}
|
||||
|
||||
if (this.getModel().id.startsWith("anthropic/")) {
|
||||
|
||||
@@ -48,6 +48,37 @@ export class OpenAiNativeHandler implements ApiHandler, SingleCompletionHandler
|
||||
}
|
||||
break
|
||||
}
|
||||
case "o3-mini":
|
||||
case "o3-mini-low":
|
||||
case "o3-mini-high": {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: "o3-mini",
|
||||
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
reasoning_effort: this.getModel().info.reasoningEffort,
|
||||
})
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: this.getModel().id,
|
||||
@@ -104,6 +135,16 @@ export class OpenAiNativeHandler implements ApiHandler, SingleCompletionHandler
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
}
|
||||
break
|
||||
case "o3-mini":
|
||||
case "o3-mini-low":
|
||||
case "o3-mini-high":
|
||||
// o3 doesn't support non-1 temp
|
||||
requestOptions = {
|
||||
model: "o3-mini",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
reasoning_effort: this.getModel().info.reasoningEffort,
|
||||
}
|
||||
break
|
||||
default:
|
||||
requestOptions = {
|
||||
model: modelId,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -96,6 +96,7 @@ export class Cline {
|
||||
didFinishAborting = false
|
||||
abandoned = false
|
||||
private diffViewProvider: DiffViewProvider
|
||||
private lastApiRequestTime?: number
|
||||
|
||||
// streaming
|
||||
private currentStreamingContentIndex = 0
|
||||
@@ -793,12 +794,43 @@ export class Cline {
|
||||
}
|
||||
}
|
||||
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
async *attemptApiRequest(previousApiReqIndex: number, retryAttempt: number = 0): ApiStream {
|
||||
let mcpHub: McpHub | undefined
|
||||
|
||||
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } =
|
||||
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds, rateLimitSeconds } =
|
||||
(await this.providerRef.deref()?.getState()) ?? {}
|
||||
|
||||
let finalDelay = 0
|
||||
|
||||
// Only apply rate limiting if this isn't the first request
|
||||
if (this.lastApiRequestTime) {
|
||||
const now = Date.now()
|
||||
const timeSinceLastRequest = now - this.lastApiRequestTime
|
||||
const rateLimit = rateLimitSeconds || 0
|
||||
const rateLimitDelay = Math.max(0, rateLimit * 1000 - timeSinceLastRequest)
|
||||
finalDelay = rateLimitDelay
|
||||
}
|
||||
|
||||
// Add exponential backoff delay for retries
|
||||
if (retryAttempt > 0) {
|
||||
const baseDelay = requestDelaySeconds || 5
|
||||
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt)) * 1000
|
||||
finalDelay = Math.max(finalDelay, exponentialDelay)
|
||||
}
|
||||
|
||||
if (finalDelay > 0) {
|
||||
// Show countdown timer
|
||||
for (let i = Math.ceil(finalDelay / 1000); i > 0; i--) {
|
||||
const delayMessage =
|
||||
retryAttempt > 0 ? `Retrying in ${i} seconds...` : `Rate limiting for ${i} seconds...`
|
||||
await this.say("api_req_retry_delayed", delayMessage, undefined, true)
|
||||
await delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
// Update last request time before making the request
|
||||
this.lastApiRequestTime = Date.now()
|
||||
|
||||
if (mcpEnabled ?? true) {
|
||||
mcpHub = this.providerRef.deref()?.mcpHub
|
||||
if (!mcpHub) {
|
||||
@@ -810,8 +842,14 @@ export class Cline {
|
||||
})
|
||||
}
|
||||
|
||||
const { browserViewportSize, mode, customModePrompts, preferredLanguage, experiments } =
|
||||
(await this.providerRef.deref()?.getState()) ?? {}
|
||||
const {
|
||||
browserViewportSize,
|
||||
mode,
|
||||
customModePrompts,
|
||||
preferredLanguage,
|
||||
experiments,
|
||||
enableMcpServerCreation,
|
||||
} = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
const { customModes } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
const systemPrompt = await (async () => {
|
||||
const provider = this.providerRef.deref()
|
||||
@@ -832,6 +870,7 @@ export class Cline {
|
||||
preferredLanguage,
|
||||
this.diffEnabled,
|
||||
experiments,
|
||||
enableMcpServerCreation,
|
||||
)
|
||||
})()
|
||||
|
||||
@@ -887,21 +926,29 @@ export class Cline {
|
||||
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
|
||||
if (alwaysApproveResubmit) {
|
||||
const errorMsg = error.message ?? "Unknown error"
|
||||
const requestDelay = requestDelaySeconds || 5
|
||||
// Automatically retry with delay
|
||||
// Show countdown timer in error color
|
||||
for (let i = requestDelay; i > 0; i--) {
|
||||
const baseDelay = requestDelaySeconds || 5
|
||||
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt))
|
||||
|
||||
// Show countdown timer with exponential backoff
|
||||
for (let i = exponentialDelay; i > 0; i--) {
|
||||
await this.say(
|
||||
"api_req_retry_delayed",
|
||||
`${errorMsg}\n\nRetrying in ${i} seconds...`,
|
||||
`${errorMsg}\n\nRetry attempt ${retryAttempt + 1}\nRetrying in ${i} seconds...`,
|
||||
undefined,
|
||||
true,
|
||||
)
|
||||
await delay(1000)
|
||||
}
|
||||
await this.say("api_req_retry_delayed", `${errorMsg}\n\nRetrying now...`, undefined, false)
|
||||
// delegate generator output from the recursive call
|
||||
yield* this.attemptApiRequest(previousApiReqIndex)
|
||||
|
||||
await this.say(
|
||||
"api_req_retry_delayed",
|
||||
`${errorMsg}\n\nRetry attempt ${retryAttempt + 1}\nRetrying now...`,
|
||||
undefined,
|
||||
false,
|
||||
)
|
||||
|
||||
// delegate generator output from the recursive call with incremented retry count
|
||||
yield* this.attemptApiRequest(previousApiReqIndex, retryAttempt + 1)
|
||||
return
|
||||
} else {
|
||||
const { response } = await this.ask(
|
||||
@@ -1085,34 +1132,22 @@ 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
|
||||
// ),
|
||||
// })
|
||||
} else {
|
||||
pushToolResult(formatResponse.toolDenied())
|
||||
}
|
||||
this.didRejectTool = true
|
||||
return false
|
||||
}
|
||||
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)
|
||||
|
||||
141
src/core/EditorUtils.ts
Normal file
141
src/core/EditorUtils.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
|
||||
export interface EffectiveRange {
|
||||
range: vscode.Range
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface DiagnosticData {
|
||||
message: string
|
||||
severity: vscode.DiagnosticSeverity
|
||||
code?: string | number | { value: string | number; target: vscode.Uri }
|
||||
source?: string
|
||||
range: vscode.Range
|
||||
}
|
||||
|
||||
export interface EditorContext {
|
||||
filePath: string
|
||||
selectedText: string
|
||||
diagnostics?: DiagnosticData[]
|
||||
}
|
||||
|
||||
export class EditorUtils {
|
||||
// Cache file paths for performance
|
||||
private static readonly filePathCache = new WeakMap<vscode.TextDocument, string>()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
static 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
|
||||
}
|
||||
}
|
||||
|
||||
static createDiagnosticData(diagnostic: vscode.Diagnostic): DiagnosticData {
|
||||
return {
|
||||
message: diagnostic.message,
|
||||
severity: diagnostic.severity,
|
||||
code: diagnostic.code,
|
||||
source: diagnostic.source,
|
||||
range: diagnostic.range,
|
||||
}
|
||||
}
|
||||
|
||||
static hasIntersectingRange(range1: vscode.Range, range2: vscode.Range): boolean {
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
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: {
|
||||
@@ -730,25 +731,19 @@ describe("Cline", () => {
|
||||
const iterator = cline.attemptApiRequest(0)
|
||||
await iterator.next()
|
||||
|
||||
// Calculate expected delay for first retry
|
||||
const baseDelay = 3 // from requestDelaySeconds
|
||||
|
||||
// Verify countdown messages
|
||||
for (let i = baseDelay; i > 0; i--) {
|
||||
expect(saySpy).toHaveBeenCalledWith(
|
||||
"api_req_retry_delayed",
|
||||
expect.stringContaining("Retrying in 3 seconds"),
|
||||
undefined,
|
||||
true,
|
||||
)
|
||||
expect(saySpy).toHaveBeenCalledWith(
|
||||
"api_req_retry_delayed",
|
||||
expect.stringContaining("Retrying in 2 seconds"),
|
||||
undefined,
|
||||
true,
|
||||
)
|
||||
expect(saySpy).toHaveBeenCalledWith(
|
||||
"api_req_retry_delayed",
|
||||
expect.stringContaining("Retrying in 1 seconds"),
|
||||
expect.stringContaining(`Retrying in ${i} seconds`),
|
||||
undefined,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
expect(saySpy).toHaveBeenCalledWith(
|
||||
"api_req_retry_delayed",
|
||||
expect.stringContaining("Retrying now"),
|
||||
@@ -756,13 +751,18 @@ describe("Cline", () => {
|
||||
false,
|
||||
)
|
||||
|
||||
// Verify delay was called correctly
|
||||
expect(mockDelay).toHaveBeenCalledTimes(3)
|
||||
// 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
|
||||
const errorMessage = saySpy.mock.calls.find((call) => call[1]?.includes(mockError.message))?.[1]
|
||||
expect(errorMessage).toBe(`${mockError.message}\n\nRetrying in 3 seconds...`)
|
||||
expect(errorMessage).toBe(
|
||||
`${mockError.message}\n\nRetry attempt 1\nRetrying in ${baseDelay} seconds...`,
|
||||
)
|
||||
})
|
||||
|
||||
describe("loadContext", () => {
|
||||
|
||||
@@ -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({
|
||||
// Setup default EditorUtils mocks
|
||||
;(EditorUtils.getEffectiveRange as jest.Mock).mockReturnValue({
|
||||
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")
|
||||
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()
|
||||
})
|
||||
|
||||
75
src/core/__tests__/EditorUtils.test.ts
Normal file
75
src/core/__tests__/EditorUtils.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import * as vscode from "vscode"
|
||||
import { EditorUtils } from "../EditorUtils"
|
||||
|
||||
// Mock VSCode API
|
||||
jest.mock("vscode", () => ({
|
||||
Range: jest.fn().mockImplementation((startLine, startChar, endLine, endChar) => ({
|
||||
start: { line: startLine, character: startChar },
|
||||
end: { line: endLine, character: endChar },
|
||||
})),
|
||||
Position: jest.fn().mockImplementation((line, character) => ({
|
||||
line,
|
||||
character,
|
||||
})),
|
||||
workspace: {
|
||||
getWorkspaceFolder: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
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(0, 0, 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(0, 0, 0, 10)
|
||||
mockDocument.getText.mockReturnValue("")
|
||||
mockDocument.lineAt.mockReturnValue({ text: "", lineNumber: 0 })
|
||||
|
||||
const result = EditorUtils.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 = 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")
|
||||
})
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -174,6 +174,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -194,6 +195,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -216,6 +218,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -236,6 +239,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -256,6 +260,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -276,6 +281,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // preferredLanguage
|
||||
true, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toContain("apply_diff")
|
||||
@@ -297,6 +303,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // preferredLanguage
|
||||
false, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).not.toContain("apply_diff")
|
||||
@@ -318,6 +325,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).not.toContain("apply_diff")
|
||||
@@ -339,6 +347,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
"Spanish", // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toContain("Language Preference:")
|
||||
@@ -371,6 +380,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Role definition should be at the top
|
||||
@@ -406,6 +416,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Role definition from promptComponent should be at the top
|
||||
@@ -436,6 +447,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Should use the default mode's role definition
|
||||
@@ -458,6 +470,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments, // experiments - undefined should disable all experimental tools
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Verify experimental tools are not included in the prompt
|
||||
@@ -485,6 +498,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Verify experimental tools are included in the prompt when enabled
|
||||
@@ -512,6 +526,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Verify only enabled experimental tools are included
|
||||
@@ -539,6 +554,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined,
|
||||
true, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Verify base instruction lists all available tools
|
||||
@@ -568,6 +584,7 @@ describe("SYSTEM_PROMPT", () => {
|
||||
undefined,
|
||||
true,
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
// Verify detailed instructions for each tool
|
||||
@@ -623,6 +640,7 @@ describe("addCustomInstructions", () => {
|
||||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
@@ -643,11 +661,60 @@ describe("addCustomInstructions", () => {
|
||||
undefined,
|
||||
undefined,
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should include MCP server creation info when enabled", async () => {
|
||||
const mockMcpHub = createMockMcpHub()
|
||||
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false, // supportsComputerUse
|
||||
mockMcpHub, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes,
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
true, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).toContain("Creating an MCP Server")
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should exclude MCP server creation info when disabled", async () => {
|
||||
const mockMcpHub = createMockMcpHub()
|
||||
|
||||
const prompt = await SYSTEM_PROMPT(
|
||||
mockContext,
|
||||
"/test/path",
|
||||
false, // supportsComputerUse
|
||||
mockMcpHub, // mcpHub
|
||||
undefined, // diffStrategy
|
||||
undefined, // browserViewportSize
|
||||
defaultModeSlug, // mode
|
||||
undefined, // customModePrompts
|
||||
undefined, // customModes,
|
||||
undefined, // globalCustomInstructions
|
||||
undefined, // preferredLanguage
|
||||
undefined, // diffEnabled
|
||||
experiments,
|
||||
false, // enableMcpServerCreation
|
||||
)
|
||||
|
||||
expect(prompt).not.toContain("Creating an MCP Server")
|
||||
expect(prompt).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should prioritize mode-specific rules for code mode", async () => {
|
||||
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
|
||||
expect(instructions).toMatchSnapshot()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,15 +19,7 @@ import { findLast } from "../../shared/array"
|
||||
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
import { WebviewMessage } from "../../shared/WebviewMessage"
|
||||
import {
|
||||
Mode,
|
||||
modes,
|
||||
CustomModePrompts,
|
||||
PromptComponent,
|
||||
ModeConfig,
|
||||
defaultModeSlug,
|
||||
getModeBySlug,
|
||||
} from "../../shared/modes"
|
||||
import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug } from "../../shared/modes"
|
||||
import { SYSTEM_PROMPT } from "../prompts/system"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { Cline } from "../Cline"
|
||||
@@ -37,7 +29,7 @@ import { getUri } from "./getUri"
|
||||
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
|
||||
import { checkExistKey } from "../../shared/checkExistApiConfig"
|
||||
import { singleCompletionHandler } from "../../utils/single-completion-handler"
|
||||
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
|
||||
import { searchCommits } from "../../utils/git"
|
||||
import { ConfigManager } from "../config/ConfigManager"
|
||||
import { CustomModesManager } from "../config/CustomModesManager"
|
||||
import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
|
||||
@@ -110,8 +102,10 @@ type GlobalStateKey =
|
||||
| "writeDelayMs"
|
||||
| "terminalOutputLineLimit"
|
||||
| "mcpEnabled"
|
||||
| "enableMcpServerCreation"
|
||||
| "alwaysApproveResubmit"
|
||||
| "requestDelaySeconds"
|
||||
| "rateLimitSeconds"
|
||||
| "currentApiConfigName"
|
||||
| "listApiConfigMeta"
|
||||
| "vsCodeLmModelSelector"
|
||||
@@ -139,6 +133,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
private static activeInstances: Set<ClineProvider> = new Set()
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private view?: vscode.WebviewView | vscode.WebviewPanel
|
||||
private isViewLaunched = false
|
||||
private cline?: Cline
|
||||
private workspaceTracker?: WorkspaceTracker
|
||||
mcpHub?: McpHub
|
||||
@@ -238,6 +233,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",
|
||||
@@ -269,7 +274,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
|
||||
? 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
|
||||
@@ -393,6 +402,62 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.view?.webview.postMessage(message)
|
||||
}
|
||||
|
||||
private getHMRHtmlContent(webview: vscode.Webview): string {
|
||||
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 localPort = "5173"
|
||||
const localServerUrl = `localhost:${localPort}`
|
||||
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.
|
||||
*
|
||||
@@ -409,15 +474,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
// then convert it to a uri we can use in the webview.
|
||||
|
||||
// The CSS file from the React build output
|
||||
const stylesUri = getUri(webview, this.context.extensionUri, [
|
||||
"webview-ui",
|
||||
"build",
|
||||
"static",
|
||||
"css",
|
||||
"main.css",
|
||||
])
|
||||
const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
|
||||
// The JS file from the React build output
|
||||
const scriptUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "static", "js", "main.js"])
|
||||
const scriptUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.js"])
|
||||
|
||||
// The codicon font from the React build output
|
||||
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
|
||||
@@ -554,7 +613,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
let currentConfigName = (await this.getGlobalState("currentApiConfigName")) as string
|
||||
const currentConfigName = (await this.getGlobalState("currentApiConfigName")) as string
|
||||
|
||||
if (currentConfigName) {
|
||||
if (!(await this.configManager.hasConfig(currentConfigName))) {
|
||||
@@ -587,6 +646,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
),
|
||||
)
|
||||
|
||||
this.isViewLaunched = true
|
||||
break
|
||||
case "newTask":
|
||||
// Code that should run in response to the hello message command
|
||||
@@ -782,6 +842,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.updateGlobalState("mcpEnabled", mcpEnabled)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "enableMcpServerCreation":
|
||||
await this.updateGlobalState("enableMcpServerCreation", message.bool ?? true)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "playSound":
|
||||
if (message.audioType) {
|
||||
const soundPath = path.join(this.context.extensionPath, "audio", `${message.audioType}.wav`)
|
||||
@@ -822,6 +886,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.updateGlobalState("requestDelaySeconds", message.value ?? 5)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "rateLimitSeconds":
|
||||
await this.updateGlobalState("rateLimitSeconds", message.value ?? 0)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "preferredLanguage":
|
||||
await this.updateGlobalState("preferredLanguage", message.text)
|
||||
await this.postStateToWebview()
|
||||
@@ -1066,6 +1134,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
mcpEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
experiments,
|
||||
enableMcpServerCreation,
|
||||
} = await this.getState()
|
||||
|
||||
// Create diffStrategy based on current model and settings
|
||||
@@ -1094,6 +1163,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
preferredLanguage,
|
||||
diffEnabled,
|
||||
experiments,
|
||||
enableMcpServerCreation,
|
||||
)
|
||||
|
||||
await this.postMessageToWebview({
|
||||
@@ -1130,7 +1200,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
if (message.text && message.apiConfiguration) {
|
||||
try {
|
||||
await this.configManager.saveConfig(message.text, message.apiConfiguration)
|
||||
let listApiConfig = await this.configManager.listConfig()
|
||||
const listApiConfig = await this.configManager.listConfig()
|
||||
|
||||
await Promise.all([
|
||||
this.updateGlobalState("listApiConfigMeta", listApiConfig),
|
||||
@@ -1152,10 +1222,14 @@ 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)
|
||||
|
||||
let listApiConfig = await this.configManager.listConfig()
|
||||
const listApiConfig = await this.configManager.listConfig()
|
||||
const config = listApiConfig?.find((c) => c.name === newName)
|
||||
|
||||
// Update listApiConfigMeta first to ensure UI has latest data
|
||||
@@ -1213,7 +1287,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
|
||||
|
||||
// If this was the current config, switch to first available
|
||||
let currentApiConfigName = await this.getGlobalState("currentApiConfigName")
|
||||
const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
|
||||
if (message.text === currentApiConfigName && listApiConfig?.[0]?.name) {
|
||||
const apiConfig = await this.configManager.loadConfig(listApiConfig[0].name)
|
||||
await Promise.all([
|
||||
@@ -1233,7 +1307,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
break
|
||||
case "getListApiConfiguration":
|
||||
try {
|
||||
let listApiConfig = await this.configManager.listConfig()
|
||||
const listApiConfig = await this.configManager.listConfig()
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
|
||||
this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
|
||||
} catch (error) {
|
||||
@@ -1273,7 +1347,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.outputChannel.appendLine(
|
||||
`Failed to update timeout for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
|
||||
)
|
||||
vscode.window.showErrorMessage(`Failed to update server timeout`)
|
||||
vscode.window.showErrorMessage("Failed to update server timeout")
|
||||
}
|
||||
}
|
||||
break
|
||||
@@ -1626,7 +1700,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
async refreshGlamaModels() {
|
||||
const glamaModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.glamaModels)
|
||||
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
try {
|
||||
const response = await axios.get("https://glama.ai/api/gateway/v1/models")
|
||||
/*
|
||||
@@ -1716,7 +1790,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
GlobalFileNames.openRouterModels,
|
||||
)
|
||||
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
try {
|
||||
const response = await axios.get("https://openrouter.ai/api/v1/models")
|
||||
/*
|
||||
@@ -1931,8 +2005,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
terminalOutputLineLimit,
|
||||
fuzzyMatchThreshold,
|
||||
mcpEnabled,
|
||||
enableMcpServerCreation,
|
||||
alwaysApproveResubmit,
|
||||
requestDelaySeconds,
|
||||
rateLimitSeconds,
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
mode,
|
||||
@@ -1972,8 +2048,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
|
||||
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
|
||||
mcpEnabled: mcpEnabled ?? true,
|
||||
enableMcpServerCreation: enableMcpServerCreation ?? true,
|
||||
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
|
||||
requestDelaySeconds: requestDelaySeconds ?? 10,
|
||||
rateLimitSeconds: rateLimitSeconds ?? 0,
|
||||
currentApiConfigName: currentApiConfigName ?? "default",
|
||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||
mode: mode ?? defaultModeSlug,
|
||||
@@ -2095,8 +2173,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
screenshotQuality,
|
||||
terminalOutputLineLimit,
|
||||
mcpEnabled,
|
||||
enableMcpServerCreation,
|
||||
alwaysApproveResubmit,
|
||||
requestDelaySeconds,
|
||||
rateLimitSeconds,
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
vsCodeLmModelSelector,
|
||||
@@ -2167,8 +2247,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.getGlobalState("screenshotQuality") as Promise<number | undefined>,
|
||||
this.getGlobalState("terminalOutputLineLimit") as Promise<number | undefined>,
|
||||
this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("enableMcpServerCreation") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
|
||||
this.getGlobalState("rateLimitSeconds") as Promise<number | undefined>,
|
||||
this.getGlobalState("currentApiConfigName") as Promise<string | undefined>,
|
||||
this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
|
||||
this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
|
||||
@@ -2289,8 +2371,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
return langMap[vscodeLang.split("-")[0]] ?? "English"
|
||||
})(),
|
||||
mcpEnabled: mcpEnabled ?? true,
|
||||
enableMcpServerCreation: enableMcpServerCreation ?? true,
|
||||
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
|
||||
requestDelaySeconds: Math.max(5, requestDelaySeconds ?? 10),
|
||||
rateLimitSeconds: rateLimitSeconds ?? 0,
|
||||
currentApiConfigName: currentApiConfigName ?? "default",
|
||||
listApiConfigMeta: listApiConfigMeta ?? [],
|
||||
modeApiConfigs: modeApiConfigs ?? ({} as Record<Mode, string>),
|
||||
@@ -2348,7 +2432,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
// secrets
|
||||
|
||||
private async storeSecret(key: SecretKey, value?: string) {
|
||||
public async storeSecret(key: SecretKey, value?: string) {
|
||||
if (value) {
|
||||
await this.context.secrets.store(key, value)
|
||||
} else {
|
||||
@@ -2402,4 +2486,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
await this.postStateToWebview()
|
||||
await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
|
||||
}
|
||||
|
||||
// integration tests
|
||||
|
||||
get viewLaunched() {
|
||||
return this.isViewLaunched
|
||||
}
|
||||
|
||||
get messages() {
|
||||
return this.cline?.clineMessages || []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,11 @@ jest.mock("vscode", () => ({
|
||||
uriScheme: "vscode",
|
||||
language: "en",
|
||||
},
|
||||
ExtensionMode: {
|
||||
Production: 1,
|
||||
Development: 2,
|
||||
Test: 3,
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock sound utility
|
||||
@@ -318,7 +323,9 @@ describe("ClineProvider", () => {
|
||||
browserViewportSize: "900x600",
|
||||
fuzzyMatchThreshold: 1.0,
|
||||
mcpEnabled: true,
|
||||
enableMcpServerCreation: false,
|
||||
requestDelaySeconds: 5,
|
||||
rateLimitSeconds: 0,
|
||||
mode: defaultModeSlug,
|
||||
customModes: [],
|
||||
experiments: experimentDefault,
|
||||
@@ -889,6 +896,7 @@ describe("ClineProvider", () => {
|
||||
},
|
||||
},
|
||||
mcpEnabled: true,
|
||||
enableMcpServerCreation: false,
|
||||
mode: "code" as const,
|
||||
experiments: experimentDefault,
|
||||
} as any)
|
||||
@@ -921,6 +929,7 @@ describe("ClineProvider", () => {
|
||||
},
|
||||
},
|
||||
mcpEnabled: false,
|
||||
enableMcpServerCreation: false,
|
||||
mode: "code" as const,
|
||||
experiments: experimentDefault,
|
||||
} as any)
|
||||
@@ -985,6 +994,7 @@ describe("ClineProvider", () => {
|
||||
},
|
||||
customModePrompts: {},
|
||||
mode: "code",
|
||||
enableMcpServerCreation: true,
|
||||
mcpEnabled: false,
|
||||
browserViewportSize: "900x600",
|
||||
experimentalDiffStrategy: true,
|
||||
@@ -1019,6 +1029,7 @@ describe("ClineProvider", () => {
|
||||
undefined, // preferredLanguage
|
||||
true, // diffEnabled
|
||||
experimentDefault,
|
||||
true,
|
||||
)
|
||||
|
||||
// Run the test again to verify it's consistent
|
||||
@@ -1042,6 +1053,7 @@ describe("ClineProvider", () => {
|
||||
diffEnabled: false,
|
||||
fuzzyMatchThreshold: 0.8,
|
||||
experiments: experimentDefault,
|
||||
enableMcpServerCreation: true,
|
||||
} as any)
|
||||
|
||||
// Mock SYSTEM_PROMPT to verify diffEnabled is passed as false
|
||||
@@ -1070,6 +1082,7 @@ describe("ClineProvider", () => {
|
||||
undefined, // preferredLanguage
|
||||
false, // diffEnabled
|
||||
experimentDefault,
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1084,6 +1097,7 @@ describe("ClineProvider", () => {
|
||||
architect: { customInstructions: "Architect mode instructions" },
|
||||
},
|
||||
mode: "architect",
|
||||
enableMcpServerCreation: false,
|
||||
mcpEnabled: false,
|
||||
browserViewportSize: "900x600",
|
||||
experiments: experimentDefault,
|
||||
|
||||
232
src/extension.ts
232
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)
|
||||
|
||||
|
||||
@@ -50,13 +50,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>
|
||||
@@ -94,6 +99,7 @@ export interface ExtensionState {
|
||||
alwaysApproveResubmit?: boolean
|
||||
alwaysAllowModeSwitch?: boolean
|
||||
requestDelaySeconds: number
|
||||
rateLimitSeconds: number // Minimum time between successive requests (0 = disabled)
|
||||
uriScheme?: string
|
||||
allowedCommands?: string[]
|
||||
soundEnabled?: boolean
|
||||
@@ -106,6 +112,7 @@ export interface ExtensionState {
|
||||
writeDelayMs: number
|
||||
terminalOutputLineLimit?: number
|
||||
mcpEnabled: boolean
|
||||
enableMcpServerCreation: boolean
|
||||
mode: Mode
|
||||
modeApiConfigs?: Record<Mode, string>
|
||||
enhancementApiConfigId?: string
|
||||
|
||||
@@ -62,10 +62,12 @@ export interface WebviewMessage {
|
||||
| "deleteMessage"
|
||||
| "terminalOutputLineLimit"
|
||||
| "mcpEnabled"
|
||||
| "enableMcpServerCreation"
|
||||
| "searchCommits"
|
||||
| "refreshGlamaModels"
|
||||
| "alwaysApproveResubmit"
|
||||
| "requestDelaySeconds"
|
||||
| "rateLimitSeconds"
|
||||
| "setApiConfigPassword"
|
||||
| "requestVsCodeLmModels"
|
||||
| "mode"
|
||||
|
||||
@@ -80,6 +80,7 @@ export interface ModelInfo {
|
||||
cacheWritesPrice?: number
|
||||
cacheReadsPrice?: number
|
||||
description?: string
|
||||
reasoningEffort?: "low" | "medium" | "high"
|
||||
}
|
||||
|
||||
// Anthropic
|
||||
@@ -510,6 +511,33 @@ export type OpenAiNativeModelId = keyof typeof openAiNativeModels
|
||||
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4o"
|
||||
export const openAiNativeModels = {
|
||||
// don't support tool use yet
|
||||
"o3-mini": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.1,
|
||||
outputPrice: 4.4,
|
||||
reasoningEffort: "medium",
|
||||
},
|
||||
"o3-mini-high": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.1,
|
||||
outputPrice: 4.4,
|
||||
reasoningEffort: "high",
|
||||
},
|
||||
"o3-mini-low": {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.1,
|
||||
outputPrice: 4.4,
|
||||
reasoningEffort: "low",
|
||||
},
|
||||
o1: {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
@@ -531,8 +559,8 @@ export const openAiNativeModels = {
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3,
|
||||
outputPrice: 12,
|
||||
inputPrice: 1.1,
|
||||
outputPrice: 4.4,
|
||||
},
|
||||
"gpt-4o": {
|
||||
maxTokens: 4_096,
|
||||
|
||||
@@ -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,121 +1,18 @@
|
||||
const assert = require("assert")
|
||||
const vscode = require("vscode")
|
||||
const path = require("path")
|
||||
const fs = require("fs")
|
||||
const dotenv = require("dotenv")
|
||||
import * as assert from "assert"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
// Load test environment variables
|
||||
const testEnvPath = path.join(__dirname, ".test_env")
|
||||
dotenv.config({ path: testEnvPath })
|
||||
|
||||
suite("Roo Code Extension Test Suite", () => {
|
||||
vscode.window.showInformationMessage("Starting Roo Code extension tests.")
|
||||
|
||||
test("Extension should be present", () => {
|
||||
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
|
||||
assert.notStrictEqual(extension, undefined)
|
||||
})
|
||||
|
||||
test("Extension should activate", async () => {
|
||||
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
|
||||
if (!extension) {
|
||||
assert.fail("Extension not found")
|
||||
suite("Roo Code Extension", () => {
|
||||
test("OPENROUTER_API_KEY environment variable is set", () => {
|
||||
if (!process.env.OPENROUTER_API_KEY) {
|
||||
assert.fail("OPENROUTER_API_KEY environment variable is not set")
|
||||
}
|
||||
await extension.activate()
|
||||
assert.strictEqual(extension.isActive, true)
|
||||
})
|
||||
|
||||
test("OpenRouter API key and models should be configured correctly", function (done) {
|
||||
// @ts-ignore
|
||||
this.timeout(60000) // Increase timeout to 60s for network requests
|
||||
;(async () => {
|
||||
try {
|
||||
// Get extension instance
|
||||
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
|
||||
if (!extension) {
|
||||
done(new Error("Extension not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify API key is set and valid
|
||||
const apiKey = process.env.OPEN_ROUTER_API_KEY
|
||||
if (!apiKey) {
|
||||
done(new Error("OPEN_ROUTER_API_KEY environment variable is not set"))
|
||||
return
|
||||
}
|
||||
if (!apiKey.startsWith("sk-or-v1-")) {
|
||||
done(new Error("OpenRouter API key should have correct format"))
|
||||
return
|
||||
}
|
||||
|
||||
// Activate extension and get provider
|
||||
const api = await extension.activate()
|
||||
if (!api) {
|
||||
done(new Error("Extension API not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get the provider from the extension's exports
|
||||
const provider = api.sidebarProvider
|
||||
if (!provider) {
|
||||
done(new Error("Provider not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set up the API configuration
|
||||
await provider.updateGlobalState("apiProvider", "openrouter")
|
||||
await provider.storeSecret("openRouterApiKey", apiKey)
|
||||
|
||||
// Set up timeout to fail test if models don't load
|
||||
const timeout = setTimeout(() => {
|
||||
done(new Error("Timeout waiting for models to load"))
|
||||
}, 30000)
|
||||
|
||||
// Wait for models to be loaded
|
||||
const checkModels = setInterval(async () => {
|
||||
try {
|
||||
const models = await provider.readOpenRouterModels()
|
||||
if (!models) {
|
||||
return
|
||||
}
|
||||
|
||||
clearInterval(checkModels)
|
||||
clearTimeout(timeout)
|
||||
|
||||
// Verify expected Claude models are available
|
||||
const expectedModels = [
|
||||
"anthropic/claude-3.5-sonnet:beta",
|
||||
"anthropic/claude-3-sonnet:beta",
|
||||
"anthropic/claude-3.5-sonnet",
|
||||
"anthropic/claude-3.5-sonnet-20240620",
|
||||
"anthropic/claude-3.5-sonnet-20240620:beta",
|
||||
"anthropic/claude-3.5-haiku:beta",
|
||||
]
|
||||
|
||||
for (const modelId of expectedModels) {
|
||||
assert.strictEqual(modelId in models, true, `Model ${modelId} should be available`)
|
||||
}
|
||||
|
||||
done()
|
||||
} catch (error) {
|
||||
clearInterval(checkModels)
|
||||
clearTimeout(timeout)
|
||||
done(error)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
// Trigger model loading
|
||||
await provider.refreshOpenRouterModels()
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
})()
|
||||
})
|
||||
|
||||
test("Commands should be registered", async () => {
|
||||
const commands = await vscode.commands.getCommands(true)
|
||||
const timeout = 10 * 1_000
|
||||
const interval = 1_000
|
||||
const startTime = Date.now()
|
||||
|
||||
// Test core commands are registered
|
||||
const expectedCommands = [
|
||||
"roo-cline.plusButtonClicked",
|
||||
"roo-cline.mcpButtonClicked",
|
||||
@@ -128,204 +25,39 @@ suite("Roo Code Extension Test Suite", () => {
|
||||
"roo-cline.improveCode",
|
||||
]
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
const commands = await vscode.commands.getCommands(true)
|
||||
const missingCommands = []
|
||||
|
||||
for (const cmd of expectedCommands) {
|
||||
assert.strictEqual(commands.includes(cmd), true, `Command ${cmd} should be registered`)
|
||||
if (!commands.includes(cmd)) {
|
||||
missingCommands.push(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
if (missingCommands.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
const commands = await vscode.commands.getCommands(true)
|
||||
|
||||
for (const cmd of expectedCommands) {
|
||||
assert.ok(commands.includes(cmd), `Command ${cmd} should be registered`)
|
||||
}
|
||||
})
|
||||
|
||||
test("Views should be registered", () => {
|
||||
test("Webview panel can be created", () => {
|
||||
const view = vscode.window.createWebviewPanel(
|
||||
"roo-cline.SidebarProvider",
|
||||
"Roo Code",
|
||||
vscode.ViewColumn.One,
|
||||
{},
|
||||
)
|
||||
assert.notStrictEqual(view, undefined)
|
||||
|
||||
assert.ok(view, "Failed to create webview panel")
|
||||
view.dispose()
|
||||
})
|
||||
|
||||
test("Should handle prompt and response correctly", async function () {
|
||||
// @ts-ignore
|
||||
this.timeout(60000) // Increase timeout for API request
|
||||
|
||||
const timeout = 30000
|
||||
const interval = 1000
|
||||
|
||||
// Get extension instance
|
||||
const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline")
|
||||
if (!extension) {
|
||||
assert.fail("Extension not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Activate extension and get API
|
||||
const api = await extension.activate()
|
||||
if (!api) {
|
||||
assert.fail("Extension API not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Get provider
|
||||
const provider = api.sidebarProvider
|
||||
if (!provider) {
|
||||
assert.fail("Provider not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Set up API configuration
|
||||
await provider.updateGlobalState("apiProvider", "openrouter")
|
||||
await provider.updateGlobalState("openRouterModelId", "anthropic/claude-3.5-sonnet")
|
||||
const apiKey = process.env.OPEN_ROUTER_API_KEY
|
||||
if (!apiKey) {
|
||||
assert.fail("OPEN_ROUTER_API_KEY environment variable is not set")
|
||||
return
|
||||
}
|
||||
await provider.storeSecret("openRouterApiKey", apiKey)
|
||||
|
||||
// Create webview panel with development options
|
||||
const extensionUri = extension.extensionUri
|
||||
const panel = vscode.window.createWebviewPanel("roo-cline.SidebarProvider", "Roo Code", vscode.ViewColumn.One, {
|
||||
enableScripts: true,
|
||||
enableCommandUris: true,
|
||||
retainContextWhenHidden: true,
|
||||
localResourceRoots: [extensionUri],
|
||||
})
|
||||
|
||||
try {
|
||||
// Initialize webview with development context
|
||||
panel.webview.options = {
|
||||
enableScripts: true,
|
||||
enableCommandUris: true,
|
||||
localResourceRoots: [extensionUri],
|
||||
}
|
||||
|
||||
// Initialize provider with panel
|
||||
provider.resolveWebviewView(panel)
|
||||
|
||||
// Set up message tracking
|
||||
let webviewReady = false
|
||||
let messagesReceived = false
|
||||
const originalPostMessage = provider.postMessageToWebview.bind(provider)
|
||||
// @ts-ignore
|
||||
provider.postMessageToWebview = async (message) => {
|
||||
if (message.type === "state") {
|
||||
webviewReady = true
|
||||
console.log("Webview state received:", message)
|
||||
if (message.state?.clineMessages?.length > 0) {
|
||||
messagesReceived = true
|
||||
console.log("Messages in state:", message.state.clineMessages)
|
||||
}
|
||||
}
|
||||
await originalPostMessage(message)
|
||||
}
|
||||
|
||||
// Wait for webview to launch and receive initial state
|
||||
let startTime = Date.now()
|
||||
while (Date.now() - startTime < timeout) {
|
||||
if (webviewReady) {
|
||||
// Wait an additional second for webview to fully initialize
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
break
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
if (!webviewReady) {
|
||||
throw new Error("Timeout waiting for webview to be ready")
|
||||
}
|
||||
|
||||
// Send webviewDidLaunch to initialize chat
|
||||
await provider.postMessageToWebview({ type: "webviewDidLaunch" })
|
||||
console.log("Sent webviewDidLaunch")
|
||||
|
||||
// Wait for webview to fully initialize
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
// Restore original postMessage
|
||||
provider.postMessageToWebview = originalPostMessage
|
||||
|
||||
// Wait for OpenRouter models to be fully loaded
|
||||
startTime = Date.now()
|
||||
while (Date.now() - startTime < timeout) {
|
||||
const models = await provider.readOpenRouterModels()
|
||||
if (models && Object.keys(models).length > 0) {
|
||||
console.log("OpenRouter models loaded")
|
||||
break
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
// Send prompt
|
||||
const prompt = "Hello world, what is your name?"
|
||||
console.log("Sending prompt:", prompt)
|
||||
|
||||
// Start task
|
||||
try {
|
||||
await api.startNewTask(prompt)
|
||||
console.log("Task started")
|
||||
} catch (error) {
|
||||
console.error("Error starting task:", error)
|
||||
throw error
|
||||
}
|
||||
|
||||
// Wait for task to appear in history with tokens
|
||||
startTime = Date.now()
|
||||
while (Date.now() - startTime < timeout) {
|
||||
const state = await provider.getState()
|
||||
const task = state.taskHistory?.[0]
|
||||
if (task && task.tokensOut > 0) {
|
||||
console.log("Task completed with tokens:", task)
|
||||
break
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
// Wait for messages to be processed
|
||||
startTime = Date.now()
|
||||
let responseReceived = false
|
||||
while (Date.now() - startTime < timeout) {
|
||||
// Check provider.clineMessages
|
||||
const messages = provider.clineMessages
|
||||
if (messages && messages.length > 0) {
|
||||
console.log("Provider messages:", JSON.stringify(messages, null, 2))
|
||||
// @ts-ignore
|
||||
const hasResponse = messages.some(
|
||||
(m: { type: string; text: string }) =>
|
||||
m.type === "say" && m.text && m.text.toLowerCase().includes("cline"),
|
||||
)
|
||||
if (hasResponse) {
|
||||
console.log('Found response containing "Cline" in provider messages')
|
||||
responseReceived = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check provider.cline.clineMessages
|
||||
const clineMessages = provider.cline?.clineMessages
|
||||
if (clineMessages && clineMessages.length > 0) {
|
||||
console.log("Cline messages:", JSON.stringify(clineMessages, null, 2))
|
||||
// @ts-ignore
|
||||
const hasResponse = clineMessages.some(
|
||||
(m: { type: string; text: string }) =>
|
||||
m.type === "say" && m.text && m.text.toLowerCase().includes("cline"),
|
||||
)
|
||||
if (hasResponse) {
|
||||
console.log('Found response containing "Cline" in cline messages')
|
||||
responseReceived = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
if (!responseReceived) {
|
||||
console.log("Final provider state:", await provider.getState())
|
||||
console.log("Final cline messages:", provider.cline?.clineMessages)
|
||||
throw new Error('Did not receive expected response containing "Cline"')
|
||||
}
|
||||
} finally {
|
||||
panel.dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
77
src/test/task.test.ts
Normal file
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.
|
||||
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"]
|
||||
}
|
||||
@@ -99,3 +99,8 @@ export function getReadablePath(cwd: string, relPath?: string): string {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const toRelativePath = (filePath: string, cwd: string) => {
|
||||
const relativePath = path.relative(cwd, filePath).toPosix()
|
||||
return filePath.endsWith("/") ? relativePath + "/" : relativePath
|
||||
}
|
||||
|
||||
17
tsconfig.integration.json
Normal file
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"]
|
||||
}
|
||||
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
|
||||
|
||||
16
webview-ui/.storybook/main.ts
Normal file
16
webview-ui/.storybook/main.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite"
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-onboarding",
|
||||
"@storybook/addon-essentials",
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/addon-interactions",
|
||||
],
|
||||
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: red; /* "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"
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
const { override } = require("customize-cra")
|
||||
|
||||
module.exports = override()
|
||||
|
||||
// Jest configuration override
|
||||
module.exports.jest = function (config) {
|
||||
// Configure reporters
|
||||
config.reporters = [["jest-simple-dot-reporter", {}]]
|
||||
|
||||
// Configure module name mapper for CSS modules
|
||||
config.moduleNameMapper = {
|
||||
...config.moduleNameMapper,
|
||||
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
|
||||
}
|
||||
|
||||
// Configure transform ignore patterns for ES modules
|
||||
config.transformIgnorePatterns = [
|
||||
"/node_modules/(?!(rehype-highlight|react-remark|unist-util-visit|unist-util-find-after|vfile|unified|bail|is-plain-obj|trough|vfile-message|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|longest-streak|escape-string-regexp|unist-util-is|hast-util-to-text|@vscode/webview-ui-toolkit|@microsoft/fast-react-wrapper|@microsoft/fast-element|@microsoft/fast-foundation|@microsoft/fast-web-utilities|exenv-es6|vscrui)/)",
|
||||
]
|
||||
|
||||
return config
|
||||
}
|
||||
12
webview-ui/index.html
Normal file
12
webview-ui/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Roo Code</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
14364
webview-ui/package-lock.json
generated
14364
webview-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,65 +2,111 @@
|
||||
"name": "webview-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src --ext ts,tsx",
|
||||
"test": "jest",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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": "^16.18.101",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@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",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-remark": "^2.1.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"react-use": "^17.5.1",
|
||||
"react-virtuoso": "^4.7.13",
|
||||
"rehype-highlight": "^7.0.0",
|
||||
"rewire": "^7.0.0",
|
||||
"shell-quote": "^1.8.2",
|
||||
"styled-components": "^6.1.13",
|
||||
"typescript": "^4.9.5",
|
||||
"vscrui": "^0.2.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-app-rewired start",
|
||||
"build": "node ./scripts/build-react-no-split.js",
|
||||
"test": "react-app-rewired test --watchAll=false",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint src --ext ts,tsx"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vscrui": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@chromatic-com/storybook": "^3.2.4",
|
||||
"@storybook/addon-essentials": "^8.5.2",
|
||||
"@storybook/addon-interactions": "^8.5.2",
|
||||
"@storybook/addon-onboarding": "^8.5.2",
|
||||
"@storybook/blocks": "^8.5.2",
|
||||
"@storybook/react": "^8.5.2",
|
||||
"@storybook/react-vite": "^8.5.2",
|
||||
"@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.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",
|
||||
"customize-cra": "^1.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"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",
|
||||
"react-app-rewired": "^2.2.1"
|
||||
"storybook": "^8.5.2",
|
||||
"ts-jest": "^27.1.5",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "6.0.11"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "jsdom",
|
||||
"setupFilesAfterEnv": [
|
||||
"@testing-library/jest-dom/extend-expect"
|
||||
],
|
||||
"preset": "ts-jest",
|
||||
"reporters": [
|
||||
[
|
||||
"jest-simple-dot-reporter",
|
||||
{}
|
||||
]
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
|
||||
"^vscrui$": "<rootDir>/src/__mocks__/vscrui.ts",
|
||||
"^@vscode/webview-ui-toolkit/react$": "<rootDir>/src/__mocks__/@vscode/webview-ui-toolkit/react.ts"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"/node_modules/(?!(rehype-highlight|react-remark|unist-util-visit|unist-util-find-after|vfile|unified|bail|is-plain-obj|trough|vfile-message|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|longest-streak|escape-string-regexp|unist-util-is|hast-util-to-text|@vscode/webview-ui-toolkit|@microsoft/fast-react-wrapper|@microsoft/fast-element|@microsoft/fast-foundation|@microsoft/fast-web-utilities|exenv-es6|vscrui)/)"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.(ts|tsx)$": [
|
||||
"ts-jest",
|
||||
{
|
||||
"tsconfig": {
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"moduleDirectories": [
|
||||
"node_modules",
|
||||
"src"
|
||||
],
|
||||
"testMatch": [
|
||||
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
|
||||
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
@@ -1,113 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* A script that overrides some of the create-react-app build script configurations
|
||||
* in order to disable code splitting/chunking and rename the output build files so
|
||||
* they have no hash. (Reference: https://mtm.dev/disable-code-splitting-create-react-app).
|
||||
*
|
||||
* This is crucial for getting React webview code to run because VS Code expects a
|
||||
* single (consistently named) JavaScript and CSS file when configuring webviews.
|
||||
*/
|
||||
|
||||
const rewire = require("rewire")
|
||||
const defaults = rewire("react-scripts/scripts/build.js")
|
||||
const config = defaults.__get__("config")
|
||||
|
||||
/* Modifying Webpack Configuration for 'shared' dir
|
||||
This section uses Rewire to modify Create React App's webpack configuration without ejecting. Rewire allows us to inject and alter the internal build scripts of CRA at runtime. This allows us to maintain a flexible project structure that keeps shared code outside the webview-ui/src directory, while still adhering to CRA's security model that typically restricts imports to within src/.
|
||||
1. Uses the ModuleScopePlugin to whitelist files from the shared directory, allowing them to be imported despite being outside src/. (see: https://stackoverflow.com/questions/44114436/the-create-react-app-imports-restriction-outside-of-src-directory/58321458#58321458)
|
||||
2. Modifies the TypeScript rule to include the shared directory in compilation. This essentially transpiles and includes the ts files in shared dir in the output main.js file.
|
||||
Before, we would just import types from shared dir and specifying include (and alias to have cleaner paths) in tsconfig.json was enough. But now that we are creating values (i.e. models in api.ts) to import into the react app, we must also include these files in the webpack resolution.
|
||||
- Imports from the shared directory must use full paths relative to the src directory, without file extensions.
|
||||
- Example: import { someFunction } from '../../src/shared/utils/helpers'
|
||||
*/
|
||||
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin")
|
||||
const path = require("path")
|
||||
const fs = require("fs")
|
||||
// Get all files in the shared directory
|
||||
const sharedDir = path.resolve(__dirname, "..", "..", "src", "shared")
|
||||
|
||||
function getAllFiles(dir) {
|
||||
let files = []
|
||||
fs.readdirSync(dir).forEach((file) => {
|
||||
const filePath = path.join(dir, file)
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
files = files.concat(getAllFiles(filePath))
|
||||
} else {
|
||||
const withoutExtension = path.join(dir, path.parse(file).name)
|
||||
files.push(withoutExtension)
|
||||
}
|
||||
})
|
||||
return files
|
||||
}
|
||||
const sharedFiles = getAllFiles(sharedDir)
|
||||
// config.resolve.plugins = config.resolve.plugins.filter((plugin) => !(plugin instanceof ModuleScopePlugin))
|
||||
// Instead of excluding the whole ModuleScopePlugin, we just whitelist specific files that can be imported from outside src.
|
||||
config.resolve.plugins.forEach((plugin) => {
|
||||
if (plugin instanceof ModuleScopePlugin) {
|
||||
console.log("Whitelisting shared files: ", sharedFiles)
|
||||
sharedFiles.forEach((file) => plugin.allowedFiles.add(file))
|
||||
}
|
||||
})
|
||||
/*
|
||||
Webpack configuration
|
||||
|
||||
Webpack is a module bundler for JavaScript applications. It processes your project files, resolving dependencies and generating a deployable production build.
|
||||
The webpack config is an object that tells webpack how to process and bundle your code. It defines entry points, output settings, and how to handle different file types.
|
||||
This config.module section of the webpack config deals with how different file types (modules) should be treated.
|
||||
config.module.rules:
|
||||
Rules define how module files should be processed. Each rule can:
|
||||
- Specify which files to process (test)
|
||||
When webpack "processes" a file, it performs several operations:
|
||||
1. Reads the file
|
||||
2. Parses its content and analyzes dependencies
|
||||
3. Applies transformations (e.g., converting TypeScript to JavaScript)
|
||||
4. Potentially modifies the code (e.g., applying polyfills)
|
||||
5. Includes the processed file in the final bundle
|
||||
By specifying which files to process, we're telling webpack which files should go through this pipeline and be included in our application bundle. Files that aren't processed are ignored by webpack.
|
||||
In our case, we're ensuring that TypeScript files in our shared directory are processed, allowing us to use them in our application.
|
||||
- Define which folders to include or exclude
|
||||
- Set which loaders to use for transformation
|
||||
A loader transforms certain types of files into valid modules that webpack can process. For example, the TypeScript loader converts .ts files into JavaScript that webpack can understand.
|
||||
By modifying these rules, we can change how webpack processes different files in our project, allowing us to include files from outside the standard src directory.
|
||||
|
||||
Why we need to modify the webpack config
|
||||
|
||||
Create React App (CRA) is designed to only process files within the src directory for security reasons. (CRA limits processing to the src directory to prevent accidental inclusion of sensitive files, reduce the attack surface, and ensure predictable builds, enhancing overall project security and consistency. Therefore it's essential that if you do include files outside src, you do so explicitly.)
|
||||
To use files from the shared directory, we need to:
|
||||
1. Modify ModuleScopePlugin to allow imports from the shared directory.
|
||||
2. Update the TypeScript loader rule to process TypeScript files from the shared directory.
|
||||
These changes tell webpack it's okay to import from the shared directory and ensure that TypeScript files in this directory are properly converted to JavaScript.
|
||||
|
||||
Modify webpack configuration to process TypeScript files from shared directory
|
||||
|
||||
This code modifies the webpack configuration to allow processing of TypeScript files from our shared directory, which is outside the standard src folder.
|
||||
1. config.module.rules[1]: In Create React App's webpack config, the second rule (index 1) typically contains the rules for processing JavaScript and TypeScript files.
|
||||
2. .oneOf: This array contains a list of loaders, and webpack will use the first matching loader for each file. We iterate through these to find the TypeScript loader.
|
||||
3. We check each rule to see if it applies to TypeScript files by looking for 'ts|tsx' in the test regex.
|
||||
4. When we find the TypeScript rule, we add our shared directory to its 'include' array. This tells webpack to also process TypeScript files from the shared directory.
|
||||
Note: This code assumes a specific structure in the CRA webpack config. If CRA updates its config structure in future versions, this code might need to be adjusted.
|
||||
*/
|
||||
config.module.rules[1].oneOf.forEach((rule) => {
|
||||
if (rule.test && rule.test.toString().includes("ts|tsx")) {
|
||||
// rule.include is path to src by default, but we can update rule.include to be an array as it matches an expected schema by react-scripts
|
||||
rule.include = [rule.include, sharedDir].filter(Boolean)
|
||||
}
|
||||
})
|
||||
|
||||
// Disable code splitting
|
||||
config.optimization.splitChunks = {
|
||||
cacheGroups: {
|
||||
default: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Disable code chunks
|
||||
config.optimization.runtimeChunk = false
|
||||
|
||||
// Rename main.{hash}.js to main.js
|
||||
config.output.filename = "static/js/[name].js"
|
||||
|
||||
// Rename main.{hash}.css to main.css
|
||||
config.plugins[5].options.filename = "static/css/[name].css"
|
||||
config.plugins[5].options.moduleFilename = () => "static/css/main.css"
|
||||
117
webview-ui/src/__mocks__/@vscode/webview-ui-toolkit/react.ts
Normal file
117
webview-ui/src/__mocks__/@vscode/webview-ui-toolkit/react.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import React from "react"
|
||||
|
||||
interface VSCodeProps {
|
||||
children?: React.ReactNode
|
||||
onClick?: () => void
|
||||
onChange?: (e: any) => void
|
||||
onInput?: (e: any) => void
|
||||
appearance?: string
|
||||
checked?: boolean
|
||||
value?: string | number
|
||||
placeholder?: string
|
||||
href?: string
|
||||
"data-testid"?: string
|
||||
style?: React.CSSProperties
|
||||
slot?: string
|
||||
role?: string
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export const VSCodeButton: React.FC<VSCodeProps> = ({ children, onClick, appearance, className, ...props }) => {
|
||||
// For icon buttons, render children directly without any wrapping
|
||||
if (appearance === "icon") {
|
||||
return React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick,
|
||||
className: `${className || ""}`,
|
||||
"data-appearance": appearance,
|
||||
...props,
|
||||
},
|
||||
children,
|
||||
)
|
||||
}
|
||||
|
||||
// For regular buttons
|
||||
return React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick,
|
||||
className: className,
|
||||
...props,
|
||||
},
|
||||
children,
|
||||
)
|
||||
}
|
||||
|
||||
export const VSCodeCheckbox: React.FC<VSCodeProps> = ({ children, onChange, checked, ...props }) =>
|
||||
React.createElement("label", {}, [
|
||||
React.createElement("input", {
|
||||
key: "input",
|
||||
type: "checkbox",
|
||||
checked,
|
||||
onChange: (e: any) => onChange?.({ target: { checked: e.target.checked } }),
|
||||
"aria-label": typeof children === "string" ? children : undefined,
|
||||
...props,
|
||||
}),
|
||||
children && React.createElement("span", { key: "label" }, children),
|
||||
])
|
||||
|
||||
export const VSCodeTextField: React.FC<VSCodeProps> = ({ children, value, onInput, placeholder, ...props }) =>
|
||||
React.createElement("div", { style: { position: "relative", display: "inline-block", width: "100%" } }, [
|
||||
React.createElement("input", {
|
||||
key: "input",
|
||||
type: "text",
|
||||
value,
|
||||
onChange: (e: any) => onInput?.({ target: { value: e.target.value } }),
|
||||
placeholder,
|
||||
...props,
|
||||
}),
|
||||
children,
|
||||
])
|
||||
|
||||
export const VSCodeTextArea: React.FC<VSCodeProps> = ({ value, onChange, ...props }) =>
|
||||
React.createElement("textarea", {
|
||||
value,
|
||||
onChange: (e: any) => onChange?.({ target: { value: e.target.value } }),
|
||||
...props,
|
||||
})
|
||||
|
||||
export const VSCodeLink: React.FC<VSCodeProps> = ({ children, href, ...props }) =>
|
||||
React.createElement("a", { href: href || "#", ...props }, children)
|
||||
|
||||
export const VSCodeDropdown: React.FC<VSCodeProps> = ({ children, value, onChange, ...props }) =>
|
||||
React.createElement("select", { value, onChange, ...props }, children)
|
||||
|
||||
export const VSCodeOption: React.FC<VSCodeProps> = ({ children, value, ...props }) =>
|
||||
React.createElement("option", { value, ...props }, children)
|
||||
|
||||
export const VSCodeRadio: React.FC<VSCodeProps> = ({ children, value, checked, onChange, ...props }) =>
|
||||
React.createElement("label", { style: { display: "inline-flex", alignItems: "center" } }, [
|
||||
React.createElement("input", {
|
||||
key: "input",
|
||||
type: "radio",
|
||||
value,
|
||||
checked,
|
||||
onChange,
|
||||
...props,
|
||||
}),
|
||||
children && React.createElement("span", { key: "label", style: { marginLeft: "4px" } }, children),
|
||||
])
|
||||
|
||||
export const VSCodeRadioGroup: React.FC<VSCodeProps> = ({ children, onChange, ...props }) =>
|
||||
React.createElement("div", { role: "radiogroup", onChange, ...props }, children)
|
||||
|
||||
export const VSCodeSlider: React.FC<VSCodeProps> = ({ value, onChange, ...props }) =>
|
||||
React.createElement("input", {
|
||||
type: "range",
|
||||
value,
|
||||
onChange: (e: any) => onChange?.({ target: { value: Number(e.target.value) } }),
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
style: { flexGrow: 1, height: "2px" },
|
||||
...props,
|
||||
})
|
||||
14
webview-ui/src/__mocks__/vscrui.ts
Normal file
14
webview-ui/src/__mocks__/vscrui.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from "react"
|
||||
|
||||
export const Checkbox = ({ children, checked, onChange }: any) =>
|
||||
React.createElement("div", { "data-testid": "mock-checkbox", onClick: onChange }, children)
|
||||
|
||||
export const Dropdown = ({ children, value, onChange }: any) =>
|
||||
React.createElement("div", { "data-testid": "mock-dropdown", onClick: onChange }, children)
|
||||
|
||||
export const Pane = ({ children }: any) => React.createElement("div", { "data-testid": "mock-pane" }, children)
|
||||
|
||||
export type DropdownOption = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
@@ -37,9 +37,9 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
const isLastApiReqInterrupted = useMemo(() => {
|
||||
// Check if last api_req_started is cancelled
|
||||
const lastApiReqStarted = [...messages].reverse().find((m) => m.say === "api_req_started")
|
||||
if (lastApiReqStarted?.text != null) {
|
||||
const info = JSON.parse(lastApiReqStarted.text)
|
||||
if (info.cancelReason != null) {
|
||||
if (lastApiReqStarted?.text) {
|
||||
const info = JSON.parse(lastApiReqStarted.text) as { cancelReason: string | null }
|
||||
if (info && info.cancelReason !== null) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -103,6 +103,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
const [intendedCursorPosition, setIntendedCursorPosition] = useState<number | null>(null)
|
||||
const contextMenuContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
|
||||
// Fetch git commits when Git is selected or when typing a hash
|
||||
useEffect(() => {
|
||||
@@ -137,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) => {
|
||||
@@ -379,6 +387,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
if (!isMouseDownOnMenu) {
|
||||
setShowContextMenu(false)
|
||||
}
|
||||
setIsFocused(false)
|
||||
}, [isMouseDownOnMenu])
|
||||
|
||||
const handlePaste = useCallback(
|
||||
@@ -537,6 +546,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
backgroundColor: "var(--vscode-input-background)",
|
||||
margin: "10px 15px",
|
||||
padding: "8px",
|
||||
outline: "none",
|
||||
border: "1px solid",
|
||||
borderColor: isFocused ? "var(--vscode-focusBorder)" : "transparent",
|
||||
borderRadius: "2px",
|
||||
}}
|
||||
onDrop={async (e) => {
|
||||
e.preventDefault()
|
||||
@@ -627,7 +640,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
fontFamily: "var(--vscode-font-family)",
|
||||
fontSize: "var(--vscode-editor-font-size)",
|
||||
lineHeight: "var(--vscode-editor-line-height)",
|
||||
padding: "8px",
|
||||
padding: "2px",
|
||||
paddingRight: "8px",
|
||||
marginBottom: thumbnailsHeight > 0 ? `${thumbnailsHeight + 16}px` : 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
@@ -647,6 +661,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
handleInputChange(e)
|
||||
updateHighlights()
|
||||
}}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onBlur={handleBlur}
|
||||
@@ -660,11 +675,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
onHeightChange?.(height)
|
||||
}}
|
||||
placeholder={placeholderText}
|
||||
minRows={2}
|
||||
maxRows={20}
|
||||
minRows={3}
|
||||
maxRows={15}
|
||||
autoFocus={true}
|
||||
style={{
|
||||
width: "100%",
|
||||
outline: "none",
|
||||
boxSizing: "border-box",
|
||||
backgroundColor: "transparent",
|
||||
color: "var(--vscode-input-foreground)",
|
||||
@@ -676,11 +692,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto",
|
||||
border: "none",
|
||||
padding: "8px",
|
||||
padding: "2px",
|
||||
paddingRight: "8px",
|
||||
marginBottom: thumbnailsHeight > 0 ? `${thumbnailsHeight + 16}px` : 0,
|
||||
cursor: textAreaDisabled ? "not-allowed" : undefined,
|
||||
flex: "0 1 auto",
|
||||
zIndex: 2,
|
||||
scrollbarWidth: "none",
|
||||
}}
|
||||
onScroll={() => updateHighlights()}
|
||||
/>
|
||||
@@ -696,7 +714,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
bottom: "36px",
|
||||
left: "16px",
|
||||
zIndex: 2,
|
||||
marginBottom: "8px",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -707,7 +725,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: "auto",
|
||||
paddingTop: "8px",
|
||||
paddingTop: "2px",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -330,6 +330,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,7 +351,9 @@ 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(() => {
|
||||
const handlePrimaryButtonClick = useCallback(
|
||||
(text?: string, images?: string[]) => {
|
||||
const trimmedInput = text?.trim()
|
||||
switch (clineAsk) {
|
||||
case "api_req_failed":
|
||||
case "command":
|
||||
@@ -347,7 +363,23 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "use_mcp_server":
|
||||
case "resume_task":
|
||||
case "mistake_limit_reached":
|
||||
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
|
||||
// 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":
|
||||
@@ -359,9 +391,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
disableAutoScrollRef.current = false
|
||||
}, [clineAsk, startNewTask])
|
||||
},
|
||||
[clineAsk, startNewTask],
|
||||
)
|
||||
|
||||
const handleSecondaryButtonClick = useCallback(() => {
|
||||
const handleSecondaryButtonClick = useCallback(
|
||||
(text?: string, images?: string[]) => {
|
||||
const trimmedInput = text?.trim()
|
||||
if (isStreaming) {
|
||||
vscode.postMessage({ type: "cancelTask" })
|
||||
setDidClickCancel(true)
|
||||
@@ -378,15 +414,33 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
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" })
|
||||
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])
|
||||
},
|
||||
[clineAsk, startNewTask, isStreaming],
|
||||
)
|
||||
|
||||
const handleTaskCloseButtonClick = useCallback(() => {
|
||||
startNewTask()
|
||||
@@ -429,11 +483,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 +501,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
textAreaDisabled,
|
||||
enableButtons,
|
||||
handleSendMessage,
|
||||
handleSetChatBoxMessage,
|
||||
handlePrimaryButtonClick,
|
||||
handleSecondaryButtonClick,
|
||||
],
|
||||
@@ -1038,7 +1096,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 +1108,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
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
import { render, fireEvent, screen } from "@testing-library/react"
|
||||
import { useExtensionState } from "../../../context/ExtensionStateContext"
|
||||
import AutoApproveMenu from "../AutoApproveMenu"
|
||||
import { defaultModeSlug, defaultPrompts } from "../../../../../src/shared/modes"
|
||||
|
||||
// Mock the ExtensionStateContext hook
|
||||
jest.mock("../../../context/ExtensionStateContext")
|
||||
|
||||
const mockUseExtensionState = useExtensionState as jest.MockedFunction<typeof useExtensionState>
|
||||
|
||||
describe("AutoApproveMenu", () => {
|
||||
const defaultMockState = {
|
||||
// Required state properties
|
||||
version: "1.0.0",
|
||||
clineMessages: [],
|
||||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
allowedCommands: [],
|
||||
soundEnabled: false,
|
||||
soundVolume: 0.5,
|
||||
diffEnabled: false,
|
||||
fuzzyMatchThreshold: 1.0,
|
||||
preferredLanguage: "English",
|
||||
writeDelayMs: 1000,
|
||||
browserViewportSize: "900x600",
|
||||
screenshotQuality: 75,
|
||||
terminalOutputLineLimit: 500,
|
||||
mcpEnabled: true,
|
||||
requestDelaySeconds: 5,
|
||||
currentApiConfigName: "default",
|
||||
listApiConfigMeta: [],
|
||||
mode: defaultModeSlug,
|
||||
customModePrompts: defaultPrompts,
|
||||
customSupportPrompts: {},
|
||||
enhancementApiConfigId: "",
|
||||
didHydrateState: true,
|
||||
showWelcome: false,
|
||||
theme: {},
|
||||
glamaModels: {},
|
||||
openRouterModels: {},
|
||||
openAiModels: [],
|
||||
mcpServers: [],
|
||||
filePaths: [],
|
||||
|
||||
// Auto-approve specific properties
|
||||
alwaysAllowReadOnly: false,
|
||||
alwaysAllowWrite: false,
|
||||
alwaysAllowExecute: false,
|
||||
alwaysAllowBrowser: false,
|
||||
alwaysAllowMcp: false,
|
||||
alwaysApproveResubmit: false,
|
||||
autoApprovalEnabled: false,
|
||||
|
||||
// Required setter functions
|
||||
setApiConfiguration: jest.fn(),
|
||||
setCustomInstructions: jest.fn(),
|
||||
setAlwaysAllowReadOnly: jest.fn(),
|
||||
setAlwaysAllowWrite: jest.fn(),
|
||||
setAlwaysAllowExecute: jest.fn(),
|
||||
setAlwaysAllowBrowser: jest.fn(),
|
||||
setAlwaysAllowMcp: jest.fn(),
|
||||
setShowAnnouncement: jest.fn(),
|
||||
setAllowedCommands: jest.fn(),
|
||||
setSoundEnabled: jest.fn(),
|
||||
setSoundVolume: jest.fn(),
|
||||
setDiffEnabled: jest.fn(),
|
||||
setBrowserViewportSize: jest.fn(),
|
||||
setFuzzyMatchThreshold: jest.fn(),
|
||||
setPreferredLanguage: jest.fn(),
|
||||
setWriteDelayMs: jest.fn(),
|
||||
setScreenshotQuality: jest.fn(),
|
||||
setTerminalOutputLineLimit: jest.fn(),
|
||||
setMcpEnabled: jest.fn(),
|
||||
setAlwaysApproveResubmit: jest.fn(),
|
||||
setRequestDelaySeconds: jest.fn(),
|
||||
setCurrentApiConfigName: jest.fn(),
|
||||
setListApiConfigMeta: jest.fn(),
|
||||
onUpdateApiConfig: jest.fn(),
|
||||
setMode: jest.fn(),
|
||||
setCustomPrompts: jest.fn(),
|
||||
setEnhancementApiConfigId: jest.fn(),
|
||||
setAutoApprovalEnabled: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseExtensionState.mockReturnValue(defaultMockState)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders with initial collapsed state", () => {
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Check for main checkbox and label
|
||||
expect(screen.getByText("Auto-approve:")).toBeInTheDocument()
|
||||
expect(screen.getByText("None")).toBeInTheDocument()
|
||||
|
||||
// Verify the menu is collapsed (actions not visible)
|
||||
expect(screen.queryByText("Read files and directories")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("expands menu when clicked", () => {
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Click to expand
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Verify menu items are visible
|
||||
expect(screen.getByText("Read files and directories")).toBeInTheDocument()
|
||||
expect(screen.getByText("Edit files")).toBeInTheDocument()
|
||||
expect(screen.getByText("Execute approved commands")).toBeInTheDocument()
|
||||
expect(screen.getByText("Use the browser")).toBeInTheDocument()
|
||||
expect(screen.getByText("Use MCP servers")).toBeInTheDocument()
|
||||
expect(screen.getByText("Retry failed requests")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("toggles main auto-approval checkbox", () => {
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
const mainCheckbox = screen.getByRole("checkbox")
|
||||
fireEvent.click(mainCheckbox)
|
||||
|
||||
expect(defaultMockState.setAutoApprovalEnabled).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("toggles individual permissions", () => {
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand menu
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Click read files checkbox
|
||||
fireEvent.click(screen.getByText("Read files and directories"))
|
||||
expect(defaultMockState.setAlwaysAllowReadOnly).toHaveBeenCalledWith(true)
|
||||
|
||||
// Click edit files checkbox
|
||||
fireEvent.click(screen.getByText("Edit files"))
|
||||
expect(defaultMockState.setAlwaysAllowWrite).toHaveBeenCalledWith(true)
|
||||
|
||||
// Click execute commands checkbox
|
||||
fireEvent.click(screen.getByText("Execute approved commands"))
|
||||
expect(defaultMockState.setAlwaysAllowExecute).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("displays enabled actions in summary", () => {
|
||||
mockUseExtensionState.mockReturnValue({
|
||||
...defaultMockState,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
autoApprovalEnabled: true,
|
||||
})
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Check that enabled actions are shown in summary
|
||||
expect(screen.getByText("Read, Edit")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("preserves checkbox states", () => {
|
||||
// Mock state with some permissions enabled
|
||||
const mockState = {
|
||||
...defaultMockState,
|
||||
alwaysAllowReadOnly: true,
|
||||
alwaysAllowWrite: true,
|
||||
}
|
||||
|
||||
// Update mock to return our state
|
||||
mockUseExtensionState.mockReturnValue(mockState)
|
||||
|
||||
render(<AutoApproveMenu />)
|
||||
|
||||
// Expand menu
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Verify read and edit checkboxes are checked
|
||||
expect(screen.getByLabelText("Read files and directories")).toBeInTheDocument()
|
||||
expect(screen.getByLabelText("Edit files")).toBeInTheDocument()
|
||||
|
||||
// Verify the setters haven't been called yet
|
||||
expect(mockState.setAlwaysAllowReadOnly).not.toHaveBeenCalled()
|
||||
expect(mockState.setAlwaysAllowWrite).not.toHaveBeenCalled()
|
||||
|
||||
// Collapse menu
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Expand again
|
||||
fireEvent.click(screen.getByText("Auto-approve:"))
|
||||
|
||||
// Verify checkboxes are still present
|
||||
expect(screen.getByLabelText("Read files and directories")).toBeInTheDocument()
|
||||
expect(screen.getByLabelText("Edit files")).toBeInTheDocument()
|
||||
|
||||
// Verify the setters still haven't been called
|
||||
expect(mockState.setAlwaysAllowReadOnly).not.toHaveBeenCalled()
|
||||
expect(mockState.setAlwaysAllowWrite).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -16,7 +16,6 @@ jest.mock("../../../components/common/MarkdownBlock")
|
||||
|
||||
// Get the mocked postMessage function
|
||||
const mockPostMessage = vscode.postMessage as jest.Mock
|
||||
/* eslint-enable import/first */
|
||||
|
||||
// Mock ExtensionStateContext
|
||||
jest.mock("../../../context/ExtensionStateContext")
|
||||
@@ -42,6 +41,7 @@ describe("ChatTextArea", () => {
|
||||
// Default mock implementation for useExtensionState
|
||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
openedTabs: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: "anthropic",
|
||||
},
|
||||
@@ -52,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} />)
|
||||
@@ -69,6 +70,7 @@ describe("ChatTextArea", () => {
|
||||
|
||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
openedTabs: [],
|
||||
apiConfiguration,
|
||||
})
|
||||
|
||||
@@ -86,6 +88,7 @@ describe("ChatTextArea", () => {
|
||||
it("should not send message when input is empty", () => {
|
||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
openedTabs: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: "openrouter",
|
||||
},
|
||||
@@ -102,6 +105,7 @@ describe("ChatTextArea", () => {
|
||||
it("should show loading state while enhancing", () => {
|
||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
openedTabs: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: "openrouter",
|
||||
},
|
||||
@@ -124,6 +128,7 @@ describe("ChatTextArea", () => {
|
||||
// Update apiConfiguration
|
||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
openedTabs: [],
|
||||
apiConfiguration: {
|
||||
apiProvider: "openrouter",
|
||||
newSetting: "test",
|
||||
|
||||
@@ -202,6 +202,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
<VSCodeRadioGroup
|
||||
style={{ display: "flex", flexWrap: "wrap" }}
|
||||
value={sortOption}
|
||||
role="radiogroup"
|
||||
onChange={(e) => setSortOption((e.target as HTMLInputElement).value as SortOption)}>
|
||||
<VSCodeRadio value="newest">Newest</VSCodeRadio>
|
||||
<VSCodeRadio value="oldest">Oldest</VSCodeRadio>
|
||||
|
||||
@@ -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" }}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
|
||||
import "@testing-library/jest-dom"
|
||||
import PromptsView from "../PromptsView"
|
||||
import { ExtensionStateContext } from "../../../context/ExtensionStateContext"
|
||||
@@ -98,20 +98,14 @@ describe("PromptsView", () => {
|
||||
expect(codeTab).toHaveAttribute("data-active", "false")
|
||||
})
|
||||
|
||||
it("handles prompt changes correctly", () => {
|
||||
it("handles prompt changes correctly", async () => {
|
||||
renderPromptsView()
|
||||
|
||||
const textarea = screen.getByTestId("code-prompt-textarea")
|
||||
fireEvent(
|
||||
textarea,
|
||||
new CustomEvent("change", {
|
||||
detail: {
|
||||
target: {
|
||||
value: "New prompt value",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
// Get the textarea
|
||||
const textarea = await waitFor(() => screen.getByTestId("code-prompt-textarea"))
|
||||
fireEvent.change(textarea, {
|
||||
target: { value: "New prompt value" },
|
||||
})
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "updatePrompt",
|
||||
@@ -163,24 +157,18 @@ describe("PromptsView", () => {
|
||||
expect(screen.queryByTestId("role-definition-reset")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("handles API configuration selection", () => {
|
||||
it("handles API configuration selection", async () => {
|
||||
renderPromptsView()
|
||||
|
||||
// Click the ENHANCE tab first to show the API config dropdown
|
||||
const enhanceTab = screen.getByTestId("ENHANCE-tab")
|
||||
fireEvent.click(enhanceTab)
|
||||
|
||||
const dropdown = screen.getByTestId("api-config-dropdown")
|
||||
fireEvent(
|
||||
dropdown,
|
||||
new CustomEvent("change", {
|
||||
detail: {
|
||||
target: {
|
||||
value: "config1",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
// Wait for the ENHANCE tab click to take effect
|
||||
const dropdown = await waitFor(() => screen.getByTestId("api-config-dropdown"))
|
||||
fireEvent.change(dropdown, {
|
||||
target: { value: "config1" },
|
||||
})
|
||||
|
||||
expect(mockExtensionState.setEnhancementApiConfigId).toHaveBeenCalledWith("config1")
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
@@ -198,13 +186,9 @@ describe("PromptsView", () => {
|
||||
})
|
||||
|
||||
const textarea = screen.getByTestId("global-custom-instructions-textarea")
|
||||
const changeEvent = new CustomEvent("change", {
|
||||
detail: { target: { value: "" } },
|
||||
fireEvent.change(textarea, {
|
||||
target: { value: "" },
|
||||
})
|
||||
Object.defineProperty(changeEvent, "target", {
|
||||
value: { value: "" },
|
||||
})
|
||||
await fireEvent(textarea, changeEvent)
|
||||
|
||||
expect(setCustomInstructions).toHaveBeenCalledWith(undefined)
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -563,7 +563,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
|
||||
)}
|
||||
|
||||
{selectedProvider === "openai" && (
|
||||
<div>
|
||||
<div style={{ display: "flex", flexDirection: "column", rowGap: "5px" }}>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openAiBaseUrl || ""}
|
||||
style={{ width: "100%" }}
|
||||
|
||||
@@ -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",
|
||||
@@ -139,6 +142,20 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const sliderLabelStyle = {
|
||||
minWidth: "45px",
|
||||
textAlign: "right" as const,
|
||||
lineHeight: "20px",
|
||||
paddingBottom: "2px",
|
||||
}
|
||||
|
||||
const sliderStyle = {
|
||||
flexGrow: 1,
|
||||
maxWidth: "80%",
|
||||
accentColor: "var(--vscode-button-background)",
|
||||
height: "2px",
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -481,8 +498,9 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
|
||||
<span style={{ fontWeight: "500" }}>Screenshot quality</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
|
||||
<span style={{ fontWeight: "500", minWidth: "100px" }}>Screenshot quality</span>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
@@ -491,12 +509,11 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
value={screenshotQuality ?? 75}
|
||||
onChange={(e) => setScreenshotQuality(parseInt(e.target.value))}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
accentColor: "var(--vscode-button-background)",
|
||||
height: "2px",
|
||||
...sliderStyle,
|
||||
}}
|
||||
/>
|
||||
<span style={{ minWidth: "35px", textAlign: "left" }}>{screenshotQuality ?? 75}%</span>
|
||||
<span style={{ ...sliderLabelStyle }}>{screenshotQuality ?? 75}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
@@ -559,8 +576,29 @@ 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>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
|
||||
<span style={{ fontWeight: "500", minWidth: "150px" }}>Terminal output limit</span>
|
||||
<input
|
||||
type="range"
|
||||
min="100"
|
||||
@@ -568,15 +606,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
step="100"
|
||||
value={terminalOutputLineLimit ?? 500}
|
||||
onChange={(e) => setTerminalOutputLineLimit(parseInt(e.target.value))}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
accentColor: "var(--vscode-button-background)",
|
||||
height: "2px",
|
||||
}}
|
||||
style={{ ...sliderStyle }}
|
||||
/>
|
||||
<span style={{ minWidth: "45px", textAlign: "left" }}>
|
||||
{terminalOutputLineLimit ?? 500}
|
||||
</span>
|
||||
<span style={{ ...sliderLabelStyle }}>{terminalOutputLineLimit ?? 500}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
Maximum number of lines to include in terminal output when executing commands. When exceeded
|
||||
@@ -614,8 +647,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
enabled={experiments[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false}
|
||||
onChange={(enabled) => setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, enabled)}
|
||||
/>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "5px", marginTop: "15px" }}>
|
||||
<span style={{ fontWeight: "500", minWidth: "100px" }}>Match precision</span>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: "5px", marginTop: "15px" }}>
|
||||
<span style={{ fontWeight: "500" }}>Match precision</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
|
||||
<input
|
||||
type="range"
|
||||
min="0.8"
|
||||
@@ -626,15 +661,14 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
setFuzzyMatchThreshold(parseFloat(e.target.value))
|
||||
}}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
accentColor: "var(--vscode-button-background)",
|
||||
height: "2px",
|
||||
...sliderStyle,
|
||||
}}
|
||||
/>
|
||||
<span style={{ minWidth: "35px", textAlign: "left" }}>
|
||||
<span style={{ ...sliderLabelStyle }}>
|
||||
{Math.round((fuzzyMatchThreshold || 1) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
|
||||
@@ -41,7 +41,10 @@ describe("ApiConfigManager", () => {
|
||||
|
||||
const defaultProps = {
|
||||
currentApiConfigName: "Default Config",
|
||||
listApiConfigMeta: [{ name: "Default Config" }, { name: "Another Config" }],
|
||||
listApiConfigMeta: [
|
||||
{ id: "default", name: "Default Config" },
|
||||
{ id: "another", name: "Another Config" },
|
||||
],
|
||||
onSelectConfig: mockOnSelectConfig,
|
||||
onDeleteConfig: mockOnDeleteConfig,
|
||||
onRenameConfig: mockOnRenameConfig,
|
||||
@@ -120,7 +123,7 @@ describe("ApiConfigManager", () => {
|
||||
})
|
||||
|
||||
it("disables delete button when only one config exists", () => {
|
||||
render(<ApiConfigManager {...defaultProps} listApiConfigMeta={[{ name: "Default Config" }]} />)
|
||||
render(<ApiConfigManager {...defaultProps} listApiConfigMeta={[{ id: "default", name: "Default Config" }]} />)
|
||||
|
||||
const deleteButton = screen.getByTitle("Cannot delete the only profile")
|
||||
expect(deleteButton).toHaveAttribute("disabled")
|
||||
|
||||
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-1 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 }
|
||||
@@ -27,6 +27,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
||||
openAiModels: string[]
|
||||
mcpServers: McpServer[]
|
||||
filePaths: string[]
|
||||
openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
|
||||
setApiConfiguration: (config: ApiConfiguration) => void
|
||||
setCustomInstructions: (value?: string) => void
|
||||
setAlwaysAllowReadOnly: (value: boolean) => void
|
||||
@@ -51,10 +52,14 @@ export interface ExtensionStateContextType extends ExtensionState {
|
||||
setTerminalOutputLineLimit: (value: number) => void
|
||||
mcpEnabled: boolean
|
||||
setMcpEnabled: (value: boolean) => void
|
||||
enableMcpServerCreation: boolean
|
||||
setEnableMcpServerCreation: (value: boolean) => void
|
||||
alwaysApproveResubmit?: boolean
|
||||
setAlwaysApproveResubmit: (value: boolean) => void
|
||||
requestDelaySeconds: number
|
||||
setRequestDelaySeconds: (value: number) => void
|
||||
rateLimitSeconds: number
|
||||
setRateLimitSeconds: (value: number) => void
|
||||
setCurrentApiConfigName: (value: string) => void
|
||||
setListApiConfigMeta: (value: ApiConfigMeta[]) => void
|
||||
onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
|
||||
@@ -90,8 +95,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
screenshotQuality: 75,
|
||||
terminalOutputLineLimit: 500,
|
||||
mcpEnabled: true,
|
||||
enableMcpServerCreation: true,
|
||||
alwaysApproveResubmit: false,
|
||||
requestDelaySeconds: 5,
|
||||
rateLimitSeconds: 0, // Minimum time between successive requests (0 = disabled)
|
||||
currentApiConfigName: "default",
|
||||
listApiConfigMeta: [],
|
||||
mode: defaultModeSlug,
|
||||
@@ -110,6 +117,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
const [glamaModels, setGlamaModels] = useState<Record<string, ModelInfo>>({
|
||||
[glamaDefaultModelId]: glamaDefaultModelInfo,
|
||||
})
|
||||
const [openedTabs, setOpenedTabs] = useState<Array<{ label: string; isActive: boolean; path?: string }>>([])
|
||||
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
|
||||
})
|
||||
@@ -170,7 +178,11 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
break
|
||||
}
|
||||
case "workspaceUpdated": {
|
||||
setFilePaths(message.filePaths ?? [])
|
||||
const paths = message.filePaths ?? []
|
||||
const tabs = message.openedTabs ?? []
|
||||
|
||||
setFilePaths(paths)
|
||||
setOpenedTabs(tabs)
|
||||
break
|
||||
}
|
||||
case "partialMessage": {
|
||||
@@ -237,6 +249,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
openAiModels,
|
||||
mcpServers,
|
||||
filePaths,
|
||||
openedTabs,
|
||||
soundVolume: state.soundVolume,
|
||||
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
|
||||
writeDelayMs: state.writeDelayMs,
|
||||
@@ -269,8 +282,11 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
setTerminalOutputLineLimit: (value) =>
|
||||
setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
|
||||
setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
|
||||
setEnableMcpServerCreation: (value) =>
|
||||
setState((prevState) => ({ ...prevState, enableMcpServerCreation: value })),
|
||||
setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
|
||||
setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
|
||||
setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })),
|
||||
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
|
||||
setListApiConfigMeta,
|
||||
onUpdateApiConfig,
|
||||
|
||||
@@ -1,3 +1,94 @@
|
||||
/* @import "tailwindcss"; */
|
||||
|
||||
@layer theme, base, components, utilities;
|
||||
|
||||
@import "tailwindcss/theme.css" layer(theme);
|
||||
/* https://tailwindcss.com/docs/preflight */
|
||||
/* @import "tailwindcss/preflight.css" layer(base); */
|
||||
@import "tailwindcss/utilities.css" layer(utilities);
|
||||
|
||||
@plugin "tailwindcss-animate";
|
||||
|
||||
@theme {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: var(--vscode-editor-background);
|
||||
--foreground: var(--vscode-editor-foreground);
|
||||
--card: var(--vscode-editor-background);
|
||||
--card-foreground: var(--vscode-editor-foreground);
|
||||
--popover: var(--vscode-menu-background, var(--vscode-editor-background));
|
||||
--popover-foreground: var(--vscode-menu-foreground, var(--vscode-editor-foreground));
|
||||
--primary: var(--vscode-button-background);
|
||||
--primary-foreground: var(--vscode-button-foreground);
|
||||
--secondary: var(--vscode-button-secondaryBackground);
|
||||
--secondary-foreground: var(--vscode-button-secondaryForeground);
|
||||
--muted: var(--vscode-disabledForeground);
|
||||
--muted-foreground: var(--vscode-descriptionForeground);
|
||||
--accent: var(--vscode-input-border);
|
||||
--accent-foreground: var(--vscode-button-foreground);
|
||||
--destructive: var(--vscode-errorForeground);
|
||||
--destructive-foreground: var(--vscode-button-foreground);
|
||||
--border: var(--vscode-widget-border);
|
||||
--input: var(--vscode-input-background);
|
||||
--ring: var(--vscode-input-border);
|
||||
--chart-1: var(--vscode-charts-red);
|
||||
--chart-2: var(--vscode-charts-blue);
|
||||
--chart-3: var(--vscode-charts-yellow);
|
||||
--chart-4: var(--vscode-charts-orange);
|
||||
--chart-5: var(--vscode-charts-green);
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Border Styles */
|
||||
.border,
|
||||
.border-r,
|
||||
.border-l,
|
||||
.border-t,
|
||||
.border-b,
|
||||
.border-x,
|
||||
.border-y {
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
/* Code Block Styles */
|
||||
pre,
|
||||
code {
|
||||
background-color: var(--vscode-textCodeBlock-background);
|
||||
}
|
||||
}
|
||||
|
||||
/* Form Element Focus States */
|
||||
textarea:focus {
|
||||
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
|
||||
}
|
||||
@@ -6,10 +97,10 @@ vscode-button::part(control):focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Use vscode native scrollbar styles
|
||||
https://github.com/gitkraken/vscode-gitlens/blob/b1d71d4844523e8b2ef16f9e007068e91f46fd88/src/webviews/apps/home/home.scss
|
||||
*/
|
||||
/**
|
||||
* Use vscode native scrollbar styles
|
||||
* https://github.com/gitkraken/vscode-gitlens/blob/b1d71d4844523e8b2ef16f9e007068e91f46fd88/src/webviews/apps/home/home.scss
|
||||
*/
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
@@ -99,10 +190,11 @@ The above scrollbar styling uses some transparent background color magic to acco
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/*
|
||||
Dropdown label
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit/tree/main/src/dropdown#with-label
|
||||
*/
|
||||
/**
|
||||
* Dropdown label
|
||||
* https://github.com/microsoft/vscode-webview-ui-toolkit/tree/main/src/dropdown#with-label
|
||||
*/
|
||||
|
||||
.dropdown-container {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
@@ -110,6 +202,7 @@ https://github.com/microsoft/vscode-webview-ui-toolkit/tree/main/src/dropdown#wi
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.dropdown-container label {
|
||||
display: block;
|
||||
color: var(--vscode-foreground);
|
||||
@@ -120,6 +213,7 @@ https://github.com/microsoft/vscode-webview-ui-toolkit/tree/main/src/dropdown#wi
|
||||
}
|
||||
|
||||
/* Fix dropdown double scrollbar overflow */
|
||||
|
||||
#api-provider > div > ul {
|
||||
overflow: unset;
|
||||
}
|
||||
@@ -133,18 +227,20 @@ vscode-dropdown::part(listbox) {
|
||||
}
|
||||
|
||||
/* Faded icon buttons in textfields */
|
||||
|
||||
.input-icon-button {
|
||||
cursor: pointer;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.input-icon-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.input-icon-button.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.input-icon-button.disabled:hover {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@@ -156,10 +252,6 @@ vscode-dropdown::part(listbox) {
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 0 0.5px color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
|
||||
color: transparent;
|
||||
/* padding: 0.5px;
|
||||
margin: -0.5px;
|
||||
position: relative;
|
||||
bottom: -0.5px; */
|
||||
}
|
||||
|
||||
.mention-context-highlight {
|
||||
@@ -172,3 +264,11 @@ vscode-dropdown::part(listbox) {
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 0 0.5px color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
|
||||
}
|
||||
|
||||
/**
|
||||
* vscrui Overrides / Hacks
|
||||
*/
|
||||
|
||||
.vscrui-checkbox__listbox > ul {
|
||||
max-height: unset !important;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
|
||||
import "./index.css"
|
||||
import App from "./App"
|
||||
import reportWebVitals from "./reportWebVitals"
|
||||
import "../../node_modules/@vscode/codicons/dist/codicon.css"
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals()
|
||||
|
||||
6
webview-ui/src/lib/utils.ts
Normal file
6
webview-ui/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
1
webview-ui/src/react-app-env.d.ts
vendored
1
webview-ui/src/react-app-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -1,15 +0,0 @@
|
||||
import { ReportHandler } from "web-vitals"
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry)
|
||||
getFID(onPerfEntry)
|
||||
getFCP(onPerfEntry)
|
||||
getLCP(onPerfEntry)
|
||||
getTTFB(onPerfEntry)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default reportWebVitals
|
||||
53
webview-ui/src/stories/Button.stories.ts
Normal file
53
webview-ui/src/stories/Button.stories.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react"
|
||||
import { fn } from "@storybook/test"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
const meta = {
|
||||
title: "Example/Button",
|
||||
component: Button,
|
||||
parameters: { layout: "centered" },
|
||||
tags: ["autodocs"],
|
||||
argTypes: {},
|
||||
args: { onClick: fn(), children: "Button" },
|
||||
} satisfies Meta<typeof Button>
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
variant: "secondary",
|
||||
},
|
||||
}
|
||||
|
||||
export const Outline: Story = {
|
||||
args: {
|
||||
variant: "outline",
|
||||
},
|
||||
}
|
||||
|
||||
export const Ghost: Story = {
|
||||
args: {
|
||||
variant: "ghost",
|
||||
},
|
||||
}
|
||||
|
||||
export const Link: Story = {
|
||||
args: {
|
||||
variant: "link",
|
||||
},
|
||||
}
|
||||
|
||||
export const Destructive: Story = {
|
||||
args: {
|
||||
variant: "destructive",
|
||||
},
|
||||
}
|
||||
7
webview-ui/src/stories/Welcome.mdx
Normal file
7
webview-ui/src/stories/Welcome.mdx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Meta } from "@storybook/blocks";
|
||||
|
||||
<Meta title="Welcome" />
|
||||
|
||||
# Welcome
|
||||
|
||||
This Roo Code storybook is used to independently develop components for the Roo Code webview UI.
|
||||
0
webview-ui/src/stories/assets/.gitkeep
Normal file
0
webview-ui/src/stories/assets/.gitkeep
Normal file
@@ -48,6 +48,7 @@ export function removeMention(text: string, position: number): { newText: string
|
||||
}
|
||||
|
||||
export enum ContextMenuOptionType {
|
||||
OpenedFile = "openedFile",
|
||||
File = "file",
|
||||
Folder = "folder",
|
||||
Problems = "problems",
|
||||
@@ -80,8 +81,14 @@ export function getContextMenuOptions(
|
||||
if (query === "") {
|
||||
if (selectedType === ContextMenuOptionType.File) {
|
||||
const files = queryItems
|
||||
.filter((item) => item.type === ContextMenuOptionType.File)
|
||||
.map((item) => ({ type: ContextMenuOptionType.File, value: item.value }))
|
||||
.filter(
|
||||
(item) =>
|
||||
item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.OpenedFile,
|
||||
)
|
||||
.map((item) => ({
|
||||
type: item.type,
|
||||
value: item.value,
|
||||
}))
|
||||
return files.length > 0 ? files : [{ type: ContextMenuOptionType.NoResults }]
|
||||
}
|
||||
|
||||
@@ -162,12 +169,16 @@ export function getContextMenuOptions(
|
||||
|
||||
// Separate matches by type
|
||||
const fileMatches = matchingItems.filter(
|
||||
(item) => item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.Folder,
|
||||
(item) =>
|
||||
item.type === ContextMenuOptionType.File ||
|
||||
item.type === ContextMenuOptionType.OpenedFile ||
|
||||
item.type === ContextMenuOptionType.Folder,
|
||||
)
|
||||
const gitMatches = matchingItems.filter((item) => item.type === ContextMenuOptionType.Git)
|
||||
const otherMatches = matchingItems.filter(
|
||||
(item) =>
|
||||
item.type !== ContextMenuOptionType.File &&
|
||||
item.type !== ContextMenuOptionType.OpenedFile &&
|
||||
item.type !== ContextMenuOptionType.Folder &&
|
||||
item.type !== ContextMenuOptionType.Git,
|
||||
)
|
||||
|
||||
1
webview-ui/src/vite-env.d.ts
vendored
Normal file
1
webview-ui/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -14,7 +14,11 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "../src/shared"]
|
||||
}
|
||||
|
||||
36
webview-ui/vite.config.ts
Normal file
36
webview-ui/vite.config.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import path from "path"
|
||||
|
||||
import { defineConfig } from "vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: "build",
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `assets/[name].js`,
|
||||
chunkFileNames: `assets/[name].js`,
|
||||
assetFileNames: `assets/[name].[ext]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
hmr: {
|
||||
host: "localhost",
|
||||
protocol: "ws",
|
||||
},
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: "*",
|
||||
allowedHeaders: "*",
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user