mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
611bb70166
commit
632b86cfbe
40 changed files with 1493 additions and 488 deletions
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
79
webview-ui/src/components/settings/SearchableSetting.tsx
Normal file
79
webview-ui/src/components/settings/SearchableSetting.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
123
webview-ui/src/components/settings/SettingsSearch.tsx
Normal file
123
webview-ui/src/components/settings/SettingsSearch.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
73
webview-ui/src/components/settings/SettingsSearchInput.tsx
Normal file
73
webview-ui/src/components/settings/SettingsSearchInput.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
150
webview-ui/src/components/settings/SettingsSearchResults.tsx
Normal file
150
webview-ui/src/components/settings/SettingsSearchResults.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
153
webview-ui/src/components/settings/useSettingsSearch.ts
Normal file
153
webview-ui/src/components/settings/useSettingsSearch.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
4
webview-ui/src/i18n/locales/ca/settings.json
generated
4
webview-ui/src/i18n/locales/ca/settings.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/de/settings.json
generated
4
webview-ui/src/i18n/locales/de/settings.json
generated
|
|
@ -8,6 +8,10 @@
|
|||
"add": "Header hinzufügen",
|
||||
"remove": "Entfernen"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Einstellungen durchsuchen...",
|
||||
"noResults": "Keine Einstellungen gefunden"
|
||||
},
|
||||
"header": {
|
||||
"title": "Einstellungen",
|
||||
"saveButtonTooltip": "Änderungen speichern",
|
||||
|
|
|
|||
|
|
@ -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?",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/es/settings.json
generated
4
webview-ui/src/i18n/locales/es/settings.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/fr/settings.json
generated
4
webview-ui/src/i18n/locales/fr/settings.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/hi/settings.json
generated
4
webview-ui/src/i18n/locales/hi/settings.json
generated
|
|
@ -8,6 +8,10 @@
|
|||
"add": "हेडर जोड़ें",
|
||||
"remove": "हटाएं"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "सेटिंग्स खोजें...",
|
||||
"noResults": "कोई सेटिंग नहीं मिली"
|
||||
},
|
||||
"header": {
|
||||
"title": "सेटिंग्स",
|
||||
"saveButtonTooltip": "परिवर्तन सहेजें",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/id/settings.json
generated
4
webview-ui/src/i18n/locales/id/settings.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/it/settings.json
generated
4
webview-ui/src/i18n/locales/it/settings.json
generated
|
|
@ -8,6 +8,10 @@
|
|||
"add": "Aggiungi intestazione",
|
||||
"remove": "Rimuovi"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Cerca impostazioni...",
|
||||
"noResults": "Nessuna impostazione trovata"
|
||||
},
|
||||
"header": {
|
||||
"title": "Impostazioni",
|
||||
"saveButtonTooltip": "Salva modifiche",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ja/settings.json
generated
4
webview-ui/src/i18n/locales/ja/settings.json
generated
|
|
@ -8,6 +8,10 @@
|
|||
"add": "ヘッダーを追加",
|
||||
"remove": "削除"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "設定を検索...",
|
||||
"noResults": "設定が見つかりません"
|
||||
},
|
||||
"header": {
|
||||
"title": "設定",
|
||||
"saveButtonTooltip": "変更を保存",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ko/settings.json
generated
4
webview-ui/src/i18n/locales/ko/settings.json
generated
|
|
@ -8,6 +8,10 @@
|
|||
"add": "헤더 추가",
|
||||
"remove": "삭제"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "설정 검색...",
|
||||
"noResults": "설정을 찾을 수 없습니다"
|
||||
},
|
||||
"header": {
|
||||
"title": "설정",
|
||||
"saveButtonTooltip": "변경 사항 저장",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/nl/settings.json
generated
4
webview-ui/src/i18n/locales/nl/settings.json
generated
|
|
@ -8,6 +8,10 @@
|
|||
"add": "Header toevoegen",
|
||||
"remove": "Verwijderen"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Instellingen zoeken...",
|
||||
"noResults": "Geen instellingen gevonden"
|
||||
},
|
||||
"header": {
|
||||
"title": "Instellingen",
|
||||
"saveButtonTooltip": "Wijzigingen opslaan",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/pl/settings.json
generated
4
webview-ui/src/i18n/locales/pl/settings.json
generated
|
|
@ -8,6 +8,10 @@
|
|||
"add": "Dodaj nagłówek",
|
||||
"remove": "Usuń"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Szukaj ustawień...",
|
||||
"noResults": "Nie znaleziono ustawień"
|
||||
},
|
||||
"header": {
|
||||
"title": "Ustawienia",
|
||||
"saveButtonTooltip": "Zapisz zmiany",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
4
webview-ui/src/i18n/locales/pt-BR/settings.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ru/settings.json
generated
4
webview-ui/src/i18n/locales/ru/settings.json
generated
|
|
@ -8,6 +8,10 @@
|
|||
"add": "Добавить заголовок",
|
||||
"remove": "Удалить"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Поиск параметров...",
|
||||
"noResults": "Параметры не найдены"
|
||||
},
|
||||
"header": {
|
||||
"title": "Настройки",
|
||||
"saveButtonTooltip": "Сохранить изменения",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/tr/settings.json
generated
4
webview-ui/src/i18n/locales/tr/settings.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/vi/settings.json
generated
4
webview-ui/src/i18n/locales/vi/settings.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
4
webview-ui/src/i18n/locales/zh-CN/settings.json
generated
|
|
@ -8,6 +8,10 @@
|
|||
"add": "添加标头",
|
||||
"remove": "移除"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索设置...",
|
||||
"noResults": "未找到设置"
|
||||
},
|
||||
"header": {
|
||||
"title": "设置",
|
||||
"saveButtonTooltip": "保存更改",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
4
webview-ui/src/i18n/locales/zh-TW/settings.json
generated
|
|
@ -8,6 +8,10 @@
|
|||
"add": "新增標頭",
|
||||
"remove": "移除"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜尋設定...",
|
||||
"noResults": "找不到設定"
|
||||
},
|
||||
"header": {
|
||||
"title": "設定",
|
||||
"saveButtonTooltip": "儲存變更",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue