From a16d82eebba4c2a19540a636ec272366c013bd07 Mon Sep 17 00:00:00 2001 From: Armando Vaquera <263793884+proyectoauraorg@users.noreply.github.com> Date: Sat, 23 May 2026 18:58:06 -0600 Subject: [PATCH 1/6] feat(terminal): add inline terminal profile selection (#119) Add a 'terminalProfile' setting that lets users choose which VS Code terminal profile the inline terminal uses. On Windows the default cmd/PowerShell shell may use a non-UTF-8 code page (e.g. GBK) and garble output; selecting a UTF-8 profile such as Git Bash resolves this. The setting reuses VS Code's terminal profile concept: when set, the profile name is resolved against terminal.integrated.profiles. to derive shellPath/shellArgs for createTerminal. When empty/unset the default terminal behavior is preserved unchanged. Adds backend unit tests for profile resolution and a webview test for the settings dropdown wiring. --- packages/types/src/global-settings.ts | 1 + packages/types/src/vscode-extension-host.ts | 1 + src/core/webview/ClineProvider.ts | 5 + src/core/webview/webviewMessageHandler.ts | 2 + src/integrations/terminal/BaseTerminal.ts | 19 ++ src/integrations/terminal/Terminal.ts | 98 ++++++++- .../__tests__/TerminalProfile.spec.ts | 189 ++++++++++++++++++ .../src/components/settings/SettingsView.tsx | 3 + .../components/settings/TerminalSettings.tsx | 60 +++++- .../SettingsView.change-detection.spec.tsx | 1 + .../TerminalSettings.profile.spec.tsx | 135 +++++++++++++ .../src/context/ExtensionStateContext.tsx | 4 + webview-ui/src/i18n/locales/ca/settings.json | 5 + webview-ui/src/i18n/locales/de/settings.json | 5 + webview-ui/src/i18n/locales/en/settings.json | 5 + webview-ui/src/i18n/locales/es/settings.json | 5 + webview-ui/src/i18n/locales/fr/settings.json | 5 + webview-ui/src/i18n/locales/hi/settings.json | 5 + webview-ui/src/i18n/locales/id/settings.json | 5 + webview-ui/src/i18n/locales/it/settings.json | 5 + webview-ui/src/i18n/locales/ja/settings.json | 5 + webview-ui/src/i18n/locales/ko/settings.json | 5 + webview-ui/src/i18n/locales/nl/settings.json | 5 + webview-ui/src/i18n/locales/pl/settings.json | 5 + .../src/i18n/locales/pt-BR/settings.json | 5 + webview-ui/src/i18n/locales/ru/settings.json | 5 + webview-ui/src/i18n/locales/tr/settings.json | 5 + webview-ui/src/i18n/locales/vi/settings.json | 5 + .../src/i18n/locales/zh-CN/settings.json | 5 + .../src/i18n/locales/zh-TW/settings.json | 5 + 30 files changed, 606 insertions(+), 2 deletions(-) create mode 100644 src/integrations/terminal/__tests__/TerminalProfile.spec.ts create mode 100644 webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 3a5a99eb98..91b84b66e7 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -176,6 +176,7 @@ export const globalSettingsSchema = z.object({ terminalZshOhMy: z.boolean().optional(), terminalZshP10k: z.boolean().optional(), terminalZdotdir: z.boolean().optional(), + terminalProfile: z.string().optional(), execaShellPath: z.string().optional(), diagnosticsEnabled: z.boolean().optional(), diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index c09f22aed7..9dc2455f71 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -276,6 +276,7 @@ export type ExtensionState = Pick< | "terminalZshOhMy" | "terminalZshP10k" | "terminalZdotdir" + | "terminalProfile" | "execaShellPath" | "diagnosticsEnabled" | "language" diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 3f5af94cae..e51d083abd 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -746,6 +746,7 @@ export class ClineProvider terminalZshP10k = false, terminalPowershellCounter = false, terminalZdotdir = false, + terminalProfile, ttsEnabled, ttsSpeed, }) => { @@ -757,6 +758,7 @@ export class ClineProvider Terminal.setTerminalZshP10k(terminalZshP10k) Terminal.setPowershellCounter(terminalPowershellCounter) Terminal.setTerminalZdotdir(terminalZdotdir) + Terminal.setTerminalProfile(terminalProfile) setTtsEnabled(ttsEnabled ?? false) setTtsSpeed(ttsSpeed ?? 1) }, @@ -2049,6 +2051,7 @@ export class ClineProvider terminalZshOhMy, terminalZshP10k, terminalZdotdir, + terminalProfile, mcpEnabled, currentApiConfigName, listApiConfigMeta, @@ -2201,6 +2204,7 @@ export class ClineProvider terminalZshOhMy: terminalZshOhMy ?? false, terminalZshP10k: terminalZshP10k ?? false, terminalZdotdir: terminalZdotdir ?? false, + terminalProfile, mcpEnabled: mcpEnabled ?? true, currentApiConfigName: currentApiConfigName ?? "default", listApiConfigMeta: listApiConfigMeta ?? [], @@ -2404,6 +2408,7 @@ export class ClineProvider terminalZshOhMy: stateValues.terminalZshOhMy ?? false, terminalZshP10k: stateValues.terminalZshP10k ?? false, terminalZdotdir: stateValues.terminalZdotdir ?? false, + terminalProfile: stateValues.terminalProfile, mode: stateValues.mode ?? defaultModeSlug, language: stateValues.language ?? formatLanguage(vscode.env.language), mcpEnabled: stateValues.mcpEnabled ?? true, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 429de051b8..11901f270f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -726,6 +726,8 @@ export const webviewMessageHandler = async ( if (value !== undefined) { Terminal.setTerminalZdotdir(value as boolean) } + } else if (key === "terminalProfile") { + Terminal.setTerminalProfile(value as string | undefined) } else if (key === "execaShellPath") { Terminal.setExecaShellPath(value as string | undefined) } else if (key === "mcpEnabled") { diff --git a/src/integrations/terminal/BaseTerminal.ts b/src/integrations/terminal/BaseTerminal.ts index ee26254934..cb807a259c 100644 --- a/src/integrations/terminal/BaseTerminal.ts +++ b/src/integrations/terminal/BaseTerminal.ts @@ -161,6 +161,7 @@ export abstract class BaseTerminal implements RooTerminal { private static terminalZshOhMy: boolean = false private static terminalZshP10k: boolean = false private static terminalZdotdir: boolean = false + private static terminalProfile: string | undefined = undefined private static execaShellPath: string | undefined = undefined /** @@ -296,6 +297,24 @@ export abstract class BaseTerminal implements RooTerminal { return BaseTerminal.terminalZdotdir } + /** + * Sets the name of the VS Code terminal profile to use for the inline + * (shell-integration) terminal. An empty/undefined value falls back to + * VS Code's default terminal behavior. + * @param profile The terminal profile name, or undefined for the default + */ + public static setTerminalProfile(profile: string | undefined): void { + BaseTerminal.terminalProfile = profile && profile.trim().length > 0 ? profile : undefined + } + + /** + * Gets the name of the VS Code terminal profile to use for the inline terminal. + * @returns The terminal profile name, or undefined when the default should be used + */ + public static getTerminalProfile(): string | undefined { + return BaseTerminal.terminalProfile + } + public static setExecaShellPath(shellPath: string | undefined): void { BaseTerminal.execaShellPath = shellPath } diff --git a/src/integrations/terminal/Terminal.ts b/src/integrations/terminal/Terminal.ts index 38ace9d4b1..23d59b05ae 100644 --- a/src/integrations/terminal/Terminal.ts +++ b/src/integrations/terminal/Terminal.ts @@ -17,7 +17,28 @@ export class Terminal extends BaseTerminal { const env = Terminal.getEnv() const iconPath = new vscode.ThemeIcon("rocket") - this.terminal = terminal ?? vscode.window.createTerminal({ cwd, name: "Roo Code", iconPath, env }) + + if (terminal) { + this.terminal = terminal + } else { + const options: vscode.TerminalOptions = { cwd, name: "Roo Code", iconPath, env } + + // When the user has chosen a specific terminal profile, resolve it to a + // shell path/args so the inline terminal uses that shell (e.g. Git Bash + // with a UTF-8 charset on Windows). When unset, we leave shellPath/shellArgs + // undefined so VS Code's default terminal behavior is preserved (#119). + const profileShell = Terminal.getProfileShell() + + if (profileShell?.shellPath) { + options.shellPath = profileShell.shellPath + + if (profileShell.shellArgs) { + options.shellArgs = profileShell.shellArgs + } + } + + this.terminal = vscode.window.createTerminal(options) + } if (Terminal.getTerminalZdotdir()) { ShellIntegrationManager.terminalTmpDirs.set(id, env.ZDOTDIR) @@ -191,4 +212,79 @@ export class Terminal extends BaseTerminal { return env } + + /** + * Returns the VS Code config section key (`windows`/`osx`/`linux`) used for + * platform-specific terminal profiles. + */ + private static getPlatformProfileKey(platform: NodeJS.Platform = process.platform): "windows" | "osx" | "linux" { + if (platform === "win32") { + return "windows" + } + + if (platform === "darwin") { + return "osx" + } + + return "linux" + } + + /** + * Resolves the configured inline terminal profile (see `terminalProfile` + * setting / {@link Terminal.getTerminalProfile}) into a shell path and args by + * reading VS Code's `terminal.integrated.profiles.` configuration. + * + * This reuses VS Code's terminal profile concept so users can pick, for + * example, a Git Bash profile (UTF-8) instead of the default cmd/PowerShell + * (which may use a non-UTF-8 charset such as GBK) on Windows (#119). + * + * @returns The resolved shell path/args, or undefined when no profile is + * configured or the profile cannot be resolved (default behavior). + */ + public static getProfileShell( + platform: NodeJS.Platform = process.platform, + ): { shellPath: string; shellArgs?: string[] } | undefined { + const profileName = Terminal.getTerminalProfile() + + if (!profileName) { + return undefined + } + + const platformKey = Terminal.getPlatformProfileKey(platform) + + const profiles = vscode.workspace + .getConfiguration("terminal.integrated.profiles") + .get>(platformKey) + + const profile = profiles?.[profileName] as + | { path?: string | string[]; args?: string | string[]; source?: string } + | null + | undefined + + if (!profile) { + console.warn(`[Terminal] Configured terminal profile "${profileName}" not found for ${platformKey}.`) + return undefined + } + + // A `path` may be a single string or an array of candidate paths (VS Code + // picks the first that exists). We pass the first candidate to createTerminal. + const pathValue = Array.isArray(profile.path) ? profile.path[0] : profile.path + + if (!pathValue) { + // Profiles defined only by `source` (e.g. "PowerShell") can't be mapped to + // a shell path here, so we fall back to the default terminal. + console.warn( + `[Terminal] Terminal profile "${profileName}" has no resolvable "path"; using default terminal.`, + ) + return undefined + } + + const shellArgs = Array.isArray(profile.args) + ? profile.args + : typeof profile.args === "string" + ? [profile.args] + : undefined + + return { shellPath: pathValue, shellArgs } + } } diff --git a/src/integrations/terminal/__tests__/TerminalProfile.spec.ts b/src/integrations/terminal/__tests__/TerminalProfile.spec.ts new file mode 100644 index 0000000000..f2e252429b --- /dev/null +++ b/src/integrations/terminal/__tests__/TerminalProfile.spec.ts @@ -0,0 +1,189 @@ +// npx vitest run src/integrations/terminal/__tests__/TerminalProfile.spec.ts + +import * as vscode from "vscode" + +import { Terminal } from "../Terminal" +import { TerminalRegistry } from "../TerminalRegistry" + +vi.mock("execa", () => ({ + execa: vi.fn(), +})) + +describe("Terminal inline terminal profile (#119)", () => { + let getConfigurationSpy: ReturnType + let createTerminalSpy: ReturnType + + const mockTerminal = () => + ({ + exitStatus: undefined, + name: "Roo Code", + processId: Promise.resolve(123), + creationOptions: {}, + state: { isInteractedWith: true }, + dispose: vi.fn(), + hide: vi.fn(), + show: vi.fn(), + sendText: vi.fn(), + shellIntegration: { executeCommand: vi.fn() }, + }) as any + + // Helper to stub `terminal.integrated.profiles.` config reads. + const stubProfiles = (profilesByPlatform: Record) => { + getConfigurationSpy = vi.spyOn(vscode.workspace, "getConfiguration").mockImplementation((section?: string) => { + if (section === "terminal.integrated.profiles") { + return { + get: (platformKey: string) => profilesByPlatform[platformKey], + } as any + } + + return { get: (_key: string, defaultValue?: unknown) => defaultValue } as any + }) + } + + beforeEach(() => { + createTerminalSpy = vi.spyOn(vscode.window, "createTerminal").mockImplementation(() => mockTerminal()) + // Reset to default (unset) before each test. + Terminal.setTerminalProfile(undefined) + }) + + afterEach(() => { + Terminal.setTerminalProfile(undefined) + vi.restoreAllMocks() + }) + + describe("getTerminalProfile / setTerminalProfile", () => { + it("defaults to undefined", () => { + expect(Terminal.getTerminalProfile()).toBeUndefined() + }) + + it("stores a profile name", () => { + Terminal.setTerminalProfile("Git Bash") + expect(Terminal.getTerminalProfile()).toBe("Git Bash") + }) + + it("treats empty/whitespace strings as unset (default behavior)", () => { + Terminal.setTerminalProfile("Git Bash") + Terminal.setTerminalProfile("") + expect(Terminal.getTerminalProfile()).toBeUndefined() + + Terminal.setTerminalProfile(" ") + expect(Terminal.getTerminalProfile()).toBeUndefined() + }) + }) + + describe("getProfileShell", () => { + it("returns undefined when no profile is configured (default behavior preserved)", () => { + stubProfiles({}) + expect(Terminal.getProfileShell("win32")).toBeUndefined() + }) + + it("resolves a Windows Git Bash profile to its shell path and args", () => { + stubProfiles({ + windows: { + "Git Bash": { + path: "C:\\Program Files\\Git\\bin\\bash.exe", + args: ["--login", "-i"], + }, + }, + }) + + Terminal.setTerminalProfile("Git Bash") + + expect(Terminal.getProfileShell("win32")).toEqual({ + shellPath: "C:\\Program Files\\Git\\bin\\bash.exe", + shellArgs: ["--login", "-i"], + }) + }) + + it("uses the first path candidate when path is an array", () => { + stubProfiles({ + windows: { + "Git Bash": { + path: ["C:\\missing\\bash.exe", "C:\\Program Files\\Git\\bin\\bash.exe"], + }, + }, + }) + + Terminal.setTerminalProfile("Git Bash") + + expect(Terminal.getProfileShell("win32")).toEqual({ + shellPath: "C:\\missing\\bash.exe", + shellArgs: undefined, + }) + }) + + it("wraps a string args value into an array", () => { + stubProfiles({ + linux: { + bash: { path: "/bin/bash", args: "-l" }, + }, + }) + + Terminal.setTerminalProfile("bash") + + expect(Terminal.getProfileShell("linux")).toEqual({ + shellPath: "/bin/bash", + shellArgs: ["-l"], + }) + }) + + it("reads the osx profile section on darwin", () => { + stubProfiles({ + osx: { zsh: { path: "/bin/zsh" } }, + }) + + Terminal.setTerminalProfile("zsh") + + expect(Terminal.getProfileShell("darwin")).toEqual({ + shellPath: "/bin/zsh", + shellArgs: undefined, + }) + }) + + it("falls back to default when the configured profile is not found", () => { + stubProfiles({ windows: { PowerShell: { path: "pwsh.exe" } } }) + + Terminal.setTerminalProfile("Nonexistent") + + expect(Terminal.getProfileShell("win32")).toBeUndefined() + }) + + it("falls back to default when the profile has no resolvable path (source-only profile)", () => { + stubProfiles({ windows: { PowerShell: { source: "PowerShell" } } }) + + Terminal.setTerminalProfile("PowerShell") + + expect(Terminal.getProfileShell("win32")).toBeUndefined() + }) + }) + + describe("createTerminal integration", () => { + afterEach(() => { + TerminalRegistry["terminals"] = [] + }) + + it("does NOT pass shellPath/shellArgs when no profile is configured", () => { + stubProfiles({}) + TerminalRegistry.createTerminal("/test/path", "vscode") + + const options = createTerminalSpy.mock.calls[0][0] as vscode.TerminalOptions + expect(options.shellPath).toBeUndefined() + expect(options.shellArgs).toBeUndefined() + }) + + it("passes the resolved shellPath/shellArgs when a profile is configured", () => { + stubProfiles({ + [Terminal["getPlatformProfileKey"](process.platform)]: { + "Git Bash": { path: "/usr/bin/bash", args: ["-i"] }, + }, + }) + + Terminal.setTerminalProfile("Git Bash") + TerminalRegistry.createTerminal("/test/path", "vscode") + + const options = createTerminalSpy.mock.calls[0][0] as vscode.TerminalOptions + expect(options.shellPath).toBe("/usr/bin/bash") + expect(options.shellArgs).toEqual(["-i"]) + }) + }) +}) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 47e087615e..5e78c9efbc 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -183,6 +183,7 @@ const SettingsView = forwardRef(({ onDone, t terminalZshOhMy, terminalZshP10k, terminalZdotdir, + terminalProfile, writeDelayMs, showRooIgnoredFiles, enableSubfolderRules, @@ -396,6 +397,7 @@ const SettingsView = forwardRef(({ onDone, t terminalZshOhMy, terminalZshP10k, terminalZdotdir, + terminalProfile, terminalOutputPreviewSize: terminalOutputPreviewSize ?? "medium", mcpEnabled, maxOpenTabsContext: Math.min(Math.max(0, maxOpenTabsContext ?? 20), 500), @@ -862,6 +864,7 @@ const SettingsView = forwardRef(({ onDone, t terminalZshOhMy={terminalZshOhMy} terminalZshP10k={terminalZshP10k} terminalZdotdir={terminalZdotdir} + terminalProfile={terminalProfile} setCachedStateField={setCachedStateField} /> )} diff --git a/webview-ui/src/components/settings/TerminalSettings.tsx b/webview-ui/src/components/settings/TerminalSettings.tsx index 07f062cc01..e945edfaac 100644 --- a/webview-ui/src/components/settings/TerminalSettings.tsx +++ b/webview-ui/src/components/settings/TerminalSettings.tsx @@ -26,6 +26,7 @@ type TerminalSettingsProps = HTMLAttributes & { terminalZshOhMy?: boolean terminalZshP10k?: boolean terminalZdotdir?: boolean + terminalProfile?: string setCachedStateField: SetCachedStateField< | "terminalOutputPreviewSize" | "terminalShellIntegrationTimeout" @@ -36,9 +37,22 @@ type TerminalSettingsProps = HTMLAttributes & { | "terminalZshOhMy" | "terminalZshP10k" | "terminalZdotdir" + | "terminalProfile" > } +// Sentinel value for the "Default" option; the Select component cannot use an +// empty-string item value, so we map it to/from `undefined` in the handler. +const DEFAULT_PROFILE_VALUE = "__default__" + +// VS Code stores terminal profiles per platform; we request all of them so the +// profile dropdown works regardless of which OS the extension host runs on. +const PROFILE_SETTING_KEYS = [ + "terminal.integrated.profiles.windows", + "terminal.integrated.profiles.osx", + "terminal.integrated.profiles.linux", +] + export const TerminalSettings = ({ terminalOutputPreviewSize, terminalShellIntegrationTimeout, @@ -49,6 +63,7 @@ export const TerminalSettings = ({ terminalZshOhMy, terminalZshP10k, terminalZdotdir, + terminalProfile, setCachedStateField, className, ...props @@ -56,8 +71,12 @@ export const TerminalSettings = ({ const { t } = useAppTranslation() const [inheritEnv, setInheritEnv] = useState(true) + const [profileNames, setProfileNames] = useState([]) - useMount(() => vscode.postMessage({ type: "getVSCodeSetting", setting: "terminal.integrated.inheritEnv" })) + useMount(() => { + vscode.postMessage({ type: "getVSCodeSetting", setting: "terminal.integrated.inheritEnv" }) + PROFILE_SETTING_KEYS.forEach((setting) => vscode.postMessage({ type: "getVSCodeSetting", setting })) + }) const onMessage = useCallback((event: MessageEvent) => { const message: ExtensionMessage = event.data @@ -68,6 +87,13 @@ export const TerminalSettings = ({ case "terminal.integrated.inheritEnv": setInheritEnv(message.value ?? true) break + case "terminal.integrated.profiles.windows": + case "terminal.integrated.profiles.osx": + case "terminal.integrated.profiles.linux": { + const names = message.value && typeof message.value === "object" ? Object.keys(message.value) : [] + setProfileNames((prev) => Array.from(new Set([...prev, ...names])).sort()) + break + } default: break } @@ -139,6 +165,38 @@ export const TerminalSettings = ({
+ + + +
+ {t("settings:terminal.profile.description")} +
+
+ { terminalZshOhMy: false, terminalZshP10k: false, terminalZdotdir: false, + terminalProfile: undefined, writeDelayMs: 0, showRooIgnoredFiles: false, maxReadFileLine: -1, diff --git a/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx b/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx new file mode 100644 index 0000000000..d8b41c77d3 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx @@ -0,0 +1,135 @@ +// npx vitest run src/components/settings/__tests__/TerminalSettings.profile.spec.tsx + +import { render, screen, fireEvent, act } from "@/utils/test-utils" + +import { TerminalSettings } from "../TerminalSettings" + +// Mock translation hook to echo keys +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ t: (key: string) => key }), +})) + +vi.mock("@src/utils/docLinks", () => ({ + buildDocLink: () => "https://example.com", +})) + +const postMessageMock = vi.fn() +vi.mock("@/utils/vscode", () => ({ + vscode: { postMessage: (...args: any[]) => postMessageMock(...args) }, +})) + +// Render Select as a list of buttons so we can drive onValueChange in tests. +vi.mock("@/components/ui", () => ({ + Select: ({ children, value, onValueChange }: any) => ( +
+ {/* Recursively render items and wire their value to onValueChange */} + {renderSelectChildren(children, onValueChange)} +
+ ), + SelectTrigger: ({ children }: any) =>
{children}
, + SelectValue: ({ children }: any) =>
{children}
, + SelectContent: ({ children }: any) =>
{children}
, + SelectItem: ({ children, value }: any) =>
{children}
, + Slider: ({ value, onValueChange }: any) => ( + onValueChange([parseFloat(e.target.value)])} + /> + ), +})) + +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeCheckbox: ({ checked, onChange, children }: any) => ( + + ), + VSCodeLink: ({ children }: any) => {children}, +})) + +// Helper used by the Select mock to render SelectItem children as buttons. +function renderSelectChildren(children: any, onValueChange: (value: string) => void): any { + const React = require("react") + return React.Children.map(children, (child: any) => { + if (!child || typeof child !== "object") return child + const itemValue = child.props?.value ?? child.props?.["data-item-value"] + // SelectContent wraps SelectItems; recurse into it. + if (child.props?.children && itemValue === undefined) { + return renderSelectChildren(child.props.children, onValueChange) + } + if (itemValue !== undefined) { + return ( + + ) + } + return child + }) +} + +describe("TerminalSettings inline terminal profile (#119)", () => { + beforeEach(() => { + postMessageMock.mockClear() + }) + + const setup = (terminalProfile?: string) => { + const setCachedStateField = vi.fn() + render( + , + ) + return { setCachedStateField } + } + + it("requests the VS Code terminal profile lists on mount", () => { + setup() + + const requested = postMessageMock.mock.calls.map((c) => c[0]?.setting) + expect(requested).toContain("terminal.integrated.profiles.windows") + expect(requested).toContain("terminal.integrated.profiles.osx") + expect(requested).toContain("terminal.integrated.profiles.linux") + }) + + it("does not call setCachedStateField on init (only the Default option is shown)", () => { + const { setCachedStateField } = setup() + // No profiles received yet -> only the Default option exists. + expect(screen.getByTestId("option-__default__")).toBeInTheDocument() + expect(setCachedStateField).not.toHaveBeenCalled() + }) + + it("populates the dropdown from received profile lists and selecting one sets the profile name", () => { + const { setCachedStateField } = setup() + + // Simulate the extension responding with a Windows profile list. + act(() => { + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: "vsCodeSetting", + setting: "terminal.integrated.profiles.windows", + value: { "Git Bash": { path: "C:/Program Files/Git/bin/bash.exe" }, PowerShell: {} }, + }, + }), + ) + }) + + // User selects the Git Bash profile. + fireEvent.click(screen.getByTestId("option-Git Bash")) + + expect(setCachedStateField).toHaveBeenCalledWith("terminalProfile", "Git Bash") + }) + + it("maps the Default option back to undefined (restores default behavior)", () => { + const { setCachedStateField } = setup("Git Bash") + + fireEvent.click(screen.getByTestId("option-__default__")) + + expect(setCachedStateField).toHaveBeenCalledWith("terminalProfile", undefined) + }) +}) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 10b49e3de0..dcb48355b3 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -84,6 +84,8 @@ export interface ExtensionStateContextType extends ExtensionState { setTerminalShellIntegrationDisabled: (value: boolean) => void terminalZdotdir?: boolean setTerminalZdotdir: (value: boolean) => void + terminalProfile?: string + setTerminalProfile: (value: string | undefined) => void setTtsEnabled: (value: boolean) => void setTtsSpeed: (value: number) => void setEnableCheckpoints: (value: boolean) => void @@ -233,6 +235,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode terminalZshOhMy: false, // Default Oh My Zsh integration setting terminalZshP10k: false, // Default Powerlevel10k integration setting terminalZdotdir: false, // Default ZDOTDIR handling setting + terminalProfile: undefined, // Default inline terminal profile (use VS Code default) historyPreviewCollapsed: false, // Initialize the new state (default to expanded) reasoningBlockCollapsed: true, // Default to collapsed enterBehavior: "send", // Default: Enter sends, Shift+Enter creates newline @@ -533,6 +536,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setTerminalShellIntegrationDisabled: (value) => setState((prevState) => ({ ...prevState, terminalShellIntegrationDisabled: value })), setTerminalZdotdir: (value) => setState((prevState) => ({ ...prevState, terminalZdotdir: value })), + setTerminalProfile: (value) => setState((prevState) => ({ ...prevState, terminalProfile: value })), setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })), setTaskSyncEnabled: (value) => setState((prevState) => ({ ...prevState, taskSyncEnabled: value }) as any), setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })), diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 92ecfa7e73..2ea0e27c97 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "Hereta variables d'entorn", "description": "Activa per heretar variables d'entorn del procés pare de VS Code. <0>Aprèn-ne més" + }, + "profile": { + "label": "Perfil de terminal en línia", + "default": "Predeterminat (intèrpret d'ordres predeterminat de VS Code)", + "description": "Tria quin perfil de terminal de VS Code utilitza el terminal en línia. Selecciona un intèrpret d'ordres UTF-8 com Git Bash si el predeterminat distorsiona la sortida no ASCII (per exemple, a Windows amb pàgina de codis GBK). Deixa-ho en Predeterminat per mantenir el comportament per defecte de VS Code." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 5154d11039..08418b459a 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "Umgebungsvariablen erben", "description": "Schalte dies ein, um Umgebungsvariablen vom übergeordneten VS Code-Prozess zu erben. <0>Mehr erfahren" + }, + "profile": { + "label": "Inline-Terminal-Profil", + "default": "Standard (VS Code-Standardshell)", + "description": "Wähle, welches VS Code-Terminalprofil das Inline-Terminal verwendet. Wähle eine UTF-8-Shell wie Git Bash, wenn die Standardshell Nicht-ASCII-Ausgaben verstümmelt (z. B. unter Windows mit GBK-Codepage). Belasse es auf Standard, um das Standardverhalten von VS Code beizubehalten." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index a3c11be386..b46860203a 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -801,6 +801,11 @@ "inheritEnv": { "label": "Inherit environment variables", "description": "Turn this on to inherit environment variables from the parent VS Code process. <0>Learn more" + }, + "profile": { + "label": "Inline terminal profile", + "default": "Default (VS Code default shell)", + "description": "Choose which VS Code terminal profile the inline terminal uses. Pick a UTF-8 shell such as Git Bash if the default shell garbles non-ASCII output (e.g. on Windows with a GBK code page). Leave on Default to keep VS Code's default behavior." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 8ec3e29338..66080f9244 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "Heredar variables de entorno", "description": "Activa para heredar variables de entorno del proceso padre de VS Code. <0>Más información" + }, + "profile": { + "label": "Perfil de terminal en línea", + "default": "Predeterminado (shell predeterminado de VS Code)", + "description": "Elige qué perfil de terminal de VS Code usa el terminal en línea. Selecciona un shell UTF-8 como Git Bash si el shell predeterminado distorsiona la salida no ASCII (por ejemplo, en Windows con página de códigos GBK). Déjalo en Predeterminado para mantener el comportamiento predeterminado de VS Code." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index d746111a0a..74e7b8556a 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "Hériter des variables d'environnement", "description": "Activez pour hériter des variables d'environnement du processus parent VS Code. <0>En savoir plus" + }, + "profile": { + "label": "Profil de terminal intégré", + "default": "Par défaut (shell par défaut de VS Code)", + "description": "Choisissez le profil de terminal VS Code utilisé par le terminal intégré. Sélectionnez un shell UTF-8 tel que Git Bash si le shell par défaut altère la sortie non ASCII (par exemple sous Windows avec une page de codes GBK). Laissez sur Par défaut pour conserver le comportement par défaut de VS Code." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 9a77b69ee4..24c6f59ba9 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "पर्यावरण चर विरासत में लें", "description": "पैरेंट VS Code प्रोसेस से पर्यावरण चर विरासत में लेने के लिए इसे चालू करें। <0>अधिक जानें" + }, + "profile": { + "label": "इनलाइन टर्मिनल प्रोफ़ाइल", + "default": "डिफ़ॉल्ट (VS Code डिफ़ॉल्ट शेल)", + "description": "चुनें कि इनलाइन टर्मिनल किस VS Code टर्मिनल प्रोफ़ाइल का उपयोग करता है। यदि डिफ़ॉल्ट शेल गैर-ASCII आउटपुट को विकृत करता है (जैसे GBK कोड पेज वाले Windows पर) तो Git Bash जैसा UTF-8 शेल चुनें। VS Code के डिफ़ॉल्ट व्यवहार को बनाए रखने के लिए डिफ़ॉल्ट पर छोड़ दें।" } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 41eae1b053..847eaf7e08 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "Warisi variabel lingkungan", "description": "Aktifkan untuk mewarisi variabel lingkungan dari proses induk VS Code. <0>Pelajari lebih lanjut" + }, + "profile": { + "label": "Profil terminal sebaris", + "default": "Default (shell default VS Code)", + "description": "Pilih profil terminal VS Code yang digunakan terminal sebaris. Pilih shell UTF-8 seperti Git Bash jika shell default merusak keluaran non-ASCII (misalnya di Windows dengan code page GBK). Biarkan pada Default untuk mempertahankan perilaku default VS Code." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 806fb44462..9678c4848f 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "Eredita variabili d'ambiente", "description": "Attiva per ereditare le variabili d'ambiente dal processo padre di VS Code. <0>Scopri di più" + }, + "profile": { + "label": "Profilo del terminale inline", + "default": "Predefinito (shell predefinita di VS Code)", + "description": "Scegli quale profilo del terminale di VS Code usa il terminale inline. Seleziona una shell UTF-8 come Git Bash se la shell predefinita corrompe l'output non ASCII (ad esempio su Windows con codepage GBK). Lascia su Predefinito per mantenere il comportamento predefinito di VS Code." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index b9a0976c42..6a819de539 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "環境変数を継承", "description": "親VS Codeプロセスから環境変数を継承するには、これをオンにします。<0>詳細情報" + }, + "profile": { + "label": "インラインターミナルプロファイル", + "default": "デフォルト(VS Code のデフォルトシェル)", + "description": "インラインターミナルが使用する VS Code のターミナルプロファイルを選択します。デフォルトのシェルが ASCII 以外の出力を文字化けさせる場合(例: GBK コードページの Windows)、Git Bash などの UTF-8 シェルを選択してください。VS Code のデフォルト動作を維持するにはデフォルトのままにします。" } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 3a88c9fbde..4f02d5872a 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "환경 변수 상속", "description": "부모 VS Code 프로세���에서 환경 변수를 상속하려면 이 기능을 켜십시오. <0>자세히 알아보기" + }, + "profile": { + "label": "인라인 터미널 프로필", + "default": "기본값 (VS Code 기본 셸)", + "description": "인라인 터미널이 사용할 VS Code 터미널 프로필을 선택합니다. 기본 셸이 ASCII가 아닌 출력을 깨뜨리는 경우(예: GBK 코드 페이지의 Windows) Git Bash와 같은 UTF-8 셸을 선택하세요. VS Code 기본 동작을 유지하려면 기본값으로 두세요." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index a91bb87de5..1c9c6e6c81 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "Omgevingsvariabelen overnemen", "description": "Schakel in om omgevingsvariabelen over te nemen van het bovenliggende VS Code-proces. <0>Meer informatie" + }, + "profile": { + "label": "Inline-terminalprofiel", + "default": "Standaard (standaardshell van VS Code)", + "description": "Kies welk VS Code-terminalprofiel de inline-terminal gebruikt. Selecteer een UTF-8-shell zoals Git Bash als de standaardshell niet-ASCII-uitvoer verminkt (bijvoorbeeld op Windows met een GBK-codepagina). Laat op Standaard staan om het standaardgedrag van VS Code te behouden." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index f3751196d8..675f2a7872 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "Dziedzicz zmienne środowiskowe", "description": "Włącz, aby dziedziczyć zmienne środowiskowe z procesu nadrzędnego VS Code. <0>Dowiedz się więcej" + }, + "profile": { + "label": "Profil terminala wbudowanego", + "default": "Domyślny (domyślna powłoka VS Code)", + "description": "Wybierz, którego profilu terminala VS Code używa terminal wbudowany. Wybierz powłokę UTF-8, np. Git Bash, jeśli domyślna powłoka zniekształca dane wyjściowe spoza ASCII (np. w systemie Windows ze stroną kodową GBK). Pozostaw Domyślny, aby zachować domyślne zachowanie VS Code." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 05d3557f76..0d2e4bbe1b 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "Herdar variáveis de ambiente", "description": "Ative isso para herdar variáveis de ambiente do processo pai do VS Code. <0>Saiba mais" + }, + "profile": { + "label": "Perfil do terminal embutido", + "default": "Padrão (shell padrão do VS Code)", + "description": "Escolha qual perfil de terminal do VS Code o terminal embutido usa. Selecione um shell UTF-8 como o Git Bash se o shell padrão corromper a saída não ASCII (por exemplo, no Windows com página de código GBK). Mantenha em Padrão para preservar o comportamento padrão do VS Code." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 1aedb8a575..be0cd7f986 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "Наследовать переменные среды", "description": "Включите для наследования переменных среды от родительского процесса VS Code. <0>Подробнее" + }, + "profile": { + "label": "Профиль встроенного терминала", + "default": "По умолчанию (оболочка VS Code по умолчанию)", + "description": "Выберите профиль терминала VS Code, который использует встроенный терминал. Выберите оболочку UTF-8, например Git Bash, если оболочка по умолчанию искажает не-ASCII вывод (например, в Windows с кодовой страницей GBK). Оставьте «По умолчанию», чтобы сохранить стандартное поведение VS Code." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 22bd730488..4fad294c50 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "Ortam değişkenlerini devral", "description": "Ana VS Code işleminden ortam değişkenlerini devralmak için bunu açın. <0>Daha fazla bilgi edinin" + }, + "profile": { + "label": "Satır içi terminal profili", + "default": "Varsayılan (VS Code varsayılan kabuğu)", + "description": "Satır içi terminalin kullanacağı VS Code terminal profilini seçin. Varsayılan kabuk ASCII olmayan çıktıyı bozuyorsa (örneğin GBK kod sayfası olan Windows'ta) Git Bash gibi bir UTF-8 kabuğu seçin. VS Code varsayılan davranışını korumak için Varsayılan olarak bırakın." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 0b09ae004b..fbc7996027 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "Kế thừa biến môi trường", "description": "Bật tính năng này để kế thừa các biến môi trường từ quy trình mẹ của VS Code. <0>Tìm hiểu thêm" + }, + "profile": { + "label": "Hồ sơ terminal nội tuyến", + "default": "Mặc định (shell mặc định của VS Code)", + "description": "Chọn hồ sơ terminal VS Code mà terminal nội tuyến sử dụng. Chọn một shell UTF-8 như Git Bash nếu shell mặc định làm hỏng đầu ra không phải ASCII (ví dụ trên Windows với bảng mã GBK). Để Mặc định để giữ hành vi mặc định của VS Code." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index f58ea87f8c..1f4baddc1e 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -738,6 +738,11 @@ "inheritEnv": { "label": "继承环境变量", "description": "启用此选项以从父 VS Code 进程继承环境变量。<0>了解更多" + }, + "profile": { + "label": "内联终端配置文件", + "default": "默认(VS Code 默认 Shell)", + "description": "选择内联终端使用的 VS Code 终端配置文件。如果默认 Shell 会使非 ASCII 输出乱码(例如在使用 GBK 代码页的 Windows 上),请选择 Git Bash 等 UTF-8 Shell。保留为“默认”以保持 VS Code 的默认行为。" } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index e4268280de..d1f776b1b0 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -748,6 +748,11 @@ "inheritEnv": { "label": "繼承環境變數", "description": "啟用此選項以從父 VS Code 程序繼承環境變數。<0>了解更多" + }, + "profile": { + "label": "內嵌終端機設定檔", + "default": "預設(VS Code 預設 Shell)", + "description": "選擇內嵌終端機使用的 VS Code 終端機設定檔。如果預設 Shell 會使非 ASCII 輸出亂碼(例如在使用 GBK 字碼頁的 Windows 上),請選擇 Git Bash 等 UTF-8 Shell。保留為「預設」以維持 VS Code 的預設行為。" } }, "advancedSettings": { From 27a3b2f4ff9c5d3251db92fb8b72e789366efb45 Mon Sep 17 00:00:00 2001 From: Armando Vaquera <263793884+proyectoauraorg@users.noreply.github.com> Date: Sat, 23 May 2026 19:40:34 -0600 Subject: [PATCH 2/6] fix(test): relax spy types for overloaded VS Code APIs (#119) --- src/integrations/terminal/__tests__/TerminalProfile.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/integrations/terminal/__tests__/TerminalProfile.spec.ts b/src/integrations/terminal/__tests__/TerminalProfile.spec.ts index f2e252429b..56d7a3eea0 100644 --- a/src/integrations/terminal/__tests__/TerminalProfile.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProfile.spec.ts @@ -10,8 +10,10 @@ vi.mock("execa", () => ({ })) describe("Terminal inline terminal profile (#119)", () => { - let getConfigurationSpy: ReturnType - let createTerminalSpy: ReturnType + // VS Code's getConfiguration/createTerminal are overloaded, so the precise + // spy MockInstance type isn't worth fighting in a test — `any` keeps it simple. + let getConfigurationSpy: any + let createTerminalSpy: any const mockTerminal = () => ({ From 8653b3c45d5656d073969038e0523828c98f7373 Mon Sep 17 00:00:00 2001 From: Armando Vaquera <263793884+proyectoauraorg@users.noreply.github.com> Date: Sat, 23 May 2026 20:02:11 -0600 Subject: [PATCH 3/6] fix(test): use ES import instead of require() in terminal profile spec (#119) --- .../settings/__tests__/TerminalSettings.profile.spec.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx b/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx index d8b41c77d3..a045658c18 100644 --- a/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx @@ -1,5 +1,7 @@ // npx vitest run src/components/settings/__tests__/TerminalSettings.profile.spec.tsx +import * as React from "react" + import { render, screen, fireEvent, act } from "@/utils/test-utils" import { TerminalSettings } from "../TerminalSettings" @@ -51,7 +53,6 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({ // Helper used by the Select mock to render SelectItem children as buttons. function renderSelectChildren(children: any, onValueChange: (value: string) => void): any { - const React = require("react") return React.Children.map(children, (child: any) => { if (!child || typeof child !== "object") return child const itemValue = child.props?.value ?? child.props?.["data-item-value"] From 4f45c7cd9ec9ac3a9ed162b368e02a423e52d4ac Mon Sep 17 00:00:00 2001 From: Armando Vaquera <263793884+proyectoauraorg@users.noreply.github.com> Date: Sun, 24 May 2026 16:01:30 -0600 Subject: [PATCH 4/6] refactor(terminal): address review feedback on inline terminal profile (#119) - Route profile names through a dedicated allowlisted `requestTerminalProfiles` message instead of the generic `getVSCodeSetting` (which reads any key the webview supplies); the extension reads the profiles and returns only names. - Preserve the profile's `env` (sanitized to string/null; null unsets a var), merged onto the base env in createTerminal. - Clarify the setting copy (en + es) vs the 'Use Inline Terminal' description. - Add tests: updateSettings->setTerminalProfile bridge, resolveWebviewView startup hydration, and profile env preservation/sanitization. --- packages/types/src/vscode-extension-host.ts | 4 ++ .../webview/__tests__/ClineProvider.spec.ts | 15 +++++++ .../__tests__/webviewMessageHandler.spec.ts | 33 ++++++++++++++++ src/core/webview/webviewMessageHandler.ts | 32 +++++++++++++++ src/integrations/terminal/Terminal.ts | 39 +++++++++++++++++-- .../__tests__/TerminalProfile.spec.ts | 20 ++++++++++ .../components/settings/TerminalSettings.tsx | 30 ++++---------- .../TerminalSettings.profile.spec.tsx | 24 ++++-------- webview-ui/src/i18n/locales/en/settings.json | 2 +- webview-ui/src/i18n/locales/es/settings.json | 2 +- 10 files changed, 156 insertions(+), 45 deletions(-) diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 9dc2455f71..438067fe16 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -63,6 +63,7 @@ export interface ExtensionMessage { | "commandExecutionStatus" | "mcpExecutionStatus" | "vsCodeSetting" + | "terminalProfiles" | "authenticatedUser" | "condenseTaskContextStarted" | "condenseTaskContextResponse" @@ -153,6 +154,8 @@ export interface ExtensionMessage { error?: string setting?: string value?: any // eslint-disable-line @typescript-eslint/no-explicit-any + /** Sanitized VS Code terminal profile names for the `terminalProfiles` message. */ + profiles?: string[] hasContent?: boolean items?: MarketplaceItem[] userInfo?: CloudUserInfo @@ -455,6 +458,7 @@ export interface WebviewMessage { | "updateVSCodeSetting" | "getVSCodeSetting" | "vsCodeSetting" + | "requestTerminalProfiles" | "updateCondensingPrompt" | "playSound" | "playTts" diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index be9d705684..9203939261 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -24,6 +24,7 @@ import { Task, TaskOptions } from "../../task/Task" import { safeWriteJson } from "../../../utils/safeWriteJson" import { ClineProvider } from "../ClineProvider" +import { Terminal } from "../../../integrations/terminal/Terminal" import { MessageManager } from "../../message-manager" // Mock setup must come before imports. @@ -471,6 +472,20 @@ describe("ClineProvider", () => { expect(ClineProvider.getVisibleInstance()).toBe(provider) }) + test("resolveWebviewView hydrates the saved terminalProfile into the process-wide Terminal state", async () => { + const setTerminalProfileSpy = vi.spyOn(Terminal, "setTerminalProfile").mockImplementation(() => {}) + // Seed the persisted setting so the real getState() returns it during hydration. + await (provider as any).contextProxy.setValue("terminalProfile", "Git Bash") + + await provider.resolveWebviewView(mockWebviewView) + // The hydration runs in a getState().then(...) callback, so flush microtasks. + await new Promise((resolve) => setImmediate(resolve)) + + expect(setTerminalProfileSpy).toHaveBeenCalledWith("Git Bash") + + setTerminalProfileSpy.mockRestore() + }) + test("resolveWebviewView sets up webview correctly", async () => { await provider.resolveWebviewView(mockWebviewView) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 17e0caebb0..dfae7a7557 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -167,6 +167,7 @@ vi.mock("../../mentions/resolveImageMentions", () => ({ })) import { resolveImageMentions } from "../../mentions/resolveImageMentions" +import { Terminal } from "../../../integrations/terminal/Terminal" describe("webviewMessageHandler - requestLmStudioModels", () => { beforeEach(() => { @@ -860,6 +861,38 @@ describe("webviewMessageHandler - mcpEnabled", () => { }) }) +describe("webviewMessageHandler - terminalProfile", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("bridges a saved terminalProfile from updateSettings into the process-wide terminal state", async () => { + const setTerminalProfileSpy = vi.spyOn(Terminal, "setTerminalProfile").mockImplementation(() => {}) + + await webviewMessageHandler(mockClineProvider, { + type: "updateSettings", + updatedSettings: { terminalProfile: "Git Bash" }, + }) + + expect(setTerminalProfileSpy).toHaveBeenCalledWith("Git Bash") + + setTerminalProfileSpy.mockRestore() + }) + + it("clears the terminal profile when updateSettings sends undefined", async () => { + const setTerminalProfileSpy = vi.spyOn(Terminal, "setTerminalProfile").mockImplementation(() => {}) + + await webviewMessageHandler(mockClineProvider, { + type: "updateSettings", + updatedSettings: { terminalProfile: undefined }, + }) + + expect(setTerminalProfileSpy).toHaveBeenCalledWith(undefined) + + setTerminalProfileSpy.mockRestore() + }) +}) + describe("webviewMessageHandler - requestCommands", () => { beforeEach(() => { vi.clearAllMocks() diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 11901f270f..c8482f7fc0 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1512,6 +1512,38 @@ export const webviewMessageHandler = async ( break + case "requestTerminalProfiles": { + // Allowlisted request: read VS Code's terminal profiles server-side and + // return only the sanitized profile names. The terminal profile dropdown + // only needs names, so this avoids routing it through the generic + // `getVSCodeSetting` handler (which reads any key the webview supplies). + try { + const names = new Set() + + for (const platform of ["windows", "osx", "linux"] as const) { + const profiles = vscode.workspace + .getConfiguration("terminal.integrated.profiles") + .get>(platform) + + if (profiles && typeof profiles === "object") { + for (const name of Object.keys(profiles)) { + names.add(name) + } + } + } + + await provider.postMessageToWebview({ + type: "terminalProfiles", + profiles: Array.from(names).sort(), + }) + } catch (error) { + console.error("Failed to get terminal profiles:", error) + await provider.postMessageToWebview({ type: "terminalProfiles", profiles: [] }) + } + + break + } + case "mode": await provider.handleModeSwitch(message.text as Mode) break diff --git a/src/integrations/terminal/Terminal.ts b/src/integrations/terminal/Terminal.ts index 23d59b05ae..8fb651e5c7 100644 --- a/src/integrations/terminal/Terminal.ts +++ b/src/integrations/terminal/Terminal.ts @@ -24,7 +24,7 @@ export class Terminal extends BaseTerminal { const options: vscode.TerminalOptions = { cwd, name: "Roo Code", iconPath, env } // When the user has chosen a specific terminal profile, resolve it to a - // shell path/args so the inline terminal uses that shell (e.g. Git Bash + // shell path/args/env so the inline terminal uses that shell (e.g. Git Bash // with a UTF-8 charset on Windows). When unset, we leave shellPath/shellArgs // undefined so VS Code's default terminal behavior is preserved (#119). const profileShell = Terminal.getProfileShell() @@ -35,6 +35,12 @@ export class Terminal extends BaseTerminal { if (profileShell.shellArgs) { options.shellArgs = profileShell.shellArgs } + + // Merge the profile's own env on top of the base env so profile-specific + // variables (e.g. locale/PATH) are not lost. A `null` value unsets one. + if (profileShell.env) { + options.env = { ...env, ...profileShell.env } + } } this.terminal = vscode.window.createTerminal(options) @@ -243,7 +249,7 @@ export class Terminal extends BaseTerminal { */ public static getProfileShell( platform: NodeJS.Platform = process.platform, - ): { shellPath: string; shellArgs?: string[] } | undefined { + ): { shellPath: string; shellArgs?: string[]; env?: Record } | undefined { const profileName = Terminal.getTerminalProfile() if (!profileName) { @@ -257,7 +263,12 @@ export class Terminal extends BaseTerminal { .get>(platformKey) const profile = profiles?.[profileName] as - | { path?: string | string[]; args?: string | string[]; source?: string } + | { + path?: string | string[] + args?: string | string[] + source?: string + env?: Record + } | null | undefined @@ -285,6 +296,26 @@ export class Terminal extends BaseTerminal { ? [profile.args] : undefined - return { shellPath: pathValue, shellArgs } + // VS Code profiles may declare their own `env` (e.g. to set a UTF-8 locale or + // a custom PATH). Preserve it so the inline terminal doesn't lose environment + // the user configured on the profile. A `null` value unsets that variable. + // Values come from user `settings.json`, so sanitize to string/null only. + let env: Record | undefined + + if (profile.env && typeof profile.env === "object") { + const sanitized: Record = {} + + for (const [key, val] of Object.entries(profile.env)) { + if (typeof val === "string" || val === null) { + sanitized[key] = val + } + } + + if (Object.keys(sanitized).length > 0) { + env = sanitized + } + } + + return { shellPath: pathValue, shellArgs, env } } } diff --git a/src/integrations/terminal/__tests__/TerminalProfile.spec.ts b/src/integrations/terminal/__tests__/TerminalProfile.spec.ts index 56d7a3eea0..297c737acf 100644 --- a/src/integrations/terminal/__tests__/TerminalProfile.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProfile.spec.ts @@ -97,6 +97,26 @@ describe("Terminal inline terminal profile (#119)", () => { }) }) + it("preserves the profile's env and sanitizes non-string/null values", () => { + stubProfiles({ + linux: { + "Custom Bash": { + path: "/bin/bash", + env: { LANG: "en_US.UTF-8", UNSET_ME: null, BAD: 123 }, + }, + }, + }) + + Terminal.setTerminalProfile("Custom Bash") + + expect(Terminal.getProfileShell("linux")).toEqual({ + shellPath: "/bin/bash", + shellArgs: undefined, + // `null` is preserved (unsets the var); the numeric `BAD` is dropped. + env: { LANG: "en_US.UTF-8", UNSET_ME: null }, + }) + }) + it("uses the first path candidate when path is an array", () => { stubProfiles({ windows: { diff --git a/webview-ui/src/components/settings/TerminalSettings.tsx b/webview-ui/src/components/settings/TerminalSettings.tsx index e945edfaac..f3b6c98667 100644 --- a/webview-ui/src/components/settings/TerminalSettings.tsx +++ b/webview-ui/src/components/settings/TerminalSettings.tsx @@ -45,14 +45,6 @@ type TerminalSettingsProps = HTMLAttributes & { // empty-string item value, so we map it to/from `undefined` in the handler. const DEFAULT_PROFILE_VALUE = "__default__" -// VS Code stores terminal profiles per platform; we request all of them so the -// profile dropdown works regardless of which OS the extension host runs on. -const PROFILE_SETTING_KEYS = [ - "terminal.integrated.profiles.windows", - "terminal.integrated.profiles.osx", - "terminal.integrated.profiles.linux", -] - export const TerminalSettings = ({ terminalOutputPreviewSize, terminalShellIntegrationTimeout, @@ -75,7 +67,9 @@ export const TerminalSettings = ({ useMount(() => { vscode.postMessage({ type: "getVSCodeSetting", setting: "terminal.integrated.inheritEnv" }) - PROFILE_SETTING_KEYS.forEach((setting) => vscode.postMessage({ type: "getVSCodeSetting", setting })) + // Request the terminal profile names through a dedicated, allowlisted message + // (the extension reads the profiles and returns only sanitized names). + vscode.postMessage({ type: "requestTerminalProfiles" }) }) const onMessage = useCallback((event: MessageEvent) => { @@ -83,21 +77,13 @@ export const TerminalSettings = ({ switch (message.type) { case "vsCodeSetting": - switch (message.setting) { - case "terminal.integrated.inheritEnv": - setInheritEnv(message.value ?? true) - break - case "terminal.integrated.profiles.windows": - case "terminal.integrated.profiles.osx": - case "terminal.integrated.profiles.linux": { - const names = message.value && typeof message.value === "object" ? Object.keys(message.value) : [] - setProfileNames((prev) => Array.from(new Set([...prev, ...names])).sort()) - break - } - default: - break + if (message.setting === "terminal.integrated.inheritEnv") { + setInheritEnv(message.value ?? true) } break + case "terminalProfiles": + setProfileNames(message.profiles ?? []) + break default: break } diff --git a/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx b/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx index a045658c18..b8c6b0de30 100644 --- a/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/TerminalSettings.profile.spec.tsx @@ -33,11 +33,7 @@ vi.mock("@/components/ui", () => ({ SelectContent: ({ children }: any) =>
{children}
, SelectItem: ({ children, value }: any) =>
{children}
, Slider: ({ value, onValueChange }: any) => ( - onValueChange([parseFloat(e.target.value)])} - /> + onValueChange([parseFloat(e.target.value)])} /> ), })) @@ -88,13 +84,11 @@ describe("TerminalSettings inline terminal profile (#119)", () => { return { setCachedStateField } } - it("requests the VS Code terminal profile lists on mount", () => { + it("requests the terminal profile names on mount via the allowlisted message", () => { setup() - const requested = postMessageMock.mock.calls.map((c) => c[0]?.setting) - expect(requested).toContain("terminal.integrated.profiles.windows") - expect(requested).toContain("terminal.integrated.profiles.osx") - expect(requested).toContain("terminal.integrated.profiles.linux") + const types = postMessageMock.mock.calls.map((c) => c[0]?.type) + expect(types).toContain("requestTerminalProfiles") }) it("does not call setCachedStateField on init (only the Default option is shown)", () => { @@ -104,18 +98,14 @@ describe("TerminalSettings inline terminal profile (#119)", () => { expect(setCachedStateField).not.toHaveBeenCalled() }) - it("populates the dropdown from received profile lists and selecting one sets the profile name", () => { + it("populates the dropdown from the received profile names and selecting one sets the profile name", () => { const { setCachedStateField } = setup() - // Simulate the extension responding with a Windows profile list. + // Simulate the extension responding with the sanitized profile names. act(() => { window.dispatchEvent( new MessageEvent("message", { - data: { - type: "vsCodeSetting", - setting: "terminal.integrated.profiles.windows", - value: { "Git Bash": { path: "C:/Program Files/Git/bin/bash.exe" }, PowerShell: {} }, - }, + data: { type: "terminalProfiles", profiles: ["Git Bash", "PowerShell"] }, }), ) }) diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index b46860203a..3c51c921b6 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -805,7 +805,7 @@ "profile": { "label": "Inline terminal profile", "default": "Default (VS Code default shell)", - "description": "Choose which VS Code terminal profile the inline terminal uses. Pick a UTF-8 shell such as Git Bash if the default shell garbles non-ASCII output (e.g. on Windows with a GBK code page). Leave on Default to keep VS Code's default behavior." + "description": "Pick which shell the Inline Terminal launches, using a VS Code terminal profile's path and arguments. This only changes the shell binary — the Inline Terminal still bypasses shell integration, prompts, and plugins. Useful to choose a UTF-8 shell such as Git Bash when the default garbles non-ASCII output (e.g. on Windows with a GBK code page). Leave on Default to keep VS Code's default shell." } }, "advancedSettings": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 66080f9244..c2738b7500 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -742,7 +742,7 @@ "profile": { "label": "Perfil de terminal en línea", "default": "Predeterminado (shell predeterminado de VS Code)", - "description": "Elige qué perfil de terminal de VS Code usa el terminal en línea. Selecciona un shell UTF-8 como Git Bash si el shell predeterminado distorsiona la salida no ASCII (por ejemplo, en Windows con página de códigos GBK). Déjalo en Predeterminado para mantener el comportamiento predeterminado de VS Code." + "description": "Elige qué shell lanza el Terminal en línea, usando la ruta y argumentos de un perfil de terminal de VS Code. Esto solo cambia el binario del shell — el Terminal en línea sigue evitando la integración del shell, prompts y plugins. Útil para elegir un shell UTF-8 como Git Bash cuando el predeterminado distorsiona la salida no ASCII (por ejemplo, en Windows con página de códigos GBK). Déjalo en Predeterminado para mantener el shell predeterminado de VS Code." } }, "advancedSettings": { From cb3734f4f1312baca29a4e0d755f5c761a45728c Mon Sep 17 00:00:00 2001 From: Armando Vaquera <263793884+proyectoauraorg@users.noreply.github.com> Date: Sun, 24 May 2026 20:59:11 -0600 Subject: [PATCH 5/6] fix(terminal): resolve profile path[] to first existing candidate (#119) VS Code selects the first terminal-profile path candidate that exists on disk; mirror that instead of always taking index 0, falling back to the first non-empty candidate when none exist. Addresses CodeRabbit review on #277. --- src/integrations/terminal/Terminal.ts | 11 +++++-- .../__tests__/TerminalProfile.spec.ts | 33 ++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/integrations/terminal/Terminal.ts b/src/integrations/terminal/Terminal.ts index 8fb651e5c7..2e90f2eb1c 100644 --- a/src/integrations/terminal/Terminal.ts +++ b/src/integrations/terminal/Terminal.ts @@ -1,3 +1,5 @@ +import { existsSync } from "fs" + import * as vscode from "vscode" import pWaitFor from "p-wait-for" @@ -277,9 +279,12 @@ export class Terminal extends BaseTerminal { return undefined } - // A `path` may be a single string or an array of candidate paths (VS Code - // picks the first that exists). We pass the first candidate to createTerminal. - const pathValue = Array.isArray(profile.path) ? profile.path[0] : profile.path + // A `path` may be a single string or an array of candidate paths. VS Code + // picks the first candidate that exists on disk, so mirror that: prefer the + // first existing path, otherwise fall back to the first non-empty candidate. + const candidates = Array.isArray(profile.path) ? profile.path : [profile.path] + const nonEmpty = candidates.filter((p): p is string => typeof p === "string" && p.length > 0) + const pathValue = nonEmpty.find((p) => existsSync(p)) ?? nonEmpty[0] if (!pathValue) { // Profiles defined only by `source` (e.g. "PowerShell") can't be mapped to diff --git a/src/integrations/terminal/__tests__/TerminalProfile.spec.ts b/src/integrations/terminal/__tests__/TerminalProfile.spec.ts index 297c737acf..974b335de7 100644 --- a/src/integrations/terminal/__tests__/TerminalProfile.spec.ts +++ b/src/integrations/terminal/__tests__/TerminalProfile.spec.ts @@ -1,5 +1,7 @@ // npx vitest run src/integrations/terminal/__tests__/TerminalProfile.spec.ts +import { existsSync } from "fs" + import * as vscode from "vscode" import { Terminal } from "../Terminal" @@ -9,6 +11,12 @@ vi.mock("execa", () => ({ execa: vi.fn(), })) +vi.mock("fs", () => ({ + existsSync: vi.fn(() => false), +})) + +const mockedExistsSync = existsSync as unknown as ReturnType + describe("Terminal inline terminal profile (#119)", () => { // VS Code's getConfiguration/createTerminal are overloaded, so the precise // spy MockInstance type isn't worth fighting in a test — `any` keeps it simple. @@ -44,6 +52,9 @@ describe("Terminal inline terminal profile (#119)", () => { beforeEach(() => { createTerminalSpy = vi.spyOn(vscode.window, "createTerminal").mockImplementation(() => mockTerminal()) + // Default: no candidate path exists on disk unless a test says otherwise. + mockedExistsSync.mockReset() + mockedExistsSync.mockReturnValue(false) // Reset to default (unset) before each test. Terminal.setTerminalProfile(undefined) }) @@ -117,7 +128,7 @@ describe("Terminal inline terminal profile (#119)", () => { }) }) - it("uses the first path candidate when path is an array", () => { + it("picks the first existing path candidate when path is an array", () => { stubProfiles({ windows: { "Git Bash": { @@ -125,6 +136,26 @@ describe("Terminal inline terminal profile (#119)", () => { }, }, }) + // Only the second candidate exists on disk; VS Code would pick it. + mockedExistsSync.mockImplementation((p: string) => p === "C:\\Program Files\\Git\\bin\\bash.exe") + + Terminal.setTerminalProfile("Git Bash") + + expect(Terminal.getProfileShell("win32")).toEqual({ + shellPath: "C:\\Program Files\\Git\\bin\\bash.exe", + shellArgs: undefined, + }) + }) + + it("falls back to the first non-empty candidate when none of the paths exist", () => { + stubProfiles({ + windows: { + "Git Bash": { + path: ["C:\\missing\\bash.exe", "C:\\also-missing\\bash.exe"], + }, + }, + }) + // existsSync defaults to false for every candidate. Terminal.setTerminalProfile("Git Bash") From bee49f43565c06aac3f5dc5df5b209da69972326 Mon Sep 17 00:00:00 2001 From: Armando Vaquera <263793884+proyectoauraorg@users.noreply.github.com> Date: Wed, 27 May 2026 00:15:15 -0600 Subject: [PATCH 6/6] fix(terminal): hide inline profile picker when inline execution is off The terminal-profile dropdown only affects inline execution, which is active when shell integration is disabled. It previously stayed visible and editable even when inline mode was off, where it has no effect. Guard it with terminalShellIntegrationDisabled (defaulting to shown, matching the checkbox), mirroring the inline-only settings below. Addresses PR #277 review (edelauna). --- .../components/settings/TerminalSettings.tsx | 66 ++++++++++--------- .../TerminalSettings.profile.spec.tsx | 12 +++- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/webview-ui/src/components/settings/TerminalSettings.tsx b/webview-ui/src/components/settings/TerminalSettings.tsx index f3b6c98667..9474fe652c 100644 --- a/webview-ui/src/components/settings/TerminalSettings.tsx +++ b/webview-ui/src/components/settings/TerminalSettings.tsx @@ -151,37 +151,43 @@ export const TerminalSettings = ({
- - - + setCachedStateField( + "terminalProfile", + value === DEFAULT_PROFILE_VALUE ? undefined : value, + ) + }> + + + + + + {t("settings:terminal.profile.default")} - ))} - - -
- {t("settings:terminal.profile.description")} -
-
+ {profileNames.map((name) => ( + + {name} + + ))} + + +
+ {t("settings:terminal.profile.description")} +
+ + )} { postMessageMock.mockClear() }) + // Inline execution is active when shell integration is disabled, which is when the + // profile picker is shown; render in that state for the picker-behavior tests. const setup = (terminalProfile?: string) => { const setCachedStateField = vi.fn() render( , @@ -123,4 +125,12 @@ describe("TerminalSettings inline terminal profile (#119)", () => { expect(setCachedStateField).toHaveBeenCalledWith("terminalProfile", undefined) }) + + it("hides the profile picker when inline execution is off (shell integration enabled)", () => { + render() + // The picker is inline-only: with shell integration enabled it must not render. + expect(screen.queryByTestId("option-__default__")).not.toBeInTheDocument() + // But the names are still requested on mount (cheap, harmless, keeps state warm). + expect(postMessageMock.mock.calls.map((c) => c[0]?.type)).toContain("requestTerminalProfiles") + }) })