Browse Source

Merge branch 'main' into WEB-1756

pugazhendhi-m 11 months ago
parent
commit
26baaf89a3
57 changed files with 5388 additions and 2135 deletions
  1. 0 5
      .changeset/blue-masks-camp.md
  2. 0 5
      .changeset/breezy-badgers-refuse.md
  3. 1 2
      .eslintrc.json
  4. 8 1
      .github/workflows/marketplace-publish.yml
  5. 0 44
      .github/workflows/pages.yml
  6. 1 1
      .gitignore
  7. 0 4
      .husky/pre-commit
  8. 3 2
      .husky/pre-push
  9. 1 1
      .vscode/launch.json
  10. 6 45
      .vscode/tasks.json
  11. 2 0
      .vscodeignore
  12. 36 0
      CHANGELOG.md
  13. 18 21
      README.md
  14. 0 2
      docs/Gemfile
  15. 0 308
      docs/Gemfile.lock
  16. 0 15
      docs/_config.yml
  17. 0 10
      docs/getting-started/index.md
  18. 0 9
      docs/index.md
  19. 20 2
      package-lock.json
  20. 78 28
      package.json
  21. 1 0
      src/activate/index.ts
  22. 81 0
      src/activate/registerTerminalActions.ts
  23. 1 1
      src/api/providers/__tests__/gemini.test.ts
  24. 7 2
      src/api/providers/ollama.ts
  25. 13 3
      src/api/providers/openai.ts
  26. 21 58
      src/core/Cline.ts
  27. 88 17
      src/core/sliding-window/index.ts
  28. 35 0
      src/core/webview/ClineProvider.ts
  29. 2 1
      src/extension.ts
  30. 0 0
      src/integrations/terminal/TerminalActions.ts
  31. 53 0
      src/integrations/terminal/TerminalManager.ts
  32. 1 63
      src/shared/__tests__/modes.test.ts
  33. 25 1
      src/shared/api.ts
  34. 0 33
      src/shared/modes.ts
  35. 40 0
      src/shared/support-prompt.ts
  36. 6 0
      src/utils/__tests__/shell.test.ts
  37. 1 1
      src/utils/shell.ts
  38. 3 0
      webview-ui/.eslintrc.json
  39. 22 0
      webview-ui/jest.config.cjs
  40. 4260 1334
      webview-ui/package-lock.json
  41. 7 41
      webview-ui/package.json
  42. 40 9
      webview-ui/src/components/chat/ChatTextArea.tsx
  43. 1 1
      webview-ui/src/components/chat/ChatView.tsx
  44. 37 11
      webview-ui/src/components/chat/ContextMenu.tsx
  45. 0 1
      webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx
  46. 33 40
      webview-ui/src/components/prompts/PromptsView.tsx
  47. 0 1
      webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx
  48. 0 1
      webview-ui/src/components/settings/__tests__/ApiConfigManager.test.tsx
  49. 5 5
      webview-ui/src/components/ui/button.tsx
  50. 131 0
      webview-ui/src/components/ui/command.tsx
  51. 96 0
      webview-ui/src/components/ui/dialog.tsx
  52. 6 4
      webview-ui/src/components/ui/dropdown-menu.tsx
  53. 3 0
      webview-ui/src/components/ui/index.ts
  54. 31 0
      webview-ui/src/components/ui/popover.tsx
  55. 15 2
      webview-ui/src/index.css
  56. 99 0
      webview-ui/src/stories/Combobox.stories.tsx
  57. 50 0
      webview-ui/src/utils/context-mentions.ts

+ 0 - 5
.changeset/blue-masks-camp.md

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

+ 0 - 5
.changeset/breezy-badgers-refuse.md

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

+ 1 - 2
.eslintrc.json

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

+ 8 - 1
.github/workflows/marketplace-publish.yml

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

+ 0 - 44
.github/workflows/pages.yml

@@ -1,44 +0,0 @@
-name: Deploy Jekyll site to Pages
-
-on:
-  push:
-    branches: ["main"]
-  workflow_dispatch:
-
-permissions:
-  contents: read
-  pages: write
-  id-token: write
-
-concurrency:
-  group: "pages"
-  cancel-in-progress: false
-
-jobs:
-  # Build job
-  build:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v4
-      - name: Setup Pages
-        uses: actions/configure-pages@v5
-      - name: Build with Jekyll
-        uses: actions/jekyll-build-pages@v1
-        with:
-          source: ./docs/
-          destination: ./_site
-      - name: Upload artifact
-        uses: actions/upload-pages-artifact@v3
-
-  # Deployment job
-  deploy:
-    environment:
-      name: github-pages
-      url: ${{ steps.deployment.outputs.page_url }}
-    runs-on: ubuntu-latest
-    needs: build
-    steps:
-      - name: Deploy to GitHub Pages
-        id: deployment
-        uses: actions/deploy-pages@v4

+ 1 - 1
.gitignore

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

+ 0 - 4
.husky/pre-commit

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

+ 3 - 2
.husky/pre-push

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

+ 1 - 1
.vscode/launch.json

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

+ 6 - 45
.vscode/tasks.json

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

