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

Commit 556d703

Browse files
committed
fix: improve lock icon visibility in API config popover and add Settings toggle
- Replace subtle icon-only lock button (opacity-60) with a clearly visible button that includes a text label ("Lock across modes" / "Locked across modes") - Move the lock toggle to its own row in the popover for better visibility - Add "Use the same configuration for all modes" toggle to the Providers tab in Settings, giving users a second, more discoverable path to the setting - Update existing tests and add 3 new tests for the lock toggle button Addresses #12237
1 parent ad25634 commit 556d703

6 files changed

Lines changed: 108 additions & 21 deletions

File tree

webview-ui/src/components/chat/ApiConfigSelector.tsx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,32 @@ export const ApiConfigSelector = ({
224224
)}
225225

226226
{/* Bottom bar with buttons on left and title on right */}
227+
{/* Lock toggle row */}
228+
<div className="flex items-center gap-2 px-3 py-2 border-t border-vscode-dropdown-border">
229+
<Button
230+
variant="ghost"
231+
size="sm"
232+
onClick={onToggleLockApiConfig}
233+
data-testid="lock-api-config-toggle"
234+
className={cn(
235+
"flex items-center gap-1.5 px-2 py-1 text-xs rounded-md",
236+
lockApiConfigAcrossModes
237+
? "text-vscode-focusBorder opacity-100"
238+
: "text-vscode-descriptionForeground opacity-80 hover:opacity-100",
239+
)}>
240+
<span
241+
className={cn(
242+
"codicon text-sm",
243+
lockApiConfigAcrossModes ? "codicon-lock" : "codicon-unlock",
244+
)}
245+
/>
246+
<span>
247+
{lockApiConfigAcrossModes ? t("chat:lockToggle.locked") : t("chat:lockToggle.unlocked")}
248+
</span>
249+
</Button>
250+
</div>
251+
252+
{/* Bottom bar with edit button and title */}
227253
<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
228254
<div className="flex flex-row gap-1">
229255
<IconButton
@@ -232,16 +258,6 @@ export const ApiConfigSelector = ({
232258
onClick={handleEditClick}
233259
tooltip={false}
234260
/>
235-
<IconButton
236-
iconClass={lockApiConfigAcrossModes ? "codicon-lock" : "codicon-unlock"}
237-
title={
238-
lockApiConfigAcrossModes
239-
? t("chat:unlockApiConfigAcrossModes")
240-
: t("chat:lockApiConfigAcrossModes")
241-
}
242-
className={lockApiConfigAcrossModes ? "text-vscode-focusBorder" : "opacity-60"}
243-
onClick={onToggleLockApiConfig}
244-
/>
245261
</div>
246262

247263
{/* Info icon and title on the right with matching spacing */}

webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,47 @@ describe("ApiConfigSelector", () => {
497497
expect(pinnedStickyHeader).toHaveClass("border-b")
498498
})
499499

500+
test("renders lock toggle button with text label in unlocked state", () => {
501+
render(<ApiConfigSelector {...defaultProps} lockApiConfigAcrossModes={false} />)
502+
503+
const trigger = screen.getByTestId("dropdown-trigger")
504+
fireEvent.click(trigger)
505+
506+
const lockToggle = screen.getByTestId("lock-api-config-toggle")
507+
expect(lockToggle).toBeInTheDocument()
508+
expect(lockToggle).toHaveTextContent("chat:lockToggle.unlocked")
509+
})
510+
511+
test("renders lock toggle button with text label in locked state", () => {
512+
render(<ApiConfigSelector {...defaultProps} lockApiConfigAcrossModes={true} />)
513+
514+
const trigger = screen.getByTestId("dropdown-trigger")
515+
fireEvent.click(trigger)
516+
517+
const lockToggle = screen.getByTestId("lock-api-config-toggle")
518+
expect(lockToggle).toBeInTheDocument()
519+
expect(lockToggle).toHaveTextContent("chat:lockToggle.locked")
520+
})
521+
522+
test("calls onToggleLockApiConfig when lock toggle is clicked", () => {
523+
const mockToggleLock = vi.fn()
524+
render(
525+
<ApiConfigSelector
526+
{...defaultProps}
527+
lockApiConfigAcrossModes={false}
528+
onToggleLockApiConfig={mockToggleLock}
529+
/>,
530+
)
531+
532+
const trigger = screen.getByTestId("dropdown-trigger")
533+
fireEvent.click(trigger)
534+
535+
const lockToggle = screen.getByTestId("lock-api-config-toggle")
536+
fireEvent.click(lockToggle)
537+
538+
expect(mockToggleLock).toHaveBeenCalledTimes(1)
539+
})
540+
500541
test("displays all configs in scrollable container when no configs are pinned", () => {
501542
const manyConfigs = Array.from({ length: 10 }, (_, i) => ({
502543
id: `config${i + 1}`,

webview-ui/src/components/chat/__tests__/ChatTextArea.lockApiConfig.spec.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ describe("ChatTextArea - lockApiConfigAcrossModes toggle", () => {
5959

6060
/**
6161
* Helper: Opens the ApiConfigSelector popover by clicking the trigger,
62-
* then returns the lock toggle button by its aria-label.
62+
* then returns the lock toggle button by its data-testid.
6363
*/
64-
const openPopoverAndGetLockToggle = (ariaLabel: string) => {
64+
const openPopoverAndGetLockToggle = (_ariaLabel?: string) => {
6565
const trigger = screen.getByTestId("dropdown-trigger")
6666
fireEvent.click(trigger)
67-
return screen.getByRole("button", { name: ariaLabel })
67+
return screen.getByTestId("lock-api-config-toggle")
6868
}
6969

7070
describe("rendering", () => {
@@ -76,10 +76,10 @@ describe("ChatTextArea - lockApiConfigAcrossModes toggle", () => {
7676

7777
render(<ChatTextArea {...defaultProps} />)
7878

79-
const button = openPopoverAndGetLockToggle("chat:lockApiConfigAcrossModes")
79+
const button = openPopoverAndGetLockToggle()
8080
expect(button).toBeInTheDocument()
8181
// Unlocked state has muted opacity
82-
expect(button.className).toContain("opacity-60")
82+
expect(button.className).toContain("opacity-80")
8383
expect(button.className).not.toContain("text-vscode-focusBorder")
8484
})
8585

@@ -91,11 +91,11 @@ describe("ChatTextArea - lockApiConfigAcrossModes toggle", () => {
9191

9292
render(<ChatTextArea {...defaultProps} />)
9393

94-
const button = openPopoverAndGetLockToggle("chat:unlockApiConfigAcrossModes")
94+
const button = openPopoverAndGetLockToggle()
9595
expect(button).toBeInTheDocument()
9696
// Locked state has the focus border highlight color
9797
expect(button.className).toContain("text-vscode-focusBorder")
98-
expect(button.className).not.toContain("opacity-60")
98+
expect(button.className).toContain("opacity-100")
9999
})
100100

101101
it("renders in unlocked state when lockApiConfigAcrossModes is undefined (default)", () => {
@@ -105,10 +105,10 @@ describe("ChatTextArea - lockApiConfigAcrossModes toggle", () => {
105105

106106
render(<ChatTextArea {...defaultProps} />)
107107

108-
const button = openPopoverAndGetLockToggle("chat:lockApiConfigAcrossModes")
108+
const button = openPopoverAndGetLockToggle()
109109
expect(button).toBeInTheDocument()
110110
// Default (undefined/falsy) renders in unlocked style
111-
expect(button.className).toContain("opacity-60")
111+
expect(button.className).toContain("opacity-80")
112112
})
113113
})
114114

@@ -124,7 +124,7 @@ describe("ChatTextArea - lockApiConfigAcrossModes toggle", () => {
124124
// Clear any initialization messages
125125
mockPostMessage.mockClear()
126126

127-
const button = openPopoverAndGetLockToggle("chat:lockApiConfigAcrossModes")
127+
const button = openPopoverAndGetLockToggle()
128128
fireEvent.click(button)
129129

130130
expect(mockPostMessage).toHaveBeenCalledWith({
@@ -144,7 +144,7 @@ describe("ChatTextArea - lockApiConfigAcrossModes toggle", () => {
144144
// Clear any initialization messages
145145
mockPostMessage.mockClear()
146146

147-
const button = openPopoverAndGetLockToggle("chat:unlockApiConfigAcrossModes")
147+
const button = openPopoverAndGetLockToggle()
148148
fireEvent.click(button)
149149

150150
expect(mockPostMessage).toHaveBeenCalledWith({

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
TooltipProvider,
5959
TooltipTrigger,
6060
StandardTooltip,
61+
ToggleSwitch,
6162
} from "@src/components/ui"
6263

6364
import { Tab, TabContent, TabHeader, TabList, TabTrigger } from "../common/Tab"
@@ -203,6 +204,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
203204
includeCurrentTime,
204205
includeCurrentCost,
205206
maxGitStatusFiles,
207+
lockApiConfigAcrossModes,
206208
} = cachedState
207209

208210
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
@@ -430,6 +432,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
430432
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
431433
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
432434
vscode.postMessage({ type: "debugSetting", bool: cachedState.debug })
435+
vscode.postMessage({ type: "lockApiConfigAcrossModes", bool: !!lockApiConfigAcrossModes })
433436

434437
setChangeDetected(false)
435438
}
@@ -766,6 +769,27 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
766769
})
767770
}
768771
/>
772+
<div className="flex items-center justify-between gap-3 mt-4 mb-2 px-1">
773+
<div className="flex flex-col gap-0.5">
774+
<span className="text-sm text-vscode-foreground">
775+
{t("settings:providers.lockApiConfigAcrossModes")}
776+
</span>
777+
<span className="text-xs text-vscode-descriptionForeground">
778+
{t("settings:providers.lockApiConfigAcrossModesDescription")}
779+
</span>
780+
</div>
781+
<ToggleSwitch
782+
checked={!!lockApiConfigAcrossModes}
783+
onChange={() =>
784+
setCachedStateField(
785+
"lockApiConfigAcrossModes",
786+
!lockApiConfigAcrossModes,
787+
)
788+
}
789+
aria-label={t("settings:providers.lockApiConfigAcrossModes")}
790+
data-testid="lock-api-config-toggle"
791+
/>
792+
</div>
769793
<ApiOptions
770794
uriScheme={uriScheme}
771795
apiConfiguration={apiConfiguration}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@
142142
"selectApiConfig": "Select API configuration",
143143
"lockApiConfigAcrossModes": "Lock API configuration across all modes in this workspace",
144144
"unlockApiConfigAcrossModes": "API configuration is locked across all modes in this workspace (click to unlock)",
145+
"lockToggle": {
146+
"locked": "Locked across modes",
147+
"unlocked": "Lock across modes"
148+
},
145149
"enhancePrompt": "Enhance prompt with additional context",
146150
"modeSelector": {
147151
"title": "Modes",

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@
351351
"providerDocumentation": "{{provider}} documentation",
352352
"configProfile": "Configuration Profile",
353353
"description": "Save different API configurations to quickly switch between providers and settings.",
354+
"lockApiConfigAcrossModes": "Use the same configuration for all modes",
355+
"lockApiConfigAcrossModesDescription": "When enabled, switching modes (e.g. Code, Architect, Ask) keeps the current API configuration instead of switching to a mode-specific one.",
354356
"apiProvider": "API Provider",
355357
"apiProviderDocs": "Provider Docs",
356358
"model": "Model",

0 commit comments

Comments
 (0)