Feat: Vertical settings tabs (#2914)

Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
This commit is contained in:
dlab-anton 2025-05-06 19:48:42 +07:00 committed by GitHub
parent 78b2083467
commit c701600081
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 650 additions and 359 deletions

View file

@ -116,6 +116,8 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
telemetryService.captureTitleButtonClicked("settings")
visibleProvider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" })
// Also explicitly post the visibility message to trigger scroll reliably
visibleProvider.postMessageToWebview({ type: "action", action: "didBecomeVisible" })
},
"roo-cline.historyButtonClicked": () => {
const visibleProvider = getVisibleProviderOrLog(outputChannel)
@ -212,10 +214,26 @@ export const openClineInNewTab = async ({ context, outputChannel }: Omit<Registe
await tabProvider.resolveWebviewView(newPanel)
// Add listener for visibility changes to notify webview
newPanel.onDidChangeViewState(
(e) => {
const panel = e.webviewPanel
if (panel.visible) {
panel.webview.postMessage({ type: "action", action: "didBecomeVisible" }) // Use the same message type as in SettingsView.tsx
}
},
null, // First null is for `thisArgs`
context.subscriptions, // Register listener for disposal
)
// Handle panel closing events.
newPanel.onDidDispose(() => {
setPanel(undefined, "tab")
})
newPanel.onDidDispose(
() => {
setPanel(undefined, "tab")
},
null,
context.subscriptions, // Also register dispose listener
)
// Lock the editor group so clicking on files doesn't open them over the panel.
await delay(100)

View file

@ -1,4 +1,4 @@
import { HTMLAttributes, useCallback } from "react"
import React, { HTMLAttributes, useCallback, forwardRef } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { cn } from "@/lib/utils"
@ -6,7 +6,7 @@ import { cn } from "@/lib/utils"
type TabProps = HTMLAttributes<HTMLDivElement>
export const Tab = ({ className, children, ...props }: TabProps) => (
<div className={cn("fixed inset-0 flex flex-col overflow-hidden", className)} {...props}>
<div className={cn("fixed inset-0 flex flex-col", className)} {...props}>
{children}
</div>
)
@ -45,3 +45,47 @@ export const TabContent = ({ className, children, ...props }: TabProps) => {
</div>
)
}
export const TabList = forwardRef<
HTMLDivElement,
HTMLAttributes<HTMLDivElement> & {
value: string
onValueChange: (value: string) => void
}
>(({ children, className, value, onValueChange, ...props }, ref) => {
return (
<div ref={ref} role="tablist" className={cn("flex", className)} {...props}>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child as React.ReactElement<any>, {
isSelected: child.props.value === value,
onSelect: () => onValueChange(child.props.value),
})
}
return child
})}
</div>
)
})
export const TabTrigger = forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement> & {
value: string
isSelected?: boolean
onSelect?: () => void
}
>(({ children, className, value: _value, isSelected, onSelect, ...props }, ref) => {
return (
<button
ref={ref}
role="tab"
aria-selected={isSelected}
tabIndex={isSelected ? 0 : -1}
className={cn("focus:outline-none focus:ring-2 focus:ring-vscode-focusBorder", className)}
onClick={onSelect}
{...props}>
{children}
</button>
)
})

View file

