| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851 |
- import React, {
- forwardRef,
- memo,
- useCallback,
- useEffect,
- useImperativeHandle,
- useLayoutEffect,
- useMemo,
- useRef,
- useState,
- } from "react"
- import {
- CheckCheck,
- SquareMousePointer,
- Webhook,
- GitBranch,
- Bell,
- Database,
- SquareTerminal,
- FlaskConical,
- AlertTriangle,
- Globe,
- Info,
- MessageSquare,
- LucideIcon,
- SquareSlash,
- Glasses,
- } from "lucide-react"
- import type { ProviderSettings, ExperimentId, TelemetrySetting } from "@roo-code/types"
- import { vscode } from "@src/utils/vscode"
- import { cn } from "@src/lib/utils"
- import { useAppTranslation } from "@src/i18n/TranslationContext"
- import { ExtensionStateContextType, useExtensionState } from "@src/context/ExtensionStateContext"
- import {
- AlertDialog,
- AlertDialogContent,
- AlertDialogTitle,
- AlertDialogDescription,
- AlertDialogCancel,
- AlertDialogAction,
- AlertDialogHeader,
- AlertDialogFooter,
- Button,
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
- StandardTooltip,
- } from "@src/components/ui"
- import { Tab, TabContent, TabHeader, TabList, TabTrigger } from "../common/Tab"
- import { SetCachedStateField, SetExperimentEnabled } from "./types"
- import { SectionHeader } from "./SectionHeader"
- import ApiConfigManager from "./ApiConfigManager"
- import ApiOptions from "./ApiOptions"
- import { AutoApproveSettings } from "./AutoApproveSettings"
- import { BrowserSettings } from "./BrowserSettings"
- import { CheckpointSettings } from "./CheckpointSettings"
- import { NotificationSettings } from "./NotificationSettings"
- import { ContextManagementSettings } from "./ContextManagementSettings"
- import { TerminalSettings } from "./TerminalSettings"
- import { ExperimentalSettings } from "./ExperimentalSettings"
- import { LanguageSettings } from "./LanguageSettings"
- import { About } from "./About"
- import { Section } from "./Section"
- import PromptsSettings from "./PromptsSettings"
- import { SlashCommandsSettings } from "./SlashCommandsSettings"
- import { UISettings } from "./UISettings"
- export const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden"
- export const settingsTabList =
- "w-48 data-[compact=true]:w-12 flex-shrink-0 flex flex-col overflow-y-auto overflow-x-hidden border-r border-vscode-sideBar-background"
- export const settingsTabTrigger =
- "whitespace-nowrap overflow-hidden min-w-0 h-12 px-4 py-3 box-border flex items-center border-l-2 border-transparent text-vscode-foreground opacity-70 hover:bg-vscode-list-hoverBackground data-[compact=true]:w-12 data-[compact=true]:p-4"
- export const settingsTabTriggerActive = "opacity-100 border-vscode-focusBorder bg-vscode-list-activeSelectionBackground"
- export interface SettingsViewRef {
- checkUnsaveChanges: (then: () => void) => void
- }
- const sectionNames = [
- "providers",
- "autoApprove",
- "slashCommands",
- "browser",
- "checkpoints",
- "notifications",
- "contextManagement",
- "terminal",
- "prompts",
- "ui",
- "experimental",
- "language",
- "about",
- ] as const
- type SectionName = (typeof sectionNames)[number]
- type SettingsViewProps = {
- onDone: () => void
- targetSection?: string
- }
- const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, targetSection }, ref) => {
- const { t } = useAppTranslation()
- const extensionState = useExtensionState()
- const { currentApiConfigName, listApiConfigMeta, uriScheme, settingsImportedAt } = extensionState
- const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
- const [isChangeDetected, setChangeDetected] = useState(false)
- const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
- const [activeTab, setActiveTab] = useState<SectionName>(
- targetSection && sectionNames.includes(targetSection as SectionName)
- ? (targetSection as SectionName)
- : "providers",
- )
- const scrollPositions = useRef<Record<SectionName, number>>(
- Object.fromEntries(sectionNames.map((s) => [s, 0])) as Record<SectionName, number>,
- )
- const contentRef = useRef<HTMLDivElement | null>(null)
- const prevApiConfigName = useRef(currentApiConfigName)
- const confirmDialogHandler = useRef<() => void>()
- const [cachedState, setCachedState] = useState(() => extensionState)
- const {
- alwaysAllowReadOnly,
- alwaysAllowReadOnlyOutsideWorkspace,
- allowedCommands,
- deniedCommands,
- allowedMaxRequests,
- allowedMaxCost,
- language,
- alwaysAllowBrowser,
- alwaysAllowExecute,
- alwaysAllowMcp,
- alwaysAllowModeSwitch,
- alwaysAllowSubtasks,
- alwaysAllowWrite,
- alwaysAllowWriteOutsideWorkspace,
- alwaysAllowWriteProtected,
- alwaysApproveResubmit,
- autoCondenseContext,
- autoCondenseContextPercent,
- browserToolEnabled,
- browserViewportSize,
- enableCheckpoints,
- checkpointTimeout,
- diffEnabled,
- experiments,
- fuzzyMatchThreshold,
- maxOpenTabsContext,
- maxWorkspaceFiles,
- mcpEnabled,
- requestDelaySeconds,
- remoteBrowserHost,
- screenshotQuality,
- soundEnabled,
- ttsEnabled,
- ttsSpeed,
- soundVolume,
- telemetrySetting,
- terminalOutputLineLimit,
- terminalOutputCharacterLimit,
- terminalShellIntegrationTimeout,
- terminalShellIntegrationDisabled, // Added from upstream
- terminalCommandDelay,
- terminalPowershellCounter,
- terminalZshClearEolMark,
- terminalZshOhMy,
- terminalZshP10k,
- terminalZdotdir,
- writeDelayMs,
- showRooIgnoredFiles,
- remoteBrowserEnabled,
- maxReadFileLine,
- maxImageFileSize,
- maxTotalImageSize,
- terminalCompressProgressBar,
- maxConcurrentFileReads,
- condensingApiConfigId,
- customCondensingPrompt,
- customSupportPrompts,
- profileThresholds,
- alwaysAllowFollowupQuestions,
- alwaysAllowUpdateTodoList,
- followupAutoApproveTimeoutMs,
- includeDiagnosticMessages,
- maxDiagnosticMessages,
- includeTaskHistoryInEnhance,
- openRouterImageApiKey,
- openRouterImageGenerationSelectedModel,
- reasoningBlockCollapsed,
- includeCurrentTime,
- includeCurrentCost,
- } = cachedState
- const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
- useEffect(() => {
- // Update only when currentApiConfigName is changed.
- // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration.
- if (prevApiConfigName.current === currentApiConfigName) {
- return
- }
- setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
- prevApiConfigName.current = currentApiConfigName
- setChangeDetected(false)
- }, [currentApiConfigName, extensionState])
- // Bust the cache when settings are imported.
- useEffect(() => {
- if (settingsImportedAt) {
- setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
- setChangeDetected(false)
- }
- }, [settingsImportedAt, extensionState])
- const setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType> = useCallback((field, value) => {
- setCachedState((prevState) => {
- if (prevState[field] === value) {
- return prevState
- }
- setChangeDetected(true)
- return { ...prevState, [field]: value }
- })
- }, [])
- const setApiConfigurationField = useCallback(
- <K extends keyof ProviderSettings>(field: K, value: ProviderSettings[K], isUserAction: boolean = true) => {
- setCachedState((prevState) => {
- if (prevState.apiConfiguration?.[field] === value) {
- return prevState
- }
- const previousValue = prevState.apiConfiguration?.[field]
- // Only skip change detection for automatic initialization (not user actions)
- // This prevents the dirty state when the component initializes and auto-syncs values
- // Treat undefined, null, and empty string as uninitialized states
- const isInitialSync =
- !isUserAction &&
- (previousValue === undefined || previousValue === "" || previousValue === null) &&
- value !== undefined &&
- value !== "" &&
- value !== null
- if (!isInitialSync) {
- setChangeDetected(true)
- }
- return { ...prevState, apiConfiguration: { ...prevState.apiConfiguration, [field]: value } }
- })
- },
- [],
- )
- const setExperimentEnabled: SetExperimentEnabled = useCallback((id: ExperimentId, enabled: boolean) => {
- setCachedState((prevState) => {
- if (prevState.experiments?.[id] === enabled) {
- return prevState
- }
- setChangeDetected(true)
- return { ...prevState, experiments: { ...prevState.experiments, [id]: enabled } }
- })
- }, [])
- const setTelemetrySetting = useCallback((setting: TelemetrySetting) => {
- setCachedState((prevState) => {
- if (prevState.telemetrySetting === setting) {
- return prevState
- }
- setChangeDetected(true)
- return { ...prevState, telemetrySetting: setting }
- })
- }, [])
- const setOpenRouterImageApiKey = useCallback((apiKey: string) => {
- setCachedState((prevState) => {
- // Only set change detected if value actually changed
- if (prevState.openRouterImageApiKey !== apiKey) {
- setChangeDetected(true)
- }
- return { ...prevState, openRouterImageApiKey: apiKey }
- })
- }, [])
- const setImageGenerationSelectedModel = useCallback((model: string) => {
- setCachedState((prevState) => {
- // Only set change detected if value actually changed
- if (prevState.openRouterImageGenerationSelectedModel !== model) {
- setChangeDetected(true)
- }
- return { ...prevState, openRouterImageGenerationSelectedModel: model }
- })
- }, [])
- const setCustomSupportPromptsField = useCallback((prompts: Record<string, string | undefined>) => {
- setCachedState((prevState) => {
- const previousStr = JSON.stringify(prevState.customSupportPrompts)
- const newStr = JSON.stringify(prompts)
- if (previousStr === newStr) {
- return prevState
- }
- setChangeDetected(true)
- return { ...prevState, customSupportPrompts: prompts }
- })
- }, [])
- const isSettingValid = !errorMessage
- const handleSubmit = () => {
- if (isSettingValid) {
- vscode.postMessage({ type: "language", text: language })
- vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
- vscode.postMessage({
- type: "alwaysAllowReadOnlyOutsideWorkspace",
- bool: alwaysAllowReadOnlyOutsideWorkspace,
- })
- vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
- vscode.postMessage({ type: "alwaysAllowWriteOutsideWorkspace", bool: alwaysAllowWriteOutsideWorkspace })
- vscode.postMessage({ type: "alwaysAllowWriteProtected", bool: alwaysAllowWriteProtected })
- vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
- vscode.postMessage({ type: "alwaysAllowBrowser", bool: alwaysAllowBrowser })
- vscode.postMessage({ type: "alwaysAllowMcp", bool: alwaysAllowMcp })
- vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] })
- vscode.postMessage({ type: "deniedCommands", commands: deniedCommands ?? [] })
- vscode.postMessage({ type: "allowedMaxRequests", value: allowedMaxRequests ?? undefined })
- vscode.postMessage({ type: "allowedMaxCost", value: allowedMaxCost ?? undefined })
- vscode.postMessage({ type: "autoCondenseContext", bool: autoCondenseContext })
- vscode.postMessage({ type: "autoCondenseContextPercent", value: autoCondenseContextPercent })
- vscode.postMessage({ type: "browserToolEnabled", bool: browserToolEnabled })
- vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
- vscode.postMessage({ type: "ttsEnabled", bool: ttsEnabled })
- vscode.postMessage({ type: "ttsSpeed", value: ttsSpeed })
- vscode.postMessage({ type: "soundVolume", value: soundVolume })
- vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
- vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints })
- vscode.postMessage({ type: "checkpointTimeout", value: checkpointTimeout })
- vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize })
- vscode.postMessage({ type: "remoteBrowserHost", text: remoteBrowserHost })
- vscode.postMessage({ type: "remoteBrowserEnabled", bool: remoteBrowserEnabled })
- vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
- vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
- vscode.postMessage({ type: "screenshotQuality", value: screenshotQuality ?? 75 })
- vscode.postMessage({ type: "terminalOutputLineLimit", value: terminalOutputLineLimit ?? 500 })
- vscode.postMessage({ type: "terminalOutputCharacterLimit", value: terminalOutputCharacterLimit ?? 50000 })
- vscode.postMessage({ type: "terminalShellIntegrationTimeout", value: terminalShellIntegrationTimeout })
- vscode.postMessage({ type: "terminalShellIntegrationDisabled", bool: terminalShellIntegrationDisabled })
- vscode.postMessage({ type: "terminalCommandDelay", value: terminalCommandDelay })
- vscode.postMessage({ type: "terminalPowershellCounter", bool: terminalPowershellCounter })
- vscode.postMessage({ type: "terminalZshClearEolMark", bool: terminalZshClearEolMark })
- vscode.postMessage({ type: "terminalZshOhMy", bool: terminalZshOhMy })
- vscode.postMessage({ type: "terminalZshP10k", bool: terminalZshP10k })
- vscode.postMessage({ type: "terminalZdotdir", bool: terminalZdotdir })
- vscode.postMessage({ type: "terminalCompressProgressBar", bool: terminalCompressProgressBar })
- vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
- vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
- vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
- vscode.postMessage({ type: "maxOpenTabsContext", value: maxOpenTabsContext })
- vscode.postMessage({ type: "maxWorkspaceFiles", value: maxWorkspaceFiles ?? 200 })
- vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles })
- vscode.postMessage({ type: "maxReadFileLine", value: maxReadFileLine ?? -1 })
- vscode.postMessage({ type: "maxImageFileSize", value: maxImageFileSize ?? 5 })
- vscode.postMessage({ type: "maxTotalImageSize", value: maxTotalImageSize ?? 20 })
- vscode.postMessage({ type: "maxConcurrentFileReads", value: cachedState.maxConcurrentFileReads ?? 5 })
- vscode.postMessage({ type: "includeDiagnosticMessages", bool: includeDiagnosticMessages })
- vscode.postMessage({ type: "maxDiagnosticMessages", value: maxDiagnosticMessages ?? 50 })
- vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName })
- vscode.postMessage({ type: "updateExperimental", values: experiments })
- vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
- vscode.postMessage({ type: "alwaysAllowSubtasks", bool: alwaysAllowSubtasks })
- vscode.postMessage({ type: "alwaysAllowFollowupQuestions", bool: alwaysAllowFollowupQuestions })
- vscode.postMessage({ type: "alwaysAllowUpdateTodoList", bool: alwaysAllowUpdateTodoList })
- vscode.postMessage({ type: "followupAutoApproveTimeoutMs", value: followupAutoApproveTimeoutMs })
- vscode.postMessage({ type: "condensingApiConfigId", text: condensingApiConfigId || "" })
- vscode.postMessage({ type: "updateCondensingPrompt", text: customCondensingPrompt || "" })
- vscode.postMessage({ type: "updateSupportPrompt", values: customSupportPrompts || {} })
- vscode.postMessage({ type: "includeTaskHistoryInEnhance", bool: includeTaskHistoryInEnhance ?? true })
- vscode.postMessage({ type: "setReasoningBlockCollapsed", bool: reasoningBlockCollapsed ?? true })
- vscode.postMessage({ type: "includeCurrentTime", bool: includeCurrentTime ?? true })
- vscode.postMessage({ type: "includeCurrentCost", bool: includeCurrentCost ?? true })
- vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
- vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
- vscode.postMessage({ type: "profileThresholds", values: profileThresholds })
- vscode.postMessage({ type: "openRouterImageApiKey", text: openRouterImageApiKey })
- vscode.postMessage({
- type: "openRouterImageGenerationSelectedModel",
- text: openRouterImageGenerationSelectedModel,
- })
- setChangeDetected(false)
- }
- }
- const checkUnsaveChanges = useCallback(
- (then: () => void) => {
- if (isChangeDetected) {
- confirmDialogHandler.current = then
- setDiscardDialogShow(true)
- } else {
- then()
- }
- },
- [isChangeDetected],
- )
- useImperativeHandle(ref, () => ({ checkUnsaveChanges }), [checkUnsaveChanges])
- const onConfirmDialogResult = useCallback(
- (confirm: boolean) => {
- if (confirm) {
- // Discard changes: Reset state and flag
- setCachedState(extensionState) // Revert to original state
- setChangeDetected(false) // Reset change flag
- confirmDialogHandler.current?.() // Execute the pending action (e.g., tab switch)
- }
- // If confirm is false (Cancel), do nothing, dialog closes automatically
- },
- [extensionState], // Depend on extensionState to get the latest original state
- )
- // Handle tab changes with unsaved changes check
- const handleTabChange = useCallback(
- (newTab: SectionName) => {
- if (contentRef.current) {
- scrollPositions.current[activeTab] = contentRef.current.scrollTop
- }
- setActiveTab(newTab)
- },
- [activeTab],
- )
- useLayoutEffect(() => {
- if (contentRef.current) {
- contentRef.current.scrollTop = scrollPositions.current[activeTab] ?? 0
- }
- }, [activeTab])
- // Store direct DOM element refs for each tab
- const tabRefs = useRef<Record<SectionName, HTMLButtonElement | null>>(
- Object.fromEntries(sectionNames.map((name) => [name, null])) as Record<SectionName, HTMLButtonElement | null>,
- )
- // Track whether we're in compact mode
- const [isCompactMode, setIsCompactMode] = useState(false)
- const containerRef = useRef<HTMLDivElement>(null)
- // Setup resize observer to detect when we should switch to compact mode
- useEffect(() => {
- if (!containerRef.current) return
- const observer = new ResizeObserver((entries) => {
- for (const entry of entries) {
- // If container width is less than 500px, switch to compact mode
- setIsCompactMode(entry.contentRect.width < 500)
- }
- })
- observer.observe(containerRef.current)
- return () => {
- observer?.disconnect()
- }
- }, [])
- const sections: { id: SectionName; icon: LucideIcon }[] = useMemo(
- () => [
- { id: "providers", icon: Webhook },
- { id: "autoApprove", icon: CheckCheck },
- { id: "slashCommands", icon: SquareSlash },
- { id: "browser", icon: SquareMousePointer },
- { id: "checkpoints", icon: GitBranch },
- { id: "notifications", icon: Bell },
- { id: "contextManagement", icon: Database },
- { id: "terminal", icon: SquareTerminal },
- { id: "prompts", icon: MessageSquare },
- { id: "ui", icon: Glasses },
- { id: "experimental", icon: FlaskConical },
- { id: "language", icon: Globe },
- { id: "about", icon: Info },
- ],
- [], // No dependencies needed now
- )
- // Update target section logic to set active tab
- useEffect(() => {
- if (targetSection && sectionNames.includes(targetSection as SectionName)) {
- setActiveTab(targetSection as SectionName)
- }
- }, [targetSection])
- // Function to scroll the active tab into view for vertical layout
- const scrollToActiveTab = useCallback(() => {
- const activeTabElement = tabRefs.current[activeTab]
- if (activeTabElement) {
- activeTabElement.scrollIntoView({
- behavior: "auto",
- block: "nearest",
- })
- }
- }, [activeTab])
- // Effect to scroll when the active tab changes
- useEffect(() => {
- scrollToActiveTab()
- }, [activeTab, scrollToActiveTab])
- // Effect to scroll when the webview becomes visible
- useLayoutEffect(() => {
- const handleMessage = (event: MessageEvent) => {
- const message = event.data
- if (message.type === "action" && message.action === "didBecomeVisible") {
- scrollToActiveTab()
- }
- }
- window.addEventListener("message", handleMessage)
- return () => {
- window.removeEventListener("message", handleMessage)
- }
- }, [scrollToActiveTab])
- 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">
- <StandardTooltip
- content={
- !isSettingValid
- ? errorMessage
- : isChangeDetected
- ? t("settings:header.saveButtonTooltip")
- : t("settings:header.nothingChangedTooltip")
- }>
- <Button
- variant={isSettingValid ? "default" : "secondary"}
- className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
- onClick={handleSubmit}
- disabled={!isChangeDetected || !isSettingValid}
- data-testid="save-button">
- {t("settings:common.save")}
- </Button>
- </StandardTooltip>
- <StandardTooltip content={t("settings:header.doneButtonTooltip")}>
- <Button variant="secondary" onClick={() => checkUnsaveChanges(onDone)}>
- {t("settings:common.done")}
- </Button>
- </StandardTooltip>
- </div>
- </TabHeader>
- {/* Vertical tabs layout */}
- <div ref={containerRef} className={cn(settingsTabsContainer, isCompactMode && "narrow")}>
- {/* Tab sidebar */}
- <TabList
- value={activeTab}
- onValueChange={(value) => handleTabChange(value as SectionName)}
- className={cn(settingsTabList)}
- data-compact={isCompactMode}
- data-testid="settings-tab-list">
- {sections.map(({ id, icon: Icon }) => {
- const isSelected = id === activeTab
- const onSelect = () => handleTabChange(id)
- // Base TabTrigger component definition
- // We pass isSelected manually for styling, but onSelect is handled conditionally
- const triggerComponent = (
- <TabTrigger
- ref={(element) => (tabRefs.current[id] = element)}
- value={id}
- isSelected={isSelected} // Pass manually for styling state
- className={cn(
- isSelected // Use manual isSelected for styling
- ? `${settingsTabTrigger} ${settingsTabTriggerActive}`
- : settingsTabTrigger,
- "focus:ring-0", // Remove the focus ring styling
- )}
- data-testid={`tab-${id}`}
- data-compact={isCompactMode}>
- <div className={cn("flex items-center gap-2", isCompactMode && "justify-center")}>
- <Icon className="w-4 h-4" />
- <span className="tab-label">{t(`settings:sections.${id}`)}</span>
- </div>
- </TabTrigger>
- )
- if (isCompactMode) {
- // Wrap in Tooltip and manually add onClick to the trigger
- return (
- <TooltipProvider key={id} delayDuration={300}>
- <Tooltip>
- <TooltipTrigger asChild onClick={onSelect}>
- {/* Clone to avoid ref issues if triggerComponent itself had a key */}
- {React.cloneElement(triggerComponent)}
- </TooltipTrigger>
- <TooltipContent side="right" className="text-base">
- <p className="m-0">{t(`settings:sections.${id}`)}</p>
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- )
- } else {
- // Render trigger directly; TabList will inject onSelect via cloning
- // Ensure the element passed to TabList has the key
- return React.cloneElement(triggerComponent, { key: id })
- }
- })}
- </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>
- <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}
- alwaysApproveResubmit={alwaysApproveResubmit}
- requestDelaySeconds={requestDelaySeconds}
- alwaysAllowMcp={alwaysAllowMcp}
- alwaysAllowModeSwitch={alwaysAllowModeSwitch}
- alwaysAllowSubtasks={alwaysAllowSubtasks}
- alwaysAllowExecute={alwaysAllowExecute}
- alwaysAllowFollowupQuestions={alwaysAllowFollowupQuestions}
- alwaysAllowUpdateTodoList={alwaysAllowUpdateTodoList}
- followupAutoApproveTimeoutMs={followupAutoApproveTimeoutMs}
- allowedCommands={allowedCommands}
- allowedMaxRequests={allowedMaxRequests ?? undefined}
- allowedMaxCost={allowedMaxCost ?? undefined}
- deniedCommands={deniedCommands}
- setCachedStateField={setCachedStateField}
- />
- )}
- {/* Slash Commands Section */}
- {activeTab === "slashCommands" && <SlashCommandsSettings />}
- {/* Browser Section */}
- {activeTab === "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}
- />
- )}
- {/* Notifications Section */}
- {activeTab === "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}
- maxReadFileLine={maxReadFileLine}
- maxImageFileSize={maxImageFileSize}
- maxTotalImageSize={maxTotalImageSize}
- maxConcurrentFileReads={maxConcurrentFileReads}
- profileThresholds={profileThresholds}
- includeDiagnosticMessages={includeDiagnosticMessages}
- maxDiagnosticMessages={maxDiagnosticMessages}
- writeDelayMs={writeDelayMs}
- includeCurrentTime={includeCurrentTime}
- includeCurrentCost={includeCurrentCost}
- 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}
- />
- )}
- {/* Prompts Section */}
- {activeTab === "prompts" && (
- <PromptsSettings
- customSupportPrompts={customSupportPrompts || {}}
- setCustomSupportPrompts={setCustomSupportPromptsField}
- includeTaskHistoryInEnhance={includeTaskHistoryInEnhance}
- setIncludeTaskHistoryInEnhance={(value) =>
- setCachedStateField("includeTaskHistoryInEnhance", value)
- }
- />
- )}
- {/* UI Section */}
- {activeTab === "ui" && (
- <UISettings
- reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
- setCachedStateField={setCachedStateField}
- />
- )}
- {/* Experimental Section */}
- {activeTab === "experimental" && (
- <ExperimentalSettings
- setExperimentEnabled={setExperimentEnabled}
- experiments={experiments}
- apiConfiguration={apiConfiguration}
- setApiConfigurationField={setApiConfigurationField}
- openRouterImageApiKey={openRouterImageApiKey as string | undefined}
- openRouterImageGenerationSelectedModel={
- openRouterImageGenerationSelectedModel as string | undefined
- }
- setOpenRouterImageApiKey={setOpenRouterImageApiKey}
- setImageGenerationSelectedModel={setImageGenerationSelectedModel}
- />
- )}
- {/* Language Section */}
- {activeTab === "language" && (
- <LanguageSettings language={language || "en"} setCachedStateField={setCachedStateField} />
- )}
- {/* About Section */}
- {activeTab === "about" && (
- <About telemetrySetting={telemetrySetting} setTelemetrySetting={setTelemetrySetting} />
- )}
- </TabContent>
- </div>
- <AlertDialog open={isDiscardDialogShow} onOpenChange={setDiscardDialogShow}>
- <AlertDialogContent>
- <AlertDialogHeader>
- <AlertDialogTitle>
- <AlertTriangle className="w-5 h-5 text-yellow-500" />
- {t("settings:unsavedChangesDialog.title")}
- </AlertDialogTitle>
- <AlertDialogDescription>
- {t("settings:unsavedChangesDialog.description")}
- </AlertDialogDescription>
- </AlertDialogHeader>
- <AlertDialogFooter>
- <AlertDialogCancel onClick={() => onConfirmDialogResult(false)}>
- {t("settings:unsavedChangesDialog.cancelButton")}
- </AlertDialogCancel>
- <AlertDialogAction onClick={() => onConfirmDialogResult(true)}>
- {t("settings:unsavedChangesDialog.discardButton")}
- </AlertDialogAction>
- </AlertDialogFooter>
- </AlertDialogContent>
- </AlertDialog>
- </Tab>
- )
- })
- export default memo(SettingsView)
|