+ 2 - 0
.vscodeignore

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

+ 36 - 0
CHANGELOG.md

@@ -1,5 +1,41 @@
 # Roo Code Changelog
 
+## [3.3.14]
+
+- Should have skipped floor 13 like an elevator. This fixes the broken 3.3.13 release by reverting some changes to the deployment scripts.
+
+## [3.3.13]
+
+- Ensure the DeepSeek r1 model works with Ollama (thanks @sammcj!)
+- Enable context menu commands in the terminal (thanks @samhvw8!)
+- Improve sliding window truncation strategy for models that do not support prompt caching (thanks @nissa-seru!)
+- First step of a more fundamental fix to the bugs around switching API profiles. If you've been having issues with this please try again and let us know if works any better! More to come soon, including fixing the laggy text entry in provider settings.
+
+## [3.3.12]
+
+- Bug fix to changing a mode's API configuration on the prompts tab
+- Add new Gemini models
+
+## [3.3.11]
+
+- Safer shell profile path check to avoid an error on Windows
+- Autocomplete for slash commands
+
+## [3.3.10]
+
+- Add shortcuts to the currently open tabs in the "Add File" section of @-mentions (thanks @olup!)
+- Fix pricing for o1-mini (thanks @hesara!)
+- Fix context window size calculation (thanks @MuriloFP!)
+- Improvements to experimental unified diff strategy and selection logic in code actions (thanks @nissa-seru!)
+- Enable markdown formatting in o3 and o1 (thanks @nissa-seru!)
+- Improved terminal shell detection logic (thanks @canvrno for the original and @nissa-seru for the port!)
+- Fix occasional errors when switching between API profiles (thanks @samhvw8!)
+- Visual improvements to the list of modes on the prompts tab
+- Fix double-scrollbar in provider dropdown
+- Visual cleanup to the list of modes on the prompts tab
+- Improvements to the default prompts for Architect and Ask mode
+- Allow switching between modes with slash messages like `/ask why is the sky blue?`
+
 ## [3.3.9]
 
 - Add o3-mini-high and o3-mini-low

+ 18 - 21
README.md

@@ -1,24 +1,21 @@
-# Roo Code (prev. Roo Cline)
-
-<table>
-<tbody>
-<td align="center">
-<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline" target="_blank"><strong>Download on VS Marketplace</strong></a>
-</td>
-<td align="center">
-<a href="https://discord.gg/roocode" target="_blank"><strong>Discord</strong></a>
-</td>
-<td align="center">
-<a href="https://www.reddit.com/r/RooCode/" target="_blank"><strong>r/RooCode</strong></a>
-</td>
-<td align="center">
-<a href="https://github.com/RooVetGit/Roo-Code/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
-</td>
-<td align="center">
-<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline&ssr=false#review-details" target="_blank"><strong>Rate & Review</strong></a>
-</td>
-</tbody>
-</table>
+<div align="center">
+  <h2>Join the Roo Code Community</h2>
+  <p>Connect with developers, contribute ideas, and stay ahead with the latest AI-powered coding tools.</p>
+  
+  <a href="https://discord.gg/roocode" target="_blank"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join Discord" height="60"></a>
+  <a href="https://www.reddit.com/r/RooCode/" target="_blank"><img src="https://img.shields.io/badge/Join%20Reddit-FF4500?style=for-the-badge&logo=reddit&logoColor=white" alt="Join Reddit" height="60"></a>
+  
+</div>
+<br>
+<br>
+
+<div align="center">
+<h1>Roo Code (prev. Roo Cline)</h1>
+
+<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline" target="_blank"><img src="https://img.shields.io/badge/Download%20on%20VS%20Marketplace-blue?style=for-the-badge&logo=visualstudiocode&logoColor=white" alt="Download on VS Marketplace"></a>
+<a href="https://github.com/RooVetGit/Roo-Code/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><img src="https://img.shields.io/badge/Feature%20Requests-yellow?style=for-the-badge" alt="Feature Requests"></a>
+<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline&ssr=false#review-details" target="_blank"><img src="https://img.shields.io/badge/Rate%20%26%20Review-green?style=for-the-badge" alt="Rate & Review"></a>
+</div>
 
 **Roo Code** is an AI-powered **autonomous coding agent** that lives in your editor. It can:
 

+ 0 - 2
docs/Gemfile

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

+ 0 - 308
docs/Gemfile.lock

@@ -1,308 +0,0 @@
-GEM
-  remote: https://rubygems.org/
-  specs:
-    activesupport (8.0.1)
-      base64
-      benchmark (>= 0.3)
-      bigdecimal
-      concurrent-ruby (~> 1.0, >= 1.3.1)
-      connection_pool (>= 2.2.5)
-      drb
-      i18n (>= 1.6, < 2)
-      logger (>= 1.4.2)
-      minitest (>= 5.1)
-      securerandom (>= 0.3)
-      tzinfo (~> 2.0, >= 2.0.5)
-      uri (>= 0.13.1)
-    addressable (2.8.7)
-      public_suffix (>= 2.0.2, < 7.0)
-    base64 (0.2.0)
-    benchmark (0.4.0)
-    bigdecimal (3.1.9)
-    coffee-script (2.4.1)
-      coffee-script-source
-      execjs
-    coffee-script-source (1.12.2)
-    colorator (1.1.0)
-    commonmarker (0.23.11)
-    concurrent-ruby (1.3.5)
-    connection_pool (2.5.0)
-    csv (3.3.2)
-    dnsruby (1.72.3)
-      base64 (~> 0.2.0)
-      simpleidn (~> 0.2.1)
-    drb (2.2.1)
-    em-websocket (0.5.3)
-      eventmachine (>= 0.12.9)
-      http_parser.rb (~> 0)
-    ethon (0.16.0)
-      ffi (>= 1.15.0)
-    eventmachine (1.2.7)
-    execjs (2.10.0)
-    faraday (2.12.2)
-      faraday-net_http (>= 2.0, < 3.5)
-      json
-      logger
-    faraday-net_http (3.4.0)
-      net-http (>= 0.5.0)
-    ffi (1.17.1-aarch64-linux-gnu)
-    ffi (1.17.1-aarch64-linux-musl)
-    ffi (1.17.1-arm-linux-gnu)
-    ffi (1.17.1-arm-linux-musl)
-    ffi (1.17.1-arm64-darwin)
-    ffi (1.17.1-x86_64-darwin)
-    ffi (1.17.1-x86_64-linux-gnu)
-    ffi (1.17.1-x86_64-linux-musl)
-    forwardable-extended (2.6.0)
-    gemoji (4.1.0)
-    github-pages (232)
-      github-pages-health-check (= 1.18.2)
-      jekyll (= 3.10.0)
-      jekyll-avatar (= 0.8.0)
-      jekyll-coffeescript (= 1.2.2)
-      jekyll-commonmark-ghpages (= 0.5.1)
-      jekyll-default-layout (= 0.1.5)
-      jekyll-feed (= 0.17.0)
-      jekyll-gist (= 1.5.0)
-      jekyll-github-metadata (= 2.16.1)
-      jekyll-include-cache (= 0.2.1)
-      jekyll-mentions (= 1.6.0)
-      jekyll-optional-front-matter (= 0.3.2)
-      jekyll-paginate (= 1.1.0)
-      jekyll-readme-index (= 0.3.0)
-      jekyll-redirect-from (= 0.16.0)
-      jekyll-relative-links (= 0.6.1)
-      jekyll-remote-theme (= 0.4.3)
-      jekyll-sass-converter (= 1.5.2)
-      jekyll-seo-tag (= 2.8.0)
-      jekyll-sitemap (= 1.4.0)
-      jekyll-swiss (= 1.0.0)
-      jekyll-theme-architect (= 0.2.0)
-      jekyll-theme-cayman (= 0.2.0)
-      jekyll-theme-dinky (= 0.2.0)
-      jekyll-theme-hacker (= 0.2.0)
-      jekyll-theme-leap-day (= 0.2.0)
-      jekyll-theme-merlot (= 0.2.0)
-      jekyll-theme-midnight (= 0.2.0)
-      jekyll-theme-minimal (= 0.2.0)
-      jekyll-theme-modernist (= 0.2.0)
-      jekyll-theme-primer (= 0.6.0)
-      jekyll-theme-slate (= 0.2.0)
-      jekyll-theme-tactile (= 0.2.0)
-      jekyll-theme-time-machine (= 0.2.0)
-      jekyll-titles-from-headings (= 0.5.3)
-      jemoji (= 0.13.0)
-      kramdown (= 2.4.0)
-      kramdown-parser-gfm (= 1.1.0)
-      liquid (= 4.0.4)
-      mercenary (~> 0.3)
-      minima (= 2.5.1)
-      nokogiri (>= 1.16.2, < 2.0)
-      rouge (= 3.30.0)
-      terminal-table (~> 1.4)
-      webrick (~> 1.8)
-    github-pages-health-check (1.18.2)
-      addressable (~> 2.3)
-      dnsruby (~> 1.60)
-      octokit (>= 4, < 8)
-      public_suffix (>= 3.0, < 6.0)
-      typhoeus (~> 1.3)
-    html-pipeline (2.14.3)
-      activesupport (>= 2)
-      nokogiri (>= 1.4)
-    http_parser.rb (0.8.0)
-    i18n (1.14.7)
-      concurrent-ruby (~> 1.0)
-    jekyll (3.10.0)
-      addressable (~> 2.4)
-      colorator (~> 1.0)
-      csv (~> 3.0)
-      em-websocket (~> 0.5)
-      i18n (>= 0.7, < 2)
-      jekyll-sass-converter (~> 1.0)
-      jekyll-watch (~> 2.0)
-      kramdown (>= 1.17, < 3)
-      liquid (~> 4.0)
-      mercenary (~> 0.3.3)
-      pathutil (~> 0.9)
-      rouge (>= 1.7, < 4)
-      safe_yaml (~> 1.0)
-      webrick (>= 1.0)
-    jekyll-avatar (0.8.0)
-      jekyll (>= 3.0, < 5.0)
-    jekyll-coffeescript (1.2.2)
-      coffee-script (~> 2.2)
-      coffee-script-source (~> 1.12)
-    jekyll-commonmark (1.4.0)
-      commonmarker (~> 0.22)
-    jekyll-commonmark-ghpages (0.5.1)
-      commonmarker (>= 0.23.7, < 1.1.0)
-      jekyll (>= 3.9, < 4.0)
-      jekyll-commonmark (~> 1.4.0)
-      rouge (>= 2.0, < 5.0)
-    jekyll-default-layout (0.1.5)
-      jekyll (>= 3.0, < 5.0)
-    jekyll-feed (0.17.0)
-      jekyll (>= 3.7, < 5.0)
-    jekyll-gist (1.5.0)
-      octokit (~> 4.2)
-    jekyll-github-metadata (2.16.1)
-      jekyll (>= 3.4, < 5.0)
-      octokit (>= 4, < 7, != 4.4.0)
-    jekyll-include-cache (0.2.1)
-      jekyll (>= 3.7, < 5.0)
-    jekyll-mentions (1.6.0)
-      html-pipeline (~> 2.3)
-      jekyll (>= 3.7, < 5.0)
-    jekyll-optional-front-matter (0.3.2)
-      jekyll (>= 3.0, < 5.0)
-    jekyll-paginate (1.1.0)
-    jekyll-readme-index (0.3.0)
-      jekyll (>= 3.0, < 5.0)
-    jekyll-redirect-from (0.16.0)
-      jekyll (>= 3.3, < 5.0)
-    jekyll-relative-links (0.6.1)
-      jekyll (>= 3.3, < 5.0)
-    jekyll-remote-theme (0.4.3)
-      addressable (~> 2.0)
-      jekyll (>= 3.5, < 5.0)
-      jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
-      rubyzip (>= 1.3.0, < 3.0)
-    jekyll-sass-converter (1.5.2)
-      sass (~> 3.4)
-    jekyll-seo-tag (2.8.0)
-      jekyll (>= 3.8, < 5.0)
-    jekyll-sitemap (1.4.0)
-      jekyll (>= 3.7, < 5.0)
-    jekyll-swiss (1.0.0)
-    jekyll-theme-architect (0.2.0)
-      jekyll (> 3.5, < 5.0)
-      jekyll-seo-tag (~> 2.0)
-    jekyll-theme-cayman (0.2.0)
-      jekyll (> 3.5, < 5.0)
-      jekyll-seo-tag (~> 2.0)
-    jekyll-theme-dinky (0.2.0)
-      jekyll (> 3.5, < 5.0)
-      jekyll-seo-tag (~> 2.0)
-    jekyll-theme-hacker (0.2.0)
-      jekyll (> 3.5, < 5.0)
-      jekyll-seo-tag (~> 2.0)
-    jekyll-theme-leap-day (0.2.0)
-      jekyll (> 3.5, < 5.0)
-      jekyll-seo-tag (~> 2.0)
-    jekyll-theme-merlot (0.2.0)
-      jekyll (> 3.5, < 5.0)
-      jekyll-seo-tag (~> 2.0)
-    jekyll-theme-midnight (0.2.0)
-      jekyll (> 3.5, < 5.0)
-      jekyll-seo-tag (~> 2.0)
-    jekyll-theme-minimal (0.2.0)
-      jekyll (> 3.5, < 5.0)
-      jekyll-seo-tag (~> 2.0)
-    jekyll-theme-modernist (0.2.0)
-      jekyll (> 3.5, < 5.0)
-      jekyll-seo-tag (~> 2.0)
-    jekyll-theme-primer (0.6.0)
-      jekyll (> 3.5, < 5.0)
-      jekyll-github-metadata (~> 2.9)
-      jekyll-seo-tag (~> 2.0)
-    jekyll-theme-slate (0.2.0)
-      jekyll (> 3.5, < 5.0)
-      jekyll-seo-tag (~> 2.0)
-    jekyll-theme-tactile (0.2.0)
-      jekyll (> 3.5, < 5.0)
-      jekyll-seo-tag (~> 2.0)
-    jekyll-theme-time-machine (0.2.0)
-      jekyll (> 3.5, < 5.0)
-      jekyll-seo-tag (~> 2.0)
-    jekyll-titles-from-headings (0.5.3)
-      jekyll (>= 3.3, < 5.0)
-    jekyll-watch (2.2.1)
-      listen (~> 3.0)
-    jemoji (0.13.0)
-      gemoji (>= 3, < 5)
-      html-pipeline (~> 2.2)
-      jekyll (>= 3.0, < 5.0)
-    json (2.9.1)
-    kramdown (2.4.0)
-      rexml
-    kramdown-parser-gfm (1.1.0)
-      kramdown (~> 2.0)
-    liquid (4.0.4)
-    listen (3.9.0)
-      rb-fsevent (~> 0.10, >= 0.10.3)
-      rb-inotify (~> 0.9, >= 0.9.10)
-    logger (1.6.5)
-    mercenary (0.3.6)
-    minima (2.5.1)
-      jekyll (>= 3.5, < 5.0)
-      jekyll-feed (~> 0.9)
-      jekyll-seo-tag (~> 2.1)
-    minitest (5.25.4)
-    net-http (0.6.0)
-      uri
-    nokogiri (1.18.2-aarch64-linux-gnu)
-      racc (~> 1.4)
-    nokogiri (1.18.2-aarch64-linux-musl)
-      racc (~> 1.4)
-    nokogiri (1.18.2-arm-linux-gnu)
-      racc (~> 1.4)
-    nokogiri (1.18.2-arm-linux-musl)
-      racc (~> 1.4)
-    nokogiri (1.18.2-arm64-darwin)
-      racc (~> 1.4)
-    nokogiri (1.18.2-x86_64-darwin)
-      racc (~> 1.4)
-    nokogiri (1.18.2-x86_64-linux-gnu)
-      racc (~> 1.4)
-    nokogiri (1.18.2-x86_64-linux-musl)
-      racc (~> 1.4)
-    octokit (4.25.1)
-      faraday (>= 1, < 3)
-      sawyer (~> 0.9)
-    pathutil (0.16.2)
-      forwardable-extended (~> 2.6)
-    public_suffix (5.1.1)
-    racc (1.8.1)
-    rb-fsevent (0.11.2)
-    rb-inotify (0.11.1)
-      ffi (~> 1.0)
-    rexml (3.4.0)
-    rouge (3.30.0)
-    rubyzip (2.4.1)
-    safe_yaml (1.0.5)
-    sass (3.7.4)
-      sass-listen (~> 4.0.0)
-    sass-listen (4.0.0)
-      rb-fsevent (~> 0.9, >= 0.9.4)
-      rb-inotify (~> 0.9, >= 0.9.7)
-    sawyer (0.9.2)
-      addressable (>= 2.3.5)
-      faraday (>= 0.17.3, < 3)
-    securerandom (0.4.1)
-    simpleidn (0.2.3)
-    terminal-table (1.8.0)
-      unicode-display_width (~> 1.1, >= 1.1.1)
-    typhoeus (1.4.1)
-      ethon (>= 0.9.0)
-    tzinfo (2.0.6)
-      concurrent-ruby (~> 1.0)
-    unicode-display_width (1.8.0)
-    uri (1.0.2)
-    webrick (1.9.1)
-
-PLATFORMS
-  aarch64-linux-gnu
-  aarch64-linux-musl
-  arm-linux-gnu
-  arm-linux-musl
-  arm64-darwin
-  x86_64-darwin
-  x86_64-linux-gnu
-  x86_64-linux-musl
-
-DEPENDENCIES
-  github-pages
-
-BUNDLED WITH
-   2.5.18

+ 0 - 15
docs/_config.yml

@@ -1,15 +0,0 @@
-title: Roo Code Documentation
-description: Documentation for the Roo Code project
-remote_theme: just-the-docs/just-the-docs
-
-url: https://docs.roocode.com
-
-aux_links:
-  "Roo Code on GitHub":
-    - "//github.com/RooVetGit/Roo-Code"
-
-# Enable search
-search_enabled: true
-
-# Enable dark mode
-color_scheme: dark

+ 0 - 10
docs/getting-started/index.md

@@ -1,10 +0,0 @@
----
-title: Getting Started
-layout: default
-nav_order: 2
-has_children: true
----
-
-# Getting Started with Roo Code
-
-This section will help you get up and running with Roo Code quickly.

+ 0 - 9
docs/index.md

@@ -1,9 +0,0 @@
----
-title: Home
-layout: home
-nav_order: 1
----
-
-# Welcome to Roo Code Documentation
-
-This is the documentation for Roo Code. Choose a section from the navigation menu to get started.

+ 20 - 2
package-lock.json

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

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

+ 1 - 0
src/activate/index.ts

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

+ 81 - 0
src/activate/registerTerminalActions.ts

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

+ 1 - 1
src/api/providers/__tests__/gemini.test.ts

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

+ 7 - 2
src/api/providers/ollama.ts

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

+ 13 - 3
src/api/providers/openai.ts

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

+ 21 - 58
src/core/Cline.ts

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

+ 88 - 17
src/core/sliding-window/index.ts

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

+ 35 - 0
src/core/webview/ClineProvider.ts

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

+ 2 - 1
src/extension.ts

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

+ 0 - 0
src/integrations/terminal/TerminalActions.ts


+ 53 - 0
src/integrations/terminal/TerminalManager.ts

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

+ 1 - 63
src/shared/__tests__/modes.test.ts

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

+ 25 - 1
src/shared/api.ts

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

+ 0 - 33
src/shared/modes.ts

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

+ 40 - 0
src/shared/support-prompt.ts

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

+ 6 - 0
src/utils/__tests__/shell.test.ts

@@ -97,6 +97,12 @@ describe("Shell Detection Tests", () => {
 			expect(getShell()).toBe("C:\\Windows\\System32\\cmd.exe")
 		})
 
+		it("handles undefined profile gracefully", () => {
+			// Mock a case where defaultProfileName exists but the profile doesn't
+			mockVsCodeConfig("windows", "NonexistentProfile", {})
+			expect(getShell()).toBe("C:\\Windows\\System32\\cmd.exe")
+		})
+
 		it("respects userInfo() if no VS Code config is available", () => {
 			vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
 			;(userInfo as any) = () => ({ shell: "C:\\Custom\\PowerShell.exe" })

+ 1 - 1
src/utils/shell.ts

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

+ 3 - 0
webview-ui/.eslintrc.json

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

+ 22 - 0
webview-ui/jest.config.cjs

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

File diff suppressed because it is too large
+ 4260 - 1334
webview-ui/package-lock.json


+ 7 - 41
webview-ui/package.json

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

+ 40 - 9
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -179,6 +179,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 					return
 				}
 
+				if (type === ContextMenuOptionType.Mode && value) {
+					// Handle mode selection
+					setMode(value)
+					setInputValue("")
+					setShowContextMenu(false)
+					vscode.postMessage({
+						type: "mode",
+						text: value,
+					})
+					return
+				}
+
 				if (
 					type === ContextMenuOptionType.File ||
 					type === ContextMenuOptionType.Folder ||
@@ -226,6 +238,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 					}, 0)
 				}
 			},
+			// eslint-disable-next-line react-hooks/exhaustive-deps
 			[setInputValue, cursorPosition],
 		)
 
@@ -242,7 +255,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						event.preventDefault()
 						setSelectedMenuIndex((prevIndex) => {
 							const direction = event.key === "ArrowUp" ? -1 : 1
-							const options = getContextMenuOptions(searchQuery, selectedType, queryItems)
+							const options = getContextMenuOptions(
+								searchQuery,
+								selectedType,
+								queryItems,
+								getAllModes(customModes),
+							)
 							const optionsLength = options.length
 
 							if (optionsLength === 0) return prevIndex
@@ -272,9 +290,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 					}
 					if ((event.key === "Enter" || event.key === "Tab") && selectedMenuIndex !== -1) {
 						event.preventDefault()
-						const selectedOption = getContextMenuOptions(searchQuery, selectedType, queryItems)[
-							selectedMenuIndex
-						]
+						const selectedOption = getContextMenuOptions(
+							searchQuery,
+							selectedType,
+							queryItems,
+							getAllModes(customModes),
+						)[selectedMenuIndex]
 						if (
 							selectedOption &&
 							selectedOption.type !== ContextMenuOptionType.URL &&
@@ -340,6 +361,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				setInputValue,
 				justDeletedSpaceAfterMention,
 				queryItems,
+				customModes,
 			],
 		)
 
@@ -360,13 +382,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 
 				setShowContextMenu(showMenu)
 				if (showMenu) {
-					const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1)
-					const query = newValue.slice(lastAtIndex + 1, newCursorPosition)
-					setSearchQuery(query)
-					if (query.length > 0) {
+					if (newValue.startsWith("/")) {
+						// Handle slash command
+						const query = newValue
+						setSearchQuery(query)
 						setSelectedMenuIndex(0)
 					} else {
-						setSelectedMenuIndex(3) // Set to "File" option by default
+						// Existing @ mention handling
+						const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1)
+						const query = newValue.slice(lastAtIndex + 1, newCursorPosition)
+						setSearchQuery(query)
+						if (query.length > 0) {
+							setSelectedMenuIndex(0)
+						} else {
+							setSelectedMenuIndex(3) // Set to "File" option by default
+						}
 					}
 				} else {
 					setSearchQuery("")
@@ -614,6 +644,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 							setSelectedIndex={setSelectedMenuIndex}
 							selectedType={selectedType}
 							queryItems={queryItems}
+							modes={getAllModes(customModes)}
 						/>
 					</div>
 				)}

+ 1 - 1
webview-ui/src/components/chat/ChatView.tsx

@@ -878,7 +878,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 
 	const placeholderText = useMemo(() => {
 		const baseText = task ? "Type a message..." : "Type your task here..."
-		const contextText = "(@ to add context"
+		const contextText = "(@ to add context, / to switch modes"
 		const imageText = shouldDisableImages ? "" : ", hold shift to drag in images"
 		const helpText = imageText ? `\n${contextText}${imageText})` : `\n${contextText})`
 		return baseText + helpText

+ 37 - 11
webview-ui/src/components/chat/ContextMenu.tsx

@@ -1,6 +1,7 @@
 import React, { useEffect, useMemo, useRef } from "react"
 import { ContextMenuOptionType, ContextMenuQueryItem, getContextMenuOptions } from "../../utils/context-mentions"
 import { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
+import { ModeConfig } from "../../../../src/shared/modes"
 
 interface ContextMenuProps {
 	onSelect: (type: ContextMenuOptionType, value?: string) => void
@@ -10,6 +11,7 @@ interface ContextMenuProps {
 	setSelectedIndex: (index: number) => void
 	selectedType: ContextMenuOptionType | null
 	queryItems: ContextMenuQueryItem[]
+	modes?: ModeConfig[]
 }
 
 const ContextMenu: React.FC<ContextMenuProps> = ({
@@ -20,12 +22,13 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
 	setSelectedIndex,
 	selectedType,
 	queryItems,
+	modes,
 }) => {
 	const menuRef = useRef<HTMLDivElement>(null)
 
 	const filteredOptions = useMemo(
-		() => getContextMenuOptions(searchQuery, selectedType, queryItems),
-		[searchQuery, selectedType, queryItems],
+		() => getContextMenuOptions(searchQuery, selectedType, queryItems, modes),
+		[searchQuery, selectedType, queryItems, modes],
 	)
 
 	useEffect(() => {
@@ -46,6 +49,25 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
 
 	const renderOptionContent = (option: ContextMenuQueryItem) => {
 		switch (option.type) {
+			case ContextMenuOptionType.Mode:
+				return (
+					<div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
+						<span style={{ lineHeight: "1.2" }}>{option.label}</span>
+						{option.description && (
+							<span
+								style={{
+									opacity: 0.5,
+									fontSize: "0.9em",
+									lineHeight: "1.2",
+									whiteSpace: "nowrap",
+									overflow: "hidden",
+									textOverflow: "ellipsis",
+								}}>
+								{option.description}
+							</span>
+						)}
+					</div>
+				)
 			case ContextMenuOptionType.Problems:
 				return <span>Problems</span>
 			case ContextMenuOptionType.URL:
@@ -101,6 +123,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
 
 	const getIconForOption = (option: ContextMenuQueryItem): string => {
 		switch (option.type) {
+			case ContextMenuOptionType.Mode:
+				return "symbol-misc"
 			case ContextMenuOptionType.OpenedFile:
 				return "window"
 			case ContextMenuOptionType.File:
@@ -174,15 +198,17 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
 								overflow: "hidden",
 								paddingTop: 0,
 							}}>
-							<i
-								className={`codicon codicon-${getIconForOption(option)}`}
-								style={{
-									marginRight: "6px",
-									flexShrink: 0,
-									fontSize: "14px",
-									marginTop: 0,
-								}}
-							/>
+							{option.type !== ContextMenuOptionType.Mode && getIconForOption(option) && (
+								<i
+									className={`codicon codicon-${getIconForOption(option)}`}
+									style={{
+										marginRight: "6px",
+										flexShrink: 0,
+										fontSize: "14px",
+										marginTop: 0,
+									}}
+								/>
+							)}
 							{renderOptionContent(option)}
 						</div>
 						{(option.type === ContextMenuOptionType.File ||

+ 0 - 1
webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx

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

+ 33 - 40
webview-ui/src/components/prompts/PromptsView.tsx

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

+ 0 - 1
webview-ui/src/components/prompts/__tests__/PromptsView.test.tsx

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

+ 0 - 1
webview-ui/src/components/settings/__tests__/ApiConfigManager.test.tsx

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

+ 5 - 5
webview-ui/src/components/ui/button.tsx

@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
 import { cn } from "@/lib/utils"
 
 const buttonVariants = cva(
-	"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+	"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer active:opacity-90",
 	{
 		variants: {
 			variant: {
@@ -17,10 +17,10 @@ const buttonVariants = cva(
 				link: "text-primary underline-offset-4 hover:underline",
 			},
 			size: {
-				default: "h-9 px-4 py-2",
-				sm: "h-8 rounded-md px-3 text-xs",
-				lg: "h-10 rounded-md px-8",
-				icon: "h-9 w-9",
+				default: "h-7 px-3",
+				sm: "h-6 px-2 text-sm",
+				lg: "h-8 px-4 text-lg",
+				icon: "h-7 w-7",
 			},
 		},
 		defaultVariants: {

+ 131 - 0
webview-ui/src/components/ui/command.tsx

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

+ 96 - 0
webview-ui/src/components/ui/dialog.tsx

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

+ 6 - 4
webview-ui/src/components/ui/dropdown-menu.tsx

@@ -53,9 +53,11 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
 
 const DropdownMenuContent = React.forwardRef<
 	React.ElementRef<typeof DropdownMenuPrimitive.Content>,
-	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
->(({ className, sideOffset = 4, ...props }, ref) => (
-	<DropdownMenuPrimitive.Portal>
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & {
+		container?: HTMLElement
+	}
+>(({ className, sideOffset = 4, container, ...props }, ref) => (
+	<DropdownMenuPrimitive.Portal container={container}>
 		<DropdownMenuPrimitive.Content
 			ref={ref}
 			sideOffset={sideOffset}
@@ -79,7 +81,7 @@ const DropdownMenuItem = React.forwardRef<
 	<DropdownMenuPrimitive.Item
 		ref={ref}
 		className={cn(
-			"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
+			"relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0 active:opacity-90",
 			inset && "pl-8",
 			className,
 		)}

+ 3 - 0
webview-ui/src/components/ui/index.ts

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

+ 31 - 0
webview-ui/src/components/ui/popover.tsx

@@ -0,0 +1,31 @@
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+
+import { cn } from "@/lib/utils"
+
+const Popover = PopoverPrimitive.Root
+
+const PopoverTrigger = PopoverPrimitive.Trigger
+
+const PopoverAnchor = PopoverPrimitive.Anchor
+
+const PopoverContent = React.forwardRef<
+	React.ElementRef<typeof PopoverPrimitive.Content>,
+	React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+	<PopoverPrimitive.Portal>
+		<PopoverPrimitive.Content
+			ref={ref}
+			align={align}
+			sideOffset={sideOffset}
+			className={cn(
+				"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+				className,
+			)}
+			{...props}
+		/>
+	</PopoverPrimitive.Portal>
+))
+PopoverContent.displayName = PopoverPrimitive.Content.displayName
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

+ 15 - 2
webview-ui/src/index.css

@@ -22,6 +22,11 @@
 @plugin "tailwindcss-animate";
 
 @theme {
+	--font-display: var(--vscode-font-family);
+	--text-sm: calc(var(--vscode-font-size) * 0.9);
+	--text-base: var(--vscode-font-size);
+	--text-lg: calc(var(--vscode-font-size) * 1.1);
+
 	--color-background: var(--background);
 	--color-foreground: var(--foreground);
 	--color-card: var(--card);
@@ -65,11 +70,11 @@
 		--secondary-foreground: var(--vscode-button-secondaryForeground);
 		--muted: var(--vscode-disabledForeground);
 		--muted-foreground: var(--vscode-descriptionForeground);
-		--accent: var(--vscode-input-border);
+		--accent: var(--vscode-list-hoverBackground);
 		--accent-foreground: var(--vscode-button-foreground);
 		--destructive: var(--vscode-errorForeground);
 		--destructive-foreground: var(--vscode-button-foreground);
-		--border: var(--vscode-widget-border);
+		--border: var(--vscode-input-border);
 		--input: var(--vscode-input-background);
 		--ring: var(--vscode-input-border);
 		--chart-1: var(--vscode-charts-red);
@@ -284,3 +289,11 @@ vscode-dropdown::part(listbox) {
 .vscrui-checkbox__listbox > ul {
 	max-height: unset !important;
 }
+
+/**
+ * @shadcn/ui Overrides / Hacks
+ */
+
+input[cmdk-input]:focus {
+	outline: none;
+}

+ 99 - 0
webview-ui/src/stories/Combobox.stories.tsx

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

+ 50 - 0
webview-ui/src/utils/context-mentions.ts

@@ -1,11 +1,20 @@
 import { mentionRegex } from "../../../src/shared/context-mentions"
 import { Fzf } from "fzf"
+import { ModeConfig } from "../../../src/shared/modes"
 
 export function insertMention(
 	text: string,
 	position: number,
 	value: string,
 ): { newValue: string; mentionIndex: number } {
+	// Handle slash command
+	if (text.startsWith("/")) {
+		return {
+			newValue: value,
+			mentionIndex: 0,
+		}
+	}
+
 	const beforeCursor = text.slice(0, position)
 	const afterCursor = text.slice(position)
 
@@ -55,6 +64,7 @@ export enum ContextMenuOptionType {
 	URL = "url",
 	Git = "git",
 	NoResults = "noResults",
+	Mode = "mode", // Add mode type
 }
 
 export interface ContextMenuQueryItem {
@@ -69,7 +79,42 @@ export function getContextMenuOptions(
 	query: string,
 	selectedType: ContextMenuOptionType | null = null,
 	queryItems: ContextMenuQueryItem[],
+	modes?: ModeConfig[],
 ): ContextMenuQueryItem[] {
+	// Handle slash commands for modes
+	if (query.startsWith("/")) {
+		const modeQuery = query.slice(1)
+		if (!modes?.length) return [{ type: ContextMenuOptionType.NoResults }]
+
+		// Create searchable strings array for fzf
+		const searchableItems = modes.map((mode) => ({
+			original: mode,
+			searchStr: mode.name,
+		}))
+
+		// Initialize fzf instance for fuzzy search
+		const fzf = new Fzf(searchableItems, {
+			selector: (item) => item.searchStr,
+		})
+
+		// Get fuzzy matching items
+		const matchingModes = modeQuery
+			? fzf.find(modeQuery).map((result) => ({
+					type: ContextMenuOptionType.Mode,
+					value: result.item.original.slug,
+					label: result.item.original.name,
+					description: result.item.original.roleDefinition.split("\n")[0],
+				}))
+			: modes.map((mode) => ({
+					type: ContextMenuOptionType.Mode,
+					value: mode.slug,
+					label: mode.name,
+					description: mode.roleDefinition.split("\n")[0],
+				}))
+
+		return matchingModes.length > 0 ? matchingModes : [{ type: ContextMenuOptionType.NoResults }]
+	}
+
 	const workingChanges: ContextMenuQueryItem = {
 		type: ContextMenuOptionType.Git,
 		value: "git-changes",
@@ -203,6 +248,11 @@ export function getContextMenuOptions(
 }
 
 export function shouldShowContextMenu(text: string, position: number): boolean {
+	// Handle slash command
+	if (text.startsWith("/")) {
+		return position <= text.length && !text.includes(" ")
+	}
+
 	const beforeCursor = text.slice(0, position)
 	const atIndex = beforeCursor.lastIndexOf("@")
 

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