@ -1,4 +1,14 @@
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"
import React, {
forwardRef,
memo,
useCallback,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import {
CheckCheck,
@ -14,7 +24,6 @@ import {
Info,
LucideIcon,
} from "lucide-react"
import { CaretSortIcon } from "@radix-ui/react-icons"
import { ExperimentId } from "@roo/shared/experiments"
import { TelemetrySetting } from "@roo/shared/TelemetrySetting"
@ -32,13 +41,13 @@ import {
AlertDialogHeader,
AlertDialogFooter,
Button,
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui"
import { Tab, TabContent, TabHeader } from "../common/Tab"
import { Tab, TabContent, TabHeader, TabList, TabTrigger } from "../common/Tab"
import { SetCachedStateField, SetExperimentEnabled } from "./types"
import { SectionHeader } from "./SectionHeader"
import ApiConfigManager from "./ApiConfigManager"
@ -53,6 +62,14 @@ import { ExperimentalSettings } from "./ExperimentalSettings"
import { LanguageSettings } from "./LanguageSettings"
import { About } from "./About"
import { Section } from "./Section"
import { cn } from "@/lib/utils"
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
@ -87,6 +104,11 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
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 prevApiConfigName = useRef(currentApiConfigName)
const confirmDialogHandler = useRef<() => void>()
@ -125,7 +147,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
telemetrySetting,
terminalOutputLineLimit,
terminalShellIntegrationTimeout,
terminalShellIntegrationDisabled,
terminalShellIntegrationDisabled, // Added from upstream
terminalCommandDelay,
terminalPowershellCounter,
terminalZshClearEolMark,
@ -139,7 +161,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
terminalCompressProgressBar,
} = cachedState
// Make sure apiConfiguration is initialized and managed by SettingsView.
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
useEffect(() => {
@ -279,81 +300,116 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
useImperativeHandle(ref, () => ({ checkUnsaveChanges }), [checkUnsaveChanges])
const onConfirmDialogResult = useCallback((confirm: boolean) => {
if (confirm) {
confirmDialogHandler.current?.()
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) => {
// Directly switch tab without checking for unsaved changes
setActiveTab(newTab)
},
[], // No dependency on isChangeDetected needed anymore
)
// 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 providersRef = useRef<HTMLDivElement>(null)
const autoApproveRef = useRef<HTMLDivElement>(null)
const browserRef = useRef<HTMLDivElement>(null)
const checkpointsRef = useRef<HTMLDivElement>(null)
const notificationsRef = useRef<HTMLDivElement>(null)
const contextManagementRef = useRef<HTMLDivElement>(null)
const terminalRef = useRef<HTMLDivElement>(null)
const experimentalRef = useRef<HTMLDivElement>(null)
const languageRef = useRef<HTMLDivElement>(null)
const aboutRef = useRef<HTMLDivElement>(null)
const sections: { id: SectionName; icon: LucideIcon; ref: React.RefObject<HTMLDivElement> }[] = useMemo(
const sections: { id: SectionName; icon: LucideIcon }[] = useMemo(
() => [
{ id: "providers", icon: Webhook, ref: providersRef },
{ id: "autoApprove", icon: CheckCheck, ref: autoApproveRef },
{ id: "browser", icon: SquareMousePointer, ref: browserRef },
{ id: "checkpoints", icon: GitBranch, ref: checkpointsRef },
{ id: "notifications", icon: Bell, ref: notificationsRef },
{ id: "contextManagement", icon: Database, ref: contextManagementRef },
{ id: "terminal", icon: SquareTerminal, ref: terminalRef },
{ id: "experimental", icon: FlaskConical, ref: experimentalRef },
{ id: "language", icon: Globe, ref: languageRef },
{ id: "about", icon: Info, ref: aboutRef },
],
[
providersRef,
autoApproveRef,
browserRef,
checkpointsRef,
notificationsRef,
contextManagementRef,
terminalRef,
experimentalRef,
{ id: "providers", icon: Webhook },
{ id: "autoApprove", icon: CheckCheck },
{ id: "browser", icon: SquareMousePointer },
{ id: "checkpoints", icon: GitBranch },
{ id: "notifications", icon: Bell },
{ id: "contextManagement", icon: Database },
{ id: "terminal", icon: SquareTerminal },
{ id: "experimental", icon: FlaskConical },
{ id: "language", icon: Globe },
{ id: "about", icon: Info },
],
[], // No dependencies needed now
)
const scrollToSection = (ref: React.RefObject<HTMLDivElement>) => ref.current?.scrollIntoView()
// Scroll to target section when specified
// Update target section logic to set active tab
useEffect(() => {
if (targetSection) {
const sectionObj = sections.find((section) => section.id === targetSection)
if (sectionObj && sectionObj.ref.current) {
// Use setTimeout to ensure the scroll happens after render
setTimeout(() => scrollToSection(sectionObj.ref), 500)
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()
}
}
}, [targetSection, sections])
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>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="w-6 h-6">
<CaretSortIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" side="bottom">
{sections.map(({ id, icon: Icon, ref }) => (
<DropdownMenuItem key={id} onClick={() => scrollToSection(ref)}>
<Icon />
<span>{t(`settings:sections.${id}`)}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex gap-2">
<Button
@ -380,142 +436,214 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
</div>
</TabHeader>
<TabContent className="p-0 divide-y divide-vscode-sideBar-background">
<div ref={providersRef}>
<SectionHeader>
<div className="flex items-center gap-2">
<Webhook className="w-4" />
<div>{t("settings:sections.providers")}</div>
{/* 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={0}>
<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 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>
</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,
})
}
{/* Auto-Approve Section */}
{activeTab === "autoApprove" && (
<AutoApproveSettings
alwaysAllowReadOnly={alwaysAllowReadOnly}
alwaysAllowReadOnlyOutsideWorkspace={alwaysAllowReadOnlyOutsideWorkspace}
alwaysAllowWrite={alwaysAllowWrite}
alwaysAllowWriteOutsideWorkspace={alwaysAllowWriteOutsideWorkspace}
writeDelayMs={writeDelayMs}
alwaysAllowBrowser={alwaysAllowBrowser}
alwaysApproveResubmit={alwaysApproveResubmit}
requestDelaySeconds={requestDelaySeconds}
alwaysAllowMcp={alwaysAllowMcp}
alwaysAllowModeSwitch={alwaysAllowModeSwitch}
alwaysAllowSubtasks={alwaysAllowSubtasks}
alwaysAllowExecute={alwaysAllowExecute}
allowedCommands={allowedCommands}
setCachedStateField={setCachedStateField}
/>
<ApiOptions
uriScheme={uriScheme}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
)}
{/* Browser Section */}
{activeTab === "browser" && (
<BrowserSettings
browserToolEnabled={browserToolEnabled}
browserViewportSize={browserViewportSize}
screenshotQuality={screenshotQuality}
remoteBrowserHost={remoteBrowserHost}
remoteBrowserEnabled={remoteBrowserEnabled}
setCachedStateField={setCachedStateField}
/>
</Section>
</div>
)}
<div ref={autoApproveRef}>
<AutoApproveSettings
alwaysAllowReadOnly={alwaysAllowReadOnly}
alwaysAllowReadOnlyOutsideWorkspace={alwaysAllowReadOnlyOutsideWorkspace}
alwaysAllowWrite={alwaysAllowWrite}
alwaysAllowWriteOutsideWorkspace={alwaysAllowWriteOutsideWorkspace}
writeDelayMs={writeDelayMs}
alwaysAllowBrowser={alwaysAllowBrowser}
alwaysApproveResubmit={alwaysApproveResubmit}
requestDelaySeconds={requestDelaySeconds}
alwaysAllowMcp={alwaysAllowMcp}
alwaysAllowModeSwitch={alwaysAllowModeSwitch}
alwaysAllowSubtasks={alwaysAllowSubtasks}
alwaysAllowExecute={alwaysAllowExecute}
allowedCommands={allowedCommands}
setCachedStateField={setCachedStateField}
/>
</div>
{/* Checkpoints Section */}
{activeTab === "checkpoints" && (
<CheckpointSettings
enableCheckpoints={enableCheckpoints}
setCachedStateField={setCachedStateField}
/>
)}
<div ref={browserRef}>
<BrowserSettings
browserToolEnabled={browserToolEnabled}
browserViewportSize={browserViewportSize}
screenshotQuality={screenshotQuality}
remoteBrowserHost={remoteBrowserHost}
remoteBrowserEnabled={remoteBrowserEnabled}
setCachedStateField={setCachedStateField}
/>
</div>
{/* Notifications Section */}
{activeTab === "notifications" && (
<NotificationSettings
ttsEnabled={ttsEnabled}
ttsSpeed={ttsSpeed}
soundEnabled={soundEnabled}
soundVolume={soundVolume}
setCachedStateField={setCachedStateField}
/>
)}
<div ref={checkpointsRef}>
<CheckpointSettings
enableCheckpoints={enableCheckpoints}
setCachedStateField={setCachedStateField}
/>
</div>
{/* Context Management Section */}
{activeTab === "contextManagement" && (
<ContextManagementSettings
maxOpenTabsContext={maxOpenTabsContext}
maxWorkspaceFiles={maxWorkspaceFiles ?? 200}
showRooIgnoredFiles={showRooIgnoredFiles}
maxReadFileLine={maxReadFileLine}
setCachedStateField={setCachedStateField}
/>
)}
<div ref={notificationsRef}>
<NotificationSettings
ttsEnabled={ttsEnabled}
ttsSpeed={ttsSpeed}
soundEnabled={soundEnabled}
soundVolume={soundVolume}
setCachedStateField={setCachedStateField}
/>
</div>
{/* Terminal Section */}
{activeTab === "terminal" && (
<TerminalSettings
terminalOutputLineLimit={terminalOutputLineLimit}
terminalShellIntegrationTimeout={terminalShellIntegrationTimeout}
terminalShellIntegrationDisabled={terminalShellIntegrationDisabled}
terminalCommandDelay={terminalCommandDelay}
terminalPowershellCounter={terminalPowershellCounter}
terminalZshClearEolMark={terminalZshClearEolMark}
terminalZshOhMy={terminalZshOhMy}
terminalZshP10k={terminalZshP10k}
terminalZdotdir={terminalZdotdir}
terminalCompressProgressBar={terminalCompressProgressBar}
setCachedStateField={setCachedStateField}
/>
)}
<div ref={contextManagementRef}>
<ContextManagementSettings
maxOpenTabsContext={maxOpenTabsContext}
maxWorkspaceFiles={maxWorkspaceFiles ?? 200}
showRooIgnoredFiles={showRooIgnoredFiles}
maxReadFileLine={maxReadFileLine}
setCachedStateField={setCachedStateField}
/>
</div>
{/* Experimental Section */}
{activeTab === "experimental" && (
<ExperimentalSettings setExperimentEnabled={setExperimentEnabled} experiments={experiments} />
)}
<div ref={terminalRef}>
<TerminalSettings
terminalOutputLineLimit={terminalOutputLineLimit}
terminalShellIntegrationTimeout={terminalShellIntegrationTimeout}
terminalShellIntegrationDisabled={terminalShellIntegrationDisabled}
terminalCommandDelay={terminalCommandDelay}
terminalPowershellCounter={terminalPowershellCounter}
terminalZshClearEolMark={terminalZshClearEolMark}
terminalZshOhMy={terminalZshOhMy}
terminalZshP10k={terminalZshP10k}
terminalZdotdir={terminalZdotdir}
terminalCompressProgressBar={terminalCompressProgressBar}
setCachedStateField={setCachedStateField}
/>
</div>
{/* Language Section */}
{activeTab === "language" && (
<LanguageSettings language={language || "en"} setCachedStateField={setCachedStateField} />
)}
<div ref={experimentalRef}>
<ExperimentalSettings setExperimentEnabled={setExperimentEnabled} experiments={experiments} />
</div>
<div ref={languageRef}>
<LanguageSettings language={language || "en"} setCachedStateField={setCachedStateField} />
</div>
<div ref={aboutRef}>
<About
version={version}
telemetrySetting={telemetrySetting}
setTelemetrySetting={setTelemetrySetting}
/>
</div>
</TabContent>
{/* About Section */}
{activeTab === "about" && (
<About
version={version}
telemetrySetting={telemetrySetting}
setTelemetrySetting={setTelemetrySetting}
/>
)}
</TabContent>
</div>
<AlertDialog open={isDiscardDialogShow} onOpenChange={setDiscardDialogShow}>
<AlertDialogContent>

View file

@ -1,5 +1,4 @@
// npx jest src/components/settings/__tests__/SettingsView.test.ts
import React from "react"
import { render, screen, fireEvent } from "@testing-library/react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
@ -81,6 +80,47 @@ jest.mock("@vscode/webview-ui-toolkit/react", () => ({
VSCodeRadioGroup: ({ children, onChange }: any) => <div onChange={onChange}>{children}</div>,
}))
// Mock Tab components
jest.mock("../../../components/common/Tab", () => ({
...jest.requireActual("../../../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>,
TabList: ({ children, value, onValueChange, "data-testid": dataTestId }: any) => {
// Store onValueChange in a global variable so TabTrigger can access it
;(window as any).__onValueChange = onValueChange
return (
<div data-testid={dataTestId} data-value={value}>
{children}
</div>
)
},
TabTrigger: ({ children, value, "data-testid": dataTestId, onClick, isSelected }: any) => {
// This function simulates clicking on a tab and making its content visible
const handleClick = () => {
if (onClick) onClick()
// Access onValueChange from the global variable
const onValueChange = (window as any).__onValueChange
if (onValueChange) onValueChange(value)
// Make all tab contents invisible
document.querySelectorAll("[data-tab-content]").forEach((el) => {
;(el as HTMLElement).style.display = "none"
})
// Make this tab's content visible
const tabContent = document.querySelector(`[data-tab-content="${value}"]`)
if (tabContent) {
;(tabContent as HTMLElement).style.display = "block"
}
}
return (
<button data-testid={dataTestId} data-value={value} data-selected={isSelected} onClick={handleClick}>
{children}
</button>
)
},
}))
// Mock Slider component
jest.mock("@/components/ui", () => ({
...jest.requireActual("@/components/ui"),
@ -129,7 +169,7 @@ const renderSettingsView = () => {
const onDone = jest.fn()
const queryClient = new QueryClient()
render(
const result = render(
<ExtensionStateContextProvider>
<QueryClientProvider client={queryClient}>
<SettingsView onDone={onDone} />
@ -140,7 +180,20 @@ const renderSettingsView = () => {
// Hydrate initial state.
mockPostMessage({})
return { onDone }
// Helper function to activate a tab and ensure its content is visible
const activateTab = (tabId: string) => {
// Skip trying to find and click the tab, just directly render with the target section
// This bypasses the actual tab clicking mechanism but ensures the content is shown
result.rerender(
<ExtensionStateContextProvider>
<QueryClientProvider client={queryClient}>
<SettingsView onDone={onDone} targetSection={tabId} />
</QueryClientProvider>
</ExtensionStateContextProvider>,
)
}
return { onDone, activateTab }
}
describe("SettingsView - Sound Settings", () => {
@ -149,7 +202,11 @@ describe("SettingsView - Sound Settings", () => {
})
it("initializes with tts disabled by default", () => {
renderSettingsView()
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the notifications tab
activateTab("notifications")
const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox")
expect(ttsCheckbox).not.toBeChecked()
@ -159,7 +216,11 @@ describe("SettingsView - Sound Settings", () => {
})
it("initializes with sound disabled by default", () => {
renderSettingsView()
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the notifications tab
activateTab("notifications")
const soundCheckbox = screen.getByTestId("sound-enabled-checkbox")
expect(soundCheckbox).not.toBeChecked()
@ -169,7 +230,11 @@ describe("SettingsView - Sound Settings", () => {
})
it("toggles tts setting and sends message to VSCode", () => {
renderSettingsView()
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the notifications tab
activateTab("notifications")
const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox")
@ -190,7 +255,11 @@ describe("SettingsView - Sound Settings", () => {
})
it("toggles sound setting and sends message to VSCode", () => {
renderSettingsView()
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the notifications tab
activateTab("notifications")
const soundCheckbox = screen.getByTestId("sound-enabled-checkbox")
@ -211,7 +280,11 @@ describe("SettingsView - Sound Settings", () => {
})
it("shows tts slider when sound is enabled", () => {
renderSettingsView()
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the notifications tab
activateTab("notifications")
// Enable tts
const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox")
@ -224,7 +297,11 @@ describe("SettingsView - Sound Settings", () => {
})
it("shows volume slider when sound is enabled", () => {
renderSettingsView()
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the notifications tab
activateTab("notifications")
// Enable sound
const soundCheckbox = screen.getByTestId("sound-enabled-checkbox")
@ -237,7 +314,11 @@ describe("SettingsView - Sound Settings", () => {
})
it("updates speed and sends message to VSCode when slider changes", () => {
renderSettingsView()
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the notifications tab
activateTab("notifications")
// Enable tts
const ttsCheckbox = screen.getByTestId("tts-enabled-checkbox")
@ -259,7 +340,11 @@ describe("SettingsView - Sound Settings", () => {
})
it("updates volume and sends message to VSCode when slider changes", () => {
renderSettingsView()
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the notifications tab
activateTab("notifications")
// Enable sound
const soundCheckbox = screen.getByTestId("sound-enabled-checkbox")
@ -269,9 +354,9 @@ describe("SettingsView - Sound Settings", () => {
const volumeSlider = screen.getByTestId("sound-volume-slider")
fireEvent.change(volumeSlider, { target: { value: "0.75" } })
// Click Save to save settings
const saveButton = screen.getByTestId("save-button")
fireEvent.click(saveButton)
// Click Save to save settings - use getAllByTestId to handle multiple elements
const saveButtons = screen.getAllByTestId("save-button")
fireEvent.click(saveButtons[0])
// Verify message sent to VSCode
expect(vscode.postMessage).toHaveBeenCalledWith({
@ -299,7 +384,11 @@ describe("SettingsView - Allowed Commands", () => {
})
it("shows allowed commands section when alwaysAllowExecute is enabled", () => {
renderSettingsView()
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the autoApprove tab
activateTab("autoApprove")
// Enable always allow execute
const executeCheckbox = screen.getByTestId("always-allow-execute-toggle")
@ -310,7 +399,11 @@ describe("SettingsView - Allowed Commands", () => {
})
it("adds new command to the list", () => {
renderSettingsView()
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the autoApprove tab
activateTab("autoApprove")
// Enable always allow execute
const executeCheckbox = screen.getByTestId("always-allow-execute-toggle")
@ -334,7 +427,11 @@ describe("SettingsView - Allowed Commands", () => {
})
it("removes command from the list", () => {
renderSettingsView()
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the autoApprove tab
activateTab("autoApprove")
// Enable always allow execute
const executeCheckbox = screen.getByTestId("always-allow-execute-toggle")
@ -360,8 +457,71 @@ describe("SettingsView - Allowed Commands", () => {
})
})
describe("SettingsView - Tab Navigation", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it("renders with providers tab active by default", () => {
renderSettingsView()
// Check that the tab list is rendered
const tabList = screen.getByTestId("settings-tab-list")
expect(tabList).toBeInTheDocument()
// Check that providers content is visible
expect(screen.getByTestId("api-config-management")).toBeInTheDocument()
})
it("shows unsaved changes dialog when clicking Done with unsaved changes", () => {
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the notifications tab
activateTab("notifications")
// Make a change to create unsaved changes
const soundCheckbox = screen.getByTestId("sound-enabled-checkbox")
fireEvent.click(soundCheckbox)
// Click the Done button
const doneButton = screen.getByText("settings:common.done")
fireEvent.click(doneButton)
// Check that unsaved changes dialog is shown
expect(screen.getByText("settings:unsavedChangesDialog.title")).toBeInTheDocument()
})
it("renders with targetSection prop", () => {
// Render with a specific target section
render(
<ExtensionStateContextProvider>
<QueryClientProvider client={new QueryClient()}>
<SettingsView onDone={jest.fn()} targetSection="browser" />
</QueryClientProvider>
</ExtensionStateContextProvider>,
)
// Hydrate initial state
mockPostMessage({})
// Verify browser-related content is visible and API config is not
expect(screen.queryByTestId("api-config-management")).not.toBeInTheDocument()
})
})
})
describe("SettingsView - Duplicate Commands", () => {
beforeEach(() => {
jest.clearAllMocks()
})
it("prevents duplicate commands", () => {
renderSettingsView()
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the autoApprove tab
activateTab("autoApprove")
// Enable always allow execute
const executeCheckbox = screen.getByTestId("always-allow-execute-toggle")
@ -385,7 +545,11 @@ describe("SettingsView - Allowed Commands", () => {
})
it("saves allowed commands when clicking Save", () => {
renderSettingsView()
// Render once and get the activateTab helper
const { activateTab } = renderSettingsView()
// Activate the autoApprove tab
activateTab("autoApprove")
// Enable always allow execute
const executeCheckbox = screen.getByTestId("always-allow-execute-toggle")
@ -397,9 +561,9 @@ describe("SettingsView - Allowed Commands", () => {
const addButton = screen.getByTestId("add-command-button")
fireEvent.click(addButton)
// Click Save
const saveButton = screen.getByTestId("save-button")
fireEvent.click(saveButton)
// Click Save - use getAllByTestId to handle multiple elements
const saveButtons = screen.getAllByTestId("save-button")
fireEvent.click(saveButtons[0])
// Verify VSCode messages were sent
expect(vscode.postMessage).toHaveBeenCalledWith(

View file

@ -1,37 +1,6 @@
import styled from "styled-components"
export const DropdownWrapper = styled.div`
position: relative;
width: 100%;
`
export const DropdownList = styled.div<{ $zIndex: number }>`
position: absolute;
top: calc(100% - 3px);
left: 0;
width: calc(100% - 2px);
max-height: 200px;
overflow-y: auto;
background-color: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-list-activeSelectionBackground);
z-index: ${({ $zIndex }) => $zIndex};
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
`
export const DropdownItem = styled.div<{ $selected: boolean }>`
padding: 5px 10px;
cursor: pointer;
word-break: break-all;
white-space: normal;
background-color: ${({ $selected }) => ($selected ? "var(--vscode-list-activeSelectionBackground)" : "inherit")};
&:hover {
background-color: var(--vscode-list-activeSelectionBackground);
}
`
// Keep StyledMarkdown as it's used by ModelDescriptionMarkdown.tsx
export const StyledMarkdown = styled.div`
font-family:
var(--vscode-font-family),

View file

@ -22,17 +22,15 @@
},
"sections": {
"providers": "Proveïdors",
"autoApprove": "Aprovació automàtica",
"browser": "Navegador / Ús de l'ordinador",
"autoApprove": "Auto-aprovació",
"browser": "Accés a l'ordinador",
"checkpoints": "Punts de control",
"notifications": "Notificacions",
"contextManagement": "Gestió de context",
"contextManagement": "Context",
"terminal": "Terminal",
"advanced": "Avançat",
"experimental": "Funcions experimentals",
"experimental": "Experimental",
"language": "Idioma",
"about": "Sobre Roo Code",
"interface": "Interfície"
"about": "Sobre Roo Code"
},
"autoApprove": {
"description": "Permet que Roo realitzi operacions automàticament sense requerir aprovació. Activeu aquesta configuració només si confieu plenament en la IA i enteneu els riscos de seguretat associats.",

View file

@ -22,17 +22,15 @@
},
"sections": {
"providers": "Anbieter",
"autoApprove": "Automatische Genehmigung",
"browser": "Browser / Computer-Nutzung",
"autoApprove": "Auto-Genehmigung",
"browser": "Computerzugriff",
"checkpoints": "Kontrollpunkte",
"notifications": "Benachrichtigungen",
"contextManagement": "Kontext-Management",
"contextManagement": "Kontext",
"terminal": "Terminal",
"advanced": "Erweitert",
"experimental": "Experimentelle Funktionen",
"experimental": "Experimentell",
"language": "Sprache",
"about": "Über Roo Code",
"interface": "Oberfläche"
"about": "Über Roo Code"
},
"autoApprove": {
"description": "Erlaubt Roo, Operationen automatisch ohne Genehmigung durchzuführen. Aktiviere diese Einstellungen nur, wenn du der KI vollständig vertraust und die damit verbundenen Sicherheitsrisiken verstehst.",

View file

@ -23,16 +23,14 @@
"sections": {
"providers": "Providers",
"autoApprove": "Auto-Approve",
"browser": "Browser / Computer Use",
"browser": "Browser",
"checkpoints": "Checkpoints",
"notifications": "Notifications",
"contextManagement": "Context Management",
"contextManagement": "Context",
"terminal": "Terminal",
"advanced": "Advanced",
"experimental": "Experimental Features",
"experimental": "Experimental",
"language": "Language",
"about": "About Roo Code",
"interface": "Interface"
"about": "About Roo Code"
},
"autoApprove": {
"description": "Allow Roo to automatically perform operations without requiring approval. Enable these settings only if you fully trust the AI and understand the associated security risks.",

View file

@ -22,17 +22,15 @@
},
"sections": {
"providers": "Proveedores",
"autoApprove": "Aprobación automática",
"browser": "Navegador / Uso del ordenador",
"autoApprove": "Auto-aprobación",
"browser": "Acceso al ordenador",
"checkpoints": "Puntos de control",
"notifications": "Notificaciones",
"contextManagement": "Gestión de contexto",
"contextManagement": "Contexto",
"terminal": "Terminal",
"advanced": "Avanzado",
"experimental": "Funciones experimentales",
"experimental": "Experimental",
"language": "Idioma",
"about": "Acerca de Roo Code",
"interface": "Interfaz"
"about": "Acerca de Roo Code"
},
"autoApprove": {
"description": "Permitir que Roo realice operaciones automáticamente sin requerir aprobación. Habilite esta configuración solo si confía plenamente en la IA y comprende los riesgos de seguridad asociados.",

View file

@ -22,17 +22,15 @@
},
"sections": {
"providers": "Fournisseurs",
"autoApprove": "Approbation automatique",
"browser": "Navigateur / Utilisation de l'ordinateur",
"autoApprove": "Auto-approbation",
"browser": "Accès ordinateur",
"checkpoints": "Points de contrôle",
"notifications": "Notifications",
"contextManagement": "Gestion du contexte",
"contextManagement": "Contexte",
"terminal": "Terminal",
"advanced": "Avancé",
"experimental": "Fonctionnalités expérimentales",
"experimental": "Expérimental",
"language": "Langue",
"about": "À propos de Roo Code",
"interface": "Interface"
"about": "À propos de Roo Code"
},
"autoApprove": {
"description": "Permettre à Roo d'effectuer automatiquement des opérations sans requérir d'approbation. Activez ces paramètres uniquement si vous faites entièrement confiance à l'IA et que vous comprenez les risques de sécurité associés.",

View file

@ -22,17 +22,15 @@
},
"sections": {
"providers": "प्रदाता",
"autoApprove": "स्वतः अनुमोदन",
"browser": "ब्राउज़र / कंप्यूटर उपयोग",
"autoApprove": "अनुमोदन",
"browser": "ब्राउज़र",
"checkpoints": "चेकपॉइंट",
"notifications": "सूचनाएँ",
"contextManagement": "संदर्भ प्रबंधन",
"contextManagement": "संदर्भ",
"terminal": "टर्मिनल",
"advanced": "उन्नत",
"experimental": "प्रायोगिक सुविधाएँ",
"experimental": "प्रायोगिक",
"language": "भाषा",
"about": "Roo Code के बारे में",
"interface": "इंटरफ़ेस"
"about": "परिचय"
},
"autoApprove": {
"description": "Roo को अनुमोदन की आवश्यकता के बिना स्वचालित रूप से ऑपरेशन करने की अनुमति दें। इन सेटिंग्स को केवल तभी सक्षम करें जब आप AI पर पूरी तरह से भरोसा करते हों और संबंधित सुरक्षा जोखिमों को समझते हों।",

View file

@ -22,17 +22,15 @@
},
"sections": {
"providers": "Fornitori",
"autoApprove": "Approvazione automatica",
"browser": "Browser / Uso del computer",
"autoApprove": "Auto-approvazione",
"browser": "Accesso computer",
"checkpoints": "Punti di controllo",
"notifications": "Notifiche",
"contextManagement": "Gestione del contesto",
"contextManagement": "Contesto",
"terminal": "Terminal",
"advanced": "Avanzate",
"experimental": "Funzionalità sperimentali",
"experimental": "Sperimentale",
"language": "Lingua",
"about": "Informazioni su Roo Code",
"interface": "Interfaccia"
"about": "Informazioni su Roo Code"
},
"autoApprove": {
"description": "Permetti a Roo di eseguire automaticamente operazioni senza richiedere approvazione. Abilita queste impostazioni solo se ti fidi completamente dell'IA e comprendi i rischi di sicurezza associati.",

View file

@ -23,16 +23,14 @@
"sections": {
"providers": "プロバイダー",
"autoApprove": "自動承認",
"browser": "ブラウザ / コンピューター使用",
"browser": "コンピューターアクセス",
"checkpoints": "チェックポイント",
"notifications": "通知",
"contextManagement": "コンテキスト管理",
"contextManagement": "コンテキスト",
"terminal": "ターミナル",
"advanced": "詳細設定",
"experimental": "実験的機能",
"experimental": "実験的",
"language": "言語",
"about": "Roo Codeについて",
"interface": "インターフェース"
"about": "Roo Codeについて"
},
"autoApprove": {
"description": "Rooが承認なしで自動的に操作を実行できるようにします。AIを完全に信頼し、関連するセキュリティリスクを理解している場合にのみ、これらの設定を有効にしてください。",

View file

@ -23,16 +23,14 @@
"sections": {
"providers": "공급자",
"autoApprove": "자동 승인",
"browser": "브라우저 / 컴퓨터 사용",
"browser": "컴퓨터 접근",
"checkpoints": "체크포인트",
"notifications": "알림",
"contextManagement": "컨텍스트 관리",
"contextManagement": "컨텍스트",
"terminal": "터미널",
"advanced": "고급",
"experimental": "실험적 기능",
"experimental": "실험적",
"language": "언어",
"about": "Roo Code 정보",
"interface": "인터페이스"
"about": "Roo Code 정보"
},
"autoApprove": {
"description": "Roo가 승인 없이 자동으로 작업을 수행할 수 있도록 허용합니다. AI를 완전히 신뢰하고 관련 보안 위험을 이해하는 경우에만 이러한 설정을 활성화하세요.",

View file

@ -22,17 +22,15 @@
},
"sections": {
"providers": "Dostawcy",
"autoApprove": "Automatyczne zatwierdzanie",
"browser": "Przeglądarka / Użycie komputera",
"autoApprove": "Auto-zatwierdzanie",
"browser": "Dostęp komputera",
"checkpoints": "Punkty kontrolne",
"notifications": "Powiadomienia",
"contextManagement": "Zarządzanie kontekstem",
"contextManagement": "Kontekst",
"terminal": "Terminal",
"advanced": "Zaawansowane",
"experimental": "Funkcje eksperymentalne",
"experimental": "Eksperymentalne",
"language": "Język",
"about": "O Roo Code",
"interface": "Interfejs"
"about": "O Roo Code"
},
"autoApprove": {
"description": "Pozwól Roo na automatyczne wykonywanie operacji bez wymagania zatwierdzenia. Włącz te ustawienia tylko jeśli w pełni ufasz AI i rozumiesz związane z tym zagrożenia bezpieczeństwa.",

View file

@ -22,17 +22,15 @@
},
"sections": {
"providers": "Provedores",
"autoApprove": "Aprovação automática",
"browser": "Navegador / Uso do computador",
"checkpoints": "Pontos de verificação",
"autoApprove": "Aprovação",
"browser": "Navegador",
"checkpoints": "Checkpoints",
"notifications": "Notificações",
"contextManagement": "Gestão de contexto",
"contextManagement": "Contexto",
"terminal": "Terminal",
"advanced": "Avançado",
"experimental": "Recursos experimentais",
"experimental": "Experimental",
"language": "Idioma",
"about": "Sobre o Roo Code",
"interface": "Interface"
"about": "Sobre"
},
"autoApprove": {
"description": "Permitir que o Roo realize operações automaticamente sem exigir aprovação. Ative essas configurações apenas se confiar totalmente na IA e compreender os riscos de segurança associados.",

View file

@ -23,16 +23,14 @@
"sections": {
"providers": "Провайдеры",
"autoApprove": "Автоодобрение",
"browser": "Браузер / Использование компьютера",
"browser": "Доступ к компьютеру",
"checkpoints": "Контрольные точки",
"notifications": "Уведомления",
"contextManagement": "Управление контекстом",
"contextManagement": "Контекст",
"terminal": "Терминал",
"advanced": "Дополнительно",
"experimental": "Экспериментальные функции",
"experimental": "Экспериментальное",
"language": "Язык",
"about": "О Roo Code",
"interface": "Интерфейс"
"about": "О Roo Code"
},
"autoApprove": {
"description": "Разрешить Roo автоматически выполнять операции без необходимости одобрения. Включайте эти параметры только если полностью доверяете ИИ и понимаете связанные с этим риски безопасности.",

View file

@ -22,17 +22,15 @@
},
"sections": {
"providers": "Sağlayıcılar",
"autoApprove": "Otomatik Onay",
"browser": "Tarayıcı / Bilgisayar Kullanımı",
"autoApprove": "Oto-Onay",
"browser": "Bilgisayar Erişimi",
"checkpoints": "Kontrol Noktaları",
"notifications": "Bildirimler",
"contextManagement": "Bağlam Yönetimi",
"contextManagement": "Bağlam",
"terminal": "Terminal",
"advanced": "Gelişmiş",
"experimental": "Deneysel Özellikler",
"experimental": "Deneysel",
"language": "Dil",
"about": "Roo Code Hakkında",
"interface": "Arayüz"
"about": "Roo Code Hakkında"
},
"autoApprove": {
"description": "Roo'nun onay gerektirmeden otomatik olarak işlemler gerçekleştirmesine izin verin. Bu ayarları yalnızca yapay zekaya tamamen güveniyorsanız ve ilgili güvenlik risklerini anlıyorsanız etkinleştirin.",

View file

@ -22,17 +22,15 @@
},
"sections": {
"providers": "Nhà cung cấp",
"autoApprove": "Tự động phê duyệt",
"browser": "Trình duyệt / Sử dụng máy tính",
"autoApprove": "Phê duyệt",
"browser": "Trình duyệt",
"checkpoints": "Điểm kiểm tra",
"notifications": "Thông báo",
"contextManagement": "Quản lý ngữ cảnh",
"contextManagement": "Ngữ cảnh",
"terminal": "Terminal",
"advanced": "Nâng cao",
"experimental": "Tính năng thử nghiệm",
"experimental": "Thử nghiệm",
"language": "Ngôn ngữ",
"about": "Về Roo Code",
"interface": "Giao diện"
"about": "Giới thiệu"
},
"autoApprove": {
"description": "Cho phép Roo tự động thực hiện các hoạt động mà không cần phê duyệt. Chỉ bật những cài đặt này nếu bạn hoàn toàn tin tưởng AI và hiểu rõ các rủi ro bảo mật liên quan.",

View file

@ -23,14 +23,12 @@
"sections": {
"providers": "提供商",
"autoApprove": "自动批准",
"browser": "浏览器交互",
"browser": "计算机交互",
"checkpoints": "检查点",
"interface": "界面内容",
"notifications": "通知",
"contextManagement": "上下文管理",
"contextManagement": "上下文",
"terminal": "终端",
"advanced": "高级",
"experimental": "实验性功能",
"experimental": "实验性",
"language": "语言",
"about": "关于 Roo Code"
},

View file

@ -23,16 +23,14 @@
"sections": {
"providers": "供應商",
"autoApprove": "自動核准",
"browser": "瀏覽器/電腦使用",
"browser": "電腦存取",
"checkpoints": "檢查點",
"notifications": "通知",
"contextManagement": "上下文管理",
"contextManagement": "上下文",
"terminal": "終端機",
"advanced": "進階",
"experimental": "實驗性功能",
"experimental": "實驗性",
"language": "語言",
"about": "關於 Roo Code",
"interface": "介面"
"about": "關於 Roo Code"
},
"autoApprove": {
"description": "允許 Roo 無需核准即執行操作。僅在您完全信任 AI 並了解相關安全風險時啟用這些設定。",