mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Merge main
This commit is contained in:
commit
0dcc439b79
57 changed files with 1549 additions and 531 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -1,5 +1,17 @@
|
|||
# Roo Code Changelog
|
||||
|
||||
## [3.39.3] - 2026-01-10
|
||||
|
||||

|
||||
|
||||
- Rename Roo Code Cloud Provider to Roo Code Router for clearer branding (PR #10560 by @roomote)
|
||||
- Update Roo Code Router service name throughout the codebase (PR #10607 by @mrubens)
|
||||
- Update router name in types for consistency (PR #10605 by @mrubens)
|
||||
- Improve ExtensionHost code organization and cleanup (PR #10600 by @cte)
|
||||
- Add local installation option to CLI release script for testing (PR #10597 by @cte)
|
||||
- Reorganize CLI file structure for better maintainability (PR #10599 by @cte)
|
||||
- Add TUI to CLI (PR #10480 by @cte)
|
||||
|
||||
## [3.39.2] - 2026-01-09
|
||||
|
||||
- Fix: Ensure all tools have consistent strict mode values for Cerebras compatibility (#10334 by @brianboysen51, PR #10589 by @app/roomote)
|
||||
|
|
@ -104,7 +116,7 @@
|
|||
- Fix: Drain queued messages while waiting for ask to prevent message loss (PR #10315 by @hannesrudolph)
|
||||
- Feat: Add grace retry for empty assistant messages to improve reliability (PR #10297 by @hannesrudolph)
|
||||
- Feat: Enable mergeToolResultText for all OpenAI-compatible providers for better tool result handling (PR #10299 by @hannesrudolph)
|
||||
- Feat: Enable mergeToolResultText for Roo Code Cloud provider (PR #10301 by @hannesrudolph)
|
||||
- Feat: Enable mergeToolResultText for Roo Code Router (PR #10301 by @hannesrudolph)
|
||||
- Feat: Strengthen native tool-use guidance in prompts for improved model behavior (PR #10311 by @hannesrudolph)
|
||||
- UX: Account-centric signup flow for improved onboarding experience (PR #10306 by @brunobergher)
|
||||
|
||||
|
|
@ -398,7 +410,7 @@
|
|||
- Native tool calling support expanded across many providers: Bedrock (PR #9698 by @mrubens), Cerebras (PR #9692 by @mrubens), Chutes with auto-detection from API (PR #9715 by @daniel-lxs), DeepInfra (PR #9691 by @mrubens), DeepSeek and Doubao (PR #9671 by @daniel-lxs), Groq (PR #9673 by @daniel-lxs), LiteLLM (PR #9719 by @daniel-lxs), Ollama (PR #9696 by @mrubens), OpenAI-compatible providers (PR #9676 by @daniel-lxs), Requesty (PR #9672 by @daniel-lxs), Unbound (PR #9699 by @mrubens), Vercel AI Gateway (PR #9697 by @mrubens), Vertex Gemini (PR #9678 by @daniel-lxs), and xAI with new Grok 4 Fast and Grok 4.1 Fast models (PR #9690 by @mrubens)
|
||||
- Fix: Preserve tool_use blocks in summary for parallel tool calls (#9700 by @SilentFlower, PR #9714 by @SilentFlower)
|
||||
- Default Grok Code Fast to native tools for better performance (PR #9717 by @mrubens)
|
||||
- UX improvements to the Roo Code Cloud provider-centric onboarding flow (PR #9709 by @brunobergher)
|
||||
- UX improvements to the Roo Code Router-centric onboarding flow (PR #9709 by @brunobergher)
|
||||
- UX toolbar cleanup and settings consolidation for a cleaner interface (PR #9710 by @brunobergher)
|
||||
- Add model-specific tool customization via `excludedTools` and `includedTools` configuration (PR #9641 by @daniel-lxs)
|
||||
- Add new `apply_patch` native tool for more efficient file editing operations (PR #9663 by @hannesrudolph)
|
||||
|
|
@ -497,7 +509,7 @@
|
|||
- Show the prompt for image generation in the UI (PR #9505 by @mrubens)
|
||||
- Fix double todo list display issue (PR #9517 by @mrubens)
|
||||
- Add tracking for cloud synced messages (PR #9518 by @mrubens)
|
||||
- Enable the Roo Code Cloud provider in evals (PR #9492 by @cte)
|
||||
- Enable the Roo Code Router in evals (PR #9492 by @cte)
|
||||
|
||||
## [3.34.0] - 2025-11-21
|
||||
|
||||
|
|
@ -577,7 +589,7 @@
|
|||
- Use VSCode theme color for outline button borders (PR #9336 by @app/roomote)
|
||||
- Replace broken badgen.net badges with shields.io (PR #9318 by @app/roomote)
|
||||
- Add max git status files setting to evals (PR #9322 by @mrubens)
|
||||
- Roo Code Cloud Provider pricing page and changes elsewhere (PR #9195 by @brunobergher)
|
||||
- Roo Code Router pricing page and changes elsewhere (PR #9195 by @brunobergher)
|
||||
|
||||
## [3.32.1] - 2025-11-14
|
||||
|
||||
|
|
@ -603,7 +615,7 @@
|
|||

|
||||
|
||||
- Fix: OpenAI Native encrypted_content handling and remove gpt-5-chat-latest verbosity flag (#9225 by @politsin, PR by @hannesrudolph)
|
||||
- Fix: Roo Code Cloud provider Anthropic input token normalization to avoid double-counting (thanks @hannesrudolph!)
|
||||
- Fix: Roo Code Router Anthropic input token normalization to avoid double-counting (thanks @hannesrudolph!)
|
||||
- Refactor: Rename sliding-window to context-management and truncateConversationIfNeeded to manageContext (thanks @hannesrudolph!)
|
||||
|
||||
## [3.31.2] - 2025-11-12
|
||||
|
|
@ -743,7 +755,7 @@
|
|||
|
||||
- Add token-budget based file reading with intelligent preview to avoid context overruns (thanks @daniel-lxs!)
|
||||
- Enable browser-use tool for all image-capable models (#8116 by @hannesrudolph, PR by @app/roomote!)
|
||||
- Add dynamic model loading for Roo Code Cloud provider (thanks @app/roomote!)
|
||||
- Add dynamic model loading for Roo Code Router (thanks @app/roomote!)
|
||||
- Fix: Respect nested .gitignore files in search_files (#7921 by @hannesrudolph, PR by @daniel-lxs)
|
||||
- Fix: Preserve trailing newlines in stripLineNumbers for apply_diff (#8020 by @liyi3c, PR by @app/roomote)
|
||||
- Fix: Exclude max tokens field for models that don't support it in export (#7944 by @hannesrudolph, PR by @elianiva)
|
||||
|
|
@ -901,7 +913,7 @@
|
|||
- UX: Responsive Auto-Approve (thanks @brunobergher!)
|
||||
- Add telemetry retry queue for network resilience (thanks @daniel-lxs!)
|
||||
- Fix: Transform keybindings in nightly build to fix command+y shortcut (thanks @app/roomote!)
|
||||
- New code-supernova stealth model in the Roo Code Cloud provider (thanks @mrubens!)
|
||||
- New code-supernova stealth model in the Roo Code Router (thanks @mrubens!)
|
||||
|
||||
## [3.28.3] - 2025-09-16
|
||||
|
||||
|
|
@ -1112,11 +1124,11 @@
|
|||
|
||||
## [3.25.19] - 2025-08-19
|
||||
|
||||
- Fix issue where new users couldn't select the Roo Code Cloud provider (thanks @daniel-lxs!)
|
||||
- Fix issue where new users couldn't select the Roo Code Router (thanks @daniel-lxs!)
|
||||
|
||||
## [3.25.18] - 2025-08-19
|
||||
|
||||
- Add new stealth Sonic model through the Roo Code Cloud provider
|
||||
- Add new stealth Sonic model through the Roo Code Router
|
||||
- Fix: respect enableReasoningEffort setting when determining reasoning usage (#7048 by @ikbencasdoei, PR by @app/roomote)
|
||||
- Fix: prevent duplicate LM Studio models with case-insensitive deduplication (#6954 by @fbuechler, PR by @daniel-lxs)
|
||||
- Feat: simplify ask_followup_question prompt documentation (thanks @daniel-lxs!)
|
||||
|
|
|
|||
|
|
@ -189,8 +189,9 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
commandExecutionTimeout: 30,
|
||||
browserToolEnabled: false,
|
||||
enableCheckpoints: false,
|
||||
// Disable preventFocusDisruption experiment for CLI - it's only relevant for VSCode diff views
|
||||
// and preventing it causes tool messages to not stream during LLM generation
|
||||
// Disable preventFocusDisruption experiment for CLI - it's only
|
||||
// relevant for VSCode diff views and preventing it causes tool
|
||||
// messages to not stream during LLM generation.
|
||||
experiments: {
|
||||
multiFileApplyDiff: false,
|
||||
powerSteering: false,
|
||||
|
|
@ -368,6 +369,7 @@ export class ExtensionHost extends EventEmitter implements ExtensionHostInterfac
|
|||
|
||||
if (this.options.ephemeral) {
|
||||
this.ephemeralStorageDir = await createEphemeralStorageDir()
|
||||
storageDir = this.ephemeralStorageDir
|
||||
}
|
||||
|
||||
// Create VSCode API mock.
|
||||
|
|
|
|||
|
|
@ -98,8 +98,7 @@ const features: Feature[] = [
|
|||
{
|
||||
icon: Brain,
|
||||
title: "Model Agnostic",
|
||||
description:
|
||||
"Bring your own keys or use the Roo Code Cloud Provider with access to all top models with no markup.",
|
||||
description: "Bring your own keys or use the Roo Code Router with access to all top models with no markup.",
|
||||
},
|
||||
{
|
||||
icon: Github,
|
||||
|
|
@ -153,7 +152,7 @@ export default function CloudPage() {
|
|||
Your AI Team <span className="text-violet-500">in the Cloud</span>
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
|
||||
Create your agent team in the Cloud, give them access to GitHub, and start delegating tasks
|
||||
Create your agent team in the Cloud, give them access to GitHub, and start delegating tasks
|
||||
from Web and Slack.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ const keyBenefits = [
|
|||
{
|
||||
title: "Centralized Billing",
|
||||
description:
|
||||
"Single billing point for all team members using Cloud Agents and the Roo Code Cloud Provider. No more API key management.",
|
||||
"Single billing point for all team members using Cloud Agents and the Roo Code Router. No more API key management.",
|
||||
icon: DollarSign,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const pricingTiers: PricingTier[] = [
|
|||
featuresIntro: "Go beyond the extension with",
|
||||
features: [
|
||||
"Access to Cloud Agents: fully autonomous development you can call from Slack, Github and the web",
|
||||
"Access to the Roo Code Cloud Provider",
|
||||
"Access to the Roo Code Router",
|
||||
"Follow your tasks from anywhere",
|
||||
"Share tasks with friends and co-workers",
|
||||
"Token usage analytics",
|
||||
|
|
@ -140,7 +140,7 @@ export default function PricingPage() {
|
|||
<h1 className="text-5xl font-bold tracking-tight">Roo Code Pricing</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">
|
||||
For all of our products: the Roo Code VS Code Extension, Roo Code Cloud and the Roo Code
|
||||
Cloud inference Provider.
|
||||
Router.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -230,12 +230,12 @@ export default function PricingPage() {
|
|||
|
||||
<div className="max-w-6xl mx-auto mt-8 p-7 flex flex-col md:flex-row gap-8 md:gap-4 bg-violet-200/20 outline-violet-700/20 outline outline-1 rounded-2xl transition-all shadow-none">
|
||||
<div className="md:border-r md:pr-4">
|
||||
<h3 className="text-lg font-medium mb-1">Roo Code Provider</h3>
|
||||
<h3 className="text-lg font-medium mb-1">Roo Code Router</h3>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p className="">
|
||||
On any plan, you can use your own LLM provider API key or use the built-in Roo Code
|
||||
Cloud provider – curated models to work with Roo with no markup, including the
|
||||
latest Gemini, GPT and Claude. Paid with credits.
|
||||
Router – curated models to work with Roo with no markup, including the latest
|
||||
Gemini, GPT and Claude. Paid with credits.
|
||||
<Link href="/provider" className="underline hover:no-underline ml-1">
|
||||
See per model pricing.
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -14,16 +14,16 @@ const faqs = [
|
|||
answer: "AI model providers offer various language models with different capabilities and pricing.",
|
||||
},
|
||||
{
|
||||
question: "What is the Roo Code Cloud Provider?",
|
||||
question: "What is the Roo Code Router?",
|
||||
answer: (
|
||||
<>
|
||||
<p>This is our very own model provider, optimized to work seamlessly with Roo Code Cloud.</p>
|
||||
<p>This is our very own model router, optimized to work seamlessly with Roo Code Cloud.</p>
|
||||
<p>You don't have to use it to use Roo Code, but it's the easiest way to do it.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "Do I have to use the Roo Code Cloud Provider to use the Roo Code products?",
|
||||
question: "Do I have to use the Roo Code Router to use the Roo Code products?",
|
||||
answer: "Not at all! You can bring your own provider key, no problem. This is just meant to make it easier.",
|
||||
},
|
||||
{
|
||||
|
|
@ -32,7 +32,7 @@ const faqs = [
|
|||
},
|
||||
{
|
||||
question: "How is my data treated?",
|
||||
answer: "The Roo Code Cloud provider doesn't keep any of your data, the service only aims to make it easier to use Roo Code. Each model vendor has their own privacy policy though, and usually free models use your data for training, so keep that in mind.",
|
||||
answer: "The Roo Code Router doesn't keep any of your data, the service only aims to make it easier to use Roo Code. Each model vendor has their own privacy policy though, and usually free models use your data for training, so keep that in mind.",
|
||||
},
|
||||
{
|
||||
question: "How much does the Roo Code Cloud service cost?",
|
||||
|
|
@ -134,7 +134,7 @@ export default function ProviderPage() {
|
|||
<section className="relative overflow-hidden py-16">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight">The Roo Code Cloud Provider</h1>
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight">The Roo Code Router</h1>
|
||||
<p className="mx-auto mt-4 max-w-3xl md:text-lg text-muted-foreground">
|
||||
The easiest way to use Roo Code (in the{" "}
|
||||
<Link href="/cloud" className="underline hover:no-underline">
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
href="/provider"
|
||||
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
|
||||
<Brain className="size-3 inline mr-2 -mt-0.5" />
|
||||
Roo Code Cloud Provider
|
||||
Roo Code Router
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -194,7 +194,7 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
href="/provider"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Roo Code Cloud Provider
|
||||
Roo Code Router
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@roo-code/types",
|
||||
"version": "1.100.0",
|
||||
"version": "1.103.0",
|
||||
"description": "TypeScript type definitions for Roo Code.",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
|
|
|||
|
|
@ -690,7 +690,7 @@ export const MODELS_BY_PROVIDER: Record<
|
|||
models: Object.keys(openAiNativeModels),
|
||||
},
|
||||
"qwen-code": { id: "qwen-code", label: "Qwen Code", models: Object.keys(qwenCodeModels) },
|
||||
roo: { id: "roo", label: "Roo Code Cloud", models: [] },
|
||||
roo: { id: "roo", label: "Roo Code Router", models: [] },
|
||||
sambanova: {
|
||||
id: "sambanova",
|
||||
label: "SambaNova",
|
||||
|
|
|
|||
BIN
releases/3.39.3-release.png
Normal file
BIN
releases/3.39.3-release.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
|
|
@ -3,7 +3,7 @@
|
|||
"displayName": "%extension.displayName%",
|
||||
"description": "%extension.description%",
|
||||
"publisher": "RooVeterinaryInc",
|
||||
"version": "3.39.2",
|
||||
"version": "3.39.3",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"galleryBanner": {
|
||||
"color": "#617A91",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export const PROVIDERS = [
|
|||
{ value: "fireworks", label: "Fireworks AI" },
|
||||
{ value: "featherless", label: "Featherless AI" },
|
||||
{ value: "io-intelligence", label: "IO Intelligence" },
|
||||
{ value: "roo", label: "Roo Code Cloud" },
|
||||
{ value: "roo", label: "Roo Code Router" },
|
||||
{ value: "vercel-ai-gateway", label: "Vercel AI Gateway" },
|
||||
{ value: "minimax", label: "MiniMax" },
|
||||
{ value: "baseten", label: "Baseten" },
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ export const Roo = ({
|
|||
defaultModelId={rooDefaultModelId}
|
||||
models={routerModels?.roo ?? {}}
|
||||
modelIdKey="apiModelId"
|
||||
serviceName="Roo Code Cloud"
|
||||
serviceUrl="https://roocode.com"
|
||||
serviceName="Roo Code Router"
|
||||
serviceUrl="https://app.roocode.com"
|
||||
organizationAllowList={organizationAllowList}
|
||||
errorMessage={modelValidationError}
|
||||
simplifySettings={simplifySettings}
|
||||
|
|
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
|
@ -336,7 +336,7 @@ const WelcomeViewProvider = () => {
|
|||
(e.target as HTMLInputElement)) as HTMLInputElement
|
||||
setSelectedProvider(target.value as ProviderOption)
|
||||
}}>
|
||||
{/* Roo Code Cloud Provider Option */}
|
||||
{/* Roo Code Router Option */}
|
||||
<VSCodeRadio value="roo" className="flex items-start gap-2">
|
||||
<div className="flex-1 space-y-1 cursor-pointer">
|
||||
<p className="text-lg font-semibold block -mt-1">
|
||||
|
|
|
|||
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",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/de/welcome.json
generated
6
webview-ui/src/i18n/locales/de/welcome.json
generated
|
|
@ -22,13 +22,13 @@
|
|||
"providerSignup": {
|
||||
"heading": "Wähle deinen Anbieter",
|
||||
"chooseProvider": "Roo braucht einen LLM-Anbieter, um zu funktionieren. Wähle einen aus, um loszulegen. Du kannst später mehr hinzufügen.",
|
||||
"rooCloudProvider": "Roo Code Cloud Provider",
|
||||
"rooCloudDescription": "Der einfachste Weg loszulegen ist mit dem Roo Code Cloud Provider: eine kuratierte Mischung aus kostenlosen und bezahlten Modellen zu niedrigen Kosten.",
|
||||
"rooCloudProvider": "Roo Code Router",
|
||||
"rooCloudDescription": "Der einfachste Weg loszulegen ist mit dem Roo Code Router: eine kuratierte Mischung aus kostenlosen und bezahlten Modellen zu niedrigen Kosten.",
|
||||
"learnMore": "Weitere Informationen",
|
||||
"useAnotherProvider": "3rd-party Provider",
|
||||
"useAnotherProviderDescription": "Gib einen API-Schlüssel ein und leg los.",
|
||||
"noApiKeys": "Du möchtest dich nicht mit API-Schlüsseln und separaten Konten rumschlagen?",
|
||||
"backToRoo": "Geh mit dem Roo Code Cloud Provider.",
|
||||
"backToRoo": "Geh mit dem Roo Code Router.",
|
||||
"goBack": "Zurück",
|
||||
"finish": "Beenden"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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?",
|
||||
|
|
|
|||
|
|
@ -22,13 +22,13 @@
|
|||
"providerSignup": {
|
||||
"heading": "Choose your provider",
|
||||
"chooseProvider": "Roo needs an LLM provider to work. Choose one to get started, you can add more later.",
|
||||
"rooCloudProvider": "Roo Code Cloud Provider",
|
||||
"rooCloudDescription": "The easiest way to start is with the Roo Code Cloud Provider: a curated mix of free and paid models at a low cost.",
|
||||
"rooCloudProvider": "Roo Code Router",
|
||||
"rooCloudDescription": "The easiest way to start is with the Roo Code Router: a curated mix of free and paid models at a low cost.",
|
||||
"learnMore": "Learn more",
|
||||
"useAnotherProvider": "3rd-party Provider",
|
||||
"useAnotherProviderDescription": "Enter an API key and get going.",
|
||||
"noApiKeys": "Don't want to deal with API keys and separate accounts?",
|
||||
"backToRoo": "Go with the Roo Code Cloud Provider.",
|
||||
"backToRoo": "Go with the Roo Code Router.",
|
||||
"goBack": "Back",
|
||||
"finish": "Finish"
|
||||
},
|
||||
|
|
|
|||
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",
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/nl/welcome.json
generated
6
webview-ui/src/i18n/locales/nl/welcome.json
generated
|
|
@ -22,13 +22,13 @@
|
|||
"providerSignup": {
|
||||
"heading": "Kies je provider",
|
||||
"chooseProvider": "Roo heeft een LLM-provider nodig om te werken. Kies er een om te beginnen, je kunt er later meer toevoegen.",
|
||||
"rooCloudProvider": "Roo Code Cloud Provider",
|
||||
"rooCloudDescription": "De eenvoudigste manier om te beginnen is met de Roo Code Cloud Provider: een zorgvuldig samengesteld mix van gratis en betaalde modellen tegen lage kosten.",
|
||||
"rooCloudProvider": "Roo Code Router",
|
||||
"rooCloudDescription": "De eenvoudigste manier om te beginnen is met de Roo Code Router: een zorgvuldig samengesteld mix van gratis en betaalde modellen tegen lage kosten.",
|
||||
"learnMore": "Meer informatie",
|
||||
"useAnotherProvider": "Provider van derden",
|
||||
"useAnotherProviderDescription": "Voer een API-sleutel in en begin.",
|
||||
"noApiKeys": "Wil je je niet bezighouden met API-sleutels en aparte accounts?",
|
||||
"backToRoo": "Kies de Roo Code Cloud Provider.",
|
||||
"backToRoo": "Kies de Roo Code Router.",
|
||||
"goBack": "Terug",
|
||||
"finish": "Klaar"
|
||||
},
|
||||
|
|
|
|||
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