Skip to content

Commit 25545e9

Browse files
authored
refactor(terminal): redesign profile settings UX — unified dropdown, consistent layout, and styling (#119)(#321) (#533)
* refactor(terminal): redesign profile settings UI with unified dropdown (#119) (#321) Modified files: - TerminalSettings.tsx: reorder layout, radio→dropdown, shadcn/ui Button - TerminalSettings.profile.spec.tsx: rewrite tests for new dropdown UI - settings.json (en + 16 locales): add followVscode, remove default/overrideLabel, refine label, description, and (recommended) suffix * docs: add terminal settings before/after screenshots * fix(terminal): restore lost refinements — button stretch, Terminal icon, (recommended), i18n alignment - TerminalSettings.tsx: flex flex-col wrapper for auto-stretch, Terminal icon - TerminalSettings.profile.spec.tsx: add 4 tests for icon and i18n keys - en/settings.json: followVscode (recommended), refined description - zh-CN/settings.json: label, followVscode, description aligned with en - de/it/settings.json: label changed from override to Shell - All 16 locales: (recommended) suffix on followVscode * fix(terminal): add isProfilesLoaded guard, harden sentinel, sync i18n (#119) (#321) TerminalSettings.tsx: - Guard no-profiles hint render with isProfilesLoaded to prevent the hint from flashing before async profiles load. - Harden DEFAULT_PROFILE_VALUE sentinel from "__default__" to "__zoo_code_follow_vscode_sentinel__" to eliminate theoretical collision with a user-named VS Code profile. TerminalSettings.profile.spec.tsx: - Extend the empty-profiles test to assert the hint is absent before profiles load and present only after the empty list arrives. - Update sentinel references to match the new constant. i18n (ca/de/es/fr/hi/id/it/ja/ko/nl/pl/pt-BR/ru/tr/vi/zh-TW): - Replace outdated "override toggle" wording in label and description with unified single-dropdown profile selection language, aligning with the en/zh-CN baseline. * fix(terminal): correct data-testid placement, use nullish coalescing, and harden test patterns (#119) - TerminalSettings.tsx: move data-testid from Radix <Select> root (no DOM node) to <SelectTrigger> button so the attribute survives in production; replace || with ?? for the terminalProfile sentinel fallback so empty string is not incorrectly replaced; export DEFAULT_PROFILE_VALUE for reuse in tests. - TerminalSettings.profile.spec.tsx: import DEFAULT_PROFILE_VALUE, replace hardcoded sentinel strings; adopt rerender() over cleanup()+render() so onTerminalProfilePickerOpened is preserved across re-renders; remove unused cleanup import. - Remove PR screenshots from docs/ (after1.png, after2.png, before.png).
1 parent ccd37d1 commit 25545e9

20 files changed

Lines changed: 238 additions & 284 deletions

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

Lines changed: 76 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { HTMLAttributes, useState, useCallback, useEffect, useId } from "react"
1+
import { HTMLAttributes, useState, useCallback, useEffect } from "react"
22
import { useAppTranslation } from "@/i18n/TranslationContext"
33
import { vscode } from "@/utils/vscode"
4-
import { VSCodeCheckbox, VSCodeLink, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
4+
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
55
import { Trans } from "react-i18next"
66
import { buildDocLink } from "@src/utils/docLinks"
77
import { useEvent, useMount } from "react-use"
8+
import { Terminal } from "lucide-react"
89

910
import { type ExtensionMessage, type TerminalOutputPreviewSize } from "@roo-code/types"
1011

1112
import { cn } from "@/lib/utils"
12-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider } from "@/components/ui"
13+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider, Button } from "@/components/ui"
1314

1415
import { SetCachedStateField } from "./types"
1516
import { SectionHeader } from "./SectionHeader"
@@ -44,7 +45,7 @@ type TerminalSettingsProps = HTMLAttributes<HTMLDivElement> & {
4445

4546
// Sentinel value that maps to `undefined` (use VS Code's default shell).
4647
// The Select component cannot accept empty-string item values.
47-
const DEFAULT_PROFILE_VALUE = "__default__"
48+
export const DEFAULT_PROFILE_VALUE = "__zoo_code_follow_vscode_sentinel__"
4849

4950
export const TerminalSettings = ({
5051
terminalOutputPreviewSize,
@@ -67,10 +68,6 @@ export const TerminalSettings = ({
6768
const [inheritEnv, setInheritEnv] = useState<boolean>(true)
6869
const [profileNames, setProfileNames] = useState<string[]>([])
6970
const [isProfilesLoaded, setIsProfilesLoaded] = useState(false)
70-
const profileModeId = useId()
71-
const defaultProfileId = `${profileModeId}-default`
72-
const overrideProfileId = `${profileModeId}-override`
73-
const isProfileOverrideSelected = !!terminalProfile && (!isProfilesLoaded || profileNames.includes(terminalProfile))
7471
const isVSCodeTerminalEnabled = terminalShellIntegrationDisabled === false
7572

7673
useMount(() => {
@@ -166,111 +163,7 @@ export const TerminalSettings = ({
166163
</div>
167164
</div>
168165
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
169-
{/* Profile override — only applies when VS Code integrated terminal is active
170-
(shell integration enabled). Hidden in Execa/inline mode since getProfileShell()
171-
is not wired there. */}
172-
{isVSCodeTerminalEnabled && (
173-
<SearchableSetting
174-
settingId="terminal-profile"
175-
section="terminal"
176-
label={t("settings:terminal.profile.label")}>
177-
<label className="block font-medium mb-1">{t("settings:terminal.profile.label")}</label>
178-
179-
{/* Level 1: Default (recommended) */}
180-
<div className="flex items-center gap-2 mb-2">
181-
<input
182-
type="radio"
183-
id={defaultProfileId}
184-
name={profileModeId}
185-
checked={!isProfileOverrideSelected}
186-
onChange={() => setCachedStateField("terminalProfile", undefined)}
187-
data-testid="terminal-profile-default-radio"
188-
/>
189-
<label htmlFor={defaultProfileId} className="cursor-pointer">
190-
{t("settings:terminal.profile.default")}
191-
</label>
192-
<VSCodeButton
193-
appearance="secondary"
194-
onClick={() => {
195-
onTerminalProfilePickerOpened?.()
196-
vscode.postMessage({ type: "openTerminalProfilePicker" })
197-
}}
198-
data-testid="terminal-profile-configure-button">
199-
{t("settings:terminal.profile.configureButton")}
200-
</VSCodeButton>
201-
</div>
202-
203-
{/* Level 2: Override */}
204-
<div className="flex items-center gap-2 mb-2">
205-
<input
206-
type="radio"
207-
id={overrideProfileId}
208-
name={profileModeId}
209-
checked={isProfileOverrideSelected}
210-
disabled={profileNames.length === 0}
211-
onChange={() => {
212-
if (!terminalProfile && profileNames.length > 0) {
213-
setCachedStateField("terminalProfile", profileNames[0])
214-
}
215-
}}
216-
data-testid="terminal-profile-override-radio"
217-
/>
218-
<label
219-
htmlFor={overrideProfileId}
220-
className={
221-
profileNames.length === 0
222-
? "cursor-not-allowed text-vscode-disabledForeground"
223-
: "cursor-pointer"
224-
}>
225-
{t("settings:terminal.profile.overrideLabel")}
226-
</label>
227-
{profileNames.length === 0 && (
228-
<span
229-
className="text-vscode-descriptionForeground text-xs"
230-
data-testid="terminal-profile-no-profiles-hint">
231-
{t("settings:terminal.profile.noProfiles")}
232-
</span>
233-
)}
234-
</div>
235-
236-
{isProfileOverrideSelected && profileNames.length > 0 && (
237-
<Select
238-
value={terminalProfile || DEFAULT_PROFILE_VALUE}
239-
data-testid="terminal-profile-dropdown"
240-
onValueChange={(value) =>
241-
setCachedStateField(
242-
"terminalProfile",
243-
value === DEFAULT_PROFILE_VALUE ? undefined : value,
244-
)
245-
}>
246-
<SelectTrigger className="w-full ml-6">
247-
<SelectValue placeholder={t("settings:common.select")} />
248-
</SelectTrigger>
249-
<SelectContent>
250-
{profileNames.map((name) => (
251-
<SelectItem key={name} value={name}>
252-
{name}
253-
</SelectItem>
254-
))}
255-
</SelectContent>
256-
</Select>
257-
)}
258-
259-
<div className="text-vscode-descriptionForeground text-sm mt-1">
260-
<Trans i18nKey="settings:terminal.profile.description">
261-
<VSCodeLink
262-
href={buildDocLink(
263-
"features/shell-integration",
264-
"settings_terminal_profile",
265-
)}
266-
style={{ display: "inline" }}>
267-
{" "}
268-
</VSCodeLink>
269-
</Trans>
270-
</div>
271-
</SearchableSetting>
272-
)}
273-
166+
{/* "Use Inline Terminal" checkbox — ALWAYS at the top */}
274167
<SearchableSetting
275168
settingId="terminal-shell-integration-disabled"
276169
section="terminal"
@@ -300,6 +193,76 @@ export const TerminalSettings = ({
300193

301194
{isVSCodeTerminalEnabled && (
302195
<>
196+
{/* Profile override — unified dropdown, now below checkbox */}
197+
<SearchableSetting
198+
settingId="terminal-profile"
199+
section="terminal"
200+
label={t("settings:terminal.profile.label")}>
201+
<label className="block font-medium mb-1">
202+
{t("settings:terminal.profile.label")}
203+
</label>
204+
205+
<Select
206+
value={terminalProfile ?? DEFAULT_PROFILE_VALUE}
207+
onValueChange={(value) =>
208+
setCachedStateField(
209+
"terminalProfile",
210+
value === DEFAULT_PROFILE_VALUE ? undefined : value,
211+
)
212+
}>
213+
<SelectTrigger className="w-full" data-testid="terminal-profile-dropdown">
214+
<SelectValue placeholder={t("settings:common.select")} />
215+
</SelectTrigger>
216+
<SelectContent>
217+
<SelectItem value={DEFAULT_PROFILE_VALUE}>
218+
{t("settings:terminal.profile.followVscode")}
219+
</SelectItem>
220+
{profileNames.map((name) => (
221+
<SelectItem key={name} value={name}>
222+
{name}
223+
</SelectItem>
224+
))}
225+
</SelectContent>
226+
</Select>
227+
228+
{!terminalProfile && (
229+
<div className="mt-2 flex flex-col">
230+
<Button
231+
variant="secondary"
232+
className="py-1"
233+
onClick={() => {
234+
onTerminalProfilePickerOpened?.()
235+
vscode.postMessage({ type: "openTerminalProfilePicker" })
236+
}}
237+
data-testid="terminal-profile-configure-button">
238+
<Terminal />
239+
{t("settings:terminal.profile.configureButton")}
240+
</Button>
241+
</div>
242+
)}
243+
244+
{isProfilesLoaded && profileNames.length === 0 && (
245+
<div
246+
className="text-vscode-descriptionForeground text-xs mt-1"
247+
data-testid="terminal-profile-no-profiles-hint">
248+
{t("settings:terminal.profile.noProfiles")}
249+
</div>
250+
)}
251+
252+
<div className="text-vscode-descriptionForeground text-sm mt-1">
253+
<Trans i18nKey="settings:terminal.profile.description">
254+
<VSCodeLink
255+
href={buildDocLink(
256+
"features/shell-integration",
257+
"settings_terminal_profile",
258+
)}
259+
style={{ display: "inline" }}>
260+
{" "}
261+
</VSCodeLink>
262+
</Trans>
263+
</div>
264+
</SearchableSetting>
265+
303266
<SearchableSetting
304267
settingId="terminal-inherit-env"
305268
section="terminal"

0 commit comments

Comments
 (0)