Basic settings search (#10619)

* Prototype of a simpler searchable settings

* Fix tests

* UI improvements

* Input tweaks

* Update webview-ui/src/components/settings/SettingsSearch.tsx

Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>

* fix: remove duplicate Escape key handler dead code

* Cleanup

* Fix tests

---------

Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
Matt Rubens 2026-01-11 14:57:41 -05:00 committed by GitHub
parent 611bb70166
commit 632b86cfbe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1493 additions and 488 deletions

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

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

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

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

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

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