Merge main

This commit is contained in:
cte 2026-01-11 18:56:17 -08:00
commit 0dcc439b79
57 changed files with 1549 additions and 531 deletions

View file

@ -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!)

View file

@ -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.

View file

@ -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 <span className="text-violet-500">in the Cloud</span>
</h1>
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
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.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">

View file

@ -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,
},
{

View file

@ -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() {
<h1 className="text-5xl font-bold tracking-tight">Roo Code Pricing</h1>
<p className="mt-4 text-lg text-muted-foreground">
For all of our products: the Roo Code VS Code Extension, Roo Code Cloud and the Roo Code
Cloud inference Provider.
Router.
</p>
</div>
</div>
@ -230,12 +230,12 @@ export default function PricingPage() {
<div className="max-w-6xl mx-auto mt-8 p-7 flex flex-col md:flex-row gap-8 md:gap-4 bg-violet-200/20 outline-violet-700/20 outline outline-1 rounded-2xl transition-all shadow-none">
<div className="md:border-r md:pr-4">
<h3 className="text-lg font-medium mb-1">Roo Code Provider</h3>
<h3 className="text-lg font-medium mb-1">Roo Code Router</h3>
<div className="text-sm text-muted-foreground">
<p className="">
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.
<Link href="/provider" className="underline hover:no-underline ml-1">
See per model pricing.
</Link>

View file

@ -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: (
<>
<p>This is our very own model provider, optimized to work seamlessly with Roo Code Cloud.</p>
<p>This is our very own model router, optimized to work seamlessly with Roo Code Cloud.</p>
<p>You don&apos;t have to use it to use Roo Code, but it&apos;s the easiest way to do it.</p>
</>
),
},
{
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() {
<section className="relative overflow-hidden py-16">
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h1 className="text-4xl md:text-5xl font-bold tracking-tight">The Roo Code Cloud Provider</h1>
<h1 className="text-4xl md:text-5xl font-bold tracking-tight">The Roo Code Router</h1>
<p className="mx-auto mt-4 max-w-3xl md:text-lg text-muted-foreground">
The easiest way to use Roo Code (in the{" "}
<Link href="/cloud" className="underline hover:no-underline">

View file

@ -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">
<Brain className="size-3 inline mr-2 -mt-0.5" />
Roo Code Cloud Provider
Roo Code Router
</Link>
</div>
</div>
@ -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
</Link>
</div>

View file

@ -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",

View file

@ -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",

BIN
releases/3.39.3-release.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -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",

View file

@ -24,6 +24,7 @@ import { Button } from "@/components/ui"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
import { SearchableSetting } from "./SearchableSetting"
type AboutProps = HTMLAttributes<HTMLDivElement> & {
telemetrySetting: TelemetrySetting
@ -50,7 +51,10 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug,
</SectionHeader>
<Section>
<div>
<SearchableSetting
settingId="about-telemetry"
section="about"
label={t("settings:footer.telemetry.label")}>
<VSCodeCheckbox
checked={telemetrySetting !== "disabled"}
onChange={(e: any) => {
@ -67,7 +71,7 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug,
}}
/>
</p>
</div>
</SearchableSetting>
</Section>
<Section className="space-y-0">
@ -120,7 +124,11 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug,
</span>
</div>
{setDebug && (
<div className="flex flex-col gap-2 mt-4 pt-4 border-t border-vscode-settings-headerBorder">
<SearchableSetting
settingId="about-debug-mode"
section="about"
label={t("settings:about.debugMode.label")}
className="mt-4 pt-4 border-t border-vscode-settings-headerBorder">
<VSCodeCheckbox
checked={debug ?? false}
onChange={(e: any) => {
@ -132,30 +140,35 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug,
<p className="text-vscode-descriptionForeground text-sm mt-0">
{t("settings:about.debugMode.description")}
</p>
</div>
</SearchableSetting>
)}
</div>
</Section>
<Section className="space-y-0">
<h3>{t("settings:about.manageSettings")}</h3>
<div className="flex flex-wrap items-center gap-2">
<Button onClick={() => vscode.postMessage({ type: "exportSettings" })} className="w-28">
<Upload className="p-0.5" />
{t("settings:footer.settings.export")}
</Button>
<Button onClick={() => vscode.postMessage({ type: "importSettings" })} className="w-28">
<Download className="p-0.5" />
{t("settings:footer.settings.import")}
</Button>
<Button
variant="destructive"
onClick={() => vscode.postMessage({ type: "resetState" })}
className="w-28">
<TriangleAlert className="p-0.5" />
{t("settings:footer.settings.reset")}
</Button>
</div>
<SearchableSetting
settingId="about-manage-settings"
section="about"
label={t("settings:about.manageSettings")}>
<h3>{t("settings:about.manageSettings")}</h3>
<div className="flex flex-wrap items-center gap-2">
<Button onClick={() => vscode.postMessage({ type: "exportSettings" })} className="w-28">
<Upload className="p-0.5" />
{t("settings:footer.settings.export")}
</Button>
<Button onClick={() => vscode.postMessage({ type: "importSettings" })} className="w-28">
<Download className="p-0.5" />
{t("settings:footer.settings.import")}
</Button>
<Button
variant="destructive"
onClick={() => vscode.postMessage({ type: "resetState" })}
className="w-28">
<TriangleAlert className="p-0.5" />
{t("settings:footer.settings.reset")}
</Button>
</div>
</SearchableSetting>
</Section>
</div>
)

View file

@ -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 = ({
<Section>
<div className="space-y-4">
<VSCodeCheckbox
checked={effectiveAutoApprovalEnabled}
aria-label={t("settings:autoApprove.toggleAriaLabel")}
onChange={() => {
const newValue = !(autoApprovalEnabled ?? false)
setAutoApprovalEnabled(newValue)
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
}}>
<span className="font-medium">{t("settings:autoApprove.enabled")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1">
<p>{t("settings:autoApprove.description")}</p>
<p>
<Trans
i18nKey="settings:autoApprove.toggleShortcut"
components={{
SettingsLink: (
<a
href="#"
className="text-vscode-textLink-foreground hover:underline cursor-pointer"
onClick={(e) => {
e.preventDefault()
// Send message to open keyboard shortcuts with search for toggle command
vscode.postMessage({
type: "openKeyboardShortcuts",
text: `${Package.name}.toggleAutoApprove`,
})
}}
/>
),
}}
/>
</p>
</div>
<SearchableSetting
settingId="auto-approve-enabled"
section="autoApprove"
label={t("settings:autoApprove.enabled")}>
<VSCodeCheckbox
checked={effectiveAutoApprovalEnabled}
aria-label={t("settings:autoApprove.toggleAriaLabel")}
onChange={() => {
const newValue = !(autoApprovalEnabled ?? false)
setAutoApprovalEnabled(newValue)
vscode.postMessage({ type: "autoApprovalEnabled", bool: newValue })
}}>
<span className="font-medium">{t("settings:autoApprove.enabled")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1">
<p>{t("settings:autoApprove.description")}</p>
<p>
<Trans
i18nKey="settings:autoApprove.toggleShortcut"
components={{
SettingsLink: (
<a
href="#"
className="text-vscode-textLink-foreground hover:underline cursor-pointer"
onClick={(e) => {
e.preventDefault()
// Send message to open keyboard shortcuts with search for toggle command
vscode.postMessage({
type: "openKeyboardShortcuts",
text: `${Package.name}.toggleAutoApprove`,
})
}}
/>
),
}}
/>
</p>
</div>
</SearchableSetting>
<AutoApproveToggle
alwaysAllowReadOnly={alwaysAllowReadOnly}
@ -179,7 +185,10 @@ export const AutoApproveSettings = ({
<span className="codicon codicon-eye" />
<div>{t("settings:autoApprove.readOnly.label")}</div>
</div>
<div>
<SearchableSetting
settingId="auto-approve-readonly-outside-workspace"
section="autoApprove"
label={t("settings:autoApprove.readOnly.outsideWorkspace.label")}>
<VSCodeCheckbox
checked={alwaysAllowReadOnlyOutsideWorkspace}
onChange={(e: any) =>
@ -193,7 +202,7 @@ export const AutoApproveSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.readOnly.outsideWorkspace.description")}
</div>
</div>
</SearchableSetting>
</div>
)}
@ -203,7 +212,10 @@ export const AutoApproveSettings = ({
<span className="codicon codicon-edit" />
<div>{t("settings:autoApprove.write.label")}</div>
</div>
<div>
<SearchableSetting
settingId="auto-approve-write-outside-workspace"
section="autoApprove"
label={t("settings:autoApprove.write.outsideWorkspace.label")}>
<VSCodeCheckbox
checked={alwaysAllowWriteOutsideWorkspace}
onChange={(e: any) =>
@ -217,8 +229,11 @@ export const AutoApproveSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.write.outsideWorkspace.description")}
</div>
</div>
<div>
</SearchableSetting>
<SearchableSetting
settingId="auto-approve-write-protected"
section="autoApprove"
label={t("settings:autoApprove.write.protected.label")}>
<VSCodeCheckbox
checked={alwaysAllowWriteProtected}
onChange={(e: any) =>
@ -230,7 +245,7 @@ export const AutoApproveSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
{t("settings:autoApprove.write.protected.description")}
</div>
</div>
</SearchableSetting>
</div>
)}
@ -240,7 +255,10 @@ export const AutoApproveSettings = ({
<span className="codicon codicon-question" />
<div>{t("settings:autoApprove.followupQuestions.label")}</div>
</div>
<div>
<SearchableSetting
settingId="auto-approve-followup-timeout"
section="autoApprove"
label={t("settings:autoApprove.followupQuestions.timeoutLabel")}>
<div className="flex items-center gap-2">
<Slider
min={1000}
@ -257,7 +275,7 @@ export const AutoApproveSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.followupQuestions.timeoutLabel")}
</div>
</div>
</SearchableSetting>
</div>
)}
@ -268,14 +286,17 @@ export const AutoApproveSettings = ({
<div>{t("settings:autoApprove.execute.label")}</div>
</div>
<div>
<SearchableSetting
settingId="auto-approve-allowed-commands"
section="autoApprove"
label={t("settings:autoApprove.execute.allowedCommands")}>
<label className="block font-medium mb-1" data-testid="allowed-commands-heading">
{t("settings:autoApprove.execute.allowedCommands")}
</label>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.execute.allowedCommandsDescription")}
</div>
</div>
</SearchableSetting>
<div className="flex gap-2">
<Input
@ -320,14 +341,18 @@ export const AutoApproveSettings = ({
</div>
{/* Denied Commands Section */}
<div className="mt-6">
<SearchableSetting
settingId="auto-approve-denied-commands"
section="autoApprove"
label={t("settings:autoApprove.execute.deniedCommands")}
className="mt-6">
<label className="block font-medium mb-1" data-testid="denied-commands-heading">
{t("settings:autoApprove.execute.deniedCommands")}
</label>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.execute.deniedCommandsDescription")}
</div>
</div>
</SearchableSetting>
<div className="flex gap-2">
<Input

View file

@ -17,6 +17,7 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
import { vscode } from "@/utils/vscode"
import { buildDocLink } from "@src/utils/docLinks"
import { SearchableSetting } from "./SearchableSetting"
import { Section } from "./Section"
import { SectionHeader } from "./SectionHeader"
import { SetCachedStateField } from "./types"
@ -116,7 +117,10 @@ export const BrowserSettings = ({
</SectionHeader>
<Section>
<div>
<SearchableSetting
settingId="browser-enable"
section="browser"
label={t("settings:browser.enable.label")}>
<VSCodeCheckbox
checked={browserToolEnabled}
onChange={(e: any) => setCachedStateField("browserToolEnabled", e.target.checked)}>
@ -131,11 +135,14 @@ export const BrowserSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
</SearchableSetting>
{browserToolEnabled && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<SearchableSetting
settingId="browser-viewport"
section="browser"
label={t("settings:browser.viewport.label")}>
<label className="block font-medium mb-1">{t("settings:browser.viewport.label")}</label>
<Select
value={browserViewportSize}
@ -156,9 +163,12 @@ export const BrowserSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:browser.viewport.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="browser-screenshot-quality"
section="browser"
label={t("settings:browser.screenshotQuality.label")}>
<label className="block font-medium mb-1">
{t("settings:browser.screenshotQuality.label")}
</label>
@ -175,9 +185,12 @@ export const BrowserSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:browser.screenshotQuality.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="browser-remote"
section="browser"
label={t("settings:browser.remote.label")}>
<VSCodeCheckbox
checked={remoteBrowserEnabled}
onChange={(e: any) => {
@ -194,7 +207,7 @@ export const BrowserSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:browser.remote.description")}
</div>
</div>
</SearchableSetting>
{remoteBrowserEnabled && (
<>

View file

@ -9,6 +9,7 @@ import { Slider } from "@/components/ui"
import { SetCachedStateField } from "./types"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
import { SearchableSetting } from "./SearchableSetting"
import {
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
MAX_CHECKPOINT_TIMEOUT_SECONDS,
@ -38,7 +39,10 @@ export const CheckpointSettings = ({
</SectionHeader>
<Section>
<div>
<SearchableSetting
settingId="checkpoints-enable"
section="checkpoints"
label={t("settings:checkpoints.enable.label")}>
<VSCodeCheckbox
checked={enableCheckpoints}
onChange={(e: any) => {
@ -55,10 +59,14 @@ export const CheckpointSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
</SearchableSetting>
{enableCheckpoints && (
<div className="mt-4">
<SearchableSetting
settingId="checkpoints-timeout"
section="checkpoints"
label={t("settings:checkpoints.timeout.label")}
className="mt-4">
<label className="block text-sm font-medium mb-2">
{t("settings:checkpoints.timeout.label")}
</label>
@ -81,7 +89,7 @@ export const CheckpointSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:checkpoints.timeout.description")}
</div>
</div>
</SearchableSetting>
)}
</Section>
</div>

View file

@ -10,6 +10,7 @@ import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, S
import { SetCachedStateField } from "./types"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
import { SearchableSetting } from "./SearchableSetting"
import { vscode } from "@/utils/vscode"
type ContextManagementSettingsProps = HTMLAttributes<HTMLDivElement> & {
@ -114,7 +115,10 @@ export const ContextManagementSettings = ({
</SectionHeader>
<Section>
<div>
<SearchableSetting
settingId="context-open-tabs"
section="contextManagement"
label={t("settings:contextManagement.openTabs.label")}>
<span className="block font-medium mb-1">{t("settings:contextManagement.openTabs.label")}</span>
<div className="flex items-center gap-2">
<Slider
@ -130,9 +134,12 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:contextManagement.openTabs.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="context-workspace-files"
section="contextManagement"
label={t("settings:contextManagement.workspaceFiles.label")}>
<span className="block font-medium mb-1">
{t("settings:contextManagement.workspaceFiles.label")}
</span>
@ -150,9 +157,12 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:contextManagement.workspaceFiles.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="context-max-git-status-files"
section="contextManagement"
label={t("settings:contextManagement.maxGitStatusFiles.label")}>
<span className="block font-medium mb-1">
{t("settings:contextManagement.maxGitStatusFiles.label")}
</span>
@ -170,9 +180,12 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:contextManagement.maxGitStatusFiles.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="context-max-concurrent-file-reads"
section="contextManagement"
label={t("settings:contextManagement.maxConcurrentFileReads.label")}>
<span className="block font-medium mb-1">
{t("settings:contextManagement.maxConcurrentFileReads.label")}
</span>
@ -190,9 +203,12 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
{t("settings:contextManagement.maxConcurrentFileReads.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="context-show-rooignored-files"
section="contextManagement"
label={t("settings:contextManagement.rooignore.label")}>
<VSCodeCheckbox
checked={showRooIgnoredFiles}
onChange={(e: any) => setCachedStateField("showRooIgnoredFiles", e.target.checked)}
@ -204,9 +220,12 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
{t("settings:contextManagement.rooignore.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="context-enable-subfolder-rules"
section="contextManagement"
label={t("settings:contextManagement.enableSubfolderRules.label")}>
<VSCodeCheckbox
checked={enableSubfolderRules}
onChange={(e: any) => setCachedStateField("enableSubfolderRules", e.target.checked)}
@ -218,9 +237,12 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
{t("settings:contextManagement.enableSubfolderRules.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="context-max-read-file"
section="contextManagement"
label={t("settings:contextManagement.maxReadFile.label")}>
<div className="flex flex-col gap-2">
<span className="font-medium">{t("settings:contextManagement.maxReadFile.label")}</span>
<div className="flex items-center gap-4">
@ -254,9 +276,12 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-2">
{t("settings:contextManagement.maxReadFile.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="context-max-image-file-size"
section="contextManagement"
label={t("settings:contextManagement.maxImageFileSize.label")}>
<div className="flex flex-col gap-2">
<span className="font-medium">{t("settings:contextManagement.maxImageFileSize.label")}</span>
<div className="flex items-center gap-4">
@ -282,9 +307,12 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-2">
{t("settings:contextManagement.maxImageFileSize.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="context-max-total-image-size"
section="contextManagement"
label={t("settings:contextManagement.maxTotalImageSize.label")}>
<div className="flex flex-col gap-2">
<span className="font-medium">{t("settings:contextManagement.maxTotalImageSize.label")}</span>
<div className="flex items-center gap-4">
@ -310,9 +338,12 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-2">
{t("settings:contextManagement.maxTotalImageSize.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="context-include-diagnostic-messages"
section="contextManagement"
label={t("settings:contextManagement.diagnostics.includeMessages.label")}>
<VSCodeCheckbox
checked={includeDiagnosticMessages}
onChange={(e: any) => setCachedStateField("includeDiagnosticMessages", e.target.checked)}
@ -324,9 +355,12 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
{t("settings:contextManagement.diagnostics.includeMessages.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="context-max-diagnostic-messages"
section="contextManagement"
label={t("settings:contextManagement.diagnostics.maxMessages.label")}>
<span className="block font-medium mb-1">
{t("settings:contextManagement.diagnostics.maxMessages.label")}
</span>
@ -379,9 +413,12 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:contextManagement.diagnostics.maxMessages.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="context-write-delay"
section="contextManagement"
label={t("settings:contextManagement.diagnostics.delayAfterWrite.label")}>
<span className="block font-medium mb-1">
{t("settings:contextManagement.diagnostics.delayAfterWrite.label")}
</span>
@ -399,9 +436,12 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:contextManagement.diagnostics.delayAfterWrite.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="context-include-current-time"
section="contextManagement"
label={t("settings:contextManagement.includeCurrentTime.label")}>
<VSCodeCheckbox
checked={includeCurrentTime}
onChange={(e: any) => setCachedStateField("includeCurrentTime", e.target.checked)}
@ -413,9 +453,12 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
{t("settings:contextManagement.includeCurrentTime.description")}
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="context-include-current-cost"
section="contextManagement"
label={t("settings:contextManagement.includeCurrentCost.label")}>
<VSCodeCheckbox
checked={includeCurrentCost}
onChange={(e: any) => setCachedStateField("includeCurrentCost", e.target.checked)}
@ -427,15 +470,20 @@ export const ContextManagementSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-3">
{t("settings:contextManagement.includeCurrentCost.description")}
</div>
</div>
</SearchableSetting>
</Section>
<Section className="pt-2">
<VSCodeCheckbox
checked={autoCondenseContext}
onChange={(e: any) => setCachedStateField("autoCondenseContext", e.target.checked)}
data-testid="auto-condense-context-checkbox">
<span className="font-medium">{t("settings:contextManagement.autoCondenseContext.name")}</span>
</VSCodeCheckbox>
<SearchableSetting
settingId="context-auto-condense"
section="contextManagement"
label={t("settings:contextManagement.autoCondenseContext.name")}>
<VSCodeCheckbox
checked={autoCondenseContext}
onChange={(e: any) => setCachedStateField("autoCondenseContext", e.target.checked)}
data-testid="auto-condense-context-checkbox">
<span className="font-medium">{t("settings:contextManagement.autoCondenseContext.name")}</span>
</VSCodeCheckbox>
</SearchableSetting>
{autoCondenseContext && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div className="flex items-center gap-4 font-bold">

View file

@ -11,6 +11,7 @@ import { cn } from "@src/lib/utils"
import { SetExperimentEnabled } from "./types"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
import { SearchableSetting } from "./SearchableSetting"
import { ExperimentalFeature } from "./ExperimentalFeature"
import { ImageGenerationSettings } from "./ImageGenerationSettings"
import { CustomToolsSettings } from "./CustomToolsSettings"
@ -59,16 +60,25 @@ export const ExperimentalSettings = ({
// Hide MULTIPLE_NATIVE_TOOL_CALLS - feature is on hold
.filter(([key]) => key !== "MULTIPLE_NATIVE_TOOL_CALLS")
.map((config) => {
// Use the same translation key pattern as ExperimentalFeature
const experimentKey = config[0]
const label = t(`settings:experimental.${experimentKey}.name`)
if (config[0] === "MULTI_FILE_APPLY_DIFF") {
return (
<ExperimentalFeature
<SearchableSetting
key={config[0]}
experimentKey={config[0]}
enabled={experiments[EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF] ?? false}
onChange={(enabled) =>
setExperimentEnabled(EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF, enabled)
}
/>
settingId={`experimental-${config[0].toLowerCase()}`}
section="experimental"
label={label}>
<ExperimentalFeature
experimentKey={config[0]}
enabled={experiments[EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF] ?? false}
onChange={(enabled) =>
setExperimentEnabled(EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF, enabled)
}
/>
</SearchableSetting>
)
}
if (
@ -78,42 +88,61 @@ export const ExperimentalSettings = ({
setImageGenerationSelectedModel
) {
return (
<ImageGenerationSettings
<SearchableSetting
key={config[0]}
enabled={experiments[EXPERIMENT_IDS.IMAGE_GENERATION] ?? false}
onChange={(enabled) =>
setExperimentEnabled(EXPERIMENT_IDS.IMAGE_GENERATION, enabled)
}
imageGenerationProvider={imageGenerationProvider}
openRouterImageApiKey={openRouterImageApiKey}
openRouterImageGenerationSelectedModel={openRouterImageGenerationSelectedModel}
setImageGenerationProvider={setImageGenerationProvider}
setOpenRouterImageApiKey={setOpenRouterImageApiKey}
setImageGenerationSelectedModel={setImageGenerationSelectedModel}
/>
settingId={`experimental-${config[0].toLowerCase()}`}
section="experimental"
label={label}>
<ImageGenerationSettings
enabled={experiments[EXPERIMENT_IDS.IMAGE_GENERATION] ?? false}
onChange={(enabled) =>
setExperimentEnabled(EXPERIMENT_IDS.IMAGE_GENERATION, enabled)
}
imageGenerationProvider={imageGenerationProvider}
openRouterImageApiKey={openRouterImageApiKey}
openRouterImageGenerationSelectedModel={openRouterImageGenerationSelectedModel}
setImageGenerationProvider={setImageGenerationProvider}
setOpenRouterImageApiKey={setOpenRouterImageApiKey}
setImageGenerationSelectedModel={setImageGenerationSelectedModel}
/>
</SearchableSetting>
)
}
if (config[0] === "CUSTOM_TOOLS") {
return (
<CustomToolsSettings
<SearchableSetting
key={config[0]}
enabled={experiments[EXPERIMENT_IDS.CUSTOM_TOOLS] ?? false}
onChange={(enabled) => setExperimentEnabled(EXPERIMENT_IDS.CUSTOM_TOOLS, enabled)}
/>
settingId={`experimental-${config[0].toLowerCase()}`}
section="experimental"
label={label}>
<CustomToolsSettings
enabled={experiments[EXPERIMENT_IDS.CUSTOM_TOOLS] ?? false}
onChange={(enabled) =>
setExperimentEnabled(EXPERIMENT_IDS.CUSTOM_TOOLS, enabled)
}
/>
</SearchableSetting>
)
}
return (
<ExperimentalFeature
<SearchableSetting
key={config[0]}
experimentKey={config[0]}
enabled={experiments[EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS]] ?? false}
onChange={(enabled) =>
setExperimentEnabled(
EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS],
enabled,
)
}
/>
settingId={`experimental-${config[0].toLowerCase()}`}
section="experimental"
label={label}>
<ExperimentalFeature
experimentKey={config[0]}
enabled={
experiments[EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS]] ?? false
}
onChange={(enabled) =>
setExperimentEnabled(
EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS],
enabled,
)
}
/>
</SearchableSetting>
)
})}
</Section>

View file

@ -12,6 +12,7 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectVa
import { SetCachedStateField } from "./types"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
import { SearchableSetting } from "./SearchableSetting"
type LanguageSettingsProps = HTMLAttributes<HTMLDivElement> & {
language: string
@ -31,21 +32,28 @@ export const LanguageSettings = ({ language, setCachedStateField, className, ...
</SectionHeader>
<Section>
<Select value={language} onValueChange={(value) => setCachedStateField("language", value as Language)}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{Object.entries(LANGUAGES).map(([code, name]) => (
<SelectItem key={code} value={code}>
{name}
<span className="text-muted-foreground">({code})</span>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<SearchableSetting
settingId="language-select"
section="language"
label={t("settings:sections.language")}>
<Select
value={language}
onValueChange={(value) => setCachedStateField("language", value as Language)}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{Object.entries(LANGUAGES).map(([code, name]) => (
<SelectItem key={code} value={code}>
{name}
<span className="text-muted-foreground">({code})</span>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</SearchableSetting>
</Section>
</div>
)

View file

@ -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<HTMLDivElement> & {
@ -35,7 +36,10 @@ export const NotificationSettings = ({
</SectionHeader>
<Section>
<div>
<SearchableSetting
settingId="notifications-tts"
section="notifications"
label={t("settings:notifications.tts.label")}>
<VSCodeCheckbox
checked={ttsEnabled}
onChange={(e: any) => setCachedStateField("ttsEnabled", e.target.checked)}
@ -45,11 +49,14 @@ export const NotificationSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:notifications.tts.description")}
</div>
</div>
</SearchableSetting>
{ttsEnabled && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<SearchableSetting
settingId="notifications-tts-speed"
section="notifications"
label={t("settings:notifications.tts.speedLabel")}>
<label className="block font-medium mb-1">
{t("settings:notifications.tts.speedLabel")}
</label>
@ -64,11 +71,14 @@ export const NotificationSettings = ({
/>
<span className="w-10">{((ttsSpeed ?? 1.0) * 100).toFixed(0)}%</span>
</div>
</div>
</SearchableSetting>
</div>
)}
<div>
<SearchableSetting
settingId="notifications-sound"
section="notifications"
label={t("settings:notifications.sound.label")}>
<VSCodeCheckbox
checked={soundEnabled}
onChange={(e: any) => setCachedStateField("soundEnabled", e.target.checked)}
@ -78,11 +88,14 @@ export const NotificationSettings = ({
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:notifications.sound.description")}
</div>
</div>
</SearchableSetting>
{soundEnabled && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<SearchableSetting
settingId="notifications-sound-volume"
section="notifications"
label={t("settings:notifications.sound.volumeLabel")}>
<label className="block font-medium mb-1">
{t("settings:notifications.sound.volumeLabel")}
</label>
@ -97,7 +110,7 @@ export const NotificationSettings = ({
/>
<span className="w-10">{((soundVolume ?? 0.5) * 100).toFixed(0)}%</span>
</div>
</div>
</SearchableSetting>
</div>
)}
</Section>

View file

@ -19,6 +19,7 @@ import {
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
import { SearchableSetting } from "./SearchableSetting"
interface PromptsSettingsProps {
customSupportPrompts: Record<string, string | undefined>
@ -145,7 +146,10 @@ const PromptsSettings = ({
</SectionHeader>
<Section>
<div>
<SearchableSetting
settingId="prompts-support-prompt-select"
section="prompts"
label={t("settings:sections.prompts")}>
<Select
value={activeSupportOption}
onValueChange={(type) => setActiveSupportOption(type as SupportPromptType)}>
@ -163,7 +167,7 @@ const PromptsSettings = ({
<div className="text-sm text-vscode-descriptionForeground mt-1">
{t(`prompts:supportPrompts.types.${activeSupportOption}.description`)}
</div>
</div>
</SearchableSetting>
<div key={activeSupportOption} className="mt-4">
<div className="flex justify-between items-center mb-1">

View file

@ -0,0 +1,79 @@
import { HTMLAttributes, useEffect } from "react"
import { cn } from "@/lib/utils"
import { SectionName } from "./SettingsView"
import { useSearchIndexContext } from "./useSettingsSearch"
interface SearchableSettingProps extends HTMLAttributes<HTMLDivElement> {
/**
* Unique identifier for this setting.
* Used for finding the element after tab navigation.
*/
settingId: string
/**
* The section/tab this setting belongs to.
* Used for navigation when the setting is selected from search results.
*/
section: SectionName
/**
* The label text for this setting, used for search matching.
* This should be the translated label text.
*/
label: string
children: React.ReactNode
}
/**
* Wrapper component that marks a setting as searchable.
*
* The component registers itself with the search index context on mount,
* allowing the search system to index settings as they are rendered.
*
* @example
* ```tsx
* <SearchableSetting
* settingId="browser-enable"
* section="browser"
* label={t("settings:browser.enable.label")}
* >
* <VSCodeCheckbox>
* <span className="font-medium">{t("settings:browser.enable.label")}</span>
* </VSCodeCheckbox>
* <div className="text-vscode-descriptionForeground text-sm">
* {t("settings:browser.enable.description")}
* </div>
* </SearchableSetting>
* ```
*/
export function SearchableSetting({
settingId,
section,
label,
children,
className,
...props
}: SearchableSettingProps) {
const searchContext = useSearchIndexContext()
// Register this setting with the search index on mount
// Note: We don't unregister on unmount because settings are indexed once
// during the initial tab cycling phase and remain in the index
useEffect(() => {
if (searchContext) {
searchContext.registerSetting({ settingId, section, label })
}
}, [searchContext, settingId, section, label])
return (
<div
data-searchable
data-setting-id={settingId}
data-setting-section={section}
data-setting-label={label}
className={cn(className)}
{...props}>
{children}
</div>
)
}

View file

@ -0,0 +1,123 @@
import { useRef, useEffect, useState, useCallback } from "react"
import type { LucideIcon } from "lucide-react"
import { useSettingsSearch, SearchResult, SearchableSettingData } from "./useSettingsSearch"
import { SectionName } from "./SettingsView"
import { SettingsSearchInput } from "./SettingsSearchInput"
import { SettingsSearchResults } from "./SettingsSearchResults"
interface SettingsSearchProps {
index: SearchableSettingData[]
onNavigate: (section: SectionName, settingId: string) => void
sections: { id: SectionName; icon: LucideIcon }[]
}
export function SettingsSearch({ index, onNavigate, sections }: SettingsSearchProps) {
const inputRef = useRef<HTMLInputElement>(null)
const { searchQuery, setSearchQuery, results, isOpen, setIsOpen, clearSearch } = useSettingsSearch({ index })
const [highlightedResultId, setHighlightedResultId] = useState<string | undefined>(undefined)
// Handle selection of a search result
const handleSelectResult = useCallback(
(result: SearchResult) => {
onNavigate(result.section, result.settingId)
clearSearch()
setHighlightedResultId(undefined)
// Keep focus in the input so dropdown remains open for follow-up search
setIsOpen(true)
requestAnimationFrame(() => inputRef.current?.focus())
},
[onNavigate, clearSearch, setIsOpen],
)
// Keyboard navigation inside search results
const moveHighlight = useCallback(
(direction: 1 | -1) => {
if (!results.length) return
const flatIds = results.map((r) => r.settingId)
const currentIndex = highlightedResultId ? flatIds.indexOf(highlightedResultId) : -1
const nextIndex = (currentIndex + direction + flatIds.length) % flatIds.length
setHighlightedResultId(flatIds[nextIndex])
},
[highlightedResultId, results],
)
const handleSearchKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Escape") {
setIsOpen(false)
setHighlightedResultId(undefined)
inputRef.current?.blur()
return
}
if (!results.length) return
if (event.key === "ArrowDown") {
event.preventDefault()
moveHighlight(1)
return
}
if (event.key === "ArrowUp") {
event.preventDefault()
moveHighlight(-1)
return
}
if (event.key === "Enter" && highlightedResultId) {
event.preventDefault()
const selected = results.find((r) => r.settingId === highlightedResultId)
if (selected) {
handleSelectResult(selected)
}
return
}
},
[handleSelectResult, highlightedResultId, moveHighlight, results, setIsOpen],
)
// Reset highlight based on focus and available results
useEffect(() => {
if (!isOpen || !results.length) {
setHighlightedResultId(undefined)
return
}
setHighlightedResultId((current) =>
current && results.some((r) => r.settingId === current) ? current : results[0]?.settingId,
)
}, [isOpen, results])
// Ensure highlighted search result stays visible within dropdown
useEffect(() => {
if (!highlightedResultId || !isOpen) return
const element = document.getElementById(`settings-search-result-${highlightedResultId}`)
element?.scrollIntoView({ block: "nearest" })
}, [highlightedResultId, isOpen])
return (
<div className="relative justify-end">
<SettingsSearchInput
value={searchQuery}
onChange={setSearchQuery}
onFocus={() => setIsOpen(true)}
onBlur={() => setTimeout(() => setIsOpen(false), 200)}
onKeyDown={handleSearchKeyDown}
inputRef={inputRef}
/>
{searchQuery && isOpen && (
<div className="absolute top-full min-w-[300px] right-0 mt-2 border border-vscode-dropdown-border bg-vscode-dropdown-background rounded-2xl overflow-hidden shadow-xl z-50">
<SettingsSearchResults
results={results}
query={searchQuery}
onSelectResult={handleSelectResult}
sections={sections}
highlightedResultId={highlightedResultId}
/>
</div>
)}
</div>
)
}

View file

@ -0,0 +1,73 @@
import { useState, type RefObject } from "react"
import { Search, X } from "lucide-react"
import { cn } from "@/lib/utils"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { Input } from "@/components/ui"
export interface SettingsSearchInputProps {
value: string
onChange: (value: string) => void
onFocus?: () => void
onBlur?: () => void
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
inputRef?: RefObject<HTMLInputElement>
}
export function SettingsSearchInput({
value,
onChange,
onFocus,
onBlur,
onKeyDown,
inputRef,
}: SettingsSearchInputProps) {
const { t } = useAppTranslation()
const [isExpanded, setIsExpanded] = useState(false)
const handleFocus = () => {
setIsExpanded(true)
onFocus?.()
}
const handleBlur = () => {
// Only collapse if there's no value
if (!value) {
setIsExpanded(false)
}
onBlur?.()
}
const isWide = isExpanded || !!value
return (
<div className="relative flex items-center justify-end">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-vscode-descriptionForeground pointer-events-none z-10" />
<Input
ref={inputRef}
data-testid="settings-search-input"
type="text"
value={value}
onChange={(e) => 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 && (
<button
type="button"
onClick={() => onChange("")}
className="absolute cursor-pointer right-2 top-1/2 -translate-y-1/2 text-vscode-descriptionForeground hover:text-vscode-foreground focus:outline-none"
aria-label="Clear search">
<X className="size-3.5" />
</button>
)}
</div>
)
}

View file

@ -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<number>
}
/**
* 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 ? (
<mark key={index} className="bg-transparent font-semibold text-inherit">
{segment.text}
</mark>
) : (
<span key={index}>{segment.text}</span>
),
)}
</>
)
}
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<SectionName, SearchResult[]>,
)
}, [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 (
<div className="max-h-80 overflow-y-auto p-4 text-vscode-descriptionForeground text-sm">
{t("settings:search.noResults", { query })}
</div>
)
}
return (
<div className="max-h-80 overflow-y-auto" role="listbox">
{Object.entries(groupedResults).map(([section, sectionResults]) => {
const Icon = sectionIconMap.get(section as SectionName)
return (
<div key={section}>
{/* Section header */}
<div className="flex items-center gap-2 px-3 py-1.5 mt-4 first:mt-0 text-xs text-vscode-descriptionForeground bg-vscode-sideBar-background border-b-vscode-panel-border sticky top-0">
{Icon && <Icon className="h-3.5 w-3.5" />}
<span>{t(`settings:sections.${section}`)}</span>
</div>
{/* Result items */}
{sectionResults.map((result) => {
const isHighlighted = highlightedResultId === result.settingId
const resultDomId = `settings-search-result-${result.settingId}`
return (
<button
key={result.settingId}
id={resultDomId}
type="button"
role="option"
aria-selected={isHighlighted}
onMouseDown={(event) => event.preventDefault()}
onClick={() => onSelectResult(result)}
className={cn(
"w-full cursor-pointer text-left px-3 py-2 hover:bg-vscode-list-hoverBackground focus:bg-vscode-list-hoverBackground focus:outline-none",
isHighlighted &&
"bg-vscode-button-background text-vscode-button-foreground hover:bg-vscode-button-hoverBackground",
)}>
<div className="text-sm">
<HighlightMatch text={result.label} positions={result.positions} />
</div>
</button>
)
})}
</div>
)
})}
</div>
)
}

View file

@ -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<SettingsViewRef, SettingsViewProps>(({ 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<SectionName>(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 (
<Tab>
<TabHeader className="flex justify-between items-center gap-2">
<div className="flex items-center gap-1">
<h3 className="text-vscode-foreground m-0">{t("settings:header.title")}</h3>
</div>
<div className="flex gap-2">
<h3 className="text-vscode-foreground m-0 flex-shrink-0">{t("settings:header.title")}</h3>
<div className="flex items-center gap-2">
{isIndexingComplete && (
<SettingsSearch index={searchIndex} onNavigate={handleSearchNavigate} sections={sections} />
)}
<StandardTooltip
content={
!isSettingValid
@ -667,217 +720,222 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
})}
</TabList>
{/* Content area */}
<TabContent ref={contentRef} className="p-0 flex-1 overflow-auto">
{/* Providers Section */}
{activeTab === "providers" && (
<div>
<SectionHeader>
<div className="flex items-center gap-2">
<Webhook className="w-4" />
<div>{t("settings:sections.providers")}</div>
</div>
</SectionHeader>
{/* Content area - renders only the active tab (or indexing tab during initial indexing) */}
<TabContent
ref={contentRef}
className={cn("p-0 flex-1 overflow-auto", isIndexing && "opacity-0")}
data-testid="settings-content">
<SearchIndexProvider value={searchContextValue}>
{/* Providers Section */}
{renderTab === "providers" && (
<div>
<SectionHeader>
<div className="flex items-center gap-2">
<Webhook className="w-4" />
<div>{t("settings:sections.providers")}</div>
</div>
</SectionHeader>
<Section>
<ApiConfigManager
currentApiConfigName={currentApiConfigName}
listApiConfigMeta={listApiConfigMeta}
onSelectConfig={(configName: string) =>
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,
})
}
/>
<ApiOptions
uriScheme={uriScheme}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
/>
</Section>
</div>
)}
<Section>
<ApiConfigManager
currentApiConfigName={currentApiConfigName}
listApiConfigMeta={listApiConfigMeta}
onSelectConfig={(configName: string) =>
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,
})
}
/>
<ApiOptions
uriScheme={uriScheme}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
/>
</Section>
</div>
)}
{/* Auto-Approve Section */}
{activeTab === "autoApprove" && (
<AutoApproveSettings
alwaysAllowReadOnly={alwaysAllowReadOnly}
alwaysAllowReadOnlyOutsideWorkspace={alwaysAllowReadOnlyOutsideWorkspace}
alwaysAllowWrite={alwaysAllowWrite}
alwaysAllowWriteOutsideWorkspace={alwaysAllowWriteOutsideWorkspace}
alwaysAllowWriteProtected={alwaysAllowWriteProtected}
alwaysAllowBrowser={alwaysAllowBrowser}
alwaysAllowMcp={alwaysAllowMcp}
alwaysAllowModeSwitch={alwaysAllowModeSwitch}
alwaysAllowSubtasks={alwaysAllowSubtasks}
alwaysAllowExecute={alwaysAllowExecute}
alwaysAllowFollowupQuestions={alwaysAllowFollowupQuestions}
followupAutoApproveTimeoutMs={followupAutoApproveTimeoutMs}
allowedCommands={allowedCommands}
allowedMaxRequests={allowedMaxRequests ?? undefined}
allowedMaxCost={allowedMaxCost ?? undefined}
deniedCommands={deniedCommands}
setCachedStateField={setCachedStateField}
/>
)}
{/* Auto-Approve Section */}
{renderTab === "autoApprove" && (
<AutoApproveSettings
alwaysAllowReadOnly={alwaysAllowReadOnly}
alwaysAllowReadOnlyOutsideWorkspace={alwaysAllowReadOnlyOutsideWorkspace}
alwaysAllowWrite={alwaysAllowWrite}
alwaysAllowWriteOutsideWorkspace={alwaysAllowWriteOutsideWorkspace}
alwaysAllowWriteProtected={alwaysAllowWriteProtected}
alwaysAllowBrowser={alwaysAllowBrowser}
alwaysAllowMcp={alwaysAllowMcp}
alwaysAllowModeSwitch={alwaysAllowModeSwitch}
alwaysAllowSubtasks={alwaysAllowSubtasks}
alwaysAllowExecute={alwaysAllowExecute}
alwaysAllowFollowupQuestions={alwaysAllowFollowupQuestions}
followupAutoApproveTimeoutMs={followupAutoApproveTimeoutMs}
allowedCommands={allowedCommands}
allowedMaxRequests={allowedMaxRequests ?? undefined}
allowedMaxCost={allowedMaxCost ?? undefined}
deniedCommands={deniedCommands}
setCachedStateField={setCachedStateField}
/>
)}
{/* Slash Commands Section */}
{activeTab === "slashCommands" && <SlashCommandsSettings />}
{/* Slash Commands Section */}
{renderTab === "slashCommands" && <SlashCommandsSettings />}
{/* Browser Section */}
{activeTab === "browser" && (
<BrowserSettings
browserToolEnabled={browserToolEnabled}
browserViewportSize={browserViewportSize}
screenshotQuality={screenshotQuality}
remoteBrowserHost={remoteBrowserHost}
remoteBrowserEnabled={remoteBrowserEnabled}
setCachedStateField={setCachedStateField}
/>
)}
{/* Browser Section */}
{renderTab === "browser" && (
<BrowserSettings
browserToolEnabled={browserToolEnabled}
browserViewportSize={browserViewportSize}
screenshotQuality={screenshotQuality}
remoteBrowserHost={remoteBrowserHost}
remoteBrowserEnabled={remoteBrowserEnabled}
setCachedStateField={setCachedStateField}
/>
)}
{/* Checkpoints Section */}
{activeTab === "checkpoints" && (
<CheckpointSettings
enableCheckpoints={enableCheckpoints}
checkpointTimeout={checkpointTimeout}
setCachedStateField={setCachedStateField}
/>
)}
{/* Checkpoints Section */}
{renderTab === "checkpoints" && (
<CheckpointSettings
enableCheckpoints={enableCheckpoints}
checkpointTimeout={checkpointTimeout}
setCachedStateField={setCachedStateField}
/>
)}
{/* Notifications Section */}
{activeTab === "notifications" && (
<NotificationSettings
ttsEnabled={ttsEnabled}
ttsSpeed={ttsSpeed}
soundEnabled={soundEnabled}
soundVolume={soundVolume}
setCachedStateField={setCachedStateField}
/>
)}
{/* Notifications Section */}
{renderTab === "notifications" && (
<NotificationSettings
ttsEnabled={ttsEnabled}
ttsSpeed={ttsSpeed}
soundEnabled={soundEnabled}
soundVolume={soundVolume}
setCachedStateField={setCachedStateField}
/>
)}
{/* Context Management Section */}
{activeTab === "contextManagement" && (
<ContextManagementSettings
autoCondenseContext={autoCondenseContext}
autoCondenseContextPercent={autoCondenseContextPercent}
listApiConfigMeta={listApiConfigMeta ?? []}
maxOpenTabsContext={maxOpenTabsContext}
maxWorkspaceFiles={maxWorkspaceFiles ?? 200}
showRooIgnoredFiles={showRooIgnoredFiles}
enableSubfolderRules={enableSubfolderRules}
maxReadFileLine={maxReadFileLine}
maxImageFileSize={maxImageFileSize}
maxTotalImageSize={maxTotalImageSize}
maxConcurrentFileReads={maxConcurrentFileReads}
profileThresholds={profileThresholds}
includeDiagnosticMessages={includeDiagnosticMessages}
maxDiagnosticMessages={maxDiagnosticMessages}
writeDelayMs={writeDelayMs}
includeCurrentTime={includeCurrentTime}
includeCurrentCost={includeCurrentCost}
maxGitStatusFiles={maxGitStatusFiles}
setCachedStateField={setCachedStateField}
/>
)}
{/* Context Management Section */}
{renderTab === "contextManagement" && (
<ContextManagementSettings
autoCondenseContext={autoCondenseContext}
autoCondenseContextPercent={autoCondenseContextPercent}
listApiConfigMeta={listApiConfigMeta ?? []}
maxOpenTabsContext={maxOpenTabsContext}
maxWorkspaceFiles={maxWorkspaceFiles ?? 200}
showRooIgnoredFiles={showRooIgnoredFiles}
enableSubfolderRules={enableSubfolderRules}
maxReadFileLine={maxReadFileLine}
maxImageFileSize={maxImageFileSize}
maxTotalImageSize={maxTotalImageSize}
maxConcurrentFileReads={maxConcurrentFileReads}
profileThresholds={profileThresholds}
includeDiagnosticMessages={includeDiagnosticMessages}
maxDiagnosticMessages={maxDiagnosticMessages}
writeDelayMs={writeDelayMs}
includeCurrentTime={includeCurrentTime}
includeCurrentCost={includeCurrentCost}
maxGitStatusFiles={maxGitStatusFiles}
setCachedStateField={setCachedStateField}
/>
)}
{/* Terminal Section */}
{activeTab === "terminal" && (
<TerminalSettings
terminalOutputLineLimit={terminalOutputLineLimit}
terminalOutputCharacterLimit={terminalOutputCharacterLimit}
terminalShellIntegrationTimeout={terminalShellIntegrationTimeout}
terminalShellIntegrationDisabled={terminalShellIntegrationDisabled}
terminalCommandDelay={terminalCommandDelay}
terminalPowershellCounter={terminalPowershellCounter}
terminalZshClearEolMark={terminalZshClearEolMark}
terminalZshOhMy={terminalZshOhMy}
terminalZshP10k={terminalZshP10k}
terminalZdotdir={terminalZdotdir}
terminalCompressProgressBar={terminalCompressProgressBar}
setCachedStateField={setCachedStateField}
/>
)}
{/* Terminal Section */}
{renderTab === "terminal" && (
<TerminalSettings
terminalOutputLineLimit={terminalOutputLineLimit}
terminalOutputCharacterLimit={terminalOutputCharacterLimit}
terminalShellIntegrationTimeout={terminalShellIntegrationTimeout}
terminalShellIntegrationDisabled={terminalShellIntegrationDisabled}
terminalCommandDelay={terminalCommandDelay}
terminalPowershellCounter={terminalPowershellCounter}
terminalZshClearEolMark={terminalZshClearEolMark}
terminalZshOhMy={terminalZshOhMy}
terminalZshP10k={terminalZshP10k}
terminalZdotdir={terminalZdotdir}
terminalCompressProgressBar={terminalCompressProgressBar}
setCachedStateField={setCachedStateField}
/>
)}
{/* Modes Section */}
{activeTab === "modes" && <ModesView />}
{/* Modes Section */}
{renderTab === "modes" && <ModesView />}
{/* MCP Section */}
{activeTab === "mcp" && <McpView />}
{/* MCP Section */}
{renderTab === "mcp" && <McpView />}
{/* Prompts Section */}
{activeTab === "prompts" && (
<PromptsSettings
customSupportPrompts={customSupportPrompts || {}}
setCustomSupportPrompts={setCustomSupportPromptsField}
includeTaskHistoryInEnhance={includeTaskHistoryInEnhance}
setIncludeTaskHistoryInEnhance={(value) =>
setCachedStateField("includeTaskHistoryInEnhance", value)
}
/>
)}
{/* Prompts Section */}
{renderTab === "prompts" && (
<PromptsSettings
customSupportPrompts={customSupportPrompts || {}}
setCustomSupportPrompts={setCustomSupportPromptsField}
includeTaskHistoryInEnhance={includeTaskHistoryInEnhance}
setIncludeTaskHistoryInEnhance={(value) =>
setCachedStateField("includeTaskHistoryInEnhance", value)
}
/>
)}
{/* UI Section */}
{activeTab === "ui" && (
<UISettings
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
enterBehavior={enterBehavior ?? "send"}
setCachedStateField={setCachedStateField}
/>
)}
{/* UI Section */}
{renderTab === "ui" && (
<UISettings
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
enterBehavior={enterBehavior ?? "send"}
setCachedStateField={setCachedStateField}
/>
)}
{/* Experimental Section */}
{activeTab === "experimental" && (
<ExperimentalSettings
setExperimentEnabled={setExperimentEnabled}
experiments={experiments}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
imageGenerationProvider={imageGenerationProvider}
openRouterImageApiKey={openRouterImageApiKey as string | undefined}
openRouterImageGenerationSelectedModel={
openRouterImageGenerationSelectedModel as string | undefined
}
setImageGenerationProvider={setImageGenerationProvider}
setOpenRouterImageApiKey={setOpenRouterImageApiKey}
setImageGenerationSelectedModel={setImageGenerationSelectedModel}
/>
)}
{/* Experimental Section */}
{renderTab === "experimental" && (
<ExperimentalSettings
setExperimentEnabled={setExperimentEnabled}
experiments={experiments}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
imageGenerationProvider={imageGenerationProvider}
openRouterImageApiKey={openRouterImageApiKey as string | undefined}
openRouterImageGenerationSelectedModel={
openRouterImageGenerationSelectedModel as string | undefined
}
setImageGenerationProvider={setImageGenerationProvider}
setOpenRouterImageApiKey={setOpenRouterImageApiKey}
setImageGenerationSelectedModel={setImageGenerationSelectedModel}
/>
)}
{/* Language Section */}
{activeTab === "language" && (
<LanguageSettings language={language || "en"} setCachedStateField={setCachedStateField} />
)}
{/* Language Section */}
{renderTab === "language" && (
<LanguageSettings language={language || "en"} setCachedStateField={setCachedStateField} />
)}
{/* About Section */}
{activeTab === "about" && (
<About
telemetrySetting={telemetrySetting}
setTelemetrySetting={setTelemetrySetting}
debug={cachedState.debug}
setDebug={setDebug}
/>
)}
{/* About Section */}
{renderTab === "about" && (
<About
telemetrySetting={telemetrySetting}
setTelemetrySetting={setTelemetrySetting}
debug={cachedState.debug}
setDebug={setDebug}
/>
)}
</SearchIndexProvider>
</TabContent>
</div>

View file

@ -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 = () => {
<Section>
{/* Description section */}
<div className="mb-4">
<SearchableSetting
settingId="slash-commands-description"
section="slashCommands"
label={t("settings:sections.slashCommands")}
className="mb-4">
<p className="text-sm text-vscode-descriptionForeground">
<Trans
i18nKey="settings:slashCommands.description"
@ -128,10 +133,14 @@ export const SlashCommandsSettings: React.FC = () => {
}}
/>
</p>
</div>
</SearchableSetting>
{/* Global Commands Section */}
<div className="mb-6">
<SearchableSetting
settingId="slash-commands-global"
section="slashCommands"
label={t("chat:slashCommands.globalCommands")}
className="mb-6">
<div className="flex items-center gap-1.5 mb-2">
<Globe className="w-3 h-3" />
<h4 className="text-sm font-medium m-0">{t("chat:slashCommands.globalCommands")}</h4>
@ -169,11 +178,15 @@ export const SlashCommandsSettings: React.FC = () => {
</Button>
</div>
</div>
</div>
</SearchableSetting>
{/* Workspace Commands Section - Only show if in a workspace */}
{hasWorkspace && (
<div className="mb-6">
<SearchableSetting
settingId="slash-commands-workspace"
section="slashCommands"
label={t("chat:slashCommands.workspaceCommands")}
className="mb-6">
<div className="flex items-center gap-1.5 mb-2">
<Folder className="w-3 h-3" />
<h4 className="text-sm font-medium m-0">{t("chat:slashCommands.workspaceCommands")}</h4>
@ -211,12 +224,16 @@ export const SlashCommandsSettings: React.FC = () => {
</Button>
</div>
</div>
</div>
</SearchableSetting>
)}
{/* Built-in Commands Section */}
{builtInCommands.length > 0 && (
<div className="mb-6">
<SearchableSetting
settingId="slash-commands-built-in"
section="slashCommands"
label={t("chat:slashCommands.builtInCommands")}
className="mb-6">
<div className="flex items-center gap-1.5 mb-2">
<Settings className="w-3 h-3" />
<h4 className="text-sm font-medium m-0">{t("chat:slashCommands.builtInCommands")}</h4>
@ -231,7 +248,7 @@ export const SlashCommandsSettings: React.FC = () => {
/>
))}
</div>
</div>
</SearchableSetting>
)}
</Section>

View file

@ -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<HTMLDivElement> & {
terminalOutputLineLimit?: number
@ -104,7 +105,10 @@ export const TerminalSettings = ({
</div>
</div>
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<SearchableSetting
settingId="terminal-output-line-limit"
section="terminal"
label={t("settings:terminal.outputLineLimit.label")}>
<label className="block font-medium mb-1">
{t("settings:terminal.outputLineLimit.label")}
</label>
@ -131,8 +135,11 @@ export const TerminalSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
<div>
</SearchableSetting>
<SearchableSetting
settingId="terminal-output-character-limit"
section="terminal"
label={t("settings:terminal.outputCharacterLimit.label")}>
<label className="block font-medium mb-1">
{t("settings:terminal.outputCharacterLimit.label")}
</label>
@ -161,8 +168,11 @@ export const TerminalSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
<div>
</SearchableSetting>
<SearchableSetting
settingId="terminal-compress-progress-bar"
section="terminal"
label={t("settings:terminal.compressProgressBar.label")}>
<VSCodeCheckbox
checked={terminalCompressProgressBar ?? true}
onChange={(e: any) =>
@ -183,7 +193,7 @@ export const TerminalSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
</SearchableSetting>
</div>
</div>
@ -199,7 +209,10 @@ export const TerminalSettings = ({
</div>
</div>
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<SearchableSetting
settingId="terminal-shell-integration-disabled"
section="terminal"
label={t("settings:terminal.shellIntegrationDisabled.label")}>
<VSCodeCheckbox
checked={terminalShellIntegrationDisabled ?? true}
onChange={(e: any) =>
@ -221,11 +234,14 @@ export const TerminalSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
</SearchableSetting>
{!terminalShellIntegrationDisabled && (
<>
<div>
<SearchableSetting
settingId="terminal-inherit-env"
section="terminal"
label={t("settings:terminal.inheritEnv.label")}>
<VSCodeCheckbox
checked={inheritEnv}
onChange={(e: any) => {
@ -251,9 +267,12 @@ export const TerminalSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="terminal-shell-integration-timeout"
section="terminal"
label={t("settings:terminal.shellIntegrationTimeout.label")}>
<label className="block font-medium mb-1">
{t("settings:terminal.shellIntegrationTimeout.label")}
</label>
@ -286,9 +305,12 @@ export const TerminalSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="terminal-command-delay"
section="terminal"
label={t("settings:terminal.commandDelay.label")}>
<label className="block font-medium mb-1">
{t("settings:terminal.commandDelay.label")}
</label>
@ -319,9 +341,12 @@ export const TerminalSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="terminal-powershell-counter"
section="terminal"
label={t("settings:terminal.powershellCounter.label")}>
<VSCodeCheckbox
checked={terminalPowershellCounter ?? false}
onChange={(e: any) =>
@ -344,9 +369,12 @@ export const TerminalSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="terminal-zsh-clear-eol-mark"
section="terminal"
label={t("settings:terminal.zshClearEolMark.label")}>
<VSCodeCheckbox
checked={terminalZshClearEolMark ?? true}
onChange={(e: any) =>
@ -369,9 +397,12 @@ export const TerminalSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="terminal-zsh-oh-my"
section="terminal"
label={t("settings:terminal.zshOhMy.label")}>
<VSCodeCheckbox
checked={terminalZshOhMy ?? false}
onChange={(e: any) => setCachedStateField("terminalZshOhMy", e.target.checked)}
@ -390,9 +421,12 @@ export const TerminalSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="terminal-zsh-p10k"
section="terminal"
label={t("settings:terminal.zshP10k.label")}>
<VSCodeCheckbox
checked={terminalZshP10k ?? false}
onChange={(e: any) => setCachedStateField("terminalZshP10k", e.target.checked)}
@ -411,9 +445,12 @@ export const TerminalSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
</SearchableSetting>
<div>
<SearchableSetting
settingId="terminal-zdotdir"
section="terminal"
label={t("settings:terminal.zdotdir.label")}>
<VSCodeCheckbox
checked={terminalZdotdir ?? false}
onChange={(e: any) => setCachedStateField("terminalZdotdir", e.target.checked)}
@ -432,7 +469,7 @@ export const TerminalSettings = ({
</VSCodeLink>
</Trans>
</div>
</div>
</SearchableSetting>
</>
)}
</div>

View file

@ -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<HTMLDivElement> {
@ -60,32 +61,42 @@ export const UISettings = ({
<Section>
<div className="space-y-6">
{/* Collapse Thinking Messages Setting */}
<div className="flex flex-col gap-1">
<VSCodeCheckbox
checked={reasoningBlockCollapsed}
onChange={(e: any) => handleReasoningBlockCollapsedChange(e.target.checked)}
data-testid="collapse-thinking-checkbox">
<span className="font-medium">{t("settings:ui.collapseThinking.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
{t("settings:ui.collapseThinking.description")}
<SearchableSetting
settingId="ui-collapse-thinking"
section="ui"
label={t("settings:ui.collapseThinking.label")}>
<div className="flex flex-col gap-1">
<VSCodeCheckbox
checked={reasoningBlockCollapsed}
onChange={(e: any) => handleReasoningBlockCollapsedChange(e.target.checked)}
data-testid="collapse-thinking-checkbox">
<span className="font-medium">{t("settings:ui.collapseThinking.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
{t("settings:ui.collapseThinking.description")}
</div>
</div>
</div>
</SearchableSetting>
{/* Enter Key Behavior Setting */}
<div className="flex flex-col gap-1">
<VSCodeCheckbox
checked={enterBehavior === "newline"}
onChange={(e: any) => handleEnterBehaviorChange(e.target.checked)}
data-testid="enter-behavior-checkbox">
<span className="font-medium">
{t("settings:ui.requireCtrlEnterToSend.label", { primaryMod })}
</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
{t("settings:ui.requireCtrlEnterToSend.description", { primaryMod })}
<SearchableSetting
settingId="ui-enter-behavior"
section="ui"
label={t("settings:ui.requireCtrlEnterToSend.label", { primaryMod })}>
<div className="flex flex-col gap-1">
<VSCodeCheckbox
checked={enterBehavior === "newline"}
onChange={(e: any) => handleEnterBehaviorChange(e.target.checked)}
data-testid="enter-behavior-checkbox">
<span className="font-medium">
{t("settings:ui.requireCtrlEnterToSend.label", { primaryMod })}
</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
{t("settings:ui.requireCtrlEnterToSend.description", { primaryMod })}
</div>
</div>
</div>
</SearchableSetting>
</div>
</Section>
</div>

View file

@ -42,6 +42,22 @@ vi.mock("@src/components/ui", () => ({
</button>
),
StandardTooltip: ({ children }: any) => <>{children}</>,
Popover: ({ children }: any) => <>{children}</>,
PopoverTrigger: ({ children }: any) => <>{children}</>,
PopoverContent: ({ children }: any) => <div>{children}</div>,
Tooltip: ({ children }: any) => <>{children}</>,
TooltipProvider: ({ children }: any) => <>{children}</>,
TooltipTrigger: ({ children }: any) => <>{children}</>,
TooltipContent: ({ children }: any) => <div>{children}</div>,
}))
// 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

View file

@ -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) => <div data-testid="tab-container">{children}</div>,
TabHeader: ({ children }: any) => <div data-testid="tab-header">{children}</div>,
TabContent: ({ children }: any) => <div data-testid="tab-content">{children}</div>,
TabContent: ({ children, "data-testid": dataTestId }: any) => (
<div data-testid={dataTestId || "tab-content"}>{children}</div>
),
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) => (
<input
type="range"
value={value[0]}
onChange={(e) => 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(

View file

@ -47,6 +47,18 @@ vi.mock("@src/components/ui", () => ({
TooltipProvider: ({ children }: any) => <>{children}</>,
TooltipTrigger: ({ children }: any) => <>{children}</>,
StandardTooltip: ({ children, content }: any) => <div title={content}>{children}</div>,
Popover: ({ children }: any) => <>{children}</>,
PopoverTrigger: ({ children }: any) => <>{children}</>,
PopoverContent: ({ children }: any) => <div>{children}</div>,
}))
// 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) => <div>{children}</div>,
}))
vi.mock("../SettingsSearch", () => ({
SettingsSearch: () => null,
}))
import { useExtensionState } from "@src/context/ExtensionStateContext"
import ApiOptions from "../ApiOptions"

View file

@ -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" },

View file

@ -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}

View file

@ -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<number>
}
/**
* Context for collecting searchable settings as they mount.
* This allows building the search index without rendering all sections.
*/
interface SearchIndexContextValue {
registerSetting: (setting: Omit<SearchableSettingData, "sectionLabel">) => void
}
const SearchIndexContext = createContext<SearchIndexContextValue | null>(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<Map<string, Omit<SearchableSettingData, "sectionLabel">>>(new Map())
const [index, setIndex] = useState<SearchableSettingData[]>([])
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<SearchIndexContextValue>(
() => ({
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,
}
}

View file

@ -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 */}
<VSCodeRadio value="roo" className="flex items-start gap-2">
<div className="flex-1 space-y-1 cursor-pointer">
<p className="text-lg font-semibold block -mt-1">

View file

@ -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",

View file

@ -8,6 +8,10 @@
"add": "Header hinzufügen",
"remove": "Entfernen"
},
"search": {
"placeholder": "Einstellungen durchsuchen...",
"noResults": "Keine Einstellungen gefunden"
},
"header": {
"title": "Einstellungen",
"saveButtonTooltip": "Änderungen speichern",

View file

@ -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"
},

View file

@ -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?",

View file

@ -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"
},

View file

@ -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",

View file

@ -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",

View file

@ -8,6 +8,10 @@
"add": "हेडर जोड़ें",
"remove": "हटाएं"
},
"search": {
"placeholder": "सेटिंग्स खोजें...",
"noResults": "कोई सेटिंग नहीं मिली"
},
"header": {
"title": "सेटिंग्स",
"saveButtonTooltip": "परिवर्तन सहेजें",

View file

@ -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",

View file

@ -8,6 +8,10 @@
"add": "Aggiungi intestazione",
"remove": "Rimuovi"
},
"search": {
"placeholder": "Cerca impostazioni...",
"noResults": "Nessuna impostazione trovata"
},
"header": {
"title": "Impostazioni",
"saveButtonTooltip": "Salva modifiche",

View file

@ -8,6 +8,10 @@
"add": "ヘッダーを追加",
"remove": "削除"
},
"search": {
"placeholder": "設定を検索...",
"noResults": "設定が見つかりません"
},
"header": {
"title": "設定",
"saveButtonTooltip": "変更を保存",

View file

@ -8,6 +8,10 @@
"add": "헤더 추가",
"remove": "삭제"
},
"search": {
"placeholder": "설정 검색...",
"noResults": "설정을 찾을 수 없습니다"
},
"header": {
"title": "설정",
"saveButtonTooltip": "변경 사항 저장",

View file

@ -8,6 +8,10 @@
"add": "Header toevoegen",
"remove": "Verwijderen"
},
"search": {
"placeholder": "Instellingen zoeken...",
"noResults": "Geen instellingen gevonden"
},
"header": {
"title": "Instellingen",
"saveButtonTooltip": "Wijzigingen opslaan",

View file

@ -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"
},

View file

@ -8,6 +8,10 @@
"add": "Dodaj nagłówek",
"remove": "Usuń"
},
"search": {
"placeholder": "Szukaj ustawień...",
"noResults": "Nie znaleziono ustawień"
},
"header": {
"title": "Ustawienia",
"saveButtonTooltip": "Zapisz zmiany",

View file

@ -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",

View file

@ -8,6 +8,10 @@
"add": "Добавить заголовок",
"remove": "Удалить"
},
"search": {
"placeholder": "Поиск параметров...",
"noResults": "Параметры не найдены"
},
"header": {
"title": "Настройки",
"saveButtonTooltip": "Сохранить изменения",

View file

@ -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",

View file

@ -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",

View file

@ -8,6 +8,10 @@
"add": "添加标头",
"remove": "移除"
},
"search": {
"placeholder": "搜索设置...",
"noResults": "未找到设置"
},
"header": {
"title": "设置",
"saveButtonTooltip": "保存更改",

View file

@ -8,6 +8,10 @@
"add": "新增標頭",
"remove": "移除"
},
"search": {
"placeholder": "搜尋設定...",
"noResults": "找不到設定"
},
"header": {
"title": "設定",
"saveButtonTooltip": "儲存變更",

View file

@ -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;
}