From a04182e315fae4b9ea5a41f2ef9d935af6239e66 Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 26 Aug 2025 16:24:18 -0700 Subject: [PATCH 1/7] Random chat text area cleanup --- apps/web-evals/package.json | 2 +- pnpm-lock.yaml | 166 +++++- src/core/webview/webviewMessageHandler.ts | 7 +- src/shared/TelemetrySetting.ts | 1 - src/shared/combineCommandSequences.ts | 3 +- .../src/components/chat/ApiConfigSelector.tsx | 113 ++-- webview-ui/src/components/chat/ChatRow.tsx | 138 +---- .../src/components/chat/ChatTextArea.tsx | 539 ++++++++---------- webview-ui/src/components/chat/ChatView.tsx | 28 +- .../src/components/chat/CodeIndexPopover.tsx | 14 +- .../src/components/chat/EditModeControls.tsx | 115 ---- webview-ui/src/components/chat/IconButton.tsx | 54 +- .../components/chat/IndexingStatusBadge.tsx | 133 ++--- .../src/components/chat/ModeSelector.tsx | 130 ++--- .../components/chat/SlashCommandsPopover.tsx | 34 +- .../chat/__tests__/ChatTextArea.spec.tsx | 55 +- .../chat/__tests__/EditModeControls.spec.tsx | 138 ----- .../chat/__tests__/IconButton.spec.tsx | 91 +++ .../__tests__/IndexingStatusBadge.spec.tsx | 10 - .../chat/__tests__/ModeSelector.spec.tsx | 56 +- .../src/components/common/TelemetryBanner.tsx | 2 +- webview-ui/src/components/settings/About.tsx | 4 +- .../src/components/settings/SettingsView.tsx | 6 +- .../src/components/ui/standard-tooltip.tsx | 9 +- webview-ui/src/components/ui/tooltip.tsx | 57 +- .../src/context/ExtensionStateContext.tsx | 3 +- webview-ui/src/hooks/useTooltip.ts | 39 -- webview-ui/src/index.css | 1 + webview-ui/src/utils/TelemetryClient.ts | 2 +- webview-ui/src/utils/test-utils.tsx | 5 +- 30 files changed, 833 insertions(+), 1122 deletions(-) delete mode 100644 src/shared/TelemetrySetting.ts delete mode 100644 webview-ui/src/components/chat/EditModeControls.tsx delete mode 100644 webview-ui/src/components/chat/__tests__/EditModeControls.spec.tsx create mode 100644 webview-ui/src/components/chat/__tests__/IconButton.spec.tsx delete mode 100644 webview-ui/src/hooks/useTooltip.ts diff --git a/apps/web-evals/package.json b/apps/web-evals/package.json index df8efec11581..869355100f50 100644 --- a/apps/web-evals/package.json +++ b/apps/web-evals/package.json @@ -24,7 +24,7 @@ "@radix-ui/react-slider": "^1.2.4", "@radix-ui/react-slot": "^1.1.2", "@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/types": "workspace:^", "@tanstack/react-query": "^5.69.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00788ad28e71..905845e552e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,8 +161,8 @@ importers: specifier: ^1.1.3 version: 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tooltip': - specifier: ^1.1.8 - version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^1.2.8 + version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@roo-code/evals': specifier: workspace:^ version: link:../../packages/evals @@ -2575,6 +2575,9 @@ packages: '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-alert-dialog@1.1.13': resolution: {integrity: sha512-/uPs78OwxGxslYOG5TKeUsv9fZC0vo376cXSADdKirTmsLJU2au6L3n34c3p6W26rFDDDze/hwy4fYeNd0qdGA==} peerDependencies: @@ -2601,6 +2604,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-arrow@1.1.7': + 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/react-checkbox@1.3.1': resolution: {integrity: sha512-xTaLKAO+XXMPK/BpVTSaAAhlefmvMSACjIhK9mGsImvX2ljcTDm8VGR1CuS1uYcNdR5J+oiOhoJZc5un6bh3VQ==} peerDependencies: @@ -2719,6 +2735,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.11': + 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/react-dismissable-layer@1.1.9': resolution: {integrity: sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==} peerDependencies: @@ -2846,6 +2875,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popper@1.2.8': + 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/react-portal@1.1.8': resolution: {integrity: sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==} peerDependencies: @@ -2885,6 +2927,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.5': + 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/react-primitive@2.1.2': resolution: {integrity: sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==} peerDependencies: @@ -3046,6 +3101,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-tooltip@1.2.8': + 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/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: @@ -3131,6 +3199,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-visually-hidden@1.2.3': + 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/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} @@ -11905,6 +11986,8 @@ snapshots: '@radix-ui/primitive@1.1.2': {} + '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-alert-dialog@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -11928,6 +12011,15 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-checkbox@1.3.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -12059,6 +12151,19 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-dismissable-layer@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -12202,6 +12307,24 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-portal@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -12232,6 +12355,16 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-primitive@2.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.2.2(@types/react@18.3.23)(react@18.3.1) @@ -12418,6 +12551,26 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.23)(react@18.3.1)': dependencies: react: 18.3.1 @@ -12481,6 +12634,15 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/rect@1.1.1': {} '@redis/bloom@5.5.5(@redis/client@5.5.5)': diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index ddbc5a992e24..a20e8e821675 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -4,13 +4,12 @@ import * as os from "os" import * as fs from "fs/promises" import pWaitFor from "p-wait-for" import * as vscode from "vscode" -import * as yaml from "yaml" import { type Language, - type ProviderSettings, type GlobalState, type ClineMessage, + type TelemetrySetting, TelemetryEventName, } from "@roo-code/types" import { CloudService } from "@roo-code/cloud" @@ -21,7 +20,6 @@ import { ClineProvider } from "./ClineProvider" import { changeLanguage, t } from "../../i18n" import { Package } from "../../shared/package" import { RouterName, toRouterName, ModelRecord } from "../../shared/api" -import { supportPrompt } from "../../shared/support-prompt" import { MessageEnhancer } from "./messageEnhancer" import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage" @@ -29,7 +27,6 @@ import { checkExistKey } from "../../shared/checkExistApiConfig" import { experimentDefault } from "../../shared/experiments" import { Terminal } from "../../integrations/terminal/Terminal" import { openFile } from "../../integrations/misc/open-file" -import { CodeIndexManager } from "../../services/code-index/manager" import { openImage, saveImage } from "../../integrations/misc/image-handler" import { selectImages } from "../../integrations/misc/process-images" import { getTheme } from "../../integrations/theme/getTheme" @@ -42,9 +39,7 @@ import { exportSettings, importSettingsWithFeedback } from "../config/importExpo import { getOpenAiModels } from "../../api/providers/openai" import { getVsCodeLmModels } from "../../api/providers/vscode-lm" import { openMention } from "../mentions" -import { TelemetrySetting } from "../../shared/TelemetrySetting" import { getWorkspacePath } from "../../utils/path" -import { ensureSettingsDirectoryExists } from "../../utils/globalContext" import { Mode, defaultModeSlug } from "../../shared/modes" import { getModels, flushModels } from "../../api/providers/fetchers/modelCache" import { GetModelsOptions } from "../../shared/api" diff --git a/src/shared/TelemetrySetting.ts b/src/shared/TelemetrySetting.ts deleted file mode 100644 index 61444b5a090b..000000000000 --- a/src/shared/TelemetrySetting.ts +++ /dev/null @@ -1 +0,0 @@ -export type TelemetrySetting = "unset" | "enabled" | "disabled" diff --git a/src/shared/combineCommandSequences.ts b/src/shared/combineCommandSequences.ts index 2f655feb5415..56b97a368e5c 100644 --- a/src/shared/combineCommandSequences.ts +++ b/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" export const COMMAND_OUTPUT_STRING = "Output:" diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx index 32b87147721d..bf5f9612943a 100644 --- a/webview-ui/src/components/chat/ApiConfigSelector.tsx +++ b/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 { useRooPortal } from "@/components/ui/hooks/useRooPortal" import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" -import { IconButton } from "./IconButton" import { useAppTranslation } from "@/i18n/TranslationContext" import { vscode } from "@/utils/vscode" -import { Fzf } from "fzf" import { Button } from "@/components/ui" +import { IconButton } from "./IconButton" + interface ApiConfigSelectorProps { value: string displayName: string disabled?: boolean - title?: string + title: string onChange: (value: string) => void triggerClassName?: string listApiConfigMeta: Array<{ id: string; name: string }> @@ -24,7 +27,7 @@ export const ApiConfigSelector = ({ value, displayName, disabled = false, - title = "", + title, onChange, triggerClassName = "", listApiConfigMeta, @@ -36,30 +39,33 @@ export const ApiConfigSelector = ({ const [searchValue, setSearchValue] = useState("") 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(() => { - if (!searchValue) return listApiConfigMeta + if (!searchValue) { + return listApiConfigMeta + } const matchingItems = fzfInstance.find(searchValue).map((result) => result.item.original) return matchingItems }, [listApiConfigMeta, searchValue, fzfInstance]) - // Separate pinned and unpinned configs + // Separate pinned and unpinned configs. const { pinnedConfigs, unpinnedConfigs } = useMemo(() => { const pinned = filteredConfigs.filter((config) => pinnedApiConfigs?.[config.id]) const unpinned = filteredConfigs.filter((config) => !pinnedApiConfigs?.[config.id]) @@ -76,10 +82,7 @@ export const ApiConfigSelector = ({ ) const handleEditClick = useCallback(() => { - vscode.postMessage({ - type: "switchTab", - tab: "settings", - }) + vscode.postMessage({ type: "switchTab", tab: "settings" }) setOpen(false) }, []) @@ -112,10 +115,7 @@ export const ApiConfigSelector = ({ onClick={(e) => { e.stopPropagation() 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", { "opacity-0 group-hover:opacity-100": !isPinned && !isCurrentConfig, @@ -131,32 +131,30 @@ export const ApiConfigSelector = ({ [value, handleSelect, t, togglePinnedApiConfig], ) - const triggerContent = ( - - - {displayName} - - ) - return ( - - {title ? {triggerContent} : triggerContent} + + + + + {displayName} + + {/* Bottom bar with buttons on left and title on right */} -
+
diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 4fa921f44350..4413eab33c80 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1,18 +1,14 @@ 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 { useTranslation, Trans } from "react-i18next" import deepEqual from "fast-deep-equal" 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 { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences" import { safeJsonParse } from "@roo/safeJsonParse" -import { FollowUpData, SuggestionItem } from "@roo-code/types" import { useCopyToClipboard } from "@src/utils/clipboard" import { useExtensionState } from "@src/context/ExtensionStateContext" @@ -22,9 +18,6 @@ import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanu import { getLanguageFromPath } from "@src/utils/getLanguageFromPath" import { Button } from "@src/components/ui" -import ChatTextArea from "./ChatTextArea" -import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" - import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock" import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock" import CodeAccordian from "../common/CodeAccordian" @@ -32,6 +25,7 @@ import CodeBlock from "../common/CodeBlock" import MarkdownBlock from "../common/MarkdownBlock" import { ReasoningBlock } from "./ReasoningBlock" import Thumbnails from "../common/Thumbnails" + import McpResourceRow from "../mcp/McpResourceRow" import { Mention } from "./Mention" @@ -46,6 +40,7 @@ import { CommandExecutionError } from "./CommandExecutionError" import { AutoApprovedRequestLimitWarning } from "./AutoApprovedRequestLimitWarning" import { CondenseContextErrorRow, CondensingContextRow, ContextCondenseRow } from "./ContextCondenseRow" import CodebaseSearchResultsDisplay from "./CodebaseSearchResultsDisplay" +import { McpExecution } from "./McpExecution" interface ChatRowProps { message: ClineMessage @@ -114,69 +109,20 @@ export const ChatRowContent = ({ editable, }: ChatRowContentProps) => { const { t } = useTranslation() - const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode } = useExtensionState() + + const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState() + const [reasoningCollapsed, setReasoningCollapsed] = useState(true) const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false) const [showCopySuccess, setShowCopySuccess] = useState(false) - const [isEditing, setIsEditing] = useState(false) - const [editedContent, setEditedContent] = useState("") - const [editMode, setEditMode] = useState(mode || "code") - const [editImages, setEditImages] = useState([]) - 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(() => { 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(() => { if (message.text !== null && message.text !== undefined && message.say === "api_req_started") { const info = safeJsonParse(message.text) @@ -1061,58 +1007,26 @@ export const ChatRowContent = ({ case "user_feedback": return (
- {isEditing ? ( -
- +
+
+
- ) : ( -
-
- -
-
- - -
+
+
- )} - {!isEditing && message.images && message.images.length > 0 && ( +
+ + {message.images && message.images.length > 0 && ( )}
diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 5135eca2f2ca..ee2cc76ef368 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -1,15 +1,16 @@ import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react" import { useEvent } from "react-use" import DynamicTextArea from "react-textarea-autosize" +import { VolumeX, Image, WandSparkles, SendHorizontal } from "lucide-react" import { mentionRegex, mentionRegexGlobal, commandRegexGlobal, unescapeSpaces } from "@roo/context-mentions" import { WebviewMessage } from "@roo/WebviewMessage" import { Mode, getAllModes } from "@roo/modes" 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 { ContextMenuOptionType, getContextMenuOptions, @@ -18,20 +19,18 @@ import { shouldShowContextMenu, SearchResult, } 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 ModeSelector from "./ModeSelector" +import { ModeSelector } from "./ModeSelector" import { ApiConfigSelector } from "./ApiConfigSelector" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" -import { VolumeX, Image, WandSparkles, SendHorizontal } from "lucide-react" import { IndexingStatusBadge } from "./IndexingStatusBadge" import { SlashCommandsPopover } from "./SlashCommandsPopover" -import { cn } from "@/lib/utils" import { usePromptHistory } from "./hooks/usePromptHistory" -import { EditModeControls } from "./EditModeControls" interface ChatTextAreaProps { inputValue: string @@ -48,17 +47,13 @@ interface ChatTextAreaProps { mode: Mode setMode: (value: Mode) => void modeShortcutText: string - // Edit mode props - isEditMode?: boolean - onCancel?: () => void } -const ChatTextArea = forwardRef( +export const ChatTextArea = forwardRef( ( { inputValue, setInputValue, - sendingDisabled, selectApiConfigDisabled, placeholderText, selectedImages, @@ -70,8 +65,6 @@ const ChatTextArea = forwardRef( mode, setMode, modeShortcutText, - isEditMode = false, - onCancel, }, ref, ) => { @@ -91,12 +84,12 @@ const ChatTextArea = forwardRef( commands, } = 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 currentConfig = listApiConfigMeta?.find((config) => config.name === currentApiConfigName) return { currentConfigId: currentConfig?.id || "", - displayName: currentApiConfigName || "", // Use the name directly for display + displayName: currentApiConfigName || "", // Use the name directly for display. } }, [listApiConfigMeta, currentApiConfigName]) @@ -888,7 +881,6 @@ const ChatTextArea = forwardRef( const placeholderBottomText = `\n(${t("chat:addContext")}${shouldDisableImages ? `, ${t("chat:dragFiles")}` : `, ${t("chat:dragFilesImages")}`})` - // Common mode selector handler const handleModeChange = useCallback( (value: Mode) => { setMode(value) @@ -897,261 +889,10 @@ const ChatTextArea = forwardRef( [setMode], ) - // Helper function to render mode selector - const renderModeSelector = () => ( - - ) - - // Helper function to handle API config change const handleApiConfigChange = useCallback((value: string) => { vscode.postMessage({ type: "loadApiConfigurationById", text: value }) }, []) - // Helper function to render non-edit mode controls - const renderNonEditModeControls = () => ( -
-
-
{renderModeSelector()}
- -
- -
-
- -
- {isTtsPlaying && ( - - - - )} - - - - - -
-
- ) - - // Helper function to render the text area section - const renderTextAreaSection = () => ( -
-
- { - 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()} - /> - -
- - - -
- - {!isEditMode && ( -
- - - -
- )} - - {!inputValue && !isEditMode && ( -
- {placeholderBottomText} -
- )} -
- ) - return (
( "flex-col", "gap-1", "bg-editor-background", - isEditMode ? "px-0" : "px-1.5", + "px-1.5", "pb-1", "outline-none", "border", "border-none", - isEditMode ? "w-full" : "w-[calc(100%-16px)]", + "w-[calc(100%-16px)]", "ml-auto", "mr-auto", "box-border", @@ -1228,23 +969,168 @@ const ChatTextArea = forwardRef(
)} - {renderTextAreaSection()} -
+
+
+ { + 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()} + /> + +
+ + + +
+ +
+ + + +
- {isEditMode && ( - - )} + {!inputValue && ( +
+ {placeholderBottomText} +
+ )} +
+
{selectedImages.length > 0 && ( @@ -1259,10 +1145,81 @@ const ChatTextArea = forwardRef( /> )} - {!isEditMode && renderNonEditModeControls()} +
+
+
+ +
+
+ +
+
+
+ {isTtsPlaying && ( + + + + )} + + + + + +
+
) }, ) - -export default ChatTextArea diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 44eeb33b6614..8a93b8becc7b 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/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 useSound from "use-sound" import { LRUCache } from "lru-cache" +import { useTranslation } from "react-i18next" import { useDebounceEffect } from "@src/utils/useDebounceEffect" import { appendImages } from "@src/utils/imageUtils" @@ -30,7 +31,6 @@ import { findLongestPrefixMatch, parseCommand, } from "@src/utils/command-validation" -import { useTranslation } from "react-i18next" import { useAppTranslation } from "@src/i18n/TranslationContext" import { useExtensionState } from "@src/context/ExtensionStateContext" import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel" @@ -48,7 +48,7 @@ import HistoryPreview from "../history/HistoryPreview" import Announcement from "./Announcement" import BrowserSessionRow from "./BrowserSessionRow" import ChatRow from "./ChatRow" -import ChatTextArea from "./ChatTextArea" +import { ChatTextArea } from "./ChatTextArea" import TaskHeader from "./TaskHeader" import AutoApproveMenu from "./AutoApproveMenu" import SystemPromptWarning from "./SystemPromptWarning" @@ -68,7 +68,7 @@ export interface ChatViewRef { 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 @@ -77,13 +77,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction { const isMountedRef = useRef(true) + const [audioBaseUri] = useState(() => { const w = window as any return w.AUDIO_BASE_URI || "" }) + const { t } = useAppTranslation() const { t: tSettings } = useTranslation("settings") const modeShortcutText = `${isMac ? "⌘" : "Ctrl"} + . ${t("chat:forNextMode")}, ${isMac ? "⌘" : "Ctrl"} + Shift + . ${t("chat:forPreviousMode")}` + const { clineMessages: messages, currentTaskItem, @@ -121,6 +124,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { messagesRef.current = messages }, [messages]) @@ -238,9 +242,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - return `${audioBaseUri}/${path}` - } + const getAudioUrl = (path: string) => `${audioBaseUri}/${path}` // Use the getAudioUrl helper function const [playNotification] = useSound(getAudioUrl("notification.wav"), soundConfig) @@ -1456,17 +1458,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - // Update local state and notify extension to sync mode change + // Update local state and notify extension to sync mode change. 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], ) @@ -1760,9 +1758,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction { window.addEventListener("keydown", handleKeyDown) + return () => { window.removeEventListener("keydown", handleKeyDown) } @@ -1789,6 +1787,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction = ({ setOpen(newOpen) } }}> - {children} + {children} void - modeShortcutText: string - customModes: any - customModePrompts: any - onCancel?: () => void - onSend: () => void - onSelectImages: () => void - sendingDisabled: boolean - shouldDisableImages: boolean -} - -export const EditModeControls: React.FC = ({ - mode, - onModeChange, - modeShortcutText, - customModes, - customModePrompts, - onCancel, - onSend, - onSelectImages, - sendingDisabled, - shouldDisableImages, -}) => { - const { t } = useAppTranslation() - - return ( -
-
-
- -
-
-
- - - - - - - -
-
- ) -} diff --git a/webview-ui/src/components/chat/IconButton.tsx b/webview-ui/src/components/chat/IconButton.tsx index 75d8bc4b0bb0..00210ac5b3f5 100644 --- a/webview-ui/src/components/chat/IconButton.tsx +++ b/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 { iconClass: string title: string disabled?: boolean + tooltip?: boolean isLoading?: boolean style?: React.CSSProperties } @@ -14,39 +15,34 @@ export const IconButton: React.FC = ({ title, className, disabled, + tooltip = true, isLoading, onClick, style, ...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 = ( - - ) - - return {button} -} + + + +) diff --git a/webview-ui/src/components/chat/IndexingStatusBadge.tsx b/webview-ui/src/components/chat/IndexingStatusBadge.tsx index 2462780b1d47..4a6de39a0b26 100644 --- a/webview-ui/src/components/chat/IndexingStatusBadge.tsx +++ b/webview-ui/src/components/chat/IndexingStatusBadge.tsx @@ -1,12 +1,16 @@ import React, { useState, useEffect, useMemo } from "react" import { Database } from "lucide-react" + import { cn } from "@src/lib/utils" import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@/i18n/TranslationContext" -import { useTooltip } from "@/hooks/useTooltip" + +import type { IndexingStatus, IndexingStatusUpdateMessage } from "@roo/ExtensionMessage" + import { useExtensionState } from "@src/context/ExtensionStateContext" +import { PopoverTrigger, StandardTooltip, Button } from "@src/components/ui" + import { CodeIndexPopover } from "./CodeIndexPopover" -import type { IndexingStatus, IndexingStatusUpdateMessage } from "@roo/ExtensionMessage" interface IndexingStatusBadgeProps { className?: string @@ -15,8 +19,6 @@ interface IndexingStatusBadgeProps { export const IndexingStatusBadge: React.FC = ({ className }) => { const { t } = useAppTranslation() const { cwd } = useExtensionState() - const { showTooltip, handleMouseEnter, handleMouseLeave, cleanup } = useTooltip({ delay: 300 }) - const [isHovered, setIsHovered] = useState(false) const [indexingStatus, setIndexingStatus] = useState({ systemStatus: "Standby", @@ -26,10 +28,10 @@ export const IndexingStatusBadge: React.FC = ({ classN }) useEffect(() => { - // Request initial indexing status + // Request initial indexing status. vscode.postMessage({ type: "requestIndexingStatus" }) - // Set up message listener for status updates + // Set up message listener for status updates. const handleMessage = (event: MessageEvent) => { if (event.data.type === "indexingStatusUpdate") { const status = event.data.values @@ -43,11 +45,9 @@ export const IndexingStatusBadge: React.FC = ({ classN return () => { window.removeEventListener("message", handleMessage) - cleanup() } - }, [cleanup, cwd]) + }, [cwd]) - // Calculate progress percentage with memoization const progressPercentage = useMemo( () => indexingStatus.totalItems > 0 @@ -56,8 +56,7 @@ export const IndexingStatusBadge: React.FC = ({ classN [indexingStatus.processedItems, indexingStatus.totalItems], ) - // Get tooltip text with internationalization - const getTooltipText = () => { + const tooltipText = useMemo(() => { switch (indexingStatus.systemStatus) { case "Standby": return t("chat:indexingStatus.ready") @@ -70,93 +69,43 @@ export const IndexingStatusBadge: React.FC = ({ classN default: 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 = { - 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 ( -
- - - - {showTooltip && ( -
- {getTooltipText()} -
+ + +
- )} -
+ "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, + )}> + + + + + + ) } diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index 93dd2f1f4fd1..2ae9279fa8e3 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -1,26 +1,28 @@ import React from "react" +import { Fzf } from "fzf" 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 { useExtensionState } from "@/context/ExtensionStateContext" +import { useAppTranslation } from "@/i18n/TranslationContext" import { useRooPortal } from "@/components/ui/hooks/useRooPortal" import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" + 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 interface ModeSelectorProps { value: Mode onChange: (value: Mode) => void disabled?: boolean - title?: string + title: string triggerClassName?: string modeShortcutText: string customModes?: ModeConfig[] @@ -32,7 +34,7 @@ export const ModeSelector = ({ value, onChange, disabled = false, - title = "", + title, triggerClassName = "", modeShortcutText, customModes, @@ -47,29 +49,31 @@ export const ModeSelector = ({ const { t } = useAppTranslation() 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) - // Track first-time usage for UI purposes + // Track first-time usage for UI purposes. if (!hasOpenedModeSelector) { setHasOpenedModeSelector(true) vscode.postMessage({ type: "hasOpenedModeSelector", bool: true }) } }, [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 allModes = getAllModes(customModes) + return allModes.map((mode) => ({ ...mode, description: customModePrompts?.[mode.slug]?.description ?? mode.description, })) }, [customModes, customModePrompts]) - // Find the selected mode + // Find the selected mode. 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(() => { return modes.map((mode) => ({ original: mode, @@ -84,31 +88,29 @@ export const ModeSelector = ({ })) }, [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(() => { if (!searchValue) return modes - // First search in names/slugs + // First search in names/slugs. const nameMatches = nameFzfInstance.find(searchValue) const nameMatchedModes = new Set(nameMatches.map((result) => result.item.original.slug)) - // Then search in descriptions + // Then search in descriptions. const descriptionMatches = descriptionFzfInstance.find(searchValue) - // Combine results: name matches first, then description matches + // Combine results: name matches first, then description matches. const combinedResults = [ ...nameMatches.map((result) => result.item.original), ...descriptionMatches @@ -128,7 +130,7 @@ export const ModeSelector = ({ (modeSlug: string) => { onChange(modeSlug as Mode) setOpen(false) - // Clear search after selection + // Clear search after selection. setSearchValue("") }, [onChange], @@ -138,7 +140,8 @@ export const ModeSelector = ({ (isOpen: boolean) => { if (isOpen) trackModeSelectorOpened() setOpen(isOpen) - // Clear search when closing + + // Clear search when closing. if (!isOpen) { setSearchValue("") } @@ -146,44 +149,46 @@ export const ModeSelector = ({ [trackModeSelectorOpened], ) - // Auto-focus search input when popover opens + // Auto-focus search input when popover opens. React.useEffect(() => { if (open && searchInputRef.current) { searchInputRef.current.focus() } }, [open]) - // Determine if search should be shown + // Determine if search should be shown. 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 trigger = ( - - - {selectedMode?.name || ""} - - ) - return ( - {title ? {trigger} : trigger} - + + + + {selectedMode?.name || ""} + + { - vscode.postMessage({ - type: "switchTab", - tab: "modes", - }) + vscode.postMessage({ type: "switchTab", tab: "modes" }) setOpen(false) }} /> @@ -300,5 +302,3 @@ export const ModeSelector = ({ ) } - -export default ModeSelector diff --git a/webview-ui/src/components/chat/SlashCommandsPopover.tsx b/webview-ui/src/components/chat/SlashCommandsPopover.tsx index 3a8bb8ae4367..451eefede279 100644 --- a/webview-ui/src/components/chat/SlashCommandsPopover.tsx +++ b/webview-ui/src/components/chat/SlashCommandsPopover.tsx @@ -41,26 +41,24 @@ export const SlashCommandsPopover: React.FC = ({ clas } } - const trigger = ( - - - - ) - return ( - {trigger} + + + + + ({ vscode: { @@ -1058,54 +1057,4 @@ describe("ChatTextArea", () => { expect(apiConfigDropdown).toHaveAttribute("disabled") }) }) - describe("edit mode integration", () => { - it("should render edit mode UI when isEditMode is true", () => { - ;(useExtensionState as ReturnType).mockReturnValue({ - filePaths: [], - openedTabs: [], - taskHistory: [], - cwd: "/test/workspace", - customModes: [], - customModePrompts: {}, - }) - - render() - - // 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).mockReturnValue({ - filePaths: [], - openedTabs: [], - taskHistory: [], - cwd: "/test/workspace", - }) - - render() - - // 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() - }) - }) }) diff --git a/webview-ui/src/components/chat/__tests__/EditModeControls.spec.tsx b/webview-ui/src/components/chat/__tests__/EditModeControls.spec.tsx deleted file mode 100644 index 2b72202b329c..000000000000 --- a/webview-ui/src/components/chat/__tests__/EditModeControls.spec.tsx +++ /dev/null @@ -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) => ( - - ), - StandardTooltip: ({ children, content }: any) =>
{children}
, -})) - -// Mock ModeSelector -vi.mock("../ModeSelector", () => ({ - default: ({ value, onChange, title }: any) => ( - - ), -})) - -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() - - // 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() - - const cancelButton = screen.getByText("Cancel") - fireEvent.click(cancelButton) - - expect(defaultProps.onCancel).toHaveBeenCalledTimes(1) - }) - - it("calls onSend when send button is clicked", () => { - render() - - const sendButton = screen.getByLabelText("chat:save.tooltip") - fireEvent.click(sendButton) - - expect(defaultProps.onSend).toHaveBeenCalledTimes(1) - }) - - it("calls onSelectImages when image button is clicked", () => { - render() - - const imageButton = screen.getByLabelText("chat:addImages") - fireEvent.click(imageButton) - - expect(defaultProps.onSelectImages).toHaveBeenCalledTimes(1) - }) - - it("disables buttons when sendingDisabled is true", () => { - render() - - 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() - - const imageButton = screen.getByLabelText("chat:addImages") - expect(imageButton).toBeDisabled() - }) - - it("does not call onSelectImages when image button is disabled", () => { - render() - - 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() - - const sendButton = screen.getByLabelText("chat:save.tooltip") - fireEvent.click(sendButton) - - expect(defaultProps.onSend).not.toHaveBeenCalled() - }) - - it("calls onModeChange when mode is changed", () => { - render() - - const modeSelector = screen.getByTitle("chat:selectMode") - fireEvent.change(modeSelector, { target: { value: "architect" } }) - - expect(defaultProps.onModeChange).toHaveBeenCalledWith("architect") - }) -}) diff --git a/webview-ui/src/components/chat/__tests__/IconButton.spec.tsx b/webview-ui/src/components/chat/__tests__/IconButton.spec.tsx new file mode 100644 index 000000000000..5c7f3664e538 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/IconButton.spec.tsx @@ -0,0 +1,91 @@ +import { render, screen, waitFor } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { describe, it, expect, vi } from "vitest" +import { IconButton } from "../IconButton" +import { TooltipProvider } from "@/components/ui/tooltip" +import { STANDARD_TOOLTIP_DELAY } from "@/components/ui/standard-tooltip" + +describe("IconButton", () => { + const renderWithProvider = (ui: React.ReactElement) => { + return render({ui}) + } + + it("should render button with icon", () => { + renderWithProvider( {}} />) + + const button = screen.getByRole("button", { name: "Settings" }) + expect(button).toBeInTheDocument() + + const icon = button.querySelector(".codicon-settings-gear") + expect(icon).toBeInTheDocument() + }) + + it("should not show tooltip immediately on render", () => { + renderWithProvider( {}} />) + + // The tooltip content should not be visible immediately + // There should be no tooltip role element visible + const tooltips = screen.queryAllByRole("tooltip") + expect(tooltips).toHaveLength(0) + }) + + it("should show tooltip on hover after delay", async () => { + const user = userEvent.setup() + + renderWithProvider( {}} />) + + const button = screen.getByRole("button", { name: "Settings" }) + + // Initially no tooltip + expect(screen.queryByRole("tooltip")).not.toBeInTheDocument() + + // Hover over the button + await user.hover(button) + + // Wait for tooltip to appear after delay + await waitFor( + () => { + expect(screen.getByRole("tooltip")).toHaveTextContent("Settings") + }, + { timeout: STANDARD_TOOLTIP_DELAY + 100 }, + ) + }) + + it("should handle click events", async () => { + const user = userEvent.setup() + const handleClick = vi.fn() + + renderWithProvider() + + const button = screen.getByRole("button", { name: "Settings" }) + await user.click(button) + + expect(handleClick).toHaveBeenCalledTimes(1) + }) + + it("should not trigger click when disabled", async () => { + const user = userEvent.setup() + const handleClick = vi.fn() + + renderWithProvider( + , + ) + + const button = screen.getByRole("button", { name: "Settings" }) + expect(button).toBeDisabled() + + await user.click(button) + expect(handleClick).not.toHaveBeenCalled() + }) + + it("should show loading spinner when isLoading is true", () => { + renderWithProvider( + {}} isLoading />, + ) + + const button = screen.getByRole("button", { name: "Settings" }) + const icon = button.querySelector(".codicon-settings-gear") + + expect(icon).toHaveClass("codicon-modifier-spin") + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx index 37eb29153074..25f92757f8b2 100644 --- a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx +++ b/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 vi.mock("@/context/ExtensionStateContext", () => ({ useExtensionState: () => ({ diff --git a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx index a8291688936b..39da4128f85c 100644 --- a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx +++ b/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 { 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", () => ({ vscode: { 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[] = [] vi.mock("@roo/modes", async () => { @@ -63,19 +63,16 @@ describe("ModeSelector", () => { />, ) - // The component should be rendered expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument() }) test("falls back to default description when no custom prompt", () => { render() - // The component should be rendered expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument() }) test("shows search bar when there are more than 6 modes", () => { - // Set up mock to return 7 modes mockModes = Array.from({ length: 7 }, (_, i) => ({ slug: `mode-${i}`, name: `Mode ${i}`, @@ -86,20 +83,19 @@ describe("ModeSelector", () => { render() - // Click to open the popover + // Click to open the popover. fireEvent.click(screen.getByTestId("mode-selector-trigger")) - // Search input should be visible + // Search input should be visible. expect(screen.getByTestId("mode-search-input")).toBeInTheDocument() - // Info icon should be visible + // Info icon should be visible. expect(screen.getByText("chat:modeSelector.title")).toBeInTheDocument() const infoIcon = document.querySelector(".codicon-info") expect(infoIcon).toBeInTheDocument() }) 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) => ({ slug: `mode-${i}`, name: `Mode ${i}`, @@ -110,22 +106,21 @@ describe("ModeSelector", () => { render() - // Click to open the popover + // Click to open the popover. 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() - // Info blurb should be visible + // Info blurb should be visible. 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") expect(infoIcon).not.toBeInTheDocument() }) test("filters modes correctly when searching", () => { - // Set up mock to return 7 modes to enable search mockModes = Array.from({ length: 7 }, (_, i) => ({ slug: `mode-${i}`, name: `Mode ${i}`, @@ -136,20 +131,19 @@ describe("ModeSelector", () => { render() - // Click to open the popover + // Click to open the popover. fireEvent.click(screen.getByTestId("mode-selector-trigger")) - // Type in search + // Type in search. const searchInput = screen.getByTestId("mode-search-input") fireEvent.change(searchInput, { target: { value: "Mode 3" } }) - // Should show filtered results + // Should show filtered results. 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", () => { - // Set up mock to return 10 modes mockModes = Array.from({ length: 10 }, (_, i) => ({ slug: `mode-${i}`, name: `Mode ${i}`, @@ -162,22 +156,21 @@ describe("ModeSelector", () => { , ) - // Click to open the popover + // Click to open the popover. 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() - // Info blurb should be visible instead + // Info blurb should be visible instead. 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") expect(infoIcon).not.toBeInTheDocument() }) test("shows search when disableSearch is false (default) and modes > 6", () => { - // Set up mock to return 8 modes mockModes = Array.from({ length: 8 }, (_, i) => ({ slug: `mode-${i}`, name: `Mode ${i}`, @@ -186,16 +179,13 @@ describe("ModeSelector", () => { groups: ["read", "edit"], })) - // Don't pass disableSearch prop (should default to false) + // Don't pass disableSearch prop (should default to false). render() - // Click to open the popover fireEvent.click(screen.getByTestId("mode-selector-trigger")) - // Search input should be visible expect(screen.getByTestId("mode-search-input")).toBeInTheDocument() - // Info icon should be visible const infoIcon = document.querySelector(".codicon-info") expect(infoIcon).toBeInTheDocument() }) diff --git a/webview-ui/src/components/common/TelemetryBanner.tsx b/webview-ui/src/components/common/TelemetryBanner.tsx index 63eb262b05e5..4fcd7fa170af 100644 --- a/webview-ui/src/components/common/TelemetryBanner.tsx +++ b/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 { Trans } from "react-i18next" -import { TelemetrySetting } from "@roo/TelemetrySetting" +import type { TelemetrySetting } from "@roo-code/types" import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@src/i18n/TranslationContext" diff --git a/webview-ui/src/components/settings/About.tsx b/webview-ui/src/components/settings/About.tsx index 5075643e6e1c..c25a1aebe382 100644 --- a/webview-ui/src/components/settings/About.tsx +++ b/webview-ui/src/components/settings/About.tsx @@ -2,11 +2,11 @@ import { HTMLAttributes } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import { Trans } from "react-i18next" import { Info, Download, Upload, TriangleAlert } from "lucide-react" - import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import type { TelemetrySetting } from "@roo-code/types" + import { Package } from "@roo/package" -import { TelemetrySetting } from "@roo/TelemetrySetting" import { vscode } from "@/utils/vscode" import { cn } from "@/lib/utils" diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 2738b82632d6..4d55bc69c18c 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -25,11 +25,10 @@ import { LucideIcon, } 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 { cn } from "@src/lib/utils" import { useAppTranslation } from "@src/i18n/TranslationContext" import { ExtensionStateContextType, useExtensionState } from "@src/context/ExtensionStateContext" import { @@ -65,7 +64,6 @@ import { LanguageSettings } from "./LanguageSettings" import { About } from "./About" import { Section } from "./Section" import PromptsSettings from "./PromptsSettings" -import { cn } from "@/lib/utils" export const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden" export const settingsTabList = diff --git a/webview-ui/src/components/ui/standard-tooltip.tsx b/webview-ui/src/components/ui/standard-tooltip.tsx index 7dc4fffd67a1..a2b05386e662 100644 --- a/webview-ui/src/components/ui/standard-tooltip.tsx +++ b/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" export const STANDARD_TOOLTIP_DELAY = 300 interface StandardTooltipProps { /** The element(s) that trigger the tooltip */ - children: React.ReactNode + children: ReactNode /** The content to display in the tooltip */ - content: React.ReactNode + content: ReactNode /** The preferred side of the trigger to render the tooltip */ side?: "top" | "right" | "bottom" | "left" /** The preferred alignment against the trigger */ @@ -51,7 +52,7 @@ export function StandardTooltip({ asChild = true, maxWidth, }: 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())) { return <>{children} } diff --git a/webview-ui/src/components/ui/tooltip.tsx b/webview-ui/src/components/ui/tooltip.tsx index d09d8cec9d07..6504c755ff71 100644 --- a/webview-ui/src/components/ui/tooltip.tsx +++ b/webview-ui/src/components/ui/tooltip.tsx @@ -1,33 +1,44 @@ +"use client" + import * as React from "react" import * as TooltipPrimitive from "@radix-ui/react-tooltip" import { cn } from "@/lib/utils" -const TooltipProvider = TooltipPrimitive.Provider +function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps) { + return +} -const Tooltip = TooltipPrimitive.Root +function Tooltip({ ...props }: React.ComponentProps) { + return +} -const TooltipTrigger = TooltipPrimitive.Trigger +function TooltipTrigger({ ...props }: React.ComponentProps) { + return +} -const TooltipContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( - - - -)) -TooltipContent.displayName = TooltipPrimitive.Content.displayName +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index e632b27a3107..e308ed5e64ed 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -7,10 +7,10 @@ import { type ModeConfig, type ExperimentId, type TodoItem, + type TelemetrySetting, } from "@roo-code/types" import { type OrganizationAllowList, ORGANIZATION_ALLOW_ALL } from "@roo/cloud" - import { ExtensionMessage, ExtensionState, MarketplaceInstalledMetadata, Command } from "@roo/ExtensionMessage" import { findLastIndex } from "@roo/array" import { McpServer } from "@roo/mcp" @@ -18,7 +18,6 @@ import { checkExistKey } from "@roo/checkExistApiConfig" import { Mode, defaultModeSlug, defaultPrompts } from "@roo/modes" import { CustomSupportPrompts } from "@roo/support-prompt" import { experimentDefault } from "@roo/experiments" -import { TelemetrySetting } from "@roo/TelemetrySetting" import { RouterModels } from "@roo/api" import { vscode } from "@src/utils/vscode" diff --git a/webview-ui/src/hooks/useTooltip.ts b/webview-ui/src/hooks/useTooltip.ts deleted file mode 100644 index f098017acf1c..000000000000 --- a/webview-ui/src/hooks/useTooltip.ts +++ /dev/null @@ -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(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, - } -} diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index fbb362ca8f58..64dca5129fc9 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -107,6 +107,7 @@ --color-vscode-list-activeSelectionForeground: var(--vscode-list-activeSelectionForeground); --color-vscode-toolbar-hoverBackground: var(--vscode-toolbar-hoverBackground); + --color-vscode-toolbar-hoverOutline: var(--vscode-toolbar-hoverOutline); --color-vscode-panel-border: var(--vscode-panel-border); diff --git a/webview-ui/src/utils/TelemetryClient.ts b/webview-ui/src/utils/TelemetryClient.ts index cf2bfc54a53b..dd587f51ed9d 100644 --- a/webview-ui/src/utils/TelemetryClient.ts +++ b/webview-ui/src/utils/TelemetryClient.ts @@ -1,6 +1,6 @@ import posthog from "posthog-js" -import { TelemetrySetting } from "@roo/TelemetrySetting" +import type { TelemetrySetting } from "@roo-code/types" class TelemetryClient { private static instance: TelemetryClient diff --git a/webview-ui/src/utils/test-utils.tsx b/webview-ui/src/utils/test-utils.tsx index 05560c3065f8..ad5659ec33ad 100644 --- a/webview-ui/src/utils/test-utils.tsx +++ b/webview-ui/src/utils/test-utils.tsx @@ -1,7 +1,8 @@ import React from "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 { children: React.ReactNode From 838de98540d639f97224feea234025c3e702e031 Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 26 Aug 2025 16:25:38 -0700 Subject: [PATCH 2/7] Fix tsc error --- .../src/components/chat/__tests__/ApiConfigSelector.spec.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx index 934d14cc7bd6..cefc74fdd8e1 100644 --- a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx @@ -49,6 +49,7 @@ describe("ApiConfigSelector", () => { const defaultProps = { value: "config1", displayName: "Config 1", + title: "API Config", onChange: mockOnChange, listApiConfigMeta: [ { id: "config1", name: "Config 1" }, From bc9d181f66595cd6614f875a0d46dd029a076817 Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 26 Aug 2025 16:26:41 -0700 Subject: [PATCH 3/7] Fix tsc errors --- .../chat/__tests__/ModeSelector.spec.tsx | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx index 39da4128f85c..af3a546cdb31 100644 --- a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx @@ -67,7 +67,9 @@ describe("ModeSelector", () => { }) test("falls back to default description when no custom prompt", () => { - render() + render( + , + ) expect(screen.getByTestId("mode-selector-trigger")).toBeInTheDocument() }) @@ -81,7 +83,14 @@ describe("ModeSelector", () => { groups: ["read", "edit"], })) - render() + render( + , + ) // Click to open the popover. fireEvent.click(screen.getByTestId("mode-selector-trigger")) @@ -104,7 +113,14 @@ describe("ModeSelector", () => { groups: ["read", "edit"], })) - render() + render( + , + ) // Click to open the popover. fireEvent.click(screen.getByTestId("mode-selector-trigger")) @@ -129,7 +145,14 @@ describe("ModeSelector", () => { groups: ["read", "edit"], })) - render() + render( + , + ) // Click to open the popover. fireEvent.click(screen.getByTestId("mode-selector-trigger")) @@ -153,7 +176,13 @@ describe("ModeSelector", () => { })) render( - , + , ) // Click to open the popover. @@ -180,7 +209,14 @@ describe("ModeSelector", () => { })) // Don't pass disableSearch prop (should default to false). - render() + render( + , + ) fireEvent.click(screen.getByTestId("mode-selector-trigger")) From 65e30d013d9c93c3e80ffaed3cbdc9352e18d10c Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 26 Aug 2025 16:27:13 -0700 Subject: [PATCH 4/7] Fix tsc errors --- webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx index af3a546cdb31..e2fd310b2196 100644 --- a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx @@ -56,6 +56,7 @@ describe("ModeSelector", () => { render( Date: Tue, 26 Aug 2025 16:39:26 -0700 Subject: [PATCH 5/7] Delete webview-ui/src/components/chat/__tests__/IconButton.spec.tsx --- .../chat/__tests__/IconButton.spec.tsx | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 webview-ui/src/components/chat/__tests__/IconButton.spec.tsx diff --git a/webview-ui/src/components/chat/__tests__/IconButton.spec.tsx b/webview-ui/src/components/chat/__tests__/IconButton.spec.tsx deleted file mode 100644 index 5c7f3664e538..000000000000 --- a/webview-ui/src/components/chat/__tests__/IconButton.spec.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { render, screen, waitFor } from "@testing-library/react" -import userEvent from "@testing-library/user-event" -import { describe, it, expect, vi } from "vitest" -import { IconButton } from "../IconButton" -import { TooltipProvider } from "@/components/ui/tooltip" -import { STANDARD_TOOLTIP_DELAY } from "@/components/ui/standard-tooltip" - -describe("IconButton", () => { - const renderWithProvider = (ui: React.ReactElement) => { - return render({ui}) - } - - it("should render button with icon", () => { - renderWithProvider( {}} />) - - const button = screen.getByRole("button", { name: "Settings" }) - expect(button).toBeInTheDocument() - - const icon = button.querySelector(".codicon-settings-gear") - expect(icon).toBeInTheDocument() - }) - - it("should not show tooltip immediately on render", () => { - renderWithProvider( {}} />) - - // The tooltip content should not be visible immediately - // There should be no tooltip role element visible - const tooltips = screen.queryAllByRole("tooltip") - expect(tooltips).toHaveLength(0) - }) - - it("should show tooltip on hover after delay", async () => { - const user = userEvent.setup() - - renderWithProvider( {}} />) - - const button = screen.getByRole("button", { name: "Settings" }) - - // Initially no tooltip - expect(screen.queryByRole("tooltip")).not.toBeInTheDocument() - - // Hover over the button - await user.hover(button) - - // Wait for tooltip to appear after delay - await waitFor( - () => { - expect(screen.getByRole("tooltip")).toHaveTextContent("Settings") - }, - { timeout: STANDARD_TOOLTIP_DELAY + 100 }, - ) - }) - - it("should handle click events", async () => { - const user = userEvent.setup() - const handleClick = vi.fn() - - renderWithProvider() - - const button = screen.getByRole("button", { name: "Settings" }) - await user.click(button) - - expect(handleClick).toHaveBeenCalledTimes(1) - }) - - it("should not trigger click when disabled", async () => { - const user = userEvent.setup() - const handleClick = vi.fn() - - renderWithProvider( - , - ) - - const button = screen.getByRole("button", { name: "Settings" }) - expect(button).toBeDisabled() - - await user.click(button) - expect(handleClick).not.toHaveBeenCalled() - }) - - it("should show loading spinner when isLoading is true", () => { - renderWithProvider( - {}} isLoading />, - ) - - const button = screen.getByRole("button", { name: "Settings" }) - const icon = button.querySelector(".codicon-settings-gear") - - expect(icon).toHaveClass("codicon-modifier-spin") - }) -}) From 7bc96ecdef7a2d784660c7f1035602420f038205 Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 26 Aug 2025 16:50:46 -0700 Subject: [PATCH 6/7] Fix tests --- .../components/chat/IndexingStatusBadge.tsx | 1 + .../chat/__tests__/ApiConfigSelector.spec.tsx | 4 +- .../__tests__/ChatView.keyboard-fix.spec.tsx | 21 ++++---- .../chat/__tests__/ChatView.spec.tsx | 49 ++++++++++--------- 4 files changed, 41 insertions(+), 34 deletions(-) diff --git a/webview-ui/src/components/chat/IndexingStatusBadge.tsx b/webview-ui/src/components/chat/IndexingStatusBadge.tsx index 4a6de39a0b26..356ef29962ef 100644 --- a/webview-ui/src/components/chat/IndexingStatusBadge.tsx +++ b/webview-ui/src/components/chat/IndexingStatusBadge.tsx @@ -89,6 +89,7 @@ export const IndexingStatusBadge: React.FC = ({ classN