diff --git a/CHANGELOG.md b/CHANGELOG.md index 528c8b5e9b..9c559b5f9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Roo Code Changelog +## [3.39.3] - 2026-01-10 + +![3.39.3 Release - Roo Code Router](/releases/3.39.3-release.png) + +- Rename Roo Code Cloud Provider to Roo Code Router for clearer branding (PR #10560 by @roomote) +- Update Roo Code Router service name throughout the codebase (PR #10607 by @mrubens) +- Update router name in types for consistency (PR #10605 by @mrubens) +- Improve ExtensionHost code organization and cleanup (PR #10600 by @cte) +- Add local installation option to CLI release script for testing (PR #10597 by @cte) +- Reorganize CLI file structure for better maintainability (PR #10599 by @cte) +- Add TUI to CLI (PR #10480 by @cte) + ## [3.39.2] - 2026-01-09 - Fix: Ensure all tools have consistent strict mode values for Cerebras compatibility (#10334 by @brianboysen51, PR #10589 by @app/roomote) @@ -104,7 +116,7 @@ - Fix: Drain queued messages while waiting for ask to prevent message loss (PR #10315 by @hannesrudolph) - Feat: Add grace retry for empty assistant messages to improve reliability (PR #10297 by @hannesrudolph) - Feat: Enable mergeToolResultText for all OpenAI-compatible providers for better tool result handling (PR #10299 by @hannesrudolph) -- Feat: Enable mergeToolResultText for Roo Code Cloud provider (PR #10301 by @hannesrudolph) +- Feat: Enable mergeToolResultText for Roo Code Router (PR #10301 by @hannesrudolph) - Feat: Strengthen native tool-use guidance in prompts for improved model behavior (PR #10311 by @hannesrudolph) - UX: Account-centric signup flow for improved onboarding experience (PR #10306 by @brunobergher) @@ -398,7 +410,7 @@ - Native tool calling support expanded across many providers: Bedrock (PR #9698 by @mrubens), Cerebras (PR #9692 by @mrubens), Chutes with auto-detection from API (PR #9715 by @daniel-lxs), DeepInfra (PR #9691 by @mrubens), DeepSeek and Doubao (PR #9671 by @daniel-lxs), Groq (PR #9673 by @daniel-lxs), LiteLLM (PR #9719 by @daniel-lxs), Ollama (PR #9696 by @mrubens), OpenAI-compatible providers (PR #9676 by @daniel-lxs), Requesty (PR #9672 by @daniel-lxs), Unbound (PR #9699 by @mrubens), Vercel AI Gateway (PR #9697 by @mrubens), Vertex Gemini (PR #9678 by @daniel-lxs), and xAI with new Grok 4 Fast and Grok 4.1 Fast models (PR #9690 by @mrubens) - Fix: Preserve tool_use blocks in summary for parallel tool calls (#9700 by @SilentFlower, PR #9714 by @SilentFlower) - Default Grok Code Fast to native tools for better performance (PR #9717 by @mrubens) -- UX improvements to the Roo Code Cloud provider-centric onboarding flow (PR #9709 by @brunobergher) +- UX improvements to the Roo Code Router-centric onboarding flow (PR #9709 by @brunobergher) - UX toolbar cleanup and settings consolidation for a cleaner interface (PR #9710 by @brunobergher) - Add model-specific tool customization via `excludedTools` and `includedTools` configuration (PR #9641 by @daniel-lxs) - Add new `apply_patch` native tool for more efficient file editing operations (PR #9663 by @hannesrudolph) @@ -497,7 +509,7 @@ - Show the prompt for image generation in the UI (PR #9505 by @mrubens) - Fix double todo list display issue (PR #9517 by @mrubens) - Add tracking for cloud synced messages (PR #9518 by @mrubens) -- Enable the Roo Code Cloud provider in evals (PR #9492 by @cte) +- Enable the Roo Code Router in evals (PR #9492 by @cte) ## [3.34.0] - 2025-11-21 @@ -577,7 +589,7 @@ - Use VSCode theme color for outline button borders (PR #9336 by @app/roomote) - Replace broken badgen.net badges with shields.io (PR #9318 by @app/roomote) - Add max git status files setting to evals (PR #9322 by @mrubens) -- Roo Code Cloud Provider pricing page and changes elsewhere (PR #9195 by @brunobergher) +- Roo Code Router pricing page and changes elsewhere (PR #9195 by @brunobergher) ## [3.32.1] - 2025-11-14 @@ -603,7 +615,7 @@ ![3.31.3 Release - Kangaroo Decrypting a Message](/releases/3.31.3-release.png) - Fix: OpenAI Native encrypted_content handling and remove gpt-5-chat-latest verbosity flag (#9225 by @politsin, PR by @hannesrudolph) -- Fix: Roo Code Cloud provider Anthropic input token normalization to avoid double-counting (thanks @hannesrudolph!) +- Fix: Roo Code Router Anthropic input token normalization to avoid double-counting (thanks @hannesrudolph!) - Refactor: Rename sliding-window to context-management and truncateConversationIfNeeded to manageContext (thanks @hannesrudolph!) ## [3.31.2] - 2025-11-12 @@ -743,7 +755,7 @@ - Add token-budget based file reading with intelligent preview to avoid context overruns (thanks @daniel-lxs!) - Enable browser-use tool for all image-capable models (#8116 by @hannesrudolph, PR by @app/roomote!) -- Add dynamic model loading for Roo Code Cloud provider (thanks @app/roomote!) +- Add dynamic model loading for Roo Code Router (thanks @app/roomote!) - Fix: Respect nested .gitignore files in search_files (#7921 by @hannesrudolph, PR by @daniel-lxs) - Fix: Preserve trailing newlines in stripLineNumbers for apply_diff (#8020 by @liyi3c, PR by @app/roomote) - Fix: Exclude max tokens field for models that don't support it in export (#7944 by @hannesrudolph, PR by @elianiva) @@ -901,7 +913,7 @@ - UX: Responsive Auto-Approve (thanks @brunobergher!) - Add telemetry retry queue for network resilience (thanks @daniel-lxs!) - Fix: Transform keybindings in nightly build to fix command+y shortcut (thanks @app/roomote!) -- New code-supernova stealth model in the Roo Code Cloud provider (thanks @mrubens!) +- New code-supernova stealth model in the Roo Code Router (thanks @mrubens!) ## [3.28.3] - 2025-09-16 @@ -1112,11 +1124,11 @@ ## [3.25.19] - 2025-08-19 -- Fix issue where new users couldn't select the Roo Code Cloud provider (thanks @daniel-lxs!) +- Fix issue where new users couldn't select the Roo Code Router (thanks @daniel-lxs!) ## [3.25.18] - 2025-08-19 -- Add new stealth Sonic model through the Roo Code Cloud provider +- Add new stealth Sonic model through the Roo Code Router - Fix: respect enableReasoningEffort setting when determining reasoning usage (#7048 by @ikbencasdoei, PR by @app/roomote) - Fix: prevent duplicate LM Studio models with case-insensitive deduplication (#6954 by @fbuechler, PR by @daniel-lxs) - Feat: simplify ask_followup_question prompt documentation (thanks @daniel-lxs!) diff --git a/apps/cli/src/agent/extension-host.ts b/apps/cli/src/agent/extension-host.ts index 968de48e9f..4a9cf93ccb 100644 --- a/apps/cli/src/agent/extension-host.ts +++ b/apps/cli/src/agent/extension-host.ts @@ -189,8 +189,9 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac commandExecutionTimeout: 30, browserToolEnabled: false, enableCheckpoints: false, - // Disable preventFocusDisruption experiment for CLI - it's only relevant for VSCode diff views - // and preventing it causes tool messages to not stream during LLM generation + // Disable preventFocusDisruption experiment for CLI - it's only + // relevant for VSCode diff views and preventing it causes tool + // messages to not stream during LLM generation. experiments: { multiFileApplyDiff: false, powerSteering: false, @@ -368,6 +369,7 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac if (this.options.ephemeral) { this.ephemeralStorageDir = await createEphemeralStorageDir() + storageDir = this.ephemeralStorageDir } // Create VSCode API mock. diff --git a/apps/web-roo-code/src/app/cloud/page.tsx b/apps/web-roo-code/src/app/cloud/page.tsx index 20307112b6..a198b08f40 100644 --- a/apps/web-roo-code/src/app/cloud/page.tsx +++ b/apps/web-roo-code/src/app/cloud/page.tsx @@ -98,8 +98,7 @@ const features: Feature[] = [ { icon: Brain, title: "Model Agnostic", - description: - "Bring your own keys or use the Roo Code Cloud Provider with access to all top models with no markup.", + description: "Bring your own keys or use the Roo Code Router with access to all top models with no markup.", }, { icon: Github, @@ -153,7 +152,7 @@ export default function CloudPage() { Your AI Team in the Cloud

- Create your agent team in the Cloud, give them access to GitHub, and start delegating tasks + Create your agent team in the Cloud, give them access to GitHub, and start delegating tasks from Web and Slack.

diff --git a/apps/web-roo-code/src/app/cloud/team/page.tsx b/apps/web-roo-code/src/app/cloud/team/page.tsx index 7be79f74e2..1bf541c43c 100644 --- a/apps/web-roo-code/src/app/cloud/team/page.tsx +++ b/apps/web-roo-code/src/app/cloud/team/page.tsx @@ -75,7 +75,7 @@ const keyBenefits = [ { title: "Centralized Billing", description: - "Single billing point for all team members using Cloud Agents and the Roo Code Cloud Provider. No more API key management.", + "Single billing point for all team members using Cloud Agents and the Roo Code Router. No more API key management.", icon: DollarSign, }, { diff --git a/apps/web-roo-code/src/app/pricing/page.tsx b/apps/web-roo-code/src/app/pricing/page.tsx index 8a79470ea1..85e66aaa7c 100644 --- a/apps/web-roo-code/src/app/pricing/page.tsx +++ b/apps/web-roo-code/src/app/pricing/page.tsx @@ -98,7 +98,7 @@ const pricingTiers: PricingTier[] = [ featuresIntro: "Go beyond the extension with", features: [ "Access to Cloud Agents: fully autonomous development you can call from Slack, Github and the web", - "Access to the Roo Code Cloud Provider", + "Access to the Roo Code Router", "Follow your tasks from anywhere", "Share tasks with friends and co-workers", "Token usage analytics", @@ -140,7 +140,7 @@ export default function PricingPage() {

Roo Code Pricing

For all of our products: the Roo Code VS Code Extension, Roo Code Cloud and the Roo Code - Cloud inference Provider. + Router.

@@ -230,12 +230,12 @@ export default function PricingPage() {
-

Roo Code Provider

+

Roo Code Router

On any plan, you can use your own LLM provider API key or use the built-in Roo Code - Cloud provider – curated models to work with Roo with no markup, including the - latest Gemini, GPT and Claude. Paid with credits. + Router – curated models to work with Roo with no markup, including the latest + Gemini, GPT and Claude. Paid with credits. See per model pricing. diff --git a/apps/web-roo-code/src/app/provider/page.tsx b/apps/web-roo-code/src/app/provider/page.tsx index 7bcebae246..6caf1e6928 100644 --- a/apps/web-roo-code/src/app/provider/page.tsx +++ b/apps/web-roo-code/src/app/provider/page.tsx @@ -14,16 +14,16 @@ const faqs = [ answer: "AI model providers offer various language models with different capabilities and pricing.", }, { - question: "What is the Roo Code Cloud Provider?", + question: "What is the Roo Code Router?", answer: ( <> -

This is our very own model provider, optimized to work seamlessly with Roo Code Cloud.

+

This is our very own model router, optimized to work seamlessly with Roo Code Cloud.

You don't have to use it to use Roo Code, but it's the easiest way to do it.

), }, { - question: "Do I have to use the Roo Code Cloud Provider to use the Roo Code products?", + question: "Do I have to use the Roo Code Router to use the Roo Code products?", answer: "Not at all! You can bring your own provider key, no problem. This is just meant to make it easier.", }, { @@ -32,7 +32,7 @@ const faqs = [ }, { question: "How is my data treated?", - answer: "The Roo Code Cloud provider doesn't keep any of your data, the service only aims to make it easier to use Roo Code. Each model vendor has their own privacy policy though, and usually free models use your data for training, so keep that in mind.", + answer: "The Roo Code Router doesn't keep any of your data, the service only aims to make it easier to use Roo Code. Each model vendor has their own privacy policy though, and usually free models use your data for training, so keep that in mind.", }, { question: "How much does the Roo Code Cloud service cost?", @@ -134,7 +134,7 @@ export default function ProviderPage() {
-

The Roo Code Cloud Provider

+

The Roo Code Router

The easiest way to use Roo Code (in the{" "} diff --git a/apps/web-roo-code/src/components/chromes/nav-bar.tsx b/apps/web-roo-code/src/components/chromes/nav-bar.tsx index 046e3e0bd8..ba19700494 100644 --- a/apps/web-roo-code/src/components/chromes/nav-bar.tsx +++ b/apps/web-roo-code/src/components/chromes/nav-bar.tsx @@ -58,7 +58,7 @@ export function NavBar({ stars, downloads }: NavBarProps) { href="/provider" className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground"> - Roo Code Cloud Provider + Roo Code Router

@@ -194,7 +194,7 @@ export function NavBar({ stars, downloads }: NavBarProps) { href="/provider" className="block w-full p-5 py-3 text-left text-foreground active:opacity-50" onClick={() => setIsMenuOpen(false)}> - Roo Code Cloud Provider + Roo Code Router
diff --git a/packages/types/npm/package.metadata.json b/packages/types/npm/package.metadata.json index 426798ac2b..04552040c7 100644 --- a/packages/types/npm/package.metadata.json +++ b/packages/types/npm/package.metadata.json @@ -1,6 +1,6 @@ { "name": "@roo-code/types", - "version": "1.100.0", + "version": "1.103.0", "description": "TypeScript type definitions for Roo Code.", "publishConfig": { "access": "public", diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 3b3717ad45..d5880cd3b1 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -690,7 +690,7 @@ export const MODELS_BY_PROVIDER: Record< models: Object.keys(openAiNativeModels), }, "qwen-code": { id: "qwen-code", label: "Qwen Code", models: Object.keys(qwenCodeModels) }, - roo: { id: "roo", label: "Roo Code Cloud", models: [] }, + roo: { id: "roo", label: "Roo Code Router", models: [] }, sambanova: { id: "sambanova", label: "SambaNova", diff --git a/releases/3.39.3-release.png b/releases/3.39.3-release.png new file mode 100644 index 0000000000..f8dcd92b69 Binary files /dev/null and b/releases/3.39.3-release.png differ diff --git a/src/package.json b/src/package.json index 1c3acfa60c..44d907548a 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "RooVeterinaryInc", - "version": "3.39.2", + "version": "3.39.3", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", diff --git a/webview-ui/src/components/settings/About.tsx b/webview-ui/src/components/settings/About.tsx index 3ace32d33d..b16841e1d8 100644 --- a/webview-ui/src/components/settings/About.tsx +++ b/webview-ui/src/components/settings/About.tsx @@ -24,6 +24,7 @@ import { Button } from "@/components/ui" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" +import { SearchableSetting } from "./SearchableSetting" type AboutProps = HTMLAttributes & { telemetrySetting: TelemetrySetting @@ -50,7 +51,10 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug,
-
+ { @@ -67,7 +71,7 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug, }} />

-
+
@@ -120,7 +124,11 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug,
{setDebug && ( -
+ { @@ -132,30 +140,35 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug,

{t("settings:about.debugMode.description")}

-
+ )}
-

{t("settings:about.manageSettings")}

-
- - - -
+ +

{t("settings:about.manageSettings")}

+
+ + + +
+
) diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index 335a616a3d..fe77029afc 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -11,6 +11,7 @@ import { Button, Input, Slider } from "@/components/ui" import { SetCachedStateField } from "./types" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" +import { SearchableSetting } from "./SearchableSetting" import { AutoApproveToggle } from "./AutoApproveToggle" import { MaxLimitInputs } from "./MaxLimitInputs" import { useExtensionState } from "@/context/ExtensionStateContext" @@ -116,40 +117,45 @@ export const AutoApproveSettings = ({
- { - const newValue = !(autoApprovalEnabled ?? false) - setAutoApprovalEnabled(newValue) - vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue }) - }}> - {t("settings:autoApprove.enabled")} - -
-

{t("settings:autoApprove.description")}

-

- { - e.preventDefault() - // Send message to open keyboard shortcuts with search for toggle command - vscode.postMessage({ - type: "openKeyboardShortcuts", - text: `${Package.name}.toggleAutoApprove`, - }) - }} - /> - ), - }} - /> -

-
+ + { + const newValue = !(autoApprovalEnabled ?? false) + setAutoApprovalEnabled(newValue) + vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue }) + }}> + {t("settings:autoApprove.enabled")} + +
+

{t("settings:autoApprove.description")}

+

+ { + e.preventDefault() + // Send message to open keyboard shortcuts with search for toggle command + vscode.postMessage({ + type: "openKeyboardShortcuts", + text: `${Package.name}.toggleAutoApprove`, + }) + }} + /> + ), + }} + /> +

+
+
{t("settings:autoApprove.readOnly.label")}
-
+ @@ -193,7 +202,7 @@ export const AutoApproveSettings = ({
{t("settings:autoApprove.readOnly.outsideWorkspace.description")}
-
+ )} @@ -203,7 +212,10 @@ export const AutoApproveSettings = ({
{t("settings:autoApprove.write.label")}
-
+ @@ -217,8 +229,11 @@ export const AutoApproveSettings = ({
{t("settings:autoApprove.write.outsideWorkspace.description")}
-
-
+ + @@ -230,7 +245,7 @@ export const AutoApproveSettings = ({
{t("settings:autoApprove.write.protected.description")}
-
+ )} @@ -240,7 +255,10 @@ export const AutoApproveSettings = ({
{t("settings:autoApprove.followupQuestions.label")}
-
+
{t("settings:autoApprove.followupQuestions.timeoutLabel")}
-
+ )} @@ -268,14 +286,17 @@ export const AutoApproveSettings = ({
{t("settings:autoApprove.execute.label")}
-
+
{t("settings:autoApprove.execute.allowedCommandsDescription")}
-
+
{/* Denied Commands Section */} -
+
{t("settings:autoApprove.execute.deniedCommandsDescription")}
-
+
-
+ setCachedStateField("browserToolEnabled", e.target.checked)}> @@ -131,11 +135,14 @@ export const BrowserSettings = ({
-
+ {browserToolEnabled && (
-
+ setCachedStateField("language", value as Language)}> - - - - - - {Object.entries(LANGUAGES).map(([code, name]) => ( - - {name} - ({code}) - - ))} - - - + + +
) diff --git a/webview-ui/src/components/settings/NotificationSettings.tsx b/webview-ui/src/components/settings/NotificationSettings.tsx index 9610cabad8..db11e22c6b 100644 --- a/webview-ui/src/components/settings/NotificationSettings.tsx +++ b/webview-ui/src/components/settings/NotificationSettings.tsx @@ -6,6 +6,7 @@ import { Bell } from "lucide-react" import { SetCachedStateField } from "./types" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" +import { SearchableSetting } from "./SearchableSetting" import { Slider } from "../ui" type NotificationSettingsProps = HTMLAttributes & { @@ -35,7 +36,10 @@ export const NotificationSettings = ({
-
+ setCachedStateField("ttsEnabled", e.target.checked)} @@ -45,11 +49,14 @@ export const NotificationSettings = ({
{t("settings:notifications.tts.description")}
-
+ {ttsEnabled && (
-
+ @@ -64,11 +71,14 @@ export const NotificationSettings = ({ /> {((ttsSpeed ?? 1.0) * 100).toFixed(0)}%
-
+ )} -
+ setCachedStateField("soundEnabled", e.target.checked)} @@ -78,11 +88,14 @@ export const NotificationSettings = ({
{t("settings:notifications.sound.description")}
-
+ {soundEnabled && (
-
+ @@ -97,7 +110,7 @@ export const NotificationSettings = ({ /> {((soundVolume ?? 0.5) * 100).toFixed(0)}%
-
+ )}
diff --git a/webview-ui/src/components/settings/PromptsSettings.tsx b/webview-ui/src/components/settings/PromptsSettings.tsx index bca240c731..85fb0ce8e3 100644 --- a/webview-ui/src/components/settings/PromptsSettings.tsx +++ b/webview-ui/src/components/settings/PromptsSettings.tsx @@ -19,6 +19,7 @@ import { import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" +import { SearchableSetting } from "./SearchableSetting" interface PromptsSettingsProps { customSupportPrompts: Record @@ -145,7 +146,10 @@ const PromptsSettings = ({
-
+ onChange(e.target.value)} + onFocus={handleFocus} + onBlur={handleBlur} + onKeyDown={onKeyDown} + placeholder={isWide ? t("settings:search.placeholder") : ""} + className={cn( + "pl-8 h-7 text-sm rounded-full border border-vscode-input-border bg-vscode-input-background focus:border-vscode-focusBorder transition-all duration-200 ease-in-out", + isWide ? "w-40 pr-2.5" : "w-8 pr-0 cursor-pointer", + value && "pr-7", + )} + /> + {value && ( + + )} +
+ ) +} diff --git a/webview-ui/src/components/settings/SettingsSearchResults.tsx b/webview-ui/src/components/settings/SettingsSearchResults.tsx new file mode 100644 index 0000000000..bf9d74aefa --- /dev/null +++ b/webview-ui/src/components/settings/SettingsSearchResults.tsx @@ -0,0 +1,150 @@ +import { useMemo } from "react" +import type { LucideIcon } from "lucide-react" + +import { useAppTranslation } from "@/i18n/TranslationContext" +import { cn } from "@/lib/utils" + +import { SectionName } from "./SettingsView" +import { SearchResult } from "./useSettingsSearch" + +export interface SettingsSearchResultsProps { + results: SearchResult[] + query: string + onSelectResult: (result: SearchResult) => void + sections: { id: SectionName; icon: LucideIcon }[] + highlightedResultId?: string +} + +interface HighlightMatchProps { + text: string + /** Character positions to highlight (from fzf) */ + positions: Set +} + +/** + * Highlights matching characters using fzf's position data. + */ +function HighlightMatch({ text, positions }: HighlightMatchProps) { + if (positions.size === 0) { + return <>{text} + } + + // Build segments of highlighted and non-highlighted text + const segments: { text: string; highlighted: boolean }[] = [] + let currentSegment = "" + let currentHighlighted = positions.has(0) + + for (let i = 0; i < text.length; i++) { + const isHighlighted = positions.has(i) + if (isHighlighted === currentHighlighted) { + currentSegment += text[i] + } else { + if (currentSegment) { + segments.push({ text: currentSegment, highlighted: currentHighlighted }) + } + currentSegment = text[i] + currentHighlighted = isHighlighted + } + } + + if (currentSegment) { + segments.push({ text: currentSegment, highlighted: currentHighlighted }) + } + + return ( + <> + {segments.map((segment, index) => + segment.highlighted ? ( + + {segment.text} + + ) : ( + {segment.text} + ), + )} + + ) +} + +export function SettingsSearchResults({ + results, + query, + onSelectResult, + sections, + highlightedResultId, +}: SettingsSearchResultsProps) { + const { t } = useAppTranslation() + + // Group results by section/tab + const groupedResults = useMemo(() => { + return results.reduce( + (acc, result) => { + const section = result.section + if (!acc[section]) { + acc[section] = [] + } + acc[section].push(result) + return acc + }, + {} as Record, + ) + }, [results]) + + // Create a map of section id to icon for quick lookup + const sectionIconMap = useMemo(() => { + return new Map(sections.map((section) => [section.id, section.icon])) + }, [sections]) + + // If no results, show a message + if (results.length === 0) { + return ( +
+ {t("settings:search.noResults", { query })} +
+ ) + } + + return ( +
+ {Object.entries(groupedResults).map(([section, sectionResults]) => { + const Icon = sectionIconMap.get(section as SectionName) + + return ( +
+ {/* Section header */} +
+ {Icon && } + {t(`settings:sections.${section}`)} +
+ + {/* Result items */} + {sectionResults.map((result) => { + const isHighlighted = highlightedResultId === result.settingId + const resultDomId = `settings-search-result-${result.settingId}` + + return ( + + ) + })} +
+ ) + })} +
+ ) +} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 85143312dc..7b7151ce35 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -79,6 +79,8 @@ import { SlashCommandsSettings } from "./SlashCommandsSettings" import { UISettings } from "./UISettings" import ModesView from "../modes/ModesView" import McpView from "../mcp/McpView" +import { SettingsSearch } from "./SettingsSearch" +import { useSearchIndexRegistry, SearchIndexProvider } from "./useSettingsSearch" export const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden" export const settingsTabList = @@ -91,7 +93,7 @@ export interface SettingsViewRef { checkUnsaveChanges: (then: () => void) => void } -const sectionNames = [ +export const sectionNames = [ "providers", "autoApprove", "slashCommands", @@ -109,7 +111,7 @@ const sectionNames = [ "about", ] as const -type SectionName = (typeof sectionNames)[number] +export type SectionName = (typeof sectionNames)[number] type SettingsViewProps = { onDone: () => void @@ -577,13 +579,64 @@ const SettingsView = forwardRef(({ onDone, t } }, [scrollToActiveTab]) + // Search index registry - settings register themselves on mount + const getSectionLabel = useCallback((section: SectionName) => t(`settings:sections.${section}`), [t]) + const { contextValue: searchContextValue, index: searchIndex } = useSearchIndexRegistry(getSectionLabel) + + // Track which tabs have been indexed (visited at least once) + const [indexingTabIndex, setIndexingTabIndex] = useState(0) + const initialTab = useRef(activeTab) + const isIndexing = indexingTabIndex < sectionNames.length + const isIndexingComplete = !isIndexing + + // Index all tabs by cycling through them on mount + useLayoutEffect(() => { + if (indexingTabIndex >= sectionNames.length) { + // All tabs indexed, return to initial tab + setActiveTab(initialTab.current) + return + } + + // Move to the next tab on next render + setIndexingTabIndex((prev) => prev + 1) + }, [indexingTabIndex]) + + // Determine which tab content to render (for indexing or active display) + const renderTab = isIndexing ? sectionNames[indexingTabIndex] : activeTab + + // Handle search navigation - switch to the correct tab and scroll to the element + const handleSearchNavigate = useCallback( + (section: SectionName, settingId: string) => { + // Switch to the correct tab + handleTabChange(section) + + // Wait for the tab to render, then find element by settingId and scroll to it + requestAnimationFrame(() => { + setTimeout(() => { + const element = document.querySelector(`[data-setting-id="${settingId}"]`) + if (element) { + element.scrollIntoView({ behavior: "smooth", block: "center" }) + + // Add highlight animation + element.classList.add("settings-highlight") + setTimeout(() => { + element.classList.remove("settings-highlight") + }, 1500) + } + }, 100) // Small delay to ensure tab content is rendered + }) + }, + [handleTabChange], + ) + return ( -
-

{t("settings:header.title")}

-
-
+

{t("settings:header.title")}

+
+ {isIndexingComplete && ( + + )} (({ onDone, t })} - {/* Content area */} - - {/* Providers Section */} - {activeTab === "providers" && ( -
- -
- -
{t("settings:sections.providers")}
-
-
+ {/* Content area - renders only the active tab (or indexing tab during initial indexing) */} + + + {/* Providers Section */} + {renderTab === "providers" && ( +
+ +
+ +
{t("settings:sections.providers")}
+
+
-
- - checkUnsaveChanges(() => - vscode.postMessage({ type: "loadApiConfiguration", text: configName }), - ) - } - onDeleteConfig={(configName: string) => - vscode.postMessage({ type: "deleteApiConfiguration", text: configName }) - } - onRenameConfig={(oldName: string, newName: string) => { - vscode.postMessage({ - type: "renameApiConfiguration", - values: { oldName, newName }, - apiConfiguration, - }) - prevApiConfigName.current = newName - }} - onUpsertConfig={(configName: string) => - vscode.postMessage({ - type: "upsertApiConfiguration", - text: configName, - apiConfiguration, - }) - } - /> - -
-
- )} +
+ + checkUnsaveChanges(() => + vscode.postMessage({ type: "loadApiConfiguration", text: configName }), + ) + } + onDeleteConfig={(configName: string) => + vscode.postMessage({ type: "deleteApiConfiguration", text: configName }) + } + onRenameConfig={(oldName: string, newName: string) => { + vscode.postMessage({ + type: "renameApiConfiguration", + values: { oldName, newName }, + apiConfiguration, + }) + prevApiConfigName.current = newName + }} + onUpsertConfig={(configName: string) => + vscode.postMessage({ + type: "upsertApiConfiguration", + text: configName, + apiConfiguration, + }) + } + /> + +
+
+ )} - {/* Auto-Approve Section */} - {activeTab === "autoApprove" && ( - - )} + {/* Auto-Approve Section */} + {renderTab === "autoApprove" && ( + + )} - {/* Slash Commands Section */} - {activeTab === "slashCommands" && } + {/* Slash Commands Section */} + {renderTab === "slashCommands" && } - {/* Browser Section */} - {activeTab === "browser" && ( - - )} + {/* Browser Section */} + {renderTab === "browser" && ( + + )} - {/* Checkpoints Section */} - {activeTab === "checkpoints" && ( - - )} + {/* Checkpoints Section */} + {renderTab === "checkpoints" && ( + + )} - {/* Notifications Section */} - {activeTab === "notifications" && ( - - )} + {/* Notifications Section */} + {renderTab === "notifications" && ( + + )} - {/* Context Management Section */} - {activeTab === "contextManagement" && ( - - )} + {/* Context Management Section */} + {renderTab === "contextManagement" && ( + + )} - {/* Terminal Section */} - {activeTab === "terminal" && ( - - )} + {/* Terminal Section */} + {renderTab === "terminal" && ( + + )} - {/* Modes Section */} - {activeTab === "modes" && } + {/* Modes Section */} + {renderTab === "modes" && } - {/* MCP Section */} - {activeTab === "mcp" && } + {/* MCP Section */} + {renderTab === "mcp" && } - {/* Prompts Section */} - {activeTab === "prompts" && ( - - setCachedStateField("includeTaskHistoryInEnhance", value) - } - /> - )} + {/* Prompts Section */} + {renderTab === "prompts" && ( + + setCachedStateField("includeTaskHistoryInEnhance", value) + } + /> + )} - {/* UI Section */} - {activeTab === "ui" && ( - - )} + {/* UI Section */} + {renderTab === "ui" && ( + + )} - {/* Experimental Section */} - {activeTab === "experimental" && ( - - )} + {/* Experimental Section */} + {renderTab === "experimental" && ( + + )} - {/* Language Section */} - {activeTab === "language" && ( - - )} + {/* Language Section */} + {renderTab === "language" && ( + + )} - {/* About Section */} - {activeTab === "about" && ( - - )} + {/* About Section */} + {renderTab === "about" && ( + + )} +
diff --git a/webview-ui/src/components/settings/SlashCommandsSettings.tsx b/webview-ui/src/components/settings/SlashCommandsSettings.tsx index 58869fe8cc..5b97df4fa0 100644 --- a/webview-ui/src/components/settings/SlashCommandsSettings.tsx +++ b/webview-ui/src/components/settings/SlashCommandsSettings.tsx @@ -22,6 +22,7 @@ import { buildDocLink } from "@/utils/docLinks" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" +import { SearchableSetting } from "./SearchableSetting" import { SlashCommandItem } from "../chat/SlashCommandItem" export const SlashCommandsSettings: React.FC = () => { @@ -111,7 +112,11 @@ export const SlashCommandsSettings: React.FC = () => {
{/* Description section */} -
+

{ }} />

-
+ {/* Global Commands Section */} -
+

{t("chat:slashCommands.globalCommands")}

@@ -169,11 +178,15 @@ export const SlashCommandsSettings: React.FC = () => {
-
+ {/* Workspace Commands Section - Only show if in a workspace */} {hasWorkspace && ( -
+

{t("chat:slashCommands.workspaceCommands")}

@@ -211,12 +224,16 @@ export const SlashCommandsSettings: React.FC = () => {
- + )} {/* Built-in Commands Section */} {builtInCommands.length > 0 && ( -
+

{t("chat:slashCommands.builtInCommands")}

@@ -231,7 +248,7 @@ export const SlashCommandsSettings: React.FC = () => { /> ))}
-
+ )}
diff --git a/webview-ui/src/components/settings/TerminalSettings.tsx b/webview-ui/src/components/settings/TerminalSettings.tsx index d7ae25f0a0..2b68e003bf 100644 --- a/webview-ui/src/components/settings/TerminalSettings.tsx +++ b/webview-ui/src/components/settings/TerminalSettings.tsx @@ -15,6 +15,7 @@ import { Slider } from "@/components/ui" import { SetCachedStateField } from "./types" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" +import { SearchableSetting } from "./SearchableSetting" type TerminalSettingsProps = HTMLAttributes & { terminalOutputLineLimit?: number @@ -104,7 +105,10 @@ export const TerminalSettings = ({
-
+ @@ -131,8 +135,11 @@ export const TerminalSettings = ({
-
-
+ + @@ -161,8 +168,11 @@ export const TerminalSettings = ({
- -
+ + @@ -183,7 +193,7 @@ export const TerminalSettings = ({
- + @@ -199,7 +209,10 @@ export const TerminalSettings = ({
-
+ @@ -221,11 +234,14 @@ export const TerminalSettings = ({
-
+ {!terminalShellIntegrationDisabled && ( <> -
+ { @@ -251,9 +267,12 @@ export const TerminalSettings = ({
- + -
+ @@ -286,9 +305,12 @@ export const TerminalSettings = ({
- + -
+ @@ -319,9 +341,12 @@ export const TerminalSettings = ({
- + -
+ @@ -344,9 +369,12 @@ export const TerminalSettings = ({
- + -
+ @@ -369,9 +397,12 @@ export const TerminalSettings = ({
- + -
+ setCachedStateField("terminalZshOhMy", e.target.checked)} @@ -390,9 +421,12 @@ export const TerminalSettings = ({
- + -
+ setCachedStateField("terminalZshP10k", e.target.checked)} @@ -411,9 +445,12 @@ export const TerminalSettings = ({
- + -
+ setCachedStateField("terminalZdotdir", e.target.checked)} @@ -432,7 +469,7 @@ export const TerminalSettings = ({
- + )} diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx index b4e5a4e861..162d727260 100644 --- a/webview-ui/src/components/settings/UISettings.tsx +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -7,6 +7,7 @@ import { telemetryClient } from "@/utils/TelemetryClient" import { SetCachedStateField } from "./types" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" +import { SearchableSetting } from "./SearchableSetting" import { ExtensionStateContextType } from "@/context/ExtensionStateContext" interface UISettingsProps extends HTMLAttributes { @@ -60,32 +61,42 @@ export const UISettings = ({
{/* Collapse Thinking Messages Setting */} -
- handleReasoningBlockCollapsedChange(e.target.checked)} - data-testid="collapse-thinking-checkbox"> - {t("settings:ui.collapseThinking.label")} - -
- {t("settings:ui.collapseThinking.description")} + +
+ handleReasoningBlockCollapsedChange(e.target.checked)} + data-testid="collapse-thinking-checkbox"> + {t("settings:ui.collapseThinking.label")} + +
+ {t("settings:ui.collapseThinking.description")} +
-
+ {/* Enter Key Behavior Setting */} -
- handleEnterBehaviorChange(e.target.checked)} - data-testid="enter-behavior-checkbox"> - - {t("settings:ui.requireCtrlEnterToSend.label", { primaryMod })} - - -
- {t("settings:ui.requireCtrlEnterToSend.description", { primaryMod })} + +
+ handleEnterBehaviorChange(e.target.checked)} + data-testid="enter-behavior-checkbox"> + + {t("settings:ui.requireCtrlEnterToSend.label", { primaryMod })} + + +
+ {t("settings:ui.requireCtrlEnterToSend.description", { primaryMod })} +
-
+
diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx index 494070a135..cd68c371d6 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx @@ -42,6 +42,22 @@ vi.mock("@src/components/ui", () => ({ ), StandardTooltip: ({ children }: any) => <>{children}, + Popover: ({ children }: any) => <>{children}, + PopoverTrigger: ({ children }: any) => <>{children}, + PopoverContent: ({ children }: any) =>
{children}
, + Tooltip: ({ children }: any) => <>{children}, + TooltipProvider: ({ children }: any) => <>{children}, + TooltipTrigger: ({ children }: any) => <>{children}, + TooltipContent: ({ children }: any) =>
{children}
, +})) + +// Mock ModesView and McpView since they're rendered during indexing +vi.mock("@src/components/modes/ModesView", () => ({ + default: () => null, +})) + +vi.mock("@src/components/mcp/McpView", () => ({ + default: () => null, })) // Mock Tab components @@ -109,6 +125,10 @@ vi.mock("../UISettings", () => ({ UISettings: () => null, })) +vi.mock("../SettingsSearch", () => ({ + SettingsSearch: () => null, +})) + describe("SettingsView - Change Detection Fix", () => { let queryClient: QueryClient diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 80dfc7850f..913cce743e 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -1,6 +1,6 @@ // pnpm --filter @roo-code/vscode-webview test src/components/settings/__tests__/SettingsView.spec.tsx -import { render, screen, fireEvent } from "@/utils/test-utils" +import { render, screen, fireEvent, within } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { vscode } from "@/utils/vscode" @@ -76,7 +76,9 @@ vi.mock("../../../components/common/Tab", () => ({ ...vi.importActual("../../../components/common/Tab"), Tab: ({ children }: any) =>
{children}
, TabHeader: ({ children }: any) =>
{children}
, - TabContent: ({ children }: any) =>
{children}
, + TabContent: ({ children, "data-testid": dataTestId }: any) => ( +
{children}
+ ), TabList: ({ children, value, onValueChange, "data-testid": dataTestId }: any) => { // Store onValueChange in a global variable so TabTrigger can access it ;(window as any).__onValueChange = onValueChange @@ -132,8 +134,8 @@ vi.mock("@/components/ui", () => ({ Slider: ({ value, onValueChange, "data-testid": dataTestId }: any) => ( onValueChange([parseFloat(e.target.value)])} + value={value?.[0] ?? 0} + onChange={(e) => onValueChange?.([parseFloat(e.target.value)])} data-testid={dataTestId} /> ), @@ -258,7 +260,10 @@ const renderSettingsView = () => { ) } - return { onDone, activateTab } + // Helper to get elements within the settings content (not the indexing container) + const getSettingsContent = () => screen.getByTestId("settings-content") + + return { onDone, activateTab, getSettingsContent } } describe("SettingsView - Sound Settings", () => { @@ -268,40 +273,43 @@ describe("SettingsView - Sound Settings", () => { it("initializes with tts disabled by default", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the notifications tab activateTab("notifications") - const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox") + const content = getSettingsContent() + const ttsCheckbox = within(content).getByTestId("tts-enabled-checkbox") expect(ttsCheckbox).not.toBeChecked() // Speed slider should not be visible when tts is disabled - expect(screen.queryByTestId("tts-speed-slider")).not.toBeInTheDocument() + expect(within(content).queryByTestId("tts-speed-slider")).not.toBeInTheDocument() }) it("initializes with sound disabled by default", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the notifications tab activateTab("notifications") - const soundCheckbox = screen.getByTestId("sound-enabled-checkbox") + const content = getSettingsContent() + const soundCheckbox = within(content).getByTestId("sound-enabled-checkbox") expect(soundCheckbox).not.toBeChecked() // Volume slider should not be visible when sound is disabled - expect(screen.queryByTestId("sound-volume-slider")).not.toBeInTheDocument() + expect(within(content).queryByTestId("sound-volume-slider")).not.toBeInTheDocument() }) it("toggles tts setting and sends message to VSCode", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the notifications tab activateTab("notifications") - const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox") + const content = getSettingsContent() + const ttsCheckbox = within(content).getByTestId("tts-enabled-checkbox") // Enable tts fireEvent.click(ttsCheckbox) @@ -323,12 +331,13 @@ describe("SettingsView - Sound Settings", () => { it("toggles sound setting and sends message to VSCode", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the notifications tab activateTab("notifications") - const soundCheckbox = screen.getByTestId("sound-enabled-checkbox") + const content = getSettingsContent() + const soundCheckbox = within(content).getByTestId("sound-enabled-checkbox") // Enable sound fireEvent.click(soundCheckbox) @@ -350,51 +359,54 @@ describe("SettingsView - Sound Settings", () => { it("shows tts slider when sound is enabled", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the notifications tab activateTab("notifications") + const content = getSettingsContent() // Enable tts - const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox") + const ttsCheckbox = within(content).getByTestId("tts-enabled-checkbox") fireEvent.click(ttsCheckbox) // Speed slider should be visible - const speedSlider = screen.getByTestId("tts-speed-slider") + const speedSlider = within(content).getByTestId("tts-speed-slider") expect(speedSlider).toBeInTheDocument() expect(speedSlider).toHaveValue("1") }) it("shows volume slider when sound is enabled", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the notifications tab activateTab("notifications") + const content = getSettingsContent() // Enable sound - const soundCheckbox = screen.getByTestId("sound-enabled-checkbox") + const soundCheckbox = within(content).getByTestId("sound-enabled-checkbox") fireEvent.click(soundCheckbox) // Volume slider should be visible - const volumeSlider = screen.getByTestId("sound-volume-slider") + const volumeSlider = within(content).getByTestId("sound-volume-slider") expect(volumeSlider).toBeInTheDocument() expect(volumeSlider).toHaveValue("0.5") }) it("updates speed and sends message to VSCode when slider changes", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the notifications tab activateTab("notifications") + const content = getSettingsContent() // Enable tts - const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox") + const ttsCheckbox = within(content).getByTestId("tts-enabled-checkbox") fireEvent.click(ttsCheckbox) // Change speed - const speedSlider = screen.getByTestId("tts-speed-slider") + const speedSlider = within(content).getByTestId("tts-speed-slider") fireEvent.change(speedSlider, { target: { value: "0.75" } }) // Click Save to save settings @@ -414,22 +426,23 @@ describe("SettingsView - Sound Settings", () => { it("updates volume and sends message to VSCode when slider changes", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the notifications tab activateTab("notifications") + const content = getSettingsContent() // Enable sound - const soundCheckbox = screen.getByTestId("sound-enabled-checkbox") + const soundCheckbox = within(content).getByTestId("sound-enabled-checkbox") fireEvent.click(soundCheckbox) // Change volume - const volumeSlider = screen.getByTestId("sound-volume-slider") + const volumeSlider = within(content).getByTestId("sound-volume-slider") fireEvent.change(volumeSlider, { target: { value: "0.75" } }) - // Click Save to save settings - use getAllByTestId to handle multiple elements - const saveButtons = screen.getAllByTestId("save-button") - fireEvent.click(saveButtons[0]) + // Click Save to save settings + const saveButton = screen.getByTestId("save-button") + fireEvent.click(saveButton) // Verify message sent to VSCode expect(vscode.postMessage).toHaveBeenCalledWith( @@ -462,39 +475,41 @@ describe("SettingsView - Allowed Commands", () => { it("shows allowed commands section when alwaysAllowExecute is enabled", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the autoApprove tab activateTab("autoApprove") + const content = getSettingsContent() // Enable always allow execute - const executeCheckbox = screen.getByTestId("always-allow-execute-toggle") + const executeCheckbox = within(content).getByTestId("always-allow-execute-toggle") fireEvent.click(executeCheckbox) // Verify allowed commands section appears - expect(screen.getByTestId("allowed-commands-heading")).toBeInTheDocument() - expect(screen.getByTestId("command-input")).toBeInTheDocument() + expect(within(content).getByTestId("allowed-commands-heading")).toBeInTheDocument() + expect(within(content).getByTestId("command-input")).toBeInTheDocument() }) it("adds new command to the list", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the autoApprove tab activateTab("autoApprove") + const content = getSettingsContent() // Enable always allow execute - const executeCheckbox = screen.getByTestId("always-allow-execute-toggle") + const executeCheckbox = within(content).getByTestId("always-allow-execute-toggle") fireEvent.click(executeCheckbox) // Add a new command - const input = screen.getByTestId("command-input") + const input = within(content).getByTestId("command-input") fireEvent.change(input, { target: { value: "npm test" } }) - const addButton = screen.getByTestId("add-command-button") + const addButton = within(content).getByTestId("add-command-button") fireEvent.click(addButton) // Verify command was added - expect(screen.getByText("npm test")).toBeInTheDocument() + expect(within(content).getByText("npm test")).toBeInTheDocument() // Verify VSCode message was sent expect(vscode.postMessage).toHaveBeenCalledWith({ @@ -507,27 +522,28 @@ describe("SettingsView - Allowed Commands", () => { it("removes command from the list", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the autoApprove tab activateTab("autoApprove") + const content = getSettingsContent() // Enable always allow execute - const executeCheckbox = screen.getByTestId("always-allow-execute-toggle") + const executeCheckbox = within(content).getByTestId("always-allow-execute-toggle") fireEvent.click(executeCheckbox) // Add a command - const input = screen.getByTestId("command-input") + const input = within(content).getByTestId("command-input") fireEvent.change(input, { target: { value: "npm test" } }) - const addButton = screen.getByTestId("add-command-button") + const addButton = within(content).getByTestId("add-command-button") fireEvent.click(addButton) // Remove the command - const removeButton = screen.getByTestId("remove-command-0") + const removeButton = within(content).getByTestId("remove-command-0") fireEvent.click(removeButton) // Verify command was removed - expect(screen.queryByText("npm test")).not.toBeInTheDocument() + expect(within(content).queryByText("npm test")).not.toBeInTheDocument() // Verify VSCode message was sent expect(vscode.postMessage).toHaveBeenLastCalledWith({ @@ -556,13 +572,14 @@ describe("SettingsView - Allowed Commands", () => { it("shows unsaved changes dialog when clicking Done with unsaved changes", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the notifications tab activateTab("notifications") + const content = getSettingsContent() // Make a change to create unsaved changes - const soundCheckbox = screen.getByTestId("sound-enabled-checkbox") + const soundCheckbox = within(content).getByTestId("sound-enabled-checkbox") fireEvent.click(soundCheckbox) // Click the Done button @@ -599,18 +616,19 @@ describe("SettingsView - Duplicate Commands", () => { it("prevents duplicate commands", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the autoApprove tab activateTab("autoApprove") + const content = getSettingsContent() // Enable always allow execute - const executeCheckbox = screen.getByTestId("always-allow-execute-toggle") + const executeCheckbox = within(content).getByTestId("always-allow-execute-toggle") fireEvent.click(executeCheckbox) // Add a command twice - const input = screen.getByTestId("command-input") - const addButton = screen.getByTestId("add-command-button") + const input = within(content).getByTestId("command-input") + const addButton = within(content).getByTestId("add-command-button") // First addition fireEvent.change(input, { target: { value: "npm test" } }) @@ -620,31 +638,32 @@ describe("SettingsView - Duplicate Commands", () => { fireEvent.change(input, { target: { value: "npm test" } }) fireEvent.click(addButton) - // Verify command appears only once - const commands = screen.getAllByText("npm test") + // Verify command appears only once in active tab + const commands = within(content).getAllByText("npm test") expect(commands).toHaveLength(1) }) it("saves allowed commands when clicking Save", () => { // Render once and get the activateTab helper - const { activateTab } = renderSettingsView() + const { activateTab, getSettingsContent } = renderSettingsView() // Activate the autoApprove tab activateTab("autoApprove") + const content = getSettingsContent() // Enable always allow execute - const executeCheckbox = screen.getByTestId("always-allow-execute-toggle") + const executeCheckbox = within(content).getByTestId("always-allow-execute-toggle") fireEvent.click(executeCheckbox) // Add a command - const input = screen.getByTestId("command-input") + const input = within(content).getByTestId("command-input") fireEvent.change(input, { target: { value: "npm test" } }) - const addButton = screen.getByTestId("add-command-button") + const addButton = within(content).getByTestId("add-command-button") fireEvent.click(addButton) - // Click Save - use getAllByTestId to handle multiple elements - const saveButtons = screen.getAllByTestId("save-button") - fireEvent.click(saveButtons[0]) + // Click Save + const saveButton = screen.getByTestId("save-button") + fireEvent.click(saveButton) // Verify VSCode messages were sent expect(vscode.postMessage).toHaveBeenCalledWith( diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx index b40e908f64..6bc6c60167 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx @@ -47,6 +47,18 @@ vi.mock("@src/components/ui", () => ({ TooltipProvider: ({ children }: any) => <>{children}, TooltipTrigger: ({ children }: any) => <>{children}, StandardTooltip: ({ children, content }: any) =>
{children}
, + Popover: ({ children }: any) => <>{children}, + PopoverTrigger: ({ children }: any) => <>{children}, + PopoverContent: ({ children }: any) =>
{children}
, +})) + +// Mock ModesView and McpView since they're rendered during indexing +vi.mock("@src/components/modes/ModesView", () => ({ + default: () => null, +})) + +vi.mock("@src/components/mcp/McpView", () => ({ + default: () => null, })) // Mock Tab components @@ -115,6 +127,9 @@ vi.mock("../SectionHeader", () => ({ vi.mock("../Section", () => ({ Section: ({ children }: any) =>
{children}
, })) +vi.mock("../SettingsSearch", () => ({ + SettingsSearch: () => null, +})) import { useExtensionState } from "@src/context/ExtensionStateContext" import ApiOptions from "../ApiOptions" diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index d35ec547b4..0615d633af 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -77,7 +77,7 @@ export const PROVIDERS = [ { value: "fireworks", label: "Fireworks AI" }, { value: "featherless", label: "Featherless AI" }, { value: "io-intelligence", label: "IO Intelligence" }, - { value: "roo", label: "Roo Code Cloud" }, + { value: "roo", label: "Roo Code Router" }, { value: "vercel-ai-gateway", label: "Vercel AI Gateway" }, { value: "minimax", label: "MiniMax" }, { value: "baseten", label: "Baseten" }, diff --git a/webview-ui/src/components/settings/providers/Roo.tsx b/webview-ui/src/components/settings/providers/Roo.tsx index 48d845998d..b272d8b2f2 100644 --- a/webview-ui/src/components/settings/providers/Roo.tsx +++ b/webview-ui/src/components/settings/providers/Roo.tsx @@ -56,8 +56,8 @@ export const Roo = ({ defaultModelId={rooDefaultModelId} models={routerModels?.roo ?? {}} modelIdKey="apiModelId" - serviceName="Roo Code Cloud" - serviceUrl="https://roocode.com" + serviceName="Roo Code Router" + serviceUrl="https://app.roocode.com" organizationAllowList={organizationAllowList} errorMessage={modelValidationError} simplifySettings={simplifySettings} diff --git a/webview-ui/src/components/settings/useSettingsSearch.ts b/webview-ui/src/components/settings/useSettingsSearch.ts new file mode 100644 index 0000000000..9b2c4648c5 --- /dev/null +++ b/webview-ui/src/components/settings/useSettingsSearch.ts @@ -0,0 +1,153 @@ +import { useState, useMemo, useCallback, useRef, createContext, useContext } from "react" +import { Fzf } from "fzf" + +import { SectionName } from "./SettingsView" + +export interface SearchableSettingData { + settingId: string + section: SectionName + label: string + sectionLabel: string +} + +export interface SearchResult { + settingId: string + section: SectionName + label: string + sectionLabel: string + /** Character positions that matched the search query (for highlighting) */ + positions: Set +} + +/** + * Context for collecting searchable settings as they mount. + * This allows building the search index without rendering all sections. + */ +interface SearchIndexContextValue { + registerSetting: (setting: Omit) => void +} + +const SearchIndexContext = createContext(null) + +export const SearchIndexProvider = SearchIndexContext.Provider + +export function useSearchIndexContext() { + return useContext(SearchIndexContext) +} + +/** + * Hook to create a search index registry. + * Returns the context value and the current index. + */ +export function useSearchIndexRegistry(getSectionLabel: (section: SectionName) => string) { + const settingsRef = useRef>>(new Map()) + const [index, setIndex] = useState([]) + const updateScheduled = useRef(false) + + const scheduleUpdate = useCallback(() => { + if (updateScheduled.current) return + updateScheduled.current = true + + // Batch updates to avoid frequent re-renders + requestAnimationFrame(() => { + const settings = Array.from(settingsRef.current.values()).map((s) => ({ + ...s, + sectionLabel: getSectionLabel(s.section), + })) + setIndex(settings) + updateScheduled.current = false + }) + }, [getSectionLabel]) + + const contextValue = useMemo( + () => ({ + registerSetting: (setting) => { + settingsRef.current.set(setting.settingId, setting) + scheduleUpdate() + }, + }), + [scheduleUpdate], + ) + + return { contextValue, index } +} + +/** + * Scan the DOM for searchable settings within a container. + * This is called once on mount to build the index. + */ +export function scanDOMForSearchableSettings( + container: Element, + getSectionLabel: (section: SectionName) => string, +): SearchableSettingData[] { + const settings: SearchableSettingData[] = [] + const elements = container.querySelectorAll("[data-searchable]") + + elements.forEach((el) => { + const settingId = el.getAttribute("data-setting-id") + const section = el.getAttribute("data-setting-section") as SectionName | null + const label = el.getAttribute("data-setting-label") + + if (settingId && section && label) { + settings.push({ + settingId, + section, + label, + sectionLabel: getSectionLabel(section), + }) + } + }) + + return settings +} + +interface UseSettingsSearchOptions { + index: SearchableSettingData[] +} + +/** + * Hook for searching settings using fuzzy matching. + */ +export function useSettingsSearch({ index }: UseSettingsSearchOptions) { + const [searchQuery, setSearchQuery] = useState("") + const [isOpen, setIsOpen] = useState(false) + + // Create Fzf instance for fuzzy searching + const fzf = useMemo( + () => + new Fzf(index, { + selector: (item) => `${item.label} ${item.sectionLabel}`, + }), + [index], + ) + + // Search results + const results = useMemo((): SearchResult[] => { + if (!searchQuery.trim()) { + return [] + } + + const fzfResults = fzf.find(searchQuery) + return fzfResults.slice(0, 10).map((result) => ({ + settingId: result.item.settingId, + section: result.item.section, + label: result.item.label, + sectionLabel: result.item.sectionLabel, + positions: result.positions, + })) + }, [fzf, searchQuery]) + + const clearSearch = useCallback(() => { + setSearchQuery("") + setIsOpen(false) + }, []) + + return { + searchQuery, + setSearchQuery, + results, + isOpen, + setIsOpen, + clearSearch, + } +} diff --git a/webview-ui/src/components/welcome/WelcomeViewProvider.tsx b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx index c4d29df7a7..c44114b895 100644 --- a/webview-ui/src/components/welcome/WelcomeViewProvider.tsx +++ b/webview-ui/src/components/welcome/WelcomeViewProvider.tsx @@ -336,7 +336,7 @@ const WelcomeViewProvider = () => { (e.target as HTMLInputElement)) as HTMLInputElement setSelectedProvider(target.value as ProviderOption) }}> - {/* Roo Code Cloud Provider Option */} + {/* Roo Code Router Option */}

diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 4f92caf7f7..7c6e7560aa 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -8,6 +8,10 @@ "add": "Afegir capçalera", "remove": "Eliminar" }, + "search": { + "placeholder": "Cercar configuració...", + "noResults": "No s'ha trobat cap configuració" + }, "header": { "title": "Configuració", "saveButtonTooltip": "Desar canvis", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 30e482c820..8f95ca54ff 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -8,6 +8,10 @@ "add": "Header hinzufügen", "remove": "Entfernen" }, + "search": { + "placeholder": "Einstellungen durchsuchen...", + "noResults": "Keine Einstellungen gefunden" + }, "header": { "title": "Einstellungen", "saveButtonTooltip": "Änderungen speichern", diff --git a/webview-ui/src/i18n/locales/de/welcome.json b/webview-ui/src/i18n/locales/de/welcome.json index fd063ab4c8..aa1e4a5bb3 100644 --- a/webview-ui/src/i18n/locales/de/welcome.json +++ b/webview-ui/src/i18n/locales/de/welcome.json @@ -22,13 +22,13 @@ "providerSignup": { "heading": "Wähle deinen Anbieter", "chooseProvider": "Roo braucht einen LLM-Anbieter, um zu funktionieren. Wähle einen aus, um loszulegen. Du kannst später mehr hinzufügen.", - "rooCloudProvider": "Roo Code Cloud Provider", - "rooCloudDescription": "Der einfachste Weg loszulegen ist mit dem Roo Code Cloud Provider: eine kuratierte Mischung aus kostenlosen und bezahlten Modellen zu niedrigen Kosten.", + "rooCloudProvider": "Roo Code Router", + "rooCloudDescription": "Der einfachste Weg loszulegen ist mit dem Roo Code Router: eine kuratierte Mischung aus kostenlosen und bezahlten Modellen zu niedrigen Kosten.", "learnMore": "Weitere Informationen", "useAnotherProvider": "3rd-party Provider", "useAnotherProviderDescription": "Gib einen API-Schlüssel ein und leg los.", "noApiKeys": "Du möchtest dich nicht mit API-Schlüsseln und separaten Konten rumschlagen?", - "backToRoo": "Geh mit dem Roo Code Cloud Provider.", + "backToRoo": "Geh mit dem Roo Code Router.", "goBack": "Zurück", "finish": "Beenden" }, diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 17d14da8bd..551b8ab7ee 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -14,6 +14,10 @@ "nothingChangedTooltip": "Nothing changed", "doneButtonTooltip": "Discard unsaved changes and close settings panel" }, + "search": { + "placeholder": "Search settings...", + "noResults": "No settings found" + }, "unsavedChangesDialog": { "title": "Unsaved Changes", "description": "Do you want to discard changes and continue?", diff --git a/webview-ui/src/i18n/locales/en/welcome.json b/webview-ui/src/i18n/locales/en/welcome.json index 72d6f7f98c..2fec3e2c12 100644 --- a/webview-ui/src/i18n/locales/en/welcome.json +++ b/webview-ui/src/i18n/locales/en/welcome.json @@ -22,13 +22,13 @@ "providerSignup": { "heading": "Choose your provider", "chooseProvider": "Roo needs an LLM provider to work. Choose one to get started, you can add more later.", - "rooCloudProvider": "Roo Code Cloud Provider", - "rooCloudDescription": "The easiest way to start is with the Roo Code Cloud Provider: a curated mix of free and paid models at a low cost.", + "rooCloudProvider": "Roo Code Router", + "rooCloudDescription": "The easiest way to start is with the Roo Code Router: a curated mix of free and paid models at a low cost.", "learnMore": "Learn more", "useAnotherProvider": "3rd-party Provider", "useAnotherProviderDescription": "Enter an API key and get going.", "noApiKeys": "Don't want to deal with API keys and separate accounts?", - "backToRoo": "Go with the Roo Code Cloud Provider.", + "backToRoo": "Go with the Roo Code Router.", "goBack": "Back", "finish": "Finish" }, diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index f514ba12fc..852ea83a84 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -8,6 +8,10 @@ "add": "Añadir encabezado", "remove": "Eliminar" }, + "search": { + "placeholder": "Buscar configuración...", + "noResults": "No se encontró ninguna configuración" + }, "header": { "title": "Configuración", "saveButtonTooltip": "Guardar cambios", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 415bfa5220..a53a60e993 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -8,6 +8,10 @@ "add": "Ajouter un en-tête", "remove": "Supprimer" }, + "search": { + "placeholder": "Rechercher les paramètres...", + "noResults": "Aucun paramètre trouvé" + }, "header": { "title": "Paramètres", "saveButtonTooltip": "Enregistrer les modifications", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 96a33fafee..028aec0b6f 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -8,6 +8,10 @@ "add": "हेडर जोड़ें", "remove": "हटाएं" }, + "search": { + "placeholder": "सेटिंग्स खोजें...", + "noResults": "कोई सेटिंग नहीं मिली" + }, "header": { "title": "सेटिंग्स", "saveButtonTooltip": "परिवर्तन सहेजें", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index a184c5c092..9934b3a9d1 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -8,6 +8,10 @@ "add": "Tambah Header", "remove": "Hapus" }, + "search": { + "placeholder": "Cari pengaturan...", + "noResults": "Tidak ada pengaturan yang ditemukan" + }, "header": { "title": "Pengaturan", "saveButtonTooltip": "Simpan perubahan", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 69fd4e40c1..0a9fab0f88 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -8,6 +8,10 @@ "add": "Aggiungi intestazione", "remove": "Rimuovi" }, + "search": { + "placeholder": "Cerca impostazioni...", + "noResults": "Nessuna impostazione trovata" + }, "header": { "title": "Impostazioni", "saveButtonTooltip": "Salva modifiche", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 66d1b6579e..8eb0185d8e 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -8,6 +8,10 @@ "add": "ヘッダーを追加", "remove": "削除" }, + "search": { + "placeholder": "設定を検索...", + "noResults": "設定が見つかりません" + }, "header": { "title": "設定", "saveButtonTooltip": "変更を保存", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 691d756fed..e4c6f05927 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -8,6 +8,10 @@ "add": "헤더 추가", "remove": "삭제" }, + "search": { + "placeholder": "설정 검색...", + "noResults": "설정을 찾을 수 없습니다" + }, "header": { "title": "설정", "saveButtonTooltip": "변경 사항 저장", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index d0c3a78216..4dc398a52f 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -8,6 +8,10 @@ "add": "Header toevoegen", "remove": "Verwijderen" }, + "search": { + "placeholder": "Instellingen zoeken...", + "noResults": "Geen instellingen gevonden" + }, "header": { "title": "Instellingen", "saveButtonTooltip": "Wijzigingen opslaan", diff --git a/webview-ui/src/i18n/locales/nl/welcome.json b/webview-ui/src/i18n/locales/nl/welcome.json index aa5f8cc622..22508d6286 100644 --- a/webview-ui/src/i18n/locales/nl/welcome.json +++ b/webview-ui/src/i18n/locales/nl/welcome.json @@ -22,13 +22,13 @@ "providerSignup": { "heading": "Kies je provider", "chooseProvider": "Roo heeft een LLM-provider nodig om te werken. Kies er een om te beginnen, je kunt er later meer toevoegen.", - "rooCloudProvider": "Roo Code Cloud Provider", - "rooCloudDescription": "De eenvoudigste manier om te beginnen is met de Roo Code Cloud Provider: een zorgvuldig samengesteld mix van gratis en betaalde modellen tegen lage kosten.", + "rooCloudProvider": "Roo Code Router", + "rooCloudDescription": "De eenvoudigste manier om te beginnen is met de Roo Code Router: een zorgvuldig samengesteld mix van gratis en betaalde modellen tegen lage kosten.", "learnMore": "Meer informatie", "useAnotherProvider": "Provider van derden", "useAnotherProviderDescription": "Voer een API-sleutel in en begin.", "noApiKeys": "Wil je je niet bezighouden met API-sleutels en aparte accounts?", - "backToRoo": "Kies de Roo Code Cloud Provider.", + "backToRoo": "Kies de Roo Code Router.", "goBack": "Terug", "finish": "Klaar" }, diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 218cdf60e8..05a9f71cb8 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -8,6 +8,10 @@ "add": "Dodaj nagłówek", "remove": "Usuń" }, + "search": { + "placeholder": "Szukaj ustawień...", + "noResults": "Nie znaleziono ustawień" + }, "header": { "title": "Ustawienia", "saveButtonTooltip": "Zapisz zmiany", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index e96dc4c345..c33a7b2a96 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -8,6 +8,10 @@ "add": "Adicionar cabeçalho", "remove": "Remover" }, + "search": { + "placeholder": "Pesquisar configurações...", + "noResults": "Nenhuma configuração encontrada" + }, "header": { "title": "Configurações", "saveButtonTooltip": "Salvar alterações", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index aaae476d9c..87ab2bc6b1 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -8,6 +8,10 @@ "add": "Добавить заголовок", "remove": "Удалить" }, + "search": { + "placeholder": "Поиск параметров...", + "noResults": "Параметры не найдены" + }, "header": { "title": "Настройки", "saveButtonTooltip": "Сохранить изменения", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 0eabf2782f..37987cb74d 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -8,6 +8,10 @@ "add": "Başlık Ekle", "remove": "Kaldır" }, + "search": { + "placeholder": "Ayarları ara...", + "noResults": "Ayar bulunamadı" + }, "header": { "title": "Ayarlar", "saveButtonTooltip": "Değişiklikleri kaydet", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index debb9fe860..d18a29dde1 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -8,6 +8,10 @@ "add": "Thêm tiêu đề", "remove": "Xóa" }, + "search": { + "placeholder": "Tìm kiếm cài đặt...", + "noResults": "Không tìm thấy cài đặt" + }, "header": { "title": "Cài đặt", "saveButtonTooltip": "Lưu thay đổi", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 6137b091e7..a5416704eb 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -8,6 +8,10 @@ "add": "添加标头", "remove": "移除" }, + "search": { + "placeholder": "搜索设置...", + "noResults": "未找到设置" + }, "header": { "title": "设置", "saveButtonTooltip": "保存更改", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 84dfdb7527..fe55c4fa79 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -8,6 +8,10 @@ "add": "新增標頭", "remove": "移除" }, + "search": { + "placeholder": "搜尋設定...", + "noResults": "找不到設定" + }, "header": { "title": "設定", "saveButtonTooltip": "儲存變更", diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index b696261f91..28dbf06aef 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -569,3 +569,20 @@ input[cmdk-input]:focus { .animate-sun { animation: sun 30s linear infinite; } + +/* Settings search highlight animation */ +@keyframes settings-highlight-fade { + 0% { + background-color: color-mix(in srgb, var(--vscode-focusBorder) 40%, transparent); + } + 100% { + background-color: transparent; + } +} + +.settings-highlight { + animation: settings-highlight-fade 1.5s ease-out forwards; + border-radius: 4px; + padding: 8px; + margin: -8px; +}