Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit e3fef81

Browse files
committed
feat: add ResetToDefault component for settings with defaults
- Add ResetToDefault component with ↺ icon that shows when a setting differs from its default value - Tooltip shows the default value (e.g., 'Reset to default (true)') - Component only renders when current value !== default - Add translation keys for boolean/empty/resetToDefault formatting - Integrate with BrowserSettings as proof of concept: - browserToolEnabled checkbox - browserViewportSize dropdown - screenshotQuality slider - Add comprehensive tests for visibility, functionality, and styling
1 parent f3d1c12 commit e3fef81

4 files changed

Lines changed: 230 additions & 10 deletions

File tree

webview-ui/src/components/settings/BrowserSettings.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
1818
import { vscode } from "@/utils/vscode"
1919
import { buildDocLink } from "@src/utils/docLinks"
2020

21+
import { ResetToDefault } from "./ResetToDefault"
2122
import { SearchableSetting } from "./SearchableSetting"
2223
import { Section } from "./Section"
2324
import { SectionHeader } from "./SectionHeader"
@@ -117,11 +118,18 @@ export const BrowserSettings = ({
117118
settingId="browser-enable"
118119
section="browser"
119120
label={t("settings:browser.enable.label")}>
120-
<VSCodeCheckbox
121-
checked={browserToolEnabled}
122-
onChange={(e: any) => setCachedStateField("browserToolEnabled", e.target.checked)}>
123-
<span className="font-medium">{t("settings:browser.enable.label")}</span>
124-
</VSCodeCheckbox>
121+
<div className="flex items-center gap-1">
122+
<VSCodeCheckbox
123+
checked={browserToolEnabled}
124+
onChange={(e: any) => setCachedStateField("browserToolEnabled", e.target.checked)}>
125+
<span className="font-medium">{t("settings:browser.enable.label")}</span>
126+
</VSCodeCheckbox>
127+
<ResetToDefault
128+
settingKey="browserToolEnabled"
129+
currentValue={browserToolEnabled}
130+
onReset={() => setCachedStateField("browserToolEnabled", undefined)}
131+
/>
132+
</div>
125133
<div className="text-vscode-descriptionForeground text-sm mt-1">
126134
<Trans i18nKey="settings:browser.enable.description">
127135
<VSCodeLink
@@ -139,7 +147,14 @@ export const BrowserSettings = ({
139147
settingId="browser-viewport"
140148
section="browser"
141149
label={t("settings:browser.viewport.label")}>
142-
<label className="block font-medium mb-1">{t("settings:browser.viewport.label")}</label>
150+
<div className="flex items-center gap-1 mb-1">
151+
<label className="block font-medium">{t("settings:browser.viewport.label")}</label>
152+
<ResetToDefault
153+
settingKey="browserViewportSize"
154+
currentValue={browserViewportSize}
155+
onReset={() => setCachedStateField("browserViewportSize", undefined)}
156+
/>
157+
</div>
143158
<Select
144159
value={browserViewportSize}
145160
onValueChange={(value) => setCachedStateField("browserViewportSize", value)}>
@@ -165,9 +180,16 @@ export const BrowserSettings = ({
165180
settingId="browser-screenshot-quality"
166181
section="browser"
167182
label={t("settings:browser.screenshotQuality.label")}>
168-
<label className="block font-medium mb-1">
169-
{t("settings:browser.screenshotQuality.label")}
170-
</label>
183+
<div className="flex items-center gap-1 mb-1">
184+
<label className="block font-medium">
185+
{t("settings:browser.screenshotQuality.label")}
186+
</label>
187+
<ResetToDefault
188+
settingKey="screenshotQuality"
189+
currentValue={screenshotQuality}
190+
onReset={() => setCachedStateField("screenshotQuality", undefined)}
191+
/>
192+
</div>
171193
<div className="flex items-center gap-2">
172194
<Slider
173195
min={1}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { RotateCcw } from "lucide-react"
2+
3+
import { settingDefaults, type SettingWithDefault } from "@roo-code/types"
4+
5+
import { Button, StandardTooltip } from "@/components/ui"
6+
import { useAppTranslation } from "@/i18n/TranslationContext"
7+
8+
// Widen literal types to their base types for comparison
9+
type WidenType<T> = T extends boolean ? boolean : T extends number ? number : T extends string ? string : T
10+
11+
interface ResetToDefaultProps<K extends SettingWithDefault> {
12+
/** The setting key from settingDefaults */
13+
settingKey: K
14+
/** The current value of the setting (accepts wider types for flexibility) */
15+
currentValue: WidenType<(typeof settingDefaults)[K]> | undefined
16+
/** Callback to reset the value (called with undefined to reset) */
17+
onReset: () => void
18+
/** Optional className for the button */
19+
className?: string
20+
}
21+
22+
/**
23+
* A small reset button that appears only when a setting differs from its default.
24+
* Shows a ↺ icon with a tooltip displaying the default value.
25+
*
26+
* @example
27+
* <ResetToDefault
28+
* settingKey="browserToolEnabled"
29+
* currentValue={browserToolEnabled}
30+
* onReset={() => setCachedStateField("browserToolEnabled", undefined)}
31+
* />
32+
*/
33+
export function ResetToDefault<K extends SettingWithDefault>({
34+
settingKey,
35+
currentValue,
36+
onReset,
37+
className,
38+
}: ResetToDefaultProps<K>) {
39+
const { t } = useAppTranslation()
40+
const defaultValue = settingDefaults[settingKey]
41+
42+
// Don't show the button if the current value matches the default
43+
// undefined is treated as "using default"
44+
const isDefault = currentValue === undefined || currentValue === defaultValue
45+
46+
if (isDefault) {
47+
return null
48+
}
49+
50+
// Format the default value for display in the tooltip
51+
const formatDefaultValue = (value: unknown): string => {
52+
if (typeof value === "boolean") {
53+
return value ? t("settings:common.true") : t("settings:common.false")
54+
}
55+
if (typeof value === "number") {
56+
return String(value)
57+
}
58+
if (typeof value === "string") {
59+
return value || t("settings:common.empty")
60+
}
61+
return JSON.stringify(value)
62+
}
63+
64+
const tooltipContent = t("settings:common.resetToDefault", {
65+
defaultValue: formatDefaultValue(defaultValue),
66+
})
67+
68+
return (
69+
<StandardTooltip content={tooltipContent}>
70+
<Button
71+
variant="ghost"
72+
size="icon"
73+
className={className ?? "h-6 w-6 p-1"}
74+
onClick={onReset}
75+
data-testid={`reset-${settingKey}`}>
76+
<RotateCcw className="h-3.5 w-3.5 text-vscode-descriptionForeground hover:text-vscode-foreground" />
77+
</Button>
78+
</StandardTooltip>
79+
)
80+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { render, screen, fireEvent } from "@testing-library/react"
2+
import { ReactNode } from "react"
3+
4+
import { TooltipProvider } from "@/components/ui"
5+
6+
import { ResetToDefault } from "../ResetToDefault"
7+
8+
// Mock the translation hook
9+
vi.mock("@/i18n/TranslationContext", () => ({
10+
useAppTranslation: () => ({
11+
t: (key: string, options?: { defaultValue?: string }) =>
12+
options?.defaultValue ? `${key} (${JSON.stringify(options)})` : key,
13+
}),
14+
}))
15+
16+
// Wrapper component to provide TooltipProvider context
17+
const TestWrapper = ({ children }: { children: ReactNode }) => <TooltipProvider>{children}</TooltipProvider>
18+
19+
const renderWithWrapper = (ui: ReactNode) => render(ui, { wrapper: TestWrapper })
20+
21+
describe("ResetToDefault", () => {
22+
const mockOnReset = vi.fn()
23+
24+
beforeEach(() => {
25+
mockOnReset.mockClear()
26+
})
27+
28+
describe("visibility", () => {
29+
it("should not render when currentValue matches default", () => {
30+
// browserToolEnabled defaults to true in settingDefaults
31+
const { container } = render(
32+
<ResetToDefault settingKey="browserToolEnabled" currentValue={true} onReset={mockOnReset} />,
33+
)
34+
35+
expect(container.firstChild).toBeNull()
36+
})
37+
38+
it("should not render when currentValue is undefined (using default)", () => {
39+
const { container } = render(
40+
<ResetToDefault settingKey="browserToolEnabled" currentValue={undefined} onReset={mockOnReset} />,
41+
)
42+
43+
expect(container.firstChild).toBeNull()
44+
})
45+
46+
it("should render when currentValue differs from default (boolean)", () => {
47+
// browserToolEnabled defaults to true, so false should show reset button
48+
renderWithWrapper(
49+
<ResetToDefault settingKey="browserToolEnabled" currentValue={false} onReset={mockOnReset} />,
50+
)
51+
52+
expect(screen.getByTestId("reset-browserToolEnabled")).toBeInTheDocument()
53+
})
54+
55+
it("should render when currentValue differs from default (number)", () => {
56+
// screenshotQuality defaults to 75
57+
renderWithWrapper(<ResetToDefault settingKey="screenshotQuality" currentValue={50} onReset={mockOnReset} />)
58+
59+
expect(screen.getByTestId("reset-screenshotQuality")).toBeInTheDocument()
60+
})
61+
62+
it("should render when currentValue differs from default (string)", () => {
63+
// browserViewportSize defaults to "900x600"
64+
renderWithWrapper(
65+
<ResetToDefault settingKey="browserViewportSize" currentValue="1280x800" onReset={mockOnReset} />,
66+
)
67+
68+
expect(screen.getByTestId("reset-browserViewportSize")).toBeInTheDocument()
69+
})
70+
})
71+
72+
describe("functionality", () => {
73+
it("should call onReset when clicked", () => {
74+
renderWithWrapper(
75+
<ResetToDefault settingKey="browserToolEnabled" currentValue={false} onReset={mockOnReset} />,
76+
)
77+
78+
fireEvent.click(screen.getByTestId("reset-browserToolEnabled"))
79+
80+
expect(mockOnReset).toHaveBeenCalledTimes(1)
81+
})
82+
83+
it("should have the correct aria role (button)", () => {
84+
renderWithWrapper(<ResetToDefault settingKey="screenshotQuality" currentValue={50} onReset={mockOnReset} />)
85+
86+
expect(screen.getByRole("button")).toBeInTheDocument()
87+
})
88+
})
89+
90+
describe("custom className", () => {
91+
it("should apply custom className when provided", () => {
92+
renderWithWrapper(
93+
<ResetToDefault
94+
settingKey="browserToolEnabled"
95+
currentValue={false}
96+
onReset={mockOnReset}
97+
className="custom-class"
98+
/>,
99+
)
100+
101+
const button = screen.getByTestId("reset-browserToolEnabled")
102+
expect(button).toHaveClass("custom-class")
103+
})
104+
105+
it("should use default className when not provided", () => {
106+
renderWithWrapper(
107+
<ResetToDefault settingKey="browserToolEnabled" currentValue={false} onReset={mockOnReset} />,
108+
)
109+
110+
const button = screen.getByTestId("reset-browserToolEnabled")
111+
expect(button).toHaveClass("h-6", "w-6", "p-1")
112+
})
113+
})
114+
})

webview-ui/src/i18n/locales/en/settings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
"reset": "Reset",
88
"select": "Select",
99
"add": "Add Header",
10-
"remove": "Remove"
10+
"remove": "Remove",
11+
"true": "true",
12+
"false": "false",
13+
"empty": "(empty)",
14+
"resetToDefault": "Reset to default ({{defaultValue}})"
1115
},
1216
"header": {
1317
"title": "Settings",

0 commit comments

Comments
 (0)