Chris Estreich 4 месяцев назад
Родитель
Сommit
f02a2bb05d
32 измененных файлов с 834 добавлено и 1163 удалено
  1. 1 1
      apps/web-evals/package.json
  2. 164 2
      pnpm-lock.yaml
  3. 1 6
      src/core/webview/webviewMessageHandler.ts
  4. 0 1
      src/shared/TelemetrySetting.ts
  5. 2 1
      src/shared/combineCommandSequences.ts
  6. 56 57
      webview-ui/src/components/chat/ApiConfigSelector.tsx
  7. 26 112
      webview-ui/src/components/chat/ChatRow.tsx
  8. 248 291
      webview-ui/src/components/chat/ChatTextArea.tsx
  9. 14 14
      webview-ui/src/components/chat/ChatView.tsx
  10. 8 6
      webview-ui/src/components/chat/CodeIndexPopover.tsx
  11. 0 115
      webview-ui/src/components/chat/EditModeControls.tsx
  12. 25 29
      webview-ui/src/components/chat/IconButton.tsx
  13. 42 92
      webview-ui/src/components/chat/IndexingStatusBadge.tsx
  14. 65 65
      webview-ui/src/components/chat/ModeSelector.tsx
  15. 16 18
      webview-ui/src/components/chat/SlashCommandsPopover.tsx
  16. 3 2
      webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx
  17. 2 53
      webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx
  18. 12 9
      webview-ui/src/components/chat/__tests__/ChatView.keyboard-fix.spec.tsx
  19. 26 23
      webview-ui/src/components/chat/__tests__/ChatView.spec.tsx
  20. 0 138
      webview-ui/src/components/chat/__tests__/EditModeControls.spec.tsx
  21. 0 10
      webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx
  22. 66 39
      webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx
  23. 1 1
      webview-ui/src/components/common/TelemetryBanner.tsx
  24. 2 2
      webview-ui/src/components/settings/About.tsx
  25. 2 4
      webview-ui/src/components/settings/SettingsView.tsx
  26. 5 4
      webview-ui/src/components/ui/standard-tooltip.tsx
  27. 34 23
      webview-ui/src/components/ui/tooltip.tsx
  28. 1 2
      webview-ui/src/context/ExtensionStateContext.tsx
  29. 0 39
      webview-ui/src/hooks/useTooltip.ts
  30. 8 1
      webview-ui/src/index.css
  31. 1 1
      webview-ui/src/utils/TelemetryClient.ts
  32. 3 2
      webview-ui/src/utils/test-utils.tsx

+ 1 - 1
apps/web-evals/package.json

@@ -24,7 +24,7 @@
 		"@radix-ui/react-slider": "^1.2.4",
 		"@radix-ui/react-slider": "^1.2.4",
 		"@radix-ui/react-slot": "^1.1.2",
 		"@radix-ui/react-slot": "^1.1.2",
 		"@radix-ui/react-tabs": "^1.1.3",
 		"@radix-ui/react-tabs": "^1.1.3",
-		"@radix-ui/react-tooltip": "^1.1.8",
+		"@radix-ui/react-tooltip": "^1.2.8",
 		"@roo-code/evals": "workspace:^",
 		"@roo-code/evals": "workspace:^",
 		"@roo-code/types": "workspace:^",
 		"@roo-code/types": "workspace:^",
 		"@tanstack/react-query": "^5.69.0",
 		"@tanstack/react-query": "^5.69.0",

+ 164 - 2
pnpm-lock.yaml

@@ -161,8 +161,8 @@ importers:
         specifier: ^1.1.3
         specifier: ^1.1.3
         version: 1.1.12(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
         version: 1.1.12(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
       '@radix-ui/react-tooltip':
       '@radix-ui/react-tooltip':
-        specifier: ^1.1.8
-        version: 1.2.6(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+        specifier: ^1.2.8
+        version: 1.2.8(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
       '@roo-code/evals':
       '@roo-code/evals':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/evals
         version: link:../../packages/evals
@@ -2575,6 +2575,9 @@ packages:
   '@radix-ui/[email protected]':
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
     resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
 
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
+
   '@radix-ui/[email protected]':
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-/uPs78OwxGxslYOG5TKeUsv9fZC0vo376cXSADdKirTmsLJU2au6L3n34c3p6W26rFDDDze/hwy4fYeNd0qdGA==}
     resolution: {integrity: sha512-/uPs78OwxGxslYOG5TKeUsv9fZC0vo376cXSADdKirTmsLJU2au6L3n34c3p6W26rFDDDze/hwy4fYeNd0qdGA==}
     peerDependencies:
     peerDependencies:
@@ -2601,6 +2604,19 @@ packages:
       '@types/react-dom':
       '@types/react-dom':
         optional: true
         optional: true
 
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/[email protected]':
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-xTaLKAO+XXMPK/BpVTSaAAhlefmvMSACjIhK9mGsImvX2ljcTDm8VGR1CuS1uYcNdR5J+oiOhoJZc5un6bh3VQ==}
     resolution: {integrity: sha512-xTaLKAO+XXMPK/BpVTSaAAhlefmvMSACjIhK9mGsImvX2ljcTDm8VGR1CuS1uYcNdR5J+oiOhoJZc5un6bh3VQ==}
     peerDependencies:
     peerDependencies:
@@ -2719,6 +2735,19 @@ packages:
       '@types/react-dom':
       '@types/react-dom':
         optional: true
         optional: true
 
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/[email protected]':
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==}
     resolution: {integrity: sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==}
     peerDependencies:
     peerDependencies:
@@ -2846,6 +2875,19 @@ packages:
       '@types/react-dom':
       '@types/react-dom':
         optional: true
         optional: true
 
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/[email protected]':
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==}
     resolution: {integrity: sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==}
     peerDependencies:
     peerDependencies:
@@ -2885,6 +2927,19 @@ packages:
       '@types/react-dom':
       '@types/react-dom':
         optional: true
         optional: true
 
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/[email protected]':
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==}
     resolution: {integrity: sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==}
     peerDependencies:
     peerDependencies:
@@ -3046,6 +3101,19 @@ packages:
       '@types/react-dom':
       '@types/react-dom':
         optional: true
         optional: true
 
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/[email protected]':
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
     resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
     peerDependencies:
     peerDependencies:
@@ -3131,6 +3199,19 @@ packages:
       '@types/react-dom':
       '@types/react-dom':
         optional: true
         optional: true
 
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/[email protected]':
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
     resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
 
 
@@ -11905,6 +11986,8 @@ snapshots:
 
 
   '@radix-ui/[email protected]': {}
   '@radix-ui/[email protected]': {}
 
 
+  '@radix-ui/[email protected]': {}
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
     dependencies:
       '@radix-ui/primitive': 1.1.2
       '@radix-ui/primitive': 1.1.2
@@ -11928,6 +12011,15 @@ snapshots:
       '@types/react': 18.3.23
       '@types/react': 18.3.23
       '@types/react-dom': 18.3.7(@types/[email protected])
       '@types/react-dom': 18.3.7(@types/[email protected])
 
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      react: 18.3.1
+      react-dom: 18.3.1([email protected])
+    optionalDependencies:
+      '@types/react': 18.3.23
+      '@types/react-dom': 18.3.7(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
     dependencies:
       '@radix-ui/primitive': 1.1.2
       '@radix-ui/primitive': 1.1.2
@@ -12059,6 +12151,19 @@ snapshots:
       '@types/react': 18.3.23
       '@types/react': 18.3.23
       '@types/react-dom': 18.3.7(@types/[email protected])
       '@types/react-dom': 18.3.7(@types/[email protected])
 
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-escape-keydown': 1.1.1(@types/[email protected])([email protected])
+      react: 18.3.1
+      react-dom: 18.3.1([email protected])
+    optionalDependencies:
+      '@types/react': 18.3.23
+      '@types/react-dom': 18.3.7(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
     dependencies:
       '@radix-ui/primitive': 1.1.2
       '@radix-ui/primitive': 1.1.2
@@ -12202,6 +12307,24 @@ snapshots:
       '@types/react': 18.3.23
       '@types/react': 18.3.23
       '@types/react-dom': 18.3.7(@types/[email protected])
       '@types/react-dom': 18.3.7(@types/[email protected])
 
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@floating-ui/react-dom': 2.1.2([email protected]([email protected]))([email protected])
+      '@radix-ui/react-arrow': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-rect': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-size': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/rect': 1.1.1
+      react: 18.3.1
+      react-dom: 18.3.1([email protected])
+    optionalDependencies:
+      '@types/react': 18.3.23
+      '@types/react-dom': 18.3.7(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
     dependencies:
       '@radix-ui/react-primitive': 2.1.2(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
       '@radix-ui/react-primitive': 2.1.2(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
@@ -12232,6 +12355,16 @@ snapshots:
       '@types/react': 18.3.23
       '@types/react': 18.3.23
       '@types/react-dom': 18.3.7(@types/[email protected])
       '@types/react-dom': 18.3.7(@types/[email protected])
 
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 18.3.1
+      react-dom: 18.3.1([email protected])
+    optionalDependencies:
+      '@types/react': 18.3.23
+      '@types/react-dom': 18.3.7(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
     dependencies:
       '@radix-ui/react-slot': 1.2.2(@types/[email protected])([email protected])
       '@radix-ui/react-slot': 1.2.2(@types/[email protected])([email protected])
@@ -12418,6 +12551,26 @@ snapshots:
       '@types/react': 18.3.23
       '@types/react': 18.3.23
       '@types/react-dom': 18.3.7(@types/[email protected])
       '@types/react-dom': 18.3.7(@types/[email protected])
 
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-dismissable-layer': 1.1.11(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-popper': 1.2.8(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-portal': 1.1.9(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-presence': 1.1.5(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot': 1.2.3(@types/[email protected])([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      '@radix-ui/react-visually-hidden': 1.2.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      react: 18.3.1
+      react-dom: 18.3.1([email protected])
+    optionalDependencies:
+      '@types/react': 18.3.23
+      '@types/react-dom': 18.3.7(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected])([email protected])':
   '@radix-ui/[email protected](@types/[email protected])([email protected])':
     dependencies:
     dependencies:
       react: 18.3.1
       react: 18.3.1
@@ -12481,6 +12634,15 @@ snapshots:
       '@types/react': 18.3.23
       '@types/react': 18.3.23
       '@types/react-dom': 18.3.7(@types/[email protected])
       '@types/react-dom': 18.3.7(@types/[email protected])
 
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      react: 18.3.1
+      react-dom: 18.3.1([email protected])
+    optionalDependencies:
+      '@types/react': 18.3.23
+      '@types/react-dom': 18.3.7(@types/[email protected])
+
   '@radix-ui/[email protected]': {}
   '@radix-ui/[email protected]': {}
 
 
   '@redis/[email protected](@redis/[email protected])':
   '@redis/[email protected](@redis/[email protected])':

+ 1 - 6
src/core/webview/webviewMessageHandler.ts

@@ -4,13 +4,12 @@ import * as os from "os"
 import * as fs from "fs/promises"
 import * as fs from "fs/promises"
 import pWaitFor from "p-wait-for"
 import pWaitFor from "p-wait-for"
 import * as vscode from "vscode"
 import * as vscode from "vscode"
-import * as yaml from "yaml"
 
 
 import {
 import {
 	type Language,
 	type Language,
-	type ProviderSettings,
 	type GlobalState,
 	type GlobalState,
 	type ClineMessage,
 	type ClineMessage,
+	type TelemetrySetting,
 	TelemetryEventName,
 	TelemetryEventName,
 } from "@roo-code/types"
 } from "@roo-code/types"
 import { CloudService } from "@roo-code/cloud"
 import { CloudService } from "@roo-code/cloud"
@@ -21,7 +20,6 @@ import { ClineProvider } from "./ClineProvider"
 import { changeLanguage, t } from "../../i18n"
 import { changeLanguage, t } from "../../i18n"
 import { Package } from "../../shared/package"
 import { Package } from "../../shared/package"
 import { RouterName, toRouterName, ModelRecord } from "../../shared/api"
 import { RouterName, toRouterName, ModelRecord } from "../../shared/api"
-import { supportPrompt } from "../../shared/support-prompt"
 import { MessageEnhancer } from "./messageEnhancer"
 import { MessageEnhancer } from "./messageEnhancer"
 
 
 import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
 import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
@@ -29,7 +27,6 @@ import { checkExistKey } from "../../shared/checkExistApiConfig"
 import { experimentDefault } from "../../shared/experiments"
 import { experimentDefault } from "../../shared/experiments"
 import { Terminal } from "../../integrations/terminal/Terminal"
 import { Terminal } from "../../integrations/terminal/Terminal"
 import { openFile } from "../../integrations/misc/open-file"
 import { openFile } from "../../integrations/misc/open-file"
-import { CodeIndexManager } from "../../services/code-index/manager"
 import { openImage, saveImage } from "../../integrations/misc/image-handler"
 import { openImage, saveImage } from "../../integrations/misc/image-handler"
 import { selectImages } from "../../integrations/misc/process-images"
 import { selectImages } from "../../integrations/misc/process-images"
 import { getTheme } from "../../integrations/theme/getTheme"
 import { getTheme } from "../../integrations/theme/getTheme"
@@ -42,9 +39,7 @@ import { exportSettings, importSettingsWithFeedback } from "../config/importExpo
 import { getOpenAiModels } from "../../api/providers/openai"
 import { getOpenAiModels } from "../../api/providers/openai"
 import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
 import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
 import { openMention } from "../mentions"
 import { openMention } from "../mentions"
-import { TelemetrySetting } from "../../shared/TelemetrySetting"
 import { getWorkspacePath } from "../../utils/path"
 import { getWorkspacePath } from "../../utils/path"
-import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
 import { Mode, defaultModeSlug } from "../../shared/modes"
 import { Mode, defaultModeSlug } from "../../shared/modes"
 import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
 import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
 import { GetModelsOptions } from "../../shared/api"
 import { GetModelsOptions } from "../../shared/api"

+ 0 - 1
src/shared/TelemetrySetting.ts

@@ -1 +0,0 @@
-export type TelemetrySetting = "unset" | "enabled" | "disabled"

+ 2 - 1
src/shared/combineCommandSequences.ts

@@ -1,4 +1,5 @@
-import { ClineMessage } from "@roo-code/types"
+import type { ClineMessage } from "@roo-code/types"
+
 import { safeJsonParse } from "./safeJsonParse"
 import { safeJsonParse } from "./safeJsonParse"
 
 
 export const COMMAND_OUTPUT_STRING = "Output:"
 export const COMMAND_OUTPUT_STRING = "Output:"

+ 56 - 57
webview-ui/src/components/chat/ApiConfigSelector.tsx

@@ -1,18 +1,21 @@
-import React, { useState, useMemo, useCallback } from "react"
+import { useState, useMemo, useCallback } from "react"
+import { Fzf } from "fzf"
+import { ChevronUp } from "lucide-react"
+
 import { cn } from "@/lib/utils"
 import { cn } from "@/lib/utils"
 import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
 import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
 import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
 import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
-import { IconButton } from "./IconButton"
 import { useAppTranslation } from "@/i18n/TranslationContext"
 import { useAppTranslation } from "@/i18n/TranslationContext"
 import { vscode } from "@/utils/vscode"
 import { vscode } from "@/utils/vscode"
-import { Fzf } from "fzf"
 import { Button } from "@/components/ui"
 import { Button } from "@/components/ui"
 
 
+import { IconButton } from "./IconButton"
+
 interface ApiConfigSelectorProps {
 interface ApiConfigSelectorProps {
 	value: string
 	value: string
 	displayName: string
 	displayName: string
 	disabled?: boolean
 	disabled?: boolean
-	title?: string
+	title: string
 	onChange: (value: string) => void
 	onChange: (value: string) => void
 	triggerClassName?: string
 	triggerClassName?: string
 	listApiConfigMeta: Array<{ id: string; name: string }>
 	listApiConfigMeta: Array<{ id: string; name: string }>
@@ -24,7 +27,7 @@ export const ApiConfigSelector = ({
 	value,
 	value,
 	displayName,
 	displayName,
 	disabled = false,
 	disabled = false,
-	title = "",
+	title,
 	onChange,
 	onChange,
 	triggerClassName = "",
 	triggerClassName = "",
 	listApiConfigMeta,
 	listApiConfigMeta,
@@ -36,30 +39,33 @@ export const ApiConfigSelector = ({
 	const [searchValue, setSearchValue] = useState("")
 	const [searchValue, setSearchValue] = useState("")
 	const portalContainer = useRooPortal("roo-portal")
 	const portalContainer = useRooPortal("roo-portal")
 
 
-	// Create searchable items for fuzzy search
-	const searchableItems = useMemo(() => {
-		return listApiConfigMeta.map((config) => ({
-			original: config,
-			searchStr: config.name,
-		}))
-	}, [listApiConfigMeta])
-
-	// Create Fzf instance
-	const fzfInstance = useMemo(() => {
-		return new Fzf(searchableItems, {
-			selector: (item) => item.searchStr,
-		})
-	}, [searchableItems])
-
-	// Filter configs based on search
+	// Create searchable items for fuzzy search.
+	const searchableItems = useMemo(
+		() =>
+			listApiConfigMeta.map((config) => ({
+				original: config,
+				searchStr: config.name,
+			})),
+		[listApiConfigMeta],
+	)
+
+	// Create Fzf instance.
+	const fzfInstance = useMemo(
+		() => new Fzf(searchableItems, { selector: (item) => item.searchStr }),
+		[searchableItems],
+	)
+
+	// Filter configs based on search.
 	const filteredConfigs = useMemo(() => {
 	const filteredConfigs = useMemo(() => {
-		if (!searchValue) return listApiConfigMeta
+		if (!searchValue) {
+			return listApiConfigMeta
+		}
 
 
 		const matchingItems = fzfInstance.find(searchValue).map((result) => result.item.original)
 		const matchingItems = fzfInstance.find(searchValue).map((result) => result.item.original)
 		return matchingItems
 		return matchingItems
 	}, [listApiConfigMeta, searchValue, fzfInstance])
 	}, [listApiConfigMeta, searchValue, fzfInstance])
 
 
-	// Separate pinned and unpinned configs
+	// Separate pinned and unpinned configs.
 	const { pinnedConfigs, unpinnedConfigs } = useMemo(() => {
 	const { pinnedConfigs, unpinnedConfigs } = useMemo(() => {
 		const pinned = filteredConfigs.filter((config) => pinnedApiConfigs?.[config.id])
 		const pinned = filteredConfigs.filter((config) => pinnedApiConfigs?.[config.id])
 		const unpinned = filteredConfigs.filter((config) => !pinnedApiConfigs?.[config.id])
 		const unpinned = filteredConfigs.filter((config) => !pinnedApiConfigs?.[config.id])
@@ -76,10 +82,7 @@ export const ApiConfigSelector = ({
 	)
 	)
 
 
 	const handleEditClick = useCallback(() => {
 	const handleEditClick = useCallback(() => {
-		vscode.postMessage({
-			type: "switchTab",
-			tab: "settings",
-		})
+		vscode.postMessage({ type: "switchTab", tab: "settings" })
 		setOpen(false)
 		setOpen(false)
 	}, [])
 	}, [])
 
 
@@ -112,10 +115,7 @@ export const ApiConfigSelector = ({
 								onClick={(e) => {
 								onClick={(e) => {
 									e.stopPropagation()
 									e.stopPropagation()
 									togglePinnedApiConfig(config.id)
 									togglePinnedApiConfig(config.id)
-									vscode.postMessage({
-										type: "toggleApiConfigPin",
-										text: config.id,
-									})
+									vscode.postMessage({ type: "toggleApiConfigPin", text: config.id })
 								}}
 								}}
 								className={cn("size-5 flex items-center justify-center", {
 								className={cn("size-5 flex items-center justify-center", {
 									"opacity-0 group-hover:opacity-100": !isPinned && !isCurrentConfig,
 									"opacity-0 group-hover:opacity-100": !isPinned && !isCurrentConfig,
@@ -131,32 +131,30 @@ export const ApiConfigSelector = ({
 		[value, handleSelect, t, togglePinnedApiConfig],
 		[value, handleSelect, t, togglePinnedApiConfig],
 	)
 	)
 
 
-	const triggerContent = (
-		<PopoverTrigger
-			disabled={disabled}
-			data-testid="dropdown-trigger"
-			className={cn(
-				"w-full min-w-0 max-w-full inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
-				"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
-				"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
-				disabled
-					? "opacity-50 cursor-not-allowed"
-					: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
-				triggerClassName,
-			)}>
-			<span
-				className={cn(
-					"codicon codicon-chevron-up pointer-events-none opacity-80 flex-shrink-0 text-xs transition-transform duration-200",
-					open && "rotate-180",
-				)}
-			/>
-			<span className="truncate">{displayName}</span>
-		</PopoverTrigger>
-	)
-
 	return (
 	return (
-		<Popover open={open} onOpenChange={setOpen}>
-			{title ? <StandardTooltip content={title}>{triggerContent}</StandardTooltip> : triggerContent}
+		<Popover open={open} onOpenChange={setOpen} data-testid="api-config-selector-root">
+			<StandardTooltip content={title}>
+				<PopoverTrigger
+					disabled={disabled}
+					data-testid="dropdown-trigger"
+					className={cn(
+						"w-full min-w-0 max-w-full inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
+						"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
+						"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
+						disabled
+							? "opacity-50 cursor-not-allowed"
+							: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
+						triggerClassName,
+					)}>
+					<ChevronUp
+						className={cn(
+							"pointer-events-none opacity-80 flex-shrink-0 size-3 transition-transform duration-200",
+							open && "rotate-180",
+						)}
+					/>
+					<span className="truncate">{displayName}</span>
+				</PopoverTrigger>
+			</StandardTooltip>
 			<PopoverContent
 			<PopoverContent
 				align="start"
 				align="start"
 				sideOffset={4}
 				sideOffset={4}
@@ -214,12 +212,13 @@ export const ApiConfigSelector = ({
 					</div>
 					</div>
 
 
 					{/* Bottom bar with buttons on left and title on right */}
 					{/* Bottom bar with buttons on left and title on right */}
-					<div className="flex flex-row items-center justify-between p-2 border-t border-vscode-dropdown-border">
+					<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
 						<div className="flex flex-row gap-1">
 						<div className="flex flex-row gap-1">
 							<IconButton
 							<IconButton
 								iconClass="codicon-settings-gear"
 								iconClass="codicon-settings-gear"
 								title={t("chat:edit")}
 								title={t("chat:edit")}
 								onClick={handleEditClick}
 								onClick={handleEditClick}
+								tooltip={false}
 							/>
 							/>
 						</div>
 						</div>
 
 

+ 26 - 112
webview-ui/src/components/chat/ChatRow.tsx

@@ -1,18 +1,14 @@
 import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
 import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
-import { appendImages } from "@src/utils/imageUtils"
-import { McpExecution } from "./McpExecution"
 import { useSize } from "react-use"
 import { useSize } from "react-use"
 import { useTranslation, Trans } from "react-i18next"
 import { useTranslation, Trans } from "react-i18next"
 import deepEqual from "fast-deep-equal"
 import deepEqual from "fast-deep-equal"
 import { VSCodeBadge, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
 import { VSCodeBadge, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
 
 
-import type { ClineMessage } from "@roo-code/types"
-import { Mode } from "@roo/modes"
+import type { ClineMessage, FollowUpData, SuggestionItem } from "@roo-code/types"
 
 
 import { ClineApiReqInfo, ClineAskUseMcpServer, ClineSayTool } from "@roo/ExtensionMessage"
 import { ClineApiReqInfo, ClineAskUseMcpServer, ClineSayTool } from "@roo/ExtensionMessage"
 import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences"
 import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences"
 import { safeJsonParse } from "@roo/safeJsonParse"
 import { safeJsonParse } from "@roo/safeJsonParse"
-import { FollowUpData, SuggestionItem } from "@roo-code/types"
 
 
 import { useCopyToClipboard } from "@src/utils/clipboard"
 import { useCopyToClipboard } from "@src/utils/clipboard"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
@@ -22,9 +18,6 @@ import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanu
 import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
 import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
 import { Button } from "@src/components/ui"
 import { Button } from "@src/components/ui"
 
 
-import ChatTextArea from "./ChatTextArea"
-import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
-
 import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
 import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
 import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock"
 import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock"
 import CodeAccordian from "../common/CodeAccordian"
 import CodeAccordian from "../common/CodeAccordian"
@@ -32,6 +25,7 @@ import CodeBlock from "../common/CodeBlock"
 import MarkdownBlock from "../common/MarkdownBlock"
 import MarkdownBlock from "../common/MarkdownBlock"
 import { ReasoningBlock } from "./ReasoningBlock"
 import { ReasoningBlock } from "./ReasoningBlock"
 import Thumbnails from "../common/Thumbnails"
 import Thumbnails from "../common/Thumbnails"
+
 import McpResourceRow from "../mcp/McpResourceRow"
 import McpResourceRow from "../mcp/McpResourceRow"
 
 
 import { Mention } from "./Mention"
 import { Mention } from "./Mention"
@@ -46,6 +40,7 @@ import { CommandExecutionError } from "./CommandExecutionError"
 import { AutoApprovedRequestLimitWarning } from "./AutoApprovedRequestLimitWarning"
 import { AutoApprovedRequestLimitWarning } from "./AutoApprovedRequestLimitWarning"
 import { CondenseContextErrorRow, CondensingContextRow, ContextCondenseRow } from "./ContextCondenseRow"
 import { CondenseContextErrorRow, CondensingContextRow, ContextCondenseRow } from "./ContextCondenseRow"
 import CodebaseSearchResultsDisplay from "./CodebaseSearchResultsDisplay"
 import CodebaseSearchResultsDisplay from "./CodebaseSearchResultsDisplay"
+import { McpExecution } from "./McpExecution"
 
 
 interface ChatRowProps {
 interface ChatRowProps {
 	message: ClineMessage
 	message: ClineMessage
@@ -114,69 +109,20 @@ export const ChatRowContent = ({
 	editable,
 	editable,
 }: ChatRowContentProps) => {
 }: ChatRowContentProps) => {
 	const { t } = useTranslation()
 	const { t } = useTranslation()
-	const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode } = useExtensionState()
+
+	const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState()
+
 	const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
 	const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
 	const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
 	const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
 	const [showCopySuccess, setShowCopySuccess] = useState(false)
 	const [showCopySuccess, setShowCopySuccess] = useState(false)
-	const [isEditing, setIsEditing] = useState(false)
-	const [editedContent, setEditedContent] = useState("")
-	const [editMode, setEditMode] = useState<Mode>(mode || "code")
-	const [editImages, setEditImages] = useState<string[]>([])
-	const { copyWithFeedback } = useCopyToClipboard()
 
 
-	// Handle message events for image selection during edit mode
-	useEffect(() => {
-		const handleMessage = (event: MessageEvent) => {
-			const msg = event.data
-			if (msg.type === "selectedImages" && msg.context === "edit" && msg.messageTs === message.ts && isEditing) {
-				setEditImages((prevImages) => appendImages(prevImages, msg.images, MAX_IMAGES_PER_MESSAGE))
-			}
-		}
-
-		window.addEventListener("message", handleMessage)
-		return () => window.removeEventListener("message", handleMessage)
-	}, [isEditing, message.ts])
+	const { copyWithFeedback } = useCopyToClipboard()
 
 
-	// Memoized callback to prevent re-renders caused by inline arrow functions
+	// Memoized callback to prevent re-renders caused by inline arrow functions.
 	const handleToggleExpand = useCallback(() => {
 	const handleToggleExpand = useCallback(() => {
 		onToggleExpand(message.ts)
 		onToggleExpand(message.ts)
 	}, [onToggleExpand, message.ts])
 	}, [onToggleExpand, message.ts])
 
 
-	// Handle edit button click
-	const handleEditClick = useCallback(() => {
-		setIsEditing(true)
-		setEditedContent(message.text || "")
-		setEditImages(message.images || [])
-		setEditMode(mode || "code")
-		// Edit mode is now handled entirely in the frontend
-		// No need to notify the backend
-	}, [message.text, message.images, mode])
-
-	// Handle cancel edit
-	const handleCancelEdit = useCallback(() => {
-		setIsEditing(false)
-		setEditedContent(message.text || "")
-		setEditImages(message.images || [])
-		setEditMode(mode || "code")
-	}, [message.text, message.images, mode])
-
-	// Handle save edit
-	const handleSaveEdit = useCallback(() => {
-		setIsEditing(false)
-		// Send edited message to backend
-		vscode.postMessage({
-			type: "submitEditedMessage",
-			value: message.ts,
-			editedMessageContent: editedContent,
-			images: editImages,
-		})
-	}, [message.ts, editedContent, editImages])
-
-	// Handle image selection for editing
-	const handleSelectImages = useCallback(() => {
-		vscode.postMessage({ type: "selectImages", context: "edit", messageTs: message.ts })
-	}, [message.ts])
-
 	const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
 	const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
 		if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
 		if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
 			const info = safeJsonParse<ClineApiReqInfo>(message.text)
 			const info = safeJsonParse<ClineApiReqInfo>(message.text)
@@ -1061,58 +1007,26 @@ export const ChatRowContent = ({
 				case "user_feedback":
 				case "user_feedback":
 					return (
 					return (
 						<div className="bg-vscode-editor-background border rounded-xs p-1 overflow-hidden whitespace-pre-wrap">
 						<div className="bg-vscode-editor-background border rounded-xs p-1 overflow-hidden whitespace-pre-wrap">
-							{isEditing ? (
-								<div className="flex flex-col gap-2 p-2">
-									<ChatTextArea
-										inputValue={editedContent}
-										setInputValue={setEditedContent}
-										sendingDisabled={false}
-										selectApiConfigDisabled={true}
-										placeholderText={t("chat:editMessage.placeholder")}
-										selectedImages={editImages}
-										setSelectedImages={setEditImages}
-										onSend={handleSaveEdit}
-										onSelectImages={handleSelectImages}
-										shouldDisableImages={false}
-										mode={editMode}
-										setMode={setEditMode}
-										modeShortcutText=""
-										isEditMode={true}
-										onCancel={handleCancelEdit}
-									/>
+							<div className="flex justify-between">
+								<div className="flex-grow px-2 py-1 wrap-anywhere">
+									<Mention text={message.text} withShadow />
 								</div>
 								</div>
-							) : (
-								<div className="flex justify-between">
-									<div className="flex-grow px-2 py-1 wrap-anywhere">
-										<Mention text={message.text} withShadow />
-									</div>
-									<div className="flex">
-										<Button
-											variant="ghost"
-											size="icon"
-											className="shrink-0 hidden"
-											disabled={isStreaming}
-											onClick={(e) => {
-												e.stopPropagation()
-												handleEditClick()
-											}}>
-											<span className="codicon codicon-edit" />
-										</Button>
-										<Button
-											variant="ghost"
-											size="icon"
-											className="shrink-0"
-											disabled={isStreaming}
-											onClick={(e) => {
-												e.stopPropagation()
-												vscode.postMessage({ type: "deleteMessage", value: message.ts })
-											}}>
-											<span className="codicon codicon-trash" />
-										</Button>
-									</div>
+								<div className="flex">
+									<Button
+										variant="ghost"
+										size="icon"
+										className="shrink-0"
+										disabled={isStreaming}
+										onClick={(e) => {
+											e.stopPropagation()
+											vscode.postMessage({ type: "deleteMessage", value: message.ts })
+										}}>
+										<span className="codicon codicon-trash" />
+									</Button>
 								</div>
 								</div>
-							)}
-							{!isEditing && message.images && message.images.length > 0 && (
+							</div>
+
+							{message.images && message.images.length > 0 && (
 								<Thumbnails images={message.images} style={{ marginTop: "8px" }} />
 								<Thumbnails images={message.images} style={{ marginTop: "8px" }} />
 							)}
 							)}
 						</div>
 						</div>

+ 248 - 291
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -1,15 +1,16 @@
 import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
 import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
 import { useEvent } from "react-use"
 import { useEvent } from "react-use"
 import DynamicTextArea from "react-textarea-autosize"
 import DynamicTextArea from "react-textarea-autosize"
+import { VolumeX, Image, WandSparkles, SendHorizontal } from "lucide-react"
 
 
 import { mentionRegex, mentionRegexGlobal, commandRegexGlobal, unescapeSpaces } from "@roo/context-mentions"
 import { mentionRegex, mentionRegexGlobal, commandRegexGlobal, unescapeSpaces } from "@roo/context-mentions"
 import { WebviewMessage } from "@roo/WebviewMessage"
 import { WebviewMessage } from "@roo/WebviewMessage"
 import { Mode, getAllModes } from "@roo/modes"
 import { Mode, getAllModes } from "@roo/modes"
 import { ExtensionMessage } from "@roo/ExtensionMessage"
 import { ExtensionMessage } from "@roo/ExtensionMessage"
 
 
-import { vscode } from "@/utils/vscode"
-import { useExtensionState } from "@/context/ExtensionStateContext"
-import { useAppTranslation } from "@/i18n/TranslationContext"
+import { vscode } from "@src/utils/vscode"
+import { useExtensionState } from "@src/context/ExtensionStateContext"
+import { useAppTranslation } from "@src/i18n/TranslationContext"
 import {
 import {
 	ContextMenuOptionType,
 	ContextMenuOptionType,
 	getContextMenuOptions,
 	getContextMenuOptions,
@@ -18,20 +19,18 @@ import {
 	shouldShowContextMenu,
 	shouldShowContextMenu,
 	SearchResult,
 	SearchResult,
 } from "@src/utils/context-mentions"
 } from "@src/utils/context-mentions"
-import { convertToMentionPath } from "@/utils/path-mentions"
-import { StandardTooltip } from "@/components/ui"
+import { cn } from "@src/lib/utils"
+import { convertToMentionPath } from "@src/utils/path-mentions"
+import { StandardTooltip } from "@src/components/ui"
 
 
 import Thumbnails from "../common/Thumbnails"
 import Thumbnails from "../common/Thumbnails"
-import ModeSelector from "./ModeSelector"
+import { ModeSelector } from "./ModeSelector"
 import { ApiConfigSelector } from "./ApiConfigSelector"
 import { ApiConfigSelector } from "./ApiConfigSelector"
 import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
 import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
 import ContextMenu from "./ContextMenu"
 import ContextMenu from "./ContextMenu"
-import { VolumeX, Image, WandSparkles, SendHorizontal } from "lucide-react"
 import { IndexingStatusBadge } from "./IndexingStatusBadge"
 import { IndexingStatusBadge } from "./IndexingStatusBadge"
 import { SlashCommandsPopover } from "./SlashCommandsPopover"
 import { SlashCommandsPopover } from "./SlashCommandsPopover"
-import { cn } from "@/lib/utils"
 import { usePromptHistory } from "./hooks/usePromptHistory"
 import { usePromptHistory } from "./hooks/usePromptHistory"
-import { EditModeControls } from "./EditModeControls"
 
 
 interface ChatTextAreaProps {
 interface ChatTextAreaProps {
 	inputValue: string
 	inputValue: string
@@ -48,17 +47,13 @@ interface ChatTextAreaProps {
 	mode: Mode
 	mode: Mode
 	setMode: (value: Mode) => void
 	setMode: (value: Mode) => void
 	modeShortcutText: string
 	modeShortcutText: string
-	// Edit mode props
-	isEditMode?: boolean
-	onCancel?: () => void
 }
 }
 
 
-const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
+export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 	(
 	(
 		{
 		{
 			inputValue,
 			inputValue,
 			setInputValue,
 			setInputValue,
-			sendingDisabled,
 			selectApiConfigDisabled,
 			selectApiConfigDisabled,
 			placeholderText,
 			placeholderText,
 			selectedImages,
 			selectedImages,
@@ -70,8 +65,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 			mode,
 			mode,
 			setMode,
 			setMode,
 			modeShortcutText,
 			modeShortcutText,
-			isEditMode = false,
-			onCancel,
 		},
 		},
 		ref,
 		ref,
 	) => {
 	) => {
@@ -91,12 +84,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 			commands,
 			commands,
 		} = useExtensionState()
 		} = useExtensionState()
 
 
-		// Find the ID and display text for the currently selected API configuration
+		// Find the ID and display text for the currently selected API configuration.
 		const { currentConfigId, displayName } = useMemo(() => {
 		const { currentConfigId, displayName } = useMemo(() => {
 			const currentConfig = listApiConfigMeta?.find((config) => config.name === currentApiConfigName)
 			const currentConfig = listApiConfigMeta?.find((config) => config.name === currentApiConfigName)
 			return {
 			return {
 				currentConfigId: currentConfig?.id || "",
 				currentConfigId: currentConfig?.id || "",
-				displayName: currentApiConfigName || "", // Use the name directly for display
+				displayName: currentApiConfigName || "", // Use the name directly for display.
 			}
 			}
 		}, [listApiConfigMeta, currentApiConfigName])
 		}, [listApiConfigMeta, currentApiConfigName])
 
 
@@ -888,7 +881,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 
 
 		const placeholderBottomText = `\n(${t("chat:addContext")}${shouldDisableImages ? `, ${t("chat:dragFiles")}` : `, ${t("chat:dragFilesImages")}`})`
 		const placeholderBottomText = `\n(${t("chat:addContext")}${shouldDisableImages ? `, ${t("chat:dragFiles")}` : `, ${t("chat:dragFilesImages")}`})`
 
 
-		// Common mode selector handler
 		const handleModeChange = useCallback(
 		const handleModeChange = useCallback(
 			(value: Mode) => {
 			(value: Mode) => {
 				setMode(value)
 				setMode(value)
@@ -897,261 +889,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 			[setMode],
 			[setMode],
 		)
 		)
 
 
-		// Helper function to render mode selector
-		const renderModeSelector = () => (
-			<ModeSelector
-				value={mode}
-				title={t("chat:selectMode")}
-				onChange={handleModeChange}
-				triggerClassName="w-full"
-				modeShortcutText={modeShortcutText}
-				customModes={customModes}
-				customModePrompts={customModePrompts}
-			/>
-		)
-
-		// Helper function to handle API config change
 		const handleApiConfigChange = useCallback((value: string) => {
 		const handleApiConfigChange = useCallback((value: string) => {
 			vscode.postMessage({ type: "loadApiConfigurationById", text: value })
 			vscode.postMessage({ type: "loadApiConfigurationById", text: value })
 		}, [])
 		}, [])
 
 
-		// Helper function to render non-edit mode controls
-		const renderNonEditModeControls = () => (
-			<div className={cn("flex", "justify-between", "items-center", "mt-auto")}>
-				<div className={cn("flex", "items-center", "gap-1", "min-w-0")}>
-					<div className="shrink-0">{renderModeSelector()}</div>
-
-					<div className={cn("flex-1", "min-w-0", "overflow-hidden")}>
-						<ApiConfigSelector
-							value={currentConfigId}
-							displayName={displayName}
-							disabled={selectApiConfigDisabled}
-							title={t("chat:selectApiConfig")}
-							onChange={handleApiConfigChange}
-							triggerClassName="w-full text-ellipsis overflow-hidden"
-							listApiConfigMeta={listApiConfigMeta || []}
-							pinnedApiConfigs={pinnedApiConfigs}
-							togglePinnedApiConfig={togglePinnedApiConfig}
-						/>
-					</div>
-				</div>
-
-				<div className={cn("flex", "items-center", "gap-0.5", "shrink-0")}>
-					{isTtsPlaying && (
-						<StandardTooltip content={t("chat:stopTts")}>
-							<button
-								aria-label={t("chat:stopTts")}
-								onClick={() => vscode.postMessage({ type: "stopTts" })}
-								className={cn(
-									"relative inline-flex items-center justify-center",
-									"bg-transparent border-none p-1.5",
-									"rounded-md min-w-[28px] min-h-[28px]",
-									"text-vscode-foreground opacity-85",
-									"transition-all duration-150",
-									"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
-									"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-									"active:bg-[rgba(255,255,255,0.1)]",
-									"cursor-pointer",
-								)}>
-								<VolumeX className="w-4 h-4" />
-							</button>
-						</StandardTooltip>
-					)}
-					<SlashCommandsPopover />
-					<IndexingStatusBadge />
-					<StandardTooltip content={t("chat:addImages")}>
-						<button
-							aria-label={t("chat:addImages")}
-							disabled={shouldDisableImages}
-							onClick={!shouldDisableImages ? onSelectImages : undefined}
-							className={cn(
-								"relative inline-flex items-center justify-center",
-								"bg-transparent border-none p-1.5",
-								"rounded-md min-w-[28px] min-h-[28px]",
-								"text-vscode-foreground opacity-85",
-								"transition-all duration-150",
-								"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
-								"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-								"active:bg-[rgba(255,255,255,0.1)]",
-								!shouldDisableImages && "cursor-pointer",
-								shouldDisableImages &&
-									"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
-								"mr-1",
-							)}>
-							<Image className="w-4 h-4" />
-						</button>
-					</StandardTooltip>
-				</div>
-			</div>
-		)
-
-		// Helper function to render the text area section
-		const renderTextAreaSection = () => (
-			<div
-				className={cn(
-					"relative",
-					"flex-1",
-					"flex",
-					"flex-col-reverse",
-					"min-h-0",
-					"overflow-hidden",
-					"rounded",
-				)}>
-				<div
-					ref={highlightLayerRef}
-					data-testid="highlight-layer"
-					className={cn(
-						"absolute",
-						"inset-0",
-						"pointer-events-none",
-						"whitespace-pre-wrap",
-						"break-words",
-						"text-transparent",
-						"overflow-hidden",
-						"font-vscode-font-family",
-						"text-vscode-editor-font-size",
-						"leading-vscode-editor-line-height",
-						isFocused
-							? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
-							: isDraggingOver
-								? "border-2 border-dashed border-vscode-focusBorder"
-								: "border border-transparent",
-						isEditMode ? "pt-1.5 pb-10 px-2" : "py-1.5 px-2",
-						"px-[8px]",
-						"pr-9",
-						"z-10",
-						"forced-color-adjust-none",
-					)}
-					style={{
-						color: "transparent",
-					}}
-				/>
-				<DynamicTextArea
-					ref={(el) => {
-						if (typeof ref === "function") {
-							ref(el)
-						} else if (ref) {
-							ref.current = el
-						}
-						textAreaRef.current = el
-					}}
-					value={inputValue}
-					onChange={(e) => {
-						handleInputChange(e)
-						updateHighlights()
-					}}
-					onFocus={() => setIsFocused(true)}
-					onKeyDown={handleKeyDown}
-					onKeyUp={handleKeyUp}
-					onBlur={handleBlur}
-					onPaste={handlePaste}
-					onSelect={updateCursorPosition}
-					onMouseUp={updateCursorPosition}
-					onHeightChange={(height) => {
-						if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
-							setTextAreaBaseHeight(height)
-						}
-
-						onHeightChange?.(height)
-					}}
-					placeholder={placeholderText}
-					minRows={3}
-					maxRows={15}
-					autoFocus={true}
-					className={cn(
-						"w-full",
-						"text-vscode-input-foreground",
-						"font-vscode-font-family",
-						"text-vscode-editor-font-size",
-						"leading-vscode-editor-line-height",
-						"cursor-text",
-						isEditMode ? "pt-1.5 pb-10 px-2" : "py-1.5 px-2",
-						isFocused
-							? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
-							: isDraggingOver
-								? "border-2 border-dashed border-vscode-focusBorder"
-								: "border border-transparent",
-						isDraggingOver
-							? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]"
-							: "bg-vscode-input-background",
-						"transition-background-color duration-150 ease-in-out",
-						"will-change-background-color",
-						"min-h-[90px]",
-						"box-border",
-						"rounded",
-						"resize-none",
-						"overflow-x-hidden",
-						"overflow-y-auto",
-						"pr-9",
-						"flex-none flex-grow",
-						"z-[2]",
-						"scrollbar-none",
-						"scrollbar-hide",
-					)}
-					onScroll={() => updateHighlights()}
-				/>
-
-				<div className="absolute top-1 right-1 z-30">
-					<StandardTooltip content={t("chat:enhancePrompt")}>
-						<button
-							aria-label={t("chat:enhancePrompt")}
-							disabled={false}
-							onClick={handleEnhancePrompt}
-							className={cn(
-								"relative inline-flex items-center justify-center",
-								"bg-transparent border-none p-1.5",
-								"rounded-md min-w-[28px] min-h-[28px]",
-								"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
-								"transition-all duration-150",
-								"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
-								"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-								"active:bg-[rgba(255,255,255,0.1)]",
-								"cursor-pointer",
-							)}>
-							<WandSparkles className={cn("w-4 h-4", isEnhancingPrompt && "animate-spin")} />
-						</button>
-					</StandardTooltip>
-				</div>
-
-				{!isEditMode && (
-					<div className="absolute bottom-1 right-1 z-30">
-						<StandardTooltip content={t("chat:sendMessage")}>
-							<button
-								aria-label={t("chat:sendMessage")}
-								disabled={false}
-								onClick={onSend}
-								className={cn(
-									"relative inline-flex items-center justify-center",
-									"bg-transparent border-none p-1.5",
-									"rounded-md min-w-[28px] min-h-[28px]",
-									"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
-									"transition-all duration-150",
-									"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
-									"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-									"active:bg-[rgba(255,255,255,0.1)]",
-									"cursor-pointer",
-								)}>
-								<SendHorizontal className="w-4 h-4" />
-							</button>
-						</StandardTooltip>
-					</div>
-				)}
-
-				{!inputValue && !isEditMode && (
-					<div
-						className="absolute left-2 z-30 pr-9 flex items-center h-8 font-vscode-font-family text-vscode-editor-font-size leading-vscode-editor-line-height"
-						style={{
-							bottom: "0.25rem",
-							color: "color-mix(in oklab, var(--vscode-input-foreground) 50%, transparent)",
-							userSelect: "none",
-							pointerEvents: "none",
-						}}>
-						{placeholderBottomText}
-					</div>
-				)}
-			</div>
-		)
-
 		return (
 		return (
 			<div
 			<div
 				className={cn(
 				className={cn(
@@ -1160,12 +901,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 					"flex-col",
 					"flex-col",
 					"gap-1",
 					"gap-1",
 					"bg-editor-background",
 					"bg-editor-background",
-					isEditMode ? "px-0" : "px-1.5",
+					"px-1.5",
 					"pb-1",
 					"pb-1",
 					"outline-none",
 					"outline-none",
 					"border",
 					"border",
 					"border-none",
 					"border-none",
-					isEditMode ? "w-full" : "w-[calc(100%-16px)]",
+					"w-[calc(100%-16px)]",
 					"ml-auto",
 					"ml-auto",
 					"mr-auto",
 					"mr-auto",
 					"box-border",
 					"box-border",
@@ -1228,23 +969,168 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 							</div>
 							</div>
 						)}
 						)}
 
 
-						{renderTextAreaSection()}
-					</div>
+						<div
+							className={cn(
+								"relative",
+								"flex-1",
+								"flex",
+								"flex-col-reverse",
+								"min-h-0",
+								"overflow-hidden",
+								"rounded",
+							)}>
+							<div
+								ref={highlightLayerRef}
+								data-testid="highlight-layer"
+								className={cn(
+									"absolute",
+									"inset-0",
+									"pointer-events-none",
+									"whitespace-pre-wrap",
+									"break-words",
+									"text-transparent",
+									"overflow-hidden",
+									"font-vscode-font-family",
+									"text-vscode-editor-font-size",
+									"leading-vscode-editor-line-height",
+									isFocused
+										? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
+										: isDraggingOver
+											? "border-2 border-dashed border-vscode-focusBorder"
+											: "border border-transparent",
+									"px-[8px]",
+									"py-1.5",
+									"pr-9",
+									"z-10",
+									"forced-color-adjust-none",
+								)}
+								style={{
+									color: "transparent",
+								}}
+							/>
+							<DynamicTextArea
+								ref={(el) => {
+									if (typeof ref === "function") {
+										ref(el)
+									} else if (ref) {
+										ref.current = el
+									}
+									textAreaRef.current = el
+								}}
+								value={inputValue}
+								onChange={(e) => {
+									handleInputChange(e)
+									updateHighlights()
+								}}
+								onFocus={() => setIsFocused(true)}
+								onKeyDown={handleKeyDown}
+								onKeyUp={handleKeyUp}
+								onBlur={handleBlur}
+								onPaste={handlePaste}
+								onSelect={updateCursorPosition}
+								onMouseUp={updateCursorPosition}
+								onHeightChange={(height) => {
+									if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
+										setTextAreaBaseHeight(height)
+									}
+
+									onHeightChange?.(height)
+								}}
+								placeholder={placeholderText}
+								minRows={3}
+								maxRows={15}
+								autoFocus={true}
+								className={cn(
+									"w-full",
+									"text-vscode-input-foreground",
+									"font-vscode-font-family",
+									"text-vscode-editor-font-size",
+									"leading-vscode-editor-line-height",
+									"cursor-text",
+									"py-1.5 px-2",
+									isFocused
+										? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
+										: isDraggingOver
+											? "border-2 border-dashed border-vscode-focusBorder"
+											: "border border-transparent",
+									isDraggingOver
+										? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]"
+										: "bg-vscode-input-background",
+									"transition-background-color duration-150 ease-in-out",
+									"will-change-background-color",
+									"min-h-[90px]",
+									"box-border",
+									"rounded",
+									"resize-none",
+									"overflow-x-hidden",
+									"overflow-y-auto",
+									"pr-9",
+									"flex-none flex-grow",
+									"z-[2]",
+									"scrollbar-none",
+									"scrollbar-hide",
+								)}
+								onScroll={() => updateHighlights()}
+							/>
+
+							<div className="absolute top-1 right-1 z-30">
+								<StandardTooltip content={t("chat:enhancePrompt")}>
+									<button
+										aria-label={t("chat:enhancePrompt")}
+										disabled={false}
+										onClick={handleEnhancePrompt}
+										className={cn(
+											"relative inline-flex items-center justify-center",
+											"bg-transparent border-none p-1.5",
+											"rounded-md min-w-[28px] min-h-[28px]",
+											"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
+											"transition-all duration-150",
+											"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
+											"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
+											"active:bg-[rgba(255,255,255,0.1)]",
+											"cursor-pointer",
+										)}>
+										<WandSparkles className={cn("w-4 h-4", isEnhancingPrompt && "animate-spin")} />
+									</button>
+								</StandardTooltip>
+							</div>
+
+							<div className="absolute bottom-1 right-1 z-30">
+								<StandardTooltip content={t("chat:sendMessage")}>
+									<button
+										aria-label={t("chat:sendMessage")}
+										disabled={false}
+										onClick={onSend}
+										className={cn(
+											"relative inline-flex items-center justify-center",
+											"bg-transparent border-none p-1.5",
+											"rounded-md min-w-[28px] min-h-[28px]",
+											"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
+											"transition-all duration-150",
+											"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
+											"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
+											"active:bg-[rgba(255,255,255,0.1)]",
+											"cursor-pointer",
+										)}>
+										<SendHorizontal className="w-4 h-4" />
+									</button>
+								</StandardTooltip>
+							</div>
 
 
-					{isEditMode && (
-						<EditModeControls
-							mode={mode}
-							onModeChange={handleModeChange}
-							modeShortcutText={modeShortcutText}
-							customModes={customModes}
-							customModePrompts={customModePrompts}
-							onCancel={onCancel}
-							onSend={onSend}
-							onSelectImages={onSelectImages}
-							sendingDisabled={sendingDisabled}
-							shouldDisableImages={shouldDisableImages}
-						/>
-					)}
+							{!inputValue && (
+								<div
+									className="absolute left-2 z-30 pr-9 flex items-center h-8 font-vscode-font-family text-vscode-editor-font-size leading-vscode-editor-line-height"
+									style={{
+										bottom: "0.25rem",
+										color: "color-mix(in oklab, var(--vscode-input-foreground) 50%, transparent)",
+										userSelect: "none",
+										pointerEvents: "none",
+									}}>
+									{placeholderBottomText}
+								</div>
+							)}
+						</div>
+					</div>
 				</div>
 				</div>
 
 
 				{selectedImages.length > 0 && (
 				{selectedImages.length > 0 && (
@@ -1259,10 +1145,81 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 					/>
 					/>
 				)}
 				)}
 
 
-				{!isEditMode && renderNonEditModeControls()}
+				<div className="flex justify-between items-center">
+					<div className="flex items-center gap-1">
+						<div className="max-w-32">
+							<ModeSelector
+								value={mode}
+								title={t("chat:selectMode")}
+								onChange={handleModeChange}
+								triggerClassName="w-full"
+								modeShortcutText={modeShortcutText}
+								customModes={customModes}
+								customModePrompts={customModePrompts}
+							/>
+						</div>
+						<div className="max-w-32">
+							<ApiConfigSelector
+								value={currentConfigId}
+								displayName={displayName}
+								disabled={selectApiConfigDisabled}
+								title={t("chat:selectApiConfig")}
+								onChange={handleApiConfigChange}
+								triggerClassName="w-full text-ellipsis overflow-hidden"
+								listApiConfigMeta={listApiConfigMeta || []}
+								pinnedApiConfigs={pinnedApiConfigs}
+								togglePinnedApiConfig={togglePinnedApiConfig}
+							/>
+						</div>
+					</div>
+					<div className="flex items-center gap-0.5">
+						{isTtsPlaying && (
+							<StandardTooltip content={t("chat:stopTts")}>
+								<button
+									aria-label={t("chat:stopTts")}
+									onClick={() => vscode.postMessage({ type: "stopTts" })}
+									className={cn(
+										"relative inline-flex items-center justify-center",
+										"bg-transparent border-none p-1.5",
+										"rounded-md min-w-[28px] min-h-[28px]",
+										"text-vscode-foreground opacity-85",
+										"transition-all duration-150",
+										"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
+										"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
+										"active:bg-[rgba(255,255,255,0.1)]",
+										"cursor-pointer",
+									)}>
+									<VolumeX className="w-4 h-4" />
+								</button>
+							</StandardTooltip>
+						)}
+						<SlashCommandsPopover />
+						<IndexingStatusBadge />
+						<StandardTooltip content={t("chat:addImages")}>
+							<button
+								aria-label={t("chat:addImages")}
+								disabled={shouldDisableImages}
+								onClick={!shouldDisableImages ? onSelectImages : undefined}
+								className={cn(
+									"relative inline-flex items-center justify-center",
+									"bg-transparent border-none p-1.5",
+									"rounded-md min-w-[28px] min-h-[28px]",
+									"text-vscode-foreground opacity-85",
+									"transition-all duration-150",
+									"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
+									"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
+									"active:bg-[rgba(255,255,255,0.1)]",
+									!shouldDisableImages && "cursor-pointer",
+									shouldDisableImages &&
+										"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
+									"mr-1",
+								)}>
+								<Image className="w-4 h-4" />
+							</button>
+						</StandardTooltip>
+					</div>
+				</div>
 			</div>
 			</div>
 		)
 		)
 	},
 	},
 )
 )
-
-export default ChatTextArea

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

@@ -6,6 +6,7 @@ import removeMd from "remove-markdown"
 import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
 import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
 import useSound from "use-sound"
 import useSound from "use-sound"
 import { LRUCache } from "lru-cache"
 import { LRUCache } from "lru-cache"
+import { useTranslation } from "react-i18next"
 
 
 import { useDebounceEffect } from "@src/utils/useDebounceEffect"
 import { useDebounceEffect } from "@src/utils/useDebounceEffect"
 import { appendImages } from "@src/utils/imageUtils"
 import { appendImages } from "@src/utils/imageUtils"
@@ -30,7 +31,6 @@ import {
 	findLongestPrefixMatch,
 	findLongestPrefixMatch,
 	parseCommand,
 	parseCommand,
 } from "@src/utils/command-validation"
 } from "@src/utils/command-validation"
-import { useTranslation } from "react-i18next"
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
 import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
 import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
@@ -48,7 +48,7 @@ import HistoryPreview from "../history/HistoryPreview"
 import Announcement from "./Announcement"
 import Announcement from "./Announcement"
 import BrowserSessionRow from "./BrowserSessionRow"
 import BrowserSessionRow from "./BrowserSessionRow"
 import ChatRow from "./ChatRow"
 import ChatRow from "./ChatRow"
-import ChatTextArea from "./ChatTextArea"
+import { ChatTextArea } from "./ChatTextArea"
 import TaskHeader from "./TaskHeader"
 import TaskHeader from "./TaskHeader"
 import AutoApproveMenu from "./AutoApproveMenu"
 import AutoApproveMenu from "./AutoApproveMenu"
 import SystemPromptWarning from "./SystemPromptWarning"
 import SystemPromptWarning from "./SystemPromptWarning"
@@ -68,7 +68,7 @@ export interface ChatViewRef {
 	acceptInput: () => void
 	acceptInput: () => void
 }
 }
 
 
-export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
+export const MAX_IMAGES_PER_MESSAGE = 20 // This is the Anthropic limit.
 
 
 const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
 const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
 
 
@@ -77,13 +77,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 	ref,
 	ref,
 ) => {
 ) => {
 	const isMountedRef = useRef(true)
 	const isMountedRef = useRef(true)
+
 	const [audioBaseUri] = useState(() => {
 	const [audioBaseUri] = useState(() => {
 		const w = window as any
 		const w = window as any
 		return w.AUDIO_BASE_URI || ""
 		return w.AUDIO_BASE_URI || ""
 	})
 	})
+
 	const { t } = useAppTranslation()
 	const { t } = useAppTranslation()
 	const { t: tSettings } = useTranslation("settings")
 	const { t: tSettings } = useTranslation("settings")
 	const modeShortcutText = `${isMac ? "⌘" : "Ctrl"} + . ${t("chat:forNextMode")}, ${isMac ? "⌘" : "Ctrl"} + Shift + . ${t("chat:forPreviousMode")}`
 	const modeShortcutText = `${isMac ? "⌘" : "Ctrl"} + . ${t("chat:forNextMode")}, ${isMac ? "⌘" : "Ctrl"} + Shift + . ${t("chat:forPreviousMode")}`
+
 	const {
 	const {
 		clineMessages: messages,
 		clineMessages: messages,
 		currentTaskItem,
 		currentTaskItem,
@@ -121,6 +124,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 	} = useExtensionState()
 	} = useExtensionState()
 
 
 	const messagesRef = useRef(messages)
 	const messagesRef = useRef(messages)
+
 	useEffect(() => {
 	useEffect(() => {
 		messagesRef.current = messages
 		messagesRef.current = messages
 	}, [messages])
 	}, [messages])
@@ -238,9 +242,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		soundEnabled,
 		soundEnabled,
 	}
 	}
 
 
-	const getAudioUrl = (path: string) => {
-		return `${audioBaseUri}/${path}`
-	}
+	const getAudioUrl = (path: string) => `${audioBaseUri}/${path}`
 
 
 	// Use the getAudioUrl helper function
 	// Use the getAudioUrl helper function
 	const [playNotification] = useSound(getAudioUrl("notification.wav"), soundConfig)
 	const [playNotification] = useSound(getAudioUrl("notification.wav"), soundConfig)
@@ -1456,17 +1458,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 
 
 	const placeholderText = task ? t("chat:typeMessage") : t("chat:typeTask")
 	const placeholderText = task ? t("chat:typeMessage") : t("chat:typeTask")
 
 
-	// Function to switch to a specific mode
 	const switchToMode = useCallback(
 	const switchToMode = useCallback(
 		(modeSlug: string): void => {
 		(modeSlug: string): void => {
-			// Update local state and notify extension to sync mode change
+			// Update local state and notify extension to sync mode change.
 			setMode(modeSlug)
 			setMode(modeSlug)
 
 
-			// Send the mode switch message
-			vscode.postMessage({
-				type: "mode",
-				text: modeSlug,
-			})
+			// Send the mode switch message.
+			vscode.postMessage({ type: "mode", text: modeSlug })
 		},
 		},
 		[setMode],
 		[setMode],
 	)
 	)
@@ -1760,9 +1758,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		[switchToNextMode, switchToPreviousMode],
 		[switchToNextMode, switchToPreviousMode],
 	)
 	)
 
 
-	// Add event listener
 	useEffect(() => {
 	useEffect(() => {
 		window.addEventListener("keydown", handleKeyDown)
 		window.addEventListener("keydown", handleKeyDown)
+
 		return () => {
 		return () => {
 			window.removeEventListener("keydown", handleKeyDown)
 			window.removeEventListener("keydown", handleKeyDown)
 		}
 		}
@@ -1789,6 +1787,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 
 
 	const areButtonsVisible = showScrollToBottom || primaryButtonText || secondaryButtonText || isStreaming
 	const areButtonsVisible = showScrollToBottom || primaryButtonText || secondaryButtonText || isStreaming
 
 
+	console.log("ChatView - render()")
+
 	return (
 	return (
 		<div
 		<div
 			data-testid="chat-view"
 			data-testid="chat-view"

+ 8 - 6
webview-ui/src/components/chat/CodeIndexPopover.tsx

@@ -10,6 +10,13 @@ import {
 	VSCodeCheckbox,
 	VSCodeCheckbox,
 } from "@vscode/webview-ui-toolkit/react"
 } from "@vscode/webview-ui-toolkit/react"
 import * as ProgressPrimitive from "@radix-ui/react-progress"
 import * as ProgressPrimitive from "@radix-ui/react-progress"
+import { AlertTriangle } from "lucide-react"
+
+import { CODEBASE_INDEX_DEFAULTS } from "@roo-code/types"
+
+import type { EmbedderProvider } from "@roo/embeddingModels"
+import type { IndexingStatus } from "@roo/ExtensionMessage"
+
 import { vscode } from "@src/utils/vscode"
 import { vscode } from "@src/utils/vscode"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { useAppTranslation } from "@src/i18n/TranslationContext"
@@ -32,16 +39,11 @@ import {
 	AlertDialogTrigger,
 	AlertDialogTrigger,
 	Popover,
 	Popover,
 	PopoverContent,
 	PopoverContent,
-	PopoverTrigger,
 	Slider,
 	Slider,
 	StandardTooltip,
 	StandardTooltip,
 } from "@src/components/ui"
 } from "@src/components/ui"
-import { AlertTriangle } from "lucide-react"
 import { useRooPortal } from "@src/components/ui/hooks/useRooPortal"
 import { useRooPortal } from "@src/components/ui/hooks/useRooPortal"
 import { useEscapeKey } from "@src/hooks/useEscapeKey"
 import { useEscapeKey } from "@src/hooks/useEscapeKey"
-import type { EmbedderProvider } from "@roo/embeddingModels"
-import type { IndexingStatus } from "@roo/ExtensionMessage"
-import { CODEBASE_INDEX_DEFAULTS } from "@roo-code/types"
 
 
 // Default URLs for providers
 // Default URLs for providers
 const DEFAULT_QDRANT_URL = "http://localhost:6333"
 const DEFAULT_QDRANT_URL = "http://localhost:6333"
@@ -525,7 +527,7 @@ export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
 						setOpen(newOpen)
 						setOpen(newOpen)
 					}
 					}
 				}}>
 				}}>
-				<PopoverTrigger asChild>{children}</PopoverTrigger>
+				{children}
 				<PopoverContent
 				<PopoverContent
 					className="w-[calc(100vw-32px)] max-w-[450px] max-h-[80vh] overflow-y-auto p-0"
 					className="w-[calc(100vw-32px)] max-w-[450px] max-h-[80vh] overflow-y-auto p-0"
 					align="end"
 					align="end"

+ 0 - 115
webview-ui/src/components/chat/EditModeControls.tsx

@@ -1,115 +0,0 @@
-import React from "react"
-import { Mode } from "@roo/modes"
-import { Button, StandardTooltip } from "@/components/ui"
-import { Image, SendHorizontal } from "lucide-react"
-import { cn } from "@/lib/utils"
-import ModeSelector from "./ModeSelector"
-import { useAppTranslation } from "@/i18n/TranslationContext"
-
-interface EditModeControlsProps {
-	mode: Mode
-	onModeChange: (value: Mode) => void
-	modeShortcutText: string
-	customModes: any
-	customModePrompts: any
-	onCancel?: () => void
-	onSend: () => void
-	onSelectImages: () => void
-	sendingDisabled: boolean
-	shouldDisableImages: boolean
-}
-
-export const EditModeControls: React.FC<EditModeControlsProps> = ({
-	mode,
-	onModeChange,
-	modeShortcutText,
-	customModes,
-	customModePrompts,
-	onCancel,
-	onSend,
-	onSelectImages,
-	sendingDisabled,
-	shouldDisableImages,
-}) => {
-	const { t } = useAppTranslation()
-
-	return (
-		<div
-			className={cn(
-				"flex",
-				"items-center",
-				"justify-between",
-				"absolute",
-				"bottom-2",
-				"left-2",
-				"right-2",
-				"z-30",
-			)}>
-			<div className={cn("flex", "items-center", "gap-1", "flex-1", "min-w-0")}>
-				<div className="shrink-0">
-					<ModeSelector
-						value={mode}
-						title={t("chat:selectMode")}
-						onChange={onModeChange}
-						triggerClassName="w-full"
-						modeShortcutText={modeShortcutText}
-						customModes={customModes}
-						customModePrompts={customModePrompts}
-					/>
-				</div>
-			</div>
-			<div className={cn("flex", "items-center", "gap-0.5", "shrink-0", "ml-2")}>
-				<Button
-					variant="secondary"
-					size="sm"
-					onClick={onCancel}
-					disabled={sendingDisabled}
-					className="text-xs bg-vscode-toolbar-hoverBackground hover:bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground">
-					Cancel
-				</Button>
-				<StandardTooltip content={t("chat:addImages")}>
-					<button
-						aria-label={t("chat:addImages")}
-						disabled={shouldDisableImages}
-						onClick={!shouldDisableImages ? onSelectImages : undefined}
-						className={cn(
-							"relative inline-flex items-center justify-center",
-							"bg-transparent border-none p-1.5",
-							"rounded-md min-w-[28px] min-h-[28px]",
-							"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
-							"transition-all duration-150",
-							"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
-							"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-							"active:bg-[rgba(255,255,255,0.1)]",
-							!shouldDisableImages && "cursor-pointer",
-							shouldDisableImages &&
-								"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
-						)}>
-						<Image className="w-4 h-4" />
-					</button>
-				</StandardTooltip>
-				<StandardTooltip content={t("chat:save.tooltip")}>
-					<button
-						aria-label={t("chat:save.tooltip")}
-						disabled={sendingDisabled}
-						onClick={!sendingDisabled ? onSend : undefined}
-						className={cn(
-							"relative inline-flex items-center justify-center",
-							"bg-transparent border-none p-1.5",
-							"rounded-md min-w-[28px] min-h-[28px]",
-							"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
-							"transition-all duration-150",
-							"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
-							"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-							"active:bg-[rgba(255,255,255,0.1)]",
-							!sendingDisabled && "cursor-pointer",
-							sendingDisabled &&
-								"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
-						)}>
-						<SendHorizontal className="w-4 h-4" />
-					</button>
-				</StandardTooltip>
-			</div>
-		</div>
-	)
-}

+ 25 - 29
webview-ui/src/components/chat/IconButton.tsx

@@ -1,10 +1,11 @@
-import { cn } from "@/lib/utils"
-import { StandardTooltip } from "@/components/ui"
+import { cn } from "@src/lib/utils"
+import { Button, StandardTooltip } from "@src/components/ui"
 
 
 interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
 interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
 	iconClass: string
 	iconClass: string
 	title: string
 	title: string
 	disabled?: boolean
 	disabled?: boolean
+	tooltip?: boolean
 	isLoading?: boolean
 	isLoading?: boolean
 	style?: React.CSSProperties
 	style?: React.CSSProperties
 }
 }
@@ -14,39 +15,34 @@ export const IconButton: React.FC<IconButtonProps> = ({
 	title,
 	title,
 	className,
 	className,
 	disabled,
 	disabled,
+	tooltip = true,
 	isLoading,
 	isLoading,
 	onClick,
 	onClick,
 	style,
 	style,
 	...props
 	...props
-}) => {
-	const buttonClasses = cn(
-		"relative inline-flex items-center justify-center",
-		"bg-transparent border-none p-1.5",
-		"rounded-md min-w-[28px] min-h-[28px]",
-		"text-vscode-foreground opacity-85",
-		"transition-all duration-150",
-		"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
-		"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-		"active:bg-[rgba(255,255,255,0.1)]",
-		!disabled && "cursor-pointer",
-		disabled &&
-			"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
-		className,
-	)
-
-	const iconClasses = cn("codicon", iconClass, isLoading && "codicon-modifier-spin")
-
-	const button = (
-		<button
+}) => (
+	<StandardTooltip content={tooltip ? title : undefined}>
+		<Button
 			aria-label={title}
 			aria-label={title}
-			className={buttonClasses}
+			className={cn(
+				"relative inline-flex items-center justify-center",
+				"bg-transparent border-none p-1.5",
+				"rounded-md min-w-[28px] min-h-[28px]",
+				"text-vscode-foreground opacity-85",
+				"transition-all duration-150",
+				"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
+				"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
+				"active:bg-[rgba(255,255,255,0.1)]",
+				!disabled && "cursor-pointer",
+				disabled &&
+					"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
+				className,
+			)}
 			disabled={disabled}
 			disabled={disabled}
 			onClick={!disabled ? onClick : undefined}
 			onClick={!disabled ? onClick : undefined}
 			style={{ fontSize: 16.5, ...style }}
 			style={{ fontSize: 16.5, ...style }}
 			{...props}>
 			{...props}>
-			<span className={iconClasses} />
-		</button>
-	)
-
-	return <StandardTooltip content={title}>{button}</StandardTooltip>
-}
+			<span className={cn("codicon", iconClass, isLoading && "codicon-modifier-spin")} />
+		</Button>
+	</StandardTooltip>
+)

+ 42 - 92
webview-ui/src/components/chat/IndexingStatusBadge.tsx

@@ -1,12 +1,16 @@
 import React, { useState, useEffect, useMemo } from "react"
 import React, { useState, useEffect, useMemo } from "react"
 import { Database } from "lucide-react"
 import { Database } from "lucide-react"
+
 import { cn } from "@src/lib/utils"
 import { cn } from "@src/lib/utils"
 import { vscode } from "@src/utils/vscode"
 import { vscode } from "@src/utils/vscode"
 import { useAppTranslation } from "@/i18n/TranslationContext"
 import { useAppTranslation } from "@/i18n/TranslationContext"
-import { useTooltip } from "@/hooks/useTooltip"
+
+import type { IndexingStatus, IndexingStatusUpdateMessage } from "@roo/ExtensionMessage"
+
 import { useExtensionState } from "@src/context/ExtensionStateContext"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
+import { PopoverTrigger, StandardTooltip, Button } from "@src/components/ui"
+
 import { CodeIndexPopover } from "./CodeIndexPopover"
 import { CodeIndexPopover } from "./CodeIndexPopover"
-import type { IndexingStatus, IndexingStatusUpdateMessage } from "@roo/ExtensionMessage"
 
 
 interface IndexingStatusBadgeProps {
 interface IndexingStatusBadgeProps {
 	className?: string
 	className?: string
@@ -15,8 +19,6 @@ interface IndexingStatusBadgeProps {
 export const IndexingStatusBadge: React.FC<IndexingStatusBadgeProps> = ({ className }) => {
 export const IndexingStatusBadge: React.FC<IndexingStatusBadgeProps> = ({ className }) => {
 	const { t } = useAppTranslation()
 	const { t } = useAppTranslation()
 	const { cwd } = useExtensionState()
 	const { cwd } = useExtensionState()
-	const { showTooltip, handleMouseEnter, handleMouseLeave, cleanup } = useTooltip({ delay: 300 })
-	const [isHovered, setIsHovered] = useState(false)
 
 
 	const [indexingStatus, setIndexingStatus] = useState<IndexingStatus>({
 	const [indexingStatus, setIndexingStatus] = useState<IndexingStatus>({
 		systemStatus: "Standby",
 		systemStatus: "Standby",
@@ -26,10 +28,10 @@ export const IndexingStatusBadge: React.FC<IndexingStatusBadgeProps> = ({ classN
 	})
 	})
 
 
 	useEffect(() => {
 	useEffect(() => {
-		// Request initial indexing status
+		// Request initial indexing status.
 		vscode.postMessage({ type: "requestIndexingStatus" })
 		vscode.postMessage({ type: "requestIndexingStatus" })
 
 
-		// Set up message listener for status updates
+		// Set up message listener for status updates.
 		const handleMessage = (event: MessageEvent<IndexingStatusUpdateMessage>) => {
 		const handleMessage = (event: MessageEvent<IndexingStatusUpdateMessage>) => {
 			if (event.data.type === "indexingStatusUpdate") {
 			if (event.data.type === "indexingStatusUpdate") {
 				const status = event.data.values
 				const status = event.data.values
@@ -43,11 +45,9 @@ export const IndexingStatusBadge: React.FC<IndexingStatusBadgeProps> = ({ classN
 
 
 		return () => {
 		return () => {
 			window.removeEventListener("message", handleMessage)
 			window.removeEventListener("message", handleMessage)
-			cleanup()
 		}
 		}
-	}, [cleanup, cwd])
+	}, [cwd])
 
 
-	// Calculate progress percentage with memoization
 	const progressPercentage = useMemo(
 	const progressPercentage = useMemo(
 		() =>
 		() =>
 			indexingStatus.totalItems > 0
 			indexingStatus.totalItems > 0
@@ -56,8 +56,7 @@ export const IndexingStatusBadge: React.FC<IndexingStatusBadgeProps> = ({ classN
 		[indexingStatus.processedItems, indexingStatus.totalItems],
 		[indexingStatus.processedItems, indexingStatus.totalItems],
 	)
 	)
 
 
-	// Get tooltip text with internationalization
-	const getTooltipText = () => {
+	const tooltipText = useMemo(() => {
 		switch (indexingStatus.systemStatus) {
 		switch (indexingStatus.systemStatus) {
 			case "Standby":
 			case "Standby":
 				return t("chat:indexingStatus.ready")
 				return t("chat:indexingStatus.ready")
@@ -70,93 +69,44 @@ export const IndexingStatusBadge: React.FC<IndexingStatusBadgeProps> = ({ classN
 			default:
 			default:
 				return t("chat:indexingStatus.status")
 				return t("chat:indexingStatus.status")
 		}
 		}
-	}
-
-	const handleMouseEnterButton = () => {
-		setIsHovered(true)
-		handleMouseEnter()
-	}
+	}, [indexingStatus.systemStatus, progressPercentage, t])
 
 
-	const handleMouseLeaveButton = () => {
-		setIsHovered(false)
-		handleMouseLeave()
-	}
-
-	// Get status color classes for the badge dot
-	const getStatusColorClass = () => {
+	const statusColorClass = useMemo(() => {
 		const statusColors = {
 		const statusColors = {
-			Standby: {
-				default: "bg-vscode-descriptionForeground/60",
-				hover: "bg-vscode-descriptionForeground/80",
-			},
-			Indexing: {
-				default: "bg-yellow-500 animate-pulse",
-				hover: "bg-yellow-500 animate-pulse",
-			},
-			Indexed: {
-				default: "bg-green-500",
-				hover: "bg-green-500",
-			},
-			Error: {
-				default: "bg-red-500",
-				hover: "bg-red-500",
-			},
+			Standby: "bg-vscode-descriptionForeground/60",
+			Indexing: "bg-yellow-500 animate-pulse",
+			Indexed: "bg-green-500",
+			Error: "bg-red-500",
 		}
 		}
 
 
-		const colors = statusColors[indexingStatus.systemStatus as keyof typeof statusColors] || statusColors.Standby
-		return isHovered ? colors.hover : colors.default
-	}
+		return statusColors[indexingStatus.systemStatus as keyof typeof statusColors] || statusColors.Standby
+	}, [indexingStatus.systemStatus])
 
 
 	return (
 	return (
-		<div className={cn("relative inline-block", className)}>
-			<CodeIndexPopover indexingStatus={indexingStatus}>
-				<button
-					onMouseEnter={handleMouseEnterButton}
-					onMouseLeave={handleMouseLeaveButton}
-					className={cn(
-						"relative inline-flex items-center justify-center",
-						"bg-transparent border-none p-1.5",
-						"rounded-md min-w-[28px] min-h-[28px]",
-						"opacity-85 text-vscode-foreground",
-						"transition-all duration-150",
-						"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
-						"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-						"active:bg-[rgba(255,255,255,0.1)]",
-						"cursor-pointer",
-						className,
-					)}
-					aria-label={getTooltipText()}>
-					{/* File search icon */}
-					<Database className="w-4 h-4 text-vscode-foreground" />
-
-					{/* Status dot badge */}
-					<span
-						className={cn(
-							"absolute top-1 right-1 w-1.5 h-1.5 rounded-full transition-colors duration-200",
-							getStatusColorClass(),
-						)}
-					/>
-				</button>
-			</CodeIndexPopover>
-			{showTooltip && (
-				<div
-					className={cn(
-						"absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2",
-						"px-2 py-1 text-xs font-medium text-vscode-foreground",
-						"bg-vscode-editor-background border border-vscode-panel-border",
-						"rounded shadow-lg whitespace-nowrap z-50",
-					)}
-					role="tooltip">
-					{getTooltipText()}
-					<div
+		<CodeIndexPopover indexingStatus={indexingStatus}>
+			<StandardTooltip content={tooltipText}>
+				<PopoverTrigger asChild>
+					<Button
+						variant="ghost"
+						size="sm"
+						aria-label={tooltipText}
 						className={cn(
 						className={cn(
-							"absolute top-full left-1/2 transform -translate-x-1/2",
-							"w-0 h-0 border-l-4 border-r-4 border-t-4",
-							"border-l-transparent border-r-transparent border-t-vscode-panel-border",
-						)}
-					/>
-				</div>
-			)}
-		</div>
+							"relative h-7 w-7 p-0",
+							"text-vscode-foreground opacity-85",
+							"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)]",
+							"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
+							className,
+						)}>
+						<Database className="w-4 h-4" />
+						<span
+							className={cn(
+								"absolute top-1 right-1 w-1.5 h-1.5 rounded-full transition-colors duration-200",
+								statusColorClass,
+							)}
+						/>
+					</Button>
+				</PopoverTrigger>
+			</StandardTooltip>
+		</CodeIndexPopover>
 	)
 	)
 }
 }

+ 65 - 65
webview-ui/src/components/chat/ModeSelector.tsx

@@ -1,26 +1,28 @@
 import React from "react"
 import React from "react"
+import { Fzf } from "fzf"
 import { ChevronUp, Check, X } from "lucide-react"
 import { ChevronUp, Check, X } from "lucide-react"
+
+import { type ModeConfig, type CustomModePrompts, TelemetryEventName } from "@roo-code/types"
+
+import { type Mode, getAllModes } from "@roo/modes"
+
+import { vscode } from "@/utils/vscode"
+import { telemetryClient } from "@/utils/TelemetryClient"
 import { cn } from "@/lib/utils"
 import { cn } from "@/lib/utils"
+import { useExtensionState } from "@/context/ExtensionStateContext"
+import { useAppTranslation } from "@/i18n/TranslationContext"
 import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
 import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
 import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
 import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
+
 import { IconButton } from "./IconButton"
 import { IconButton } from "./IconButton"
-import { vscode } from "@/utils/vscode"
-import { useExtensionState } from "@/context/ExtensionStateContext"
-import { useAppTranslation } from "@/i18n/TranslationContext"
-import { Mode, getAllModes } from "@roo/modes"
-import { ModeConfig, CustomModePrompts } from "@roo-code/types"
-import { telemetryClient } from "@/utils/TelemetryClient"
-import { TelemetryEventName } from "@roo-code/types"
-import { Fzf } from "fzf"
 
 
-// Minimum number of modes required to show search functionality
 const SEARCH_THRESHOLD = 6
 const SEARCH_THRESHOLD = 6
 
 
 interface ModeSelectorProps {
 interface ModeSelectorProps {
 	value: Mode
 	value: Mode
 	onChange: (value: Mode) => void
 	onChange: (value: Mode) => void
 	disabled?: boolean
 	disabled?: boolean
-	title?: string
+	title: string
 	triggerClassName?: string
 	triggerClassName?: string
 	modeShortcutText: string
 	modeShortcutText: string
 	customModes?: ModeConfig[]
 	customModes?: ModeConfig[]
@@ -32,7 +34,7 @@ export const ModeSelector = ({
 	value,
 	value,
 	onChange,
 	onChange,
 	disabled = false,
 	disabled = false,
-	title = "",
+	title,
 	triggerClassName = "",
 	triggerClassName = "",
 	modeShortcutText,
 	modeShortcutText,
 	customModes,
 	customModes,
@@ -47,29 +49,31 @@ export const ModeSelector = ({
 	const { t } = useAppTranslation()
 	const { t } = useAppTranslation()
 
 
 	const trackModeSelectorOpened = React.useCallback(() => {
 	const trackModeSelectorOpened = React.useCallback(() => {
-		// Track telemetry every time the mode selector is opened
+		// Track telemetry every time the mode selector is opened.
 		telemetryClient.capture(TelemetryEventName.MODE_SELECTOR_OPENED)
 		telemetryClient.capture(TelemetryEventName.MODE_SELECTOR_OPENED)
 
 
-		// Track first-time usage for UI purposes
+		// Track first-time usage for UI purposes.
 		if (!hasOpenedModeSelector) {
 		if (!hasOpenedModeSelector) {
 			setHasOpenedModeSelector(true)
 			setHasOpenedModeSelector(true)
 			vscode.postMessage({ type: "hasOpenedModeSelector", bool: true })
 			vscode.postMessage({ type: "hasOpenedModeSelector", bool: true })
 		}
 		}
 	}, [hasOpenedModeSelector, setHasOpenedModeSelector])
 	}, [hasOpenedModeSelector, setHasOpenedModeSelector])
 
 
-	// Get all modes including custom modes and merge custom prompt descriptions
+	// Get all modes including custom modes and merge custom prompt descriptions.
 	const modes = React.useMemo(() => {
 	const modes = React.useMemo(() => {
 		const allModes = getAllModes(customModes)
 		const allModes = getAllModes(customModes)
+
 		return allModes.map((mode) => ({
 		return allModes.map((mode) => ({
 			...mode,
 			...mode,
 			description: customModePrompts?.[mode.slug]?.description ?? mode.description,
 			description: customModePrompts?.[mode.slug]?.description ?? mode.description,
 		}))
 		}))
 	}, [customModes, customModePrompts])
 	}, [customModes, customModePrompts])
 
 
-	// Find the selected mode
+	// Find the selected mode.
 	const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value])
 	const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value])
 
 
-	// Memoize searchable items for fuzzy search with separate name and description search
+	// Memoize searchable items for fuzzy search with separate name and
+	// description search.
 	const nameSearchItems = React.useMemo(() => {
 	const nameSearchItems = React.useMemo(() => {
 		return modes.map((mode) => ({
 		return modes.map((mode) => ({
 			original: mode,
 			original: mode,
@@ -84,31 +88,29 @@ export const ModeSelector = ({
 		}))
 		}))
 	}, [modes])
 	}, [modes])
 
 
-	// Create memoized Fzf instances for name and description searches
-	const nameFzfInstance = React.useMemo(() => {
-		return new Fzf(nameSearchItems, {
-			selector: (item) => item.searchStr,
-		})
-	}, [nameSearchItems])
+	// Create memoized Fzf instances for name and description searches.
+	const nameFzfInstance = React.useMemo(
+		() => new Fzf(nameSearchItems, { selector: (item) => item.searchStr }),
+		[nameSearchItems],
+	)
 
 
-	const descriptionFzfInstance = React.useMemo(() => {
-		return new Fzf(descriptionSearchItems, {
-			selector: (item) => item.searchStr,
-		})
-	}, [descriptionSearchItems])
+	const descriptionFzfInstance = React.useMemo(
+		() => new Fzf(descriptionSearchItems, { selector: (item) => item.searchStr }),
+		[descriptionSearchItems],
+	)
 
 
-	// Filter modes based on search value using fuzzy search with priority
+	// Filter modes based on search value using fuzzy search with priority.
 	const filteredModes = React.useMemo(() => {
 	const filteredModes = React.useMemo(() => {
 		if (!searchValue) return modes
 		if (!searchValue) return modes
 
 
-		// First search in names/slugs
+		// First search in names/slugs.
 		const nameMatches = nameFzfInstance.find(searchValue)
 		const nameMatches = nameFzfInstance.find(searchValue)
 		const nameMatchedModes = new Set(nameMatches.map((result) => result.item.original.slug))
 		const nameMatchedModes = new Set(nameMatches.map((result) => result.item.original.slug))
 
 
-		// Then search in descriptions
+		// Then search in descriptions.
 		const descriptionMatches = descriptionFzfInstance.find(searchValue)
 		const descriptionMatches = descriptionFzfInstance.find(searchValue)
 
 
-		// Combine results: name matches first, then description matches
+		// Combine results: name matches first, then description matches.
 		const combinedResults = [
 		const combinedResults = [
 			...nameMatches.map((result) => result.item.original),
 			...nameMatches.map((result) => result.item.original),
 			...descriptionMatches
 			...descriptionMatches
@@ -128,7 +130,7 @@ export const ModeSelector = ({
 		(modeSlug: string) => {
 		(modeSlug: string) => {
 			onChange(modeSlug as Mode)
 			onChange(modeSlug as Mode)
 			setOpen(false)
 			setOpen(false)
-			// Clear search after selection
+			// Clear search after selection.
 			setSearchValue("")
 			setSearchValue("")
 		},
 		},
 		[onChange],
 		[onChange],
@@ -138,7 +140,8 @@ export const ModeSelector = ({
 		(isOpen: boolean) => {
 		(isOpen: boolean) => {
 			if (isOpen) trackModeSelectorOpened()
 			if (isOpen) trackModeSelectorOpened()
 			setOpen(isOpen)
 			setOpen(isOpen)
-			// Clear search when closing
+
+			// Clear search when closing.
 			if (!isOpen) {
 			if (!isOpen) {
 				setSearchValue("")
 				setSearchValue("")
 			}
 			}
@@ -146,44 +149,46 @@ export const ModeSelector = ({
 		[trackModeSelectorOpened],
 		[trackModeSelectorOpened],
 	)
 	)
 
 
-	// Auto-focus search input when popover opens
+	// Auto-focus search input when popover opens.
 	React.useEffect(() => {
 	React.useEffect(() => {
 		if (open && searchInputRef.current) {
 		if (open && searchInputRef.current) {
 			searchInputRef.current.focus()
 			searchInputRef.current.focus()
 		}
 		}
 	}, [open])
 	}, [open])
 
 
-	// Determine if search should be shown
+	// Determine if search should be shown.
 	const showSearch = !disableSearch && modes.length > SEARCH_THRESHOLD
 	const showSearch = !disableSearch && modes.length > SEARCH_THRESHOLD
 
 
-	// Combine instruction text for tooltip
+	// Combine instruction text for tooltip.
 	const instructionText = `${t("chat:modeSelector.description")} ${modeShortcutText}`
 	const instructionText = `${t("chat:modeSelector.description")} ${modeShortcutText}`
 
 
-	const trigger = (
-		<PopoverTrigger
-			disabled={disabled}
-			data-testid="mode-selector-trigger"
-			className={cn(
-				"inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
-				"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
-				"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
-				disabled
-					? "opacity-50 cursor-not-allowed"
-					: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
-				triggerClassName,
-				!disabled && !hasOpenedModeSelector
-					? "bg-primary opacity-90 hover:bg-primary-hover text-vscode-button-foreground"
-					: null,
-			)}>
-			<ChevronUp className="pointer-events-none opacity-80 flex-shrink-0 size-3" />
-			<span className="truncate">{selectedMode?.name || ""}</span>
-		</PopoverTrigger>
-	)
-
 	return (
 	return (
 		<Popover open={open} onOpenChange={onOpenChange} data-testid="mode-selector-root">
 		<Popover open={open} onOpenChange={onOpenChange} data-testid="mode-selector-root">
-			{title ? <StandardTooltip content={title}>{trigger}</StandardTooltip> : trigger}
-
+			<StandardTooltip content={title}>
+				<PopoverTrigger
+					disabled={disabled}
+					data-testid="mode-selector-trigger"
+					className={cn(
+						"inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
+						"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
+						"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
+						disabled
+							? "opacity-50 cursor-not-allowed"
+							: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
+						triggerClassName,
+						!disabled && !hasOpenedModeSelector
+							? "bg-primary opacity-90 hover:bg-primary-hover text-vscode-button-foreground"
+							: null,
+					)}>
+					<ChevronUp
+						className={cn(
+							"pointer-events-none opacity-80 flex-shrink-0 size-3 transition-transform duration-200",
+							open && "rotate-180",
+						)}
+					/>
+					<span className="truncate">{selectedMode?.name || ""}</span>
+				</PopoverTrigger>
+			</StandardTooltip>
 			<PopoverContent
 			<PopoverContent
 				align="start"
 				align="start"
 				sideOffset={4}
 				sideOffset={4}
@@ -274,10 +279,7 @@ export const ModeSelector = ({
 								iconClass="codicon-settings-gear"
 								iconClass="codicon-settings-gear"
 								title={t("chat:modeSelector.settings")}
 								title={t("chat:modeSelector.settings")}
 								onClick={() => {
 								onClick={() => {
-									vscode.postMessage({
-										type: "switchTab",
-										tab: "modes",
-									})
+									vscode.postMessage({ type: "switchTab", tab: "modes" })
 									setOpen(false)
 									setOpen(false)
 								}}
 								}}
 							/>
 							/>
@@ -300,5 +302,3 @@ export const ModeSelector = ({
 		</Popover>
 		</Popover>
 	)
 	)
 }
 }
-
-export default ModeSelector

+ 16 - 18
webview-ui/src/components/chat/SlashCommandsPopover.tsx

@@ -41,26 +41,24 @@ export const SlashCommandsPopover: React.FC<SlashCommandsPopoverProps> = ({ clas
 		}
 		}
 	}
 	}
 
 
-	const trigger = (
-		<PopoverTrigger asChild>
-			<Button
-				variant="ghost"
-				size="sm"
-				className={cn(
-					"h-7 w-7 p-0",
-					"text-vscode-foreground opacity-85",
-					"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)]",
-					"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-					className,
-				)}>
-				<Zap className="w-4 h-4" />
-			</Button>
-		</PopoverTrigger>
-	)
-
 	return (
 	return (
 		<Popover open={isOpen} onOpenChange={handleOpenChange}>
 		<Popover open={isOpen} onOpenChange={handleOpenChange}>
-			<StandardTooltip content={t("chat:slashCommands.tooltip")}>{trigger}</StandardTooltip>
+			<StandardTooltip content={t("chat:slashCommands.tooltip")}>
+				<PopoverTrigger asChild>
+					<Button
+						variant="ghost"
+						size="sm"
+						className={cn(
+							"h-7 w-7 p-0",
+							"text-vscode-foreground opacity-85",
+							"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)]",
+							"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
+							className,
+						)}>
+						<Zap className="w-4 h-4" />
+					</Button>
+				</PopoverTrigger>
+			</StandardTooltip>
 
 
 			<PopoverContent
 			<PopoverContent
 				align="start"
 				align="start"

+ 3 - 2
webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx

@@ -49,6 +49,7 @@ describe("ApiConfigSelector", () => {
 	const defaultProps = {
 	const defaultProps = {
 		value: "config1",
 		value: "config1",
 		displayName: "Config 1",
 		displayName: "Config 1",
+		title: "API Config",
 		onChange: mockOnChange,
 		onChange: mockOnChange,
 		listApiConfigMeta: [
 		listApiConfigMeta: [
 			{ id: "config1", name: "Config 1" },
 			{ id: "config1", name: "Config 1" },
@@ -75,8 +76,8 @@ describe("ApiConfigSelector", () => {
 		render(<ApiConfigSelector {...defaultProps} />)
 		render(<ApiConfigSelector {...defaultProps} />)
 
 
 		const trigger = screen.getByTestId("dropdown-trigger")
 		const trigger = screen.getByTestId("dropdown-trigger")
-		// Check for the icon by looking for the codicon span element
-		const icon = trigger.querySelector(".codicon-chevron-up")
+		// Check for the icon by looking for the svg element (ChevronUp from lucide-react renders as svg)
+		const icon = trigger.querySelector("svg")
 		expect(icon).toBeInTheDocument()
 		expect(icon).toBeInTheDocument()
 	})
 	})
 
 

+ 2 - 53
webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx

@@ -1,12 +1,11 @@
-import { render, fireEvent, screen } from "@/utils/test-utils"
-
 import { defaultModeSlug } from "@roo/modes"
 import { defaultModeSlug } from "@roo/modes"
 
 
+import { render, fireEvent, screen } from "@src/utils/test-utils"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
 import { useExtensionState } from "@src/context/ExtensionStateContext"
 import { vscode } from "@src/utils/vscode"
 import { vscode } from "@src/utils/vscode"
 import * as pathMentions from "@src/utils/path-mentions"
 import * as pathMentions from "@src/utils/path-mentions"
 
 
-import ChatTextArea from "../ChatTextArea"
+import { ChatTextArea } from "../ChatTextArea"
 
 
 vi.mock("@src/utils/vscode", () => ({
 vi.mock("@src/utils/vscode", () => ({
 	vscode: {
 	vscode: {
@@ -1058,54 +1057,4 @@ describe("ChatTextArea", () => {
 			expect(apiConfigDropdown).toHaveAttribute("disabled")
 			expect(apiConfigDropdown).toHaveAttribute("disabled")
 		})
 		})
 	})
 	})
-	describe("edit mode integration", () => {
-		it("should render edit mode UI when isEditMode is true", () => {
-			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
-				filePaths: [],
-				openedTabs: [],
-				taskHistory: [],
-				cwd: "/test/workspace",
-				customModes: [],
-				customModePrompts: {},
-			})
-
-			render(<ChatTextArea {...defaultProps} isEditMode={true} />)
-
-			// The edit mode UI should be rendered
-			// We can verify this by checking for the presence of elements that are unique to edit mode
-			const cancelButton = screen.getByRole("button", { name: /cancel/i })
-			expect(cancelButton).toBeInTheDocument()
-
-			// Should show save button instead of send button
-			const saveButton = screen.getByRole("button", { name: /save/i })
-			expect(saveButton).toBeInTheDocument()
-
-			// Should not show send button in edit mode
-			const sendButton = screen.queryByRole("button", { name: /send.*message/i })
-			expect(sendButton).not.toBeInTheDocument()
-		})
-
-		it("should not render edit mode UI when isEditMode is false", () => {
-			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
-				filePaths: [],
-				openedTabs: [],
-				taskHistory: [],
-				cwd: "/test/workspace",
-			})
-
-			render(<ChatTextArea {...defaultProps} isEditMode={false} />)
-
-			// The edit mode UI should not be rendered
-			const cancelButton = screen.queryByRole("button", { name: /cancel/i })
-			expect(cancelButton).not.toBeInTheDocument()
-
-			// Should show send button when not in edit mode
-			const sendButton = screen.getByRole("button", { name: /send.*message/i })
-			expect(sendButton).toBeInTheDocument()
-
-			// Should not show save button when not in edit mode
-			const saveButton = screen.queryByRole("button", { name: /save/i })
-			expect(saveButton).not.toBeInTheDocument()
-		})
-	})
 })
 })

+ 12 - 9
webview-ui/src/components/chat/__tests__/ChatView.keyboard-fix.spec.tsx

@@ -73,16 +73,19 @@ vi.mock("react-i18next", () => ({
 }))
 }))
 
 
 vi.mock("../ChatTextArea", () => {
 vi.mock("../ChatTextArea", () => {
+	const ChatTextAreaComponent = React.forwardRef(function MockChatTextArea(
+		_props: any,
+		ref: React.ForwardedRef<{ focus: () => void }>,
+	) {
+		React.useImperativeHandle(ref, () => ({
+			focus: vi.fn(),
+		}))
+		return <div data-testid="chat-textarea" />
+	})
+
 	return {
 	return {
-		default: React.forwardRef(function MockChatTextArea(
-			_props: any,
-			ref: React.ForwardedRef<{ focus: () => void }>,
-		) {
-			React.useImperativeHandle(ref, () => ({
-				focus: vi.fn(),
-			}))
-			return <div data-testid="chat-textarea" />
-		}),
+		default: ChatTextAreaComponent,
+		ChatTextArea: ChatTextAreaComponent, // Export as named export too
 	}
 	}
 })
 })
 
 

+ 26 - 23
webview-ui/src/components/chat/__tests__/ChatView.spec.tsx

@@ -181,30 +181,33 @@ vi.mock("../ChatTextArea", () => {
 	// eslint-disable-next-line @typescript-eslint/no-require-imports
 	// eslint-disable-next-line @typescript-eslint/no-require-imports
 	const mockReact = require("react")
 	const mockReact = require("react")
 
 
+	const ChatTextAreaComponent = mockReact.forwardRef(function MockChatTextArea(
+		props: ChatTextAreaProps,
+		ref: React.ForwardedRef<{ focus: () => void }>,
+	) {
+		// Use useImperativeHandle to expose the mock focus method
+		mockReact.useImperativeHandle(ref, () => ({
+			focus: mockFocus,
+		}))
+
+		return (
+			<div data-testid="chat-textarea">
+				<input
+					ref={mockInputRef}
+					type="text"
+					onChange={(e) => {
+						// With message queueing, onSend is always called (it handles queueing internally)
+						props.onSend(e.target.value)
+					}}
+					data-sending-disabled={props.sendingDisabled}
+				/>
+			</div>
+		)
+	})
+
 	return {
 	return {
-		default: mockReact.forwardRef(function MockChatTextArea(
-			props: ChatTextAreaProps,
-			ref: React.ForwardedRef<{ focus: () => void }>,
-		) {
-			// Use useImperativeHandle to expose the mock focus method
-			React.useImperativeHandle(ref, () => ({
-				focus: mockFocus,
-			}))
-
-			return (
-				<div data-testid="chat-textarea">
-					<input
-						ref={mockInputRef}
-						type="text"
-						onChange={(e) => {
-							// With message queueing, onSend is always called (it handles queueing internally)
-							props.onSend(e.target.value)
-						}}
-						data-sending-disabled={props.sendingDisabled}
-					/>
-				</div>
-			)
-		}),
+		default: ChatTextAreaComponent,
+		ChatTextArea: ChatTextAreaComponent, // Export as named export too
 	}
 	}
 })
 })
 
 

+ 0 - 138
webview-ui/src/components/chat/__tests__/EditModeControls.spec.tsx

@@ -1,138 +0,0 @@
-import React from "react"
-import { render, screen, fireEvent } from "@testing-library/react"
-import { describe, it, expect, vi, beforeEach } from "vitest"
-import { EditModeControls } from "../EditModeControls"
-import { Mode } from "@roo/modes"
-
-// Mock the translation hook
-vi.mock("@/i18n/TranslationContext", () => ({
-	useAppTranslation: () => ({
-		t: (key: string) => key,
-	}),
-}))
-
-// Mock the UI components
-vi.mock("@/components/ui", () => ({
-	Button: ({ children, onClick, disabled, ...props }: any) => (
-		<button onClick={onClick} disabled={disabled} {...props}>
-			{children}
-		</button>
-	),
-	StandardTooltip: ({ children, content }: any) => <div title={content}>{children}</div>,
-}))
-
-// Mock ModeSelector
-vi.mock("../ModeSelector", () => ({
-	default: ({ value, onChange, title }: any) => (
-		<select value={value} onChange={(e) => onChange(e.target.value)} title={title}>
-			<option value="code">Code</option>
-			<option value="architect">Architect</option>
-		</select>
-	),
-}))
-
-describe("EditModeControls", () => {
-	const defaultProps = {
-		mode: "code" as Mode,
-		onModeChange: vi.fn(),
-		modeShortcutText: "Ctrl+M",
-		customModes: [],
-		customModePrompts: {},
-		onCancel: vi.fn(),
-		onSend: vi.fn(),
-		onSelectImages: vi.fn(),
-		sendingDisabled: false,
-		shouldDisableImages: false,
-	}
-
-	beforeEach(() => {
-		vi.clearAllMocks()
-	})
-
-	it("renders all controls correctly", () => {
-		render(<EditModeControls {...defaultProps} />)
-
-		// Check for mode selector
-		expect(screen.getByTitle("chat:selectMode")).toBeInTheDocument()
-
-		// Check for Cancel button
-		expect(screen.getByText("Cancel")).toBeInTheDocument()
-
-		// Check for image button
-		expect(screen.getByTitle("chat:addImages")).toBeInTheDocument()
-
-		// Check for send button
-		expect(screen.getByTitle("chat:save.tooltip")).toBeInTheDocument()
-	})
-
-	it("calls onCancel when Cancel button is clicked", () => {
-		render(<EditModeControls {...defaultProps} />)
-
-		const cancelButton = screen.getByText("Cancel")
-		fireEvent.click(cancelButton)
-
-		expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)
-	})
-
-	it("calls onSend when send button is clicked", () => {
-		render(<EditModeControls {...defaultProps} />)
-
-		const sendButton = screen.getByLabelText("chat:save.tooltip")
-		fireEvent.click(sendButton)
-
-		expect(defaultProps.onSend).toHaveBeenCalledTimes(1)
-	})
-
-	it("calls onSelectImages when image button is clicked", () => {
-		render(<EditModeControls {...defaultProps} />)
-
-		const imageButton = screen.getByLabelText("chat:addImages")
-		fireEvent.click(imageButton)
-
-		expect(defaultProps.onSelectImages).toHaveBeenCalledTimes(1)
-	})
-
-	it("disables buttons when sendingDisabled is true", () => {
-		render(<EditModeControls {...defaultProps} sendingDisabled={true} />)
-
-		const cancelButton = screen.getByText("Cancel")
-		const sendButton = screen.getByLabelText("chat:save.tooltip")
-
-		expect(cancelButton).toBeDisabled()
-		expect(sendButton).toBeDisabled()
-	})
-
-	it("disables image button when shouldDisableImages is true", () => {
-		render(<EditModeControls {...defaultProps} shouldDisableImages={true} />)
-
-		const imageButton = screen.getByLabelText("chat:addImages")
-		expect(imageButton).toBeDisabled()
-	})
-
-	it("does not call onSelectImages when image button is disabled", () => {
-		render(<EditModeControls {...defaultProps} shouldDisableImages={true} />)
-
-		const imageButton = screen.getByLabelText("chat:addImages")
-		fireEvent.click(imageButton)
-
-		expect(defaultProps.onSelectImages).not.toHaveBeenCalled()
-	})
-
-	it("does not call onSend when send button is disabled", () => {
-		render(<EditModeControls {...defaultProps} sendingDisabled={true} />)
-
-		const sendButton = screen.getByLabelText("chat:save.tooltip")
-		fireEvent.click(sendButton)
-
-		expect(defaultProps.onSend).not.toHaveBeenCalled()
-	})
-
-	it("calls onModeChange when mode is changed", () => {
-		render(<EditModeControls {...defaultProps} />)
-
-		const modeSelector = screen.getByTitle("chat:selectMode")
-		fireEvent.change(modeSelector, { target: { value: "architect" } })
-
-		expect(defaultProps.onModeChange).toHaveBeenCalledWith("architect")
-	})
-})

+ 0 - 10
webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx

@@ -55,16 +55,6 @@ vi.mock("@src/utils/vscode", () => ({
 	},
 	},
 }))
 }))
 
 
-// Mock the useTooltip hook
-vi.mock("@/hooks/useTooltip", () => ({
-	useTooltip: vi.fn(() => ({
-		showTooltip: false,
-		handleMouseEnter: vi.fn(),
-		handleMouseLeave: vi.fn(),
-		cleanup: vi.fn(),
-	})),
-}))
-
 // Mock the ExtensionStateContext
 // Mock the ExtensionStateContext
 vi.mock("@/context/ExtensionStateContext", () => ({
 vi.mock("@/context/ExtensionStateContext", () => ({
 	useExtensionState: () => ({
 	useExtensionState: () => ({

+ 66 - 39
webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx

@@ -1,11 +1,11 @@
-import React from "react"
 import { render, screen, fireEvent } from "@/utils/test-utils"
 import { render, screen, fireEvent } from "@/utils/test-utils"
-import { describe, test, expect, vi } from "vitest"
-import ModeSelector from "../ModeSelector"
-import { Mode } from "@roo/modes"
-import { ModeConfig } from "@roo-code/types"
 
 
-// Mock the dependencies
+import type { ModeConfig } from "@roo-code/types"
+
+import type { Mode } from "@roo/modes"
+
+import { ModeSelector } from "../ModeSelector"
+
 vi.mock("@/utils/vscode", () => ({
 vi.mock("@/utils/vscode", () => ({
 	vscode: {
 	vscode: {
 		postMessage: vi.fn(),
 		postMessage: vi.fn(),
@@ -35,7 +35,7 @@ vi.mock("@/utils/TelemetryClient", () => ({
 	},
 	},
 }))
 }))
 
 
-// Create a variable to control what getAllModes returns
+// Create a variable to control what getAllModes returns.
 let mockModes: ModeConfig[] = []
 let mockModes: ModeConfig[] = []
 
 
 vi.mock("@roo/modes", async () => {
 vi.mock("@roo/modes", async () => {
@@ -56,6 +56,7 @@ describe("ModeSelector", () => {
 
 
 		render(
 		render(
 			<ModeSelector
 			<ModeSelector
+				title="Mode Selector"
 				value={"code" as Mode}
 				value={"code" as Mode}
 				onChange={vi.fn()}
 				onChange={vi.fn()}
 				modeShortcutText="Ctrl+M"
 				modeShortcutText="Ctrl+M"
@@ -63,19 +64,18 @@ describe("ModeSelector", () => {
 			/>,
 			/>,
 		)
 		)
 
 
-		// The component should be rendered
 		expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument()
 		expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument()
 	})
 	})
 
 
 	test("falls back to default description when no custom prompt", () => {
 	test("falls back to default description when no custom prompt", () => {
-		render(<ModeSelector value={"code" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />)
+		render(
+			<ModeSelector title="Mode Selector" value={"code" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />,
+		)
 
 
-		// The component should be rendered
 		expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument()
 		expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument()
 	})
 	})
 
 
 	test("shows search bar when there are more than 6 modes", () => {
 	test("shows search bar when there are more than 6 modes", () => {
-		// Set up mock to return 7 modes
 		mockModes = Array.from({ length: 7 }, (_, i) => ({
 		mockModes = Array.from({ length: 7 }, (_, i) => ({
 			slug: `mode-${i}`,
 			slug: `mode-${i}`,
 			name: `Mode ${i}`,
 			name: `Mode ${i}`,
@@ -84,22 +84,28 @@ describe("ModeSelector", () => {
 			groups: ["read", "edit"],
 			groups: ["read", "edit"],
 		}))
 		}))
 
 
-		render(<ModeSelector value={"mode-0" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />)
+		render(
+			<ModeSelector
+				title="Mode Selector"
+				value={"mode-0" as Mode}
+				onChange={vi.fn()}
+				modeShortcutText="Ctrl+M"
+			/>,
+		)
 
 
-		// Click to open the popover
+		// Click to open the popover.
 		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
 		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
 
 
-		// Search input should be visible
+		// Search input should be visible.
 		expect(screen.getByTestId("mode-search-input")).toBeInTheDocument()
 		expect(screen.getByTestId("mode-search-input")).toBeInTheDocument()
 
 
-		// Info icon should be visible
+		// Info icon should be visible.
 		expect(screen.getByText("chat:modeSelector.title")).toBeInTheDocument()
 		expect(screen.getByText("chat:modeSelector.title")).toBeInTheDocument()
 		const infoIcon = document.querySelector(".codicon-info")
 		const infoIcon = document.querySelector(".codicon-info")
 		expect(infoIcon).toBeInTheDocument()
 		expect(infoIcon).toBeInTheDocument()
 	})
 	})
 
 
 	test("shows info blurb instead of search bar when there are 6 or fewer modes", () => {
 	test("shows info blurb instead of search bar when there are 6 or fewer modes", () => {
-		// Set up mock to return 5 modes
 		mockModes = Array.from({ length: 5 }, (_, i) => ({
 		mockModes = Array.from({ length: 5 }, (_, i) => ({
 			slug: `mode-${i}`,
 			slug: `mode-${i}`,
 			name: `Mode ${i}`,
 			name: `Mode ${i}`,
@@ -108,24 +114,30 @@ describe("ModeSelector", () => {
 			groups: ["read", "edit"],
 			groups: ["read", "edit"],
 		}))
 		}))
 
 
-		render(<ModeSelector value={"mode-0" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />)
+		render(
+			<ModeSelector
+				title="Mode Selector"
+				value={"mode-0" as Mode}
+				onChange={vi.fn()}
+				modeShortcutText="Ctrl+M"
+			/>,
+		)
 
 
-		// Click to open the popover
+		// Click to open the popover.
 		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
 		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
 
 
-		// Search input should NOT be visible
+		// Search input should NOT be visible.
 		expect(screen.queryByTestId("mode-search-input")).not.toBeInTheDocument()
 		expect(screen.queryByTestId("mode-search-input")).not.toBeInTheDocument()
 
 
-		// Info blurb should be visible
+		// Info blurb should be visible.
 		expect(screen.getByText(/chat:modeSelector.description/)).toBeInTheDocument()
 		expect(screen.getByText(/chat:modeSelector.description/)).toBeInTheDocument()
 
 
-		// Info icon should NOT be visible
+		// Info icon should NOT be visible.
 		const infoIcon = document.querySelector(".codicon-info")
 		const infoIcon = document.querySelector(".codicon-info")
 		expect(infoIcon).not.toBeInTheDocument()
 		expect(infoIcon).not.toBeInTheDocument()
 	})
 	})
 
 
 	test("filters modes correctly when searching", () => {
 	test("filters modes correctly when searching", () => {
-		// Set up mock to return 7 modes to enable search
 		mockModes = Array.from({ length: 7 }, (_, i) => ({
 		mockModes = Array.from({ length: 7 }, (_, i) => ({
 			slug: `mode-${i}`,
 			slug: `mode-${i}`,
 			name: `Mode ${i}`,
 			name: `Mode ${i}`,
@@ -134,22 +146,28 @@ describe("ModeSelector", () => {
 			groups: ["read", "edit"],
 			groups: ["read", "edit"],
 		}))
 		}))
 
 
-		render(<ModeSelector value={"mode-0" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />)
+		render(
+			<ModeSelector
+				title="Mode Selector"
+				value={"mode-0" as Mode}
+				onChange={vi.fn()}
+				modeShortcutText="Ctrl+M"
+			/>,
+		)
 
 
-		// Click to open the popover
+		// Click to open the popover.
 		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
 		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
 
 
-		// Type in search
+		// Type in search.
 		const searchInput = screen.getByTestId("mode-search-input")
 		const searchInput = screen.getByTestId("mode-search-input")
 		fireEvent.change(searchInput, { target: { value: "Mode 3" } })
 		fireEvent.change(searchInput, { target: { value: "Mode 3" } })
 
 
-		// Should show filtered results
+		// Should show filtered results.
 		const modeItems = screen.getAllByTestId("mode-selector-item")
 		const modeItems = screen.getAllByTestId("mode-selector-item")
-		expect(modeItems.length).toBeLessThan(7) // Should have filtered some out
+		expect(modeItems.length).toBeLessThan(7) // Should have filtered some out.
 	})
 	})
 
 
 	test("respects disableSearch prop even when there are more than 6 modes", () => {
 	test("respects disableSearch prop even when there are more than 6 modes", () => {
-		// Set up mock to return 10 modes
 		mockModes = Array.from({ length: 10 }, (_, i) => ({
 		mockModes = Array.from({ length: 10 }, (_, i) => ({
 			slug: `mode-${i}`,
 			slug: `mode-${i}`,
 			name: `Mode ${i}`,
 			name: `Mode ${i}`,
@@ -159,25 +177,30 @@ describe("ModeSelector", () => {
 		}))
 		}))
 
 
 		render(
 		render(
-			<ModeSelector value={"mode-0" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" disableSearch={true} />,
+			<ModeSelector
+				title="Mode Selector"
+				value={"mode-0" as Mode}
+				onChange={vi.fn()}
+				modeShortcutText="Ctrl+M"
+				disableSearch={true}
+			/>,
 		)
 		)
 
 
-		// Click to open the popover
+		// Click to open the popover.
 		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
 		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
 
 
-		// Search input should NOT be visible even with 10 modes
+		// Search input should NOT be visible even with 10 modes.
 		expect(screen.queryByTestId("mode-search-input")).not.toBeInTheDocument()
 		expect(screen.queryByTestId("mode-search-input")).not.toBeInTheDocument()
 
 
-		// Info blurb should be visible instead
+		// Info blurb should be visible instead.
 		expect(screen.getByText(/chat:modeSelector.description/)).toBeInTheDocument()
 		expect(screen.getByText(/chat:modeSelector.description/)).toBeInTheDocument()
 
 
-		// Info icon should NOT be visible
+		// Info icon should NOT be visible.
 		const infoIcon = document.querySelector(".codicon-info")
 		const infoIcon = document.querySelector(".codicon-info")
 		expect(infoIcon).not.toBeInTheDocument()
 		expect(infoIcon).not.toBeInTheDocument()
 	})
 	})
 
 
 	test("shows search when disableSearch is false (default) and modes > 6", () => {
 	test("shows search when disableSearch is false (default) and modes > 6", () => {
-		// Set up mock to return 8 modes
 		mockModes = Array.from({ length: 8 }, (_, i) => ({
 		mockModes = Array.from({ length: 8 }, (_, i) => ({
 			slug: `mode-${i}`,
 			slug: `mode-${i}`,
 			name: `Mode ${i}`,
 			name: `Mode ${i}`,
@@ -186,16 +209,20 @@ describe("ModeSelector", () => {
 			groups: ["read", "edit"],
 			groups: ["read", "edit"],
 		}))
 		}))
 
 
-		// Don't pass disableSearch prop (should default to false)
-		render(<ModeSelector value={"mode-0" as Mode} onChange={vi.fn()} modeShortcutText="Ctrl+M" />)
+		// Don't pass disableSearch prop (should default to false).
+		render(
+			<ModeSelector
+				title="Mode Selector"
+				value={"mode-0" as Mode}
+				onChange={vi.fn()}
+				modeShortcutText="Ctrl+M"
+			/>,
+		)
 
 
-		// Click to open the popover
 		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
 		fireEvent.click(screen.getByTestId("mode-selector-trigger"))
 
 
-		// Search input should be visible
 		expect(screen.getByTestId("mode-search-input")).toBeInTheDocument()
 		expect(screen.getByTestId("mode-search-input")).toBeInTheDocument()
 
 
-		// Info icon should be visible
 		const infoIcon = document.querySelector(".codicon-info")
 		const infoIcon = document.querySelector(".codicon-info")
 		expect(infoIcon).toBeInTheDocument()
 		expect(infoIcon).toBeInTheDocument()
 	})
 	})

+ 1 - 1
webview-ui/src/components/common/TelemetryBanner.tsx

@@ -3,7 +3,7 @@ import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
 import styled from "styled-components"
 import styled from "styled-components"
 import { Trans } from "react-i18next"
 import { Trans } from "react-i18next"
 
 
-import { TelemetrySetting } from "@roo/TelemetrySetting"
+import type { TelemetrySetting } from "@roo-code/types"
 
 
 import { vscode } from "@src/utils/vscode"
 import { vscode } from "@src/utils/vscode"
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { useAppTranslation } from "@src/i18n/TranslationContext"

+ 2 - 2
webview-ui/src/components/settings/About.tsx

@@ -2,11 +2,11 @@ import { HTMLAttributes } from "react"
 import { useAppTranslation } from "@/i18n/TranslationContext"
 import { useAppTranslation } from "@/i18n/TranslationContext"
 import { Trans } from "react-i18next"
 import { Trans } from "react-i18next"
 import { Info, Download, Upload, TriangleAlert } from "lucide-react"
 import { Info, Download, Upload, TriangleAlert } from "lucide-react"
-
 import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
 import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
 
 
+import type { TelemetrySetting } from "@roo-code/types"
+
 import { Package } from "@roo/package"
 import { Package } from "@roo/package"
-import { TelemetrySetting } from "@roo/TelemetrySetting"
 
 
 import { vscode } from "@/utils/vscode"
 import { vscode } from "@/utils/vscode"
 import { cn } from "@/lib/utils"
 import { cn } from "@/lib/utils"

+ 2 - 4
webview-ui/src/components/settings/SettingsView.tsx

@@ -25,11 +25,10 @@ import {
 	LucideIcon,
 	LucideIcon,
 } from "lucide-react"
 } from "lucide-react"
 
 
-import type { ProviderSettings, ExperimentId } from "@roo-code/types"
-
-import { TelemetrySetting } from "@roo/TelemetrySetting"
+import type { ProviderSettings, ExperimentId, TelemetrySetting } from "@roo-code/types"
 
 
 import { vscode } from "@src/utils/vscode"
 import { vscode } from "@src/utils/vscode"
+import { cn } from "@src/lib/utils"
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { useAppTranslation } from "@src/i18n/TranslationContext"
 import { ExtensionStateContextType, useExtensionState } from "@src/context/ExtensionStateContext"
 import { ExtensionStateContextType, useExtensionState } from "@src/context/ExtensionStateContext"
 import {
 import {
@@ -65,7 +64,6 @@ import { LanguageSettings } from "./LanguageSettings"
 import { About } from "./About"
 import { About } from "./About"
 import { Section } from "./Section"
 import { Section } from "./Section"
 import PromptsSettings from "./PromptsSettings"
 import PromptsSettings from "./PromptsSettings"
-import { cn } from "@/lib/utils"
 
 
 export const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden"
 export const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden"
 export const settingsTabList =
 export const settingsTabList =

+ 5 - 4
webview-ui/src/components/ui/standard-tooltip.tsx

@@ -1,13 +1,14 @@
-import * as React from "react"
+import { ReactNode } from "react"
+
 import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
 import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
 
 
 export const STANDARD_TOOLTIP_DELAY = 300
 export const STANDARD_TOOLTIP_DELAY = 300
 
 
 interface StandardTooltipProps {
 interface StandardTooltipProps {
 	/** The element(s) that trigger the tooltip */
 	/** The element(s) that trigger the tooltip */
-	children: React.ReactNode
+	children: ReactNode
 	/** The content to display in the tooltip */
 	/** The content to display in the tooltip */
-	content: React.ReactNode
+	content: ReactNode
 	/** The preferred side of the trigger to render the tooltip */
 	/** The preferred side of the trigger to render the tooltip */
 	side?: "top" | "right" | "bottom" | "left"
 	side?: "top" | "right" | "bottom" | "left"
 	/** The preferred alignment against the trigger */
 	/** The preferred alignment against the trigger */
@@ -51,7 +52,7 @@ export function StandardTooltip({
 	asChild = true,
 	asChild = true,
 	maxWidth,
 	maxWidth,
 }: StandardTooltipProps) {
 }: StandardTooltipProps) {
-	// Don't render tooltip if content is empty or only whitespace
+	// Don't render tooltip if content is empty or only whitespace.
 	if (!content || (typeof content === "string" && !content.trim())) {
 	if (!content || (typeof content === "string" && !content.trim())) {
 		return <>{children}</>
 		return <>{children}</>
 	}
 	}

+ 34 - 23
webview-ui/src/components/ui/tooltip.tsx

@@ -1,33 +1,44 @@
+"use client"
+
 import * as React from "react"
 import * as React from "react"
 import * as TooltipPrimitive from "@radix-ui/react-tooltip"
 import * as TooltipPrimitive from "@radix-ui/react-tooltip"
 
 
 import { cn } from "@/lib/utils"
 import { cn } from "@/lib/utils"
 
 
-const TooltipProvider = TooltipPrimitive.Provider
+function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
+	return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />
+}
 
 
-const Tooltip = TooltipPrimitive.Root
+function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
+	return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
+}
 
 
-const TooltipTrigger = TooltipPrimitive.Trigger
+function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
+	return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
+}
 
 
-const TooltipContent = React.forwardRef<
-	React.ElementRef<typeof TooltipPrimitive.Content>,
-	React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
->(({ className, sideOffset = 4, ...props }, ref) => (
-	<TooltipPrimitive.Portal>
-		<TooltipPrimitive.Content
-			ref={ref}
-			sideOffset={sideOffset}
-			collisionPadding={10}
-			avoidCollisions={true}
-			className={cn(
-				"z-50 rounded-xs bg-vscode-notifications-background border border-vscode-notifications-border px-3 py-1.5 text-xs text-vscode-notifications-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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",
-				"max-w-[300px] break-words",
-				className,
-			)}
-			{...props}
-		/>
-	</TooltipPrimitive.Portal>
-))
-TooltipContent.displayName = TooltipPrimitive.Content.displayName
+function TooltipContent({
+	className,
+	sideOffset = 0,
+	children,
+	...props
+}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
+	return (
+		<TooltipPrimitive.Portal>
+			<TooltipPrimitive.Content
+				data-slot="tooltip-content"
+				sideOffset={sideOffset}
+				className={cn(
+					"bg-vscode-editorHoverWidget-background outline outline-vscode-editorHoverWidget-border text-vscode-editorHoverWidget-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-sm px-2 py-1 text-xs text-balance",
+					"max-w-[300px] break-words",
+					className,
+				)}
+				{...props}>
+				{children}
+				<TooltipPrimitive.Arrow className="bg-vscode-editorHoverWidget-background border-b border-r border-vscode-editorHoverWidget-border fill-vscode-editorHoverWidget-background z-50 size-1.5 translate-y-[calc(-50%_+_1px)] rotate-45" />
+			</TooltipPrimitive.Content>
+		</TooltipPrimitive.Portal>
+	)
+}
 
 
 export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
 export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

+ 1 - 2
webview-ui/src/context/ExtensionStateContext.tsx

@@ -7,10 +7,10 @@ import {
 	type ModeConfig,
 	type ModeConfig,
 	type ExperimentId,
 	type ExperimentId,
 	type TodoItem,
 	type TodoItem,
+	type TelemetrySetting,
 } from "@roo-code/types"
 } from "@roo-code/types"
 
 
 import { type OrganizationAllowList, ORGANIZATION_ALLOW_ALL } from "@roo/cloud"
 import { type OrganizationAllowList, ORGANIZATION_ALLOW_ALL } from "@roo/cloud"
-
 import { ExtensionMessage, ExtensionState, MarketplaceInstalledMetadata, Command } from "@roo/ExtensionMessage"
 import { ExtensionMessage, ExtensionState, MarketplaceInstalledMetadata, Command } from "@roo/ExtensionMessage"
 import { findLastIndex } from "@roo/array"
 import { findLastIndex } from "@roo/array"
 import { McpServer } from "@roo/mcp"
 import { McpServer } from "@roo/mcp"
@@ -18,7 +18,6 @@ import { checkExistKey } from "@roo/checkExistApiConfig"
 import { Mode, defaultModeSlug, defaultPrompts } from "@roo/modes"
 import { Mode, defaultModeSlug, defaultPrompts } from "@roo/modes"
 import { CustomSupportPrompts } from "@roo/support-prompt"
 import { CustomSupportPrompts } from "@roo/support-prompt"
 import { experimentDefault } from "@roo/experiments"
 import { experimentDefault } from "@roo/experiments"
-import { TelemetrySetting } from "@roo/TelemetrySetting"
 import { RouterModels } from "@roo/api"
 import { RouterModels } from "@roo/api"
 
 
 import { vscode } from "@src/utils/vscode"
 import { vscode } from "@src/utils/vscode"

+ 0 - 39
webview-ui/src/hooks/useTooltip.ts

@@ -1,39 +0,0 @@
-import { useState, useCallback, useRef } from "react"
-
-interface UseTooltipOptions {
-	delay?: number
-}
-
-export const useTooltip = (options: UseTooltipOptions = {}) => {
-	const { delay = 300 } = options
-	const [showTooltip, setShowTooltip] = useState(false)
-	const timeoutRef = useRef<NodeJS.Timeout | null>(null)
-
-	const handleMouseEnter = useCallback(() => {
-		if (timeoutRef.current) clearTimeout(timeoutRef.current)
-		timeoutRef.current = setTimeout(() => setShowTooltip(true), delay)
-	}, [delay])
-
-	const handleMouseLeave = useCallback(() => {
-		if (timeoutRef.current) {
-			clearTimeout(timeoutRef.current)
-			timeoutRef.current = null
-		}
-		setShowTooltip(false)
-	}, [])
-
-	// Cleanup on unmount
-	const cleanup = useCallback(() => {
-		if (timeoutRef.current) {
-			clearTimeout(timeoutRef.current)
-			timeoutRef.current = null
-		}
-	}, [])
-
-	return {
-		showTooltip,
-		handleMouseEnter,
-		handleMouseLeave,
-		cleanup,
-	}
-}

+ 8 - 1
webview-ui/src/index.css

@@ -76,6 +76,7 @@
 	--color-vscode-button-background: var(--vscode-button-background);
 	--color-vscode-button-background: var(--vscode-button-background);
 	--color-vscode-button-secondaryForeground: var(--vscode-button-secondaryForeground);
 	--color-vscode-button-secondaryForeground: var(--vscode-button-secondaryForeground);
 	--color-vscode-button-secondaryBackground: var(--vscode-button-secondaryBackground);
 	--color-vscode-button-secondaryBackground: var(--vscode-button-secondaryBackground);
+	--color-vscode-button-hoverBackground: var(--vscode-button-hoverBackground);
 
 
 	--color-vscode-dropdown-foreground: var(--vscode-dropdown-foreground);
 	--color-vscode-dropdown-foreground: var(--vscode-dropdown-foreground);
 	--color-vscode-dropdown-background: var(--vscode-dropdown-background);
 	--color-vscode-dropdown-background: var(--vscode-dropdown-background);
@@ -107,6 +108,7 @@
 	--color-vscode-list-activeSelectionForeground: var(--vscode-list-activeSelectionForeground);
 	--color-vscode-list-activeSelectionForeground: var(--vscode-list-activeSelectionForeground);
 
 
 	--color-vscode-toolbar-hoverBackground: var(--vscode-toolbar-hoverBackground);
 	--color-vscode-toolbar-hoverBackground: var(--vscode-toolbar-hoverBackground);
+	--color-vscode-toolbar-hoverOutline: var(--vscode-toolbar-hoverOutline);
 
 
 	--color-vscode-panel-border: var(--vscode-panel-border);
 	--color-vscode-panel-border: var(--vscode-panel-border);
 
 
@@ -129,9 +131,14 @@
 	--color-vscode-inputValidation-infoBorder: var(--vscode-inputValidation-infoBorder);
 	--color-vscode-inputValidation-infoBorder: var(--vscode-inputValidation-infoBorder);
 
 
 	--color-vscode-widget-border: var(--vscode-widget-border);
 	--color-vscode-widget-border: var(--vscode-widget-border);
+
 	--color-vscode-textLink-foreground: var(--vscode-textLink-foreground);
 	--color-vscode-textLink-foreground: var(--vscode-textLink-foreground);
+
 	--color-vscode-textCodeBlock-background: var(--vscode-textCodeBlock-background);
 	--color-vscode-textCodeBlock-background: var(--vscode-textCodeBlock-background);
-	--color-vscode-button-hoverBackground: var(--vscode-button-hoverBackground);
+
+	--color-vscode-editorHoverWidget-foreground: var(--vscode-editorHoverWidget-foreground);
+	--color-vscode-editorHoverWidget-background: var(--vscode-editorHoverWidget-background);
+	--color-vscode-editorHoverWidget-border: var(--vscode-editorHoverWidget-border);
 }
 }
 
 
 @layer base {
 @layer base {

+ 1 - 1
webview-ui/src/utils/TelemetryClient.ts

@@ -1,6 +1,6 @@
 import posthog from "posthog-js"
 import posthog from "posthog-js"
 
 
-import { TelemetrySetting } from "@roo/TelemetrySetting"
+import type { TelemetrySetting } from "@roo-code/types"
 
 
 class TelemetryClient {
 class TelemetryClient {
 	private static instance: TelemetryClient
 	private static instance: TelemetryClient

+ 3 - 2
webview-ui/src/utils/test-utils.tsx

@@ -1,7 +1,8 @@
 import React from "react"
 import React from "react"
 import { render, RenderOptions } from "@testing-library/react"
 import { render, RenderOptions } from "@testing-library/react"
-import { TooltipProvider } from "@/components/ui/tooltip"
-import { STANDARD_TOOLTIP_DELAY } from "@/components/ui/standard-tooltip"
+
+import { TooltipProvider } from "@src/components/ui/tooltip"
+import { STANDARD_TOOLTIP_DELAY } from "@src/components/ui/standard-tooltip"
 
 
 interface AllTheProvidersProps {
 interface AllTheProvidersProps {
 	children: React.ReactNode
 	children: React.ReactNode