Skip to content

Commit f7b2952

Browse files
committed
feat: add "Apply to All Modes" button for API configuration
- Added new button in ApiConfigSelector dropdown to apply current config to all modes - Implemented confirmation dialog to prevent accidental changes - Added backend handler to apply configuration across all built-in and custom modes - Added comprehensive tests for the new functionality - Added all necessary translation keys Fixes #7898
1 parent 8fee312 commit f7b2952

File tree

6 files changed

+264
-95
lines changed

6 files changed

+264
-95
lines changed

src/core/webview/webviewMessageHandler.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3038,5 +3038,40 @@ export const webviewMessageHandler = async (
30383038
})
30393039
break
30403040
}
3041+
case "applyConfigToAllModes": {
3042+
if (message.configId) {
3043+
try {
3044+
// Get all available modes
3045+
const { modes } = await import("../../shared/modes")
3046+
3047+
// Apply the config to all modes
3048+
for (const mode of modes) {
3049+
await provider.providerSettingsManager.setModeConfig(mode.slug, message.configId)
3050+
}
3051+
3052+
// Also apply to custom modes
3053+
const customModes = await provider.customModesManager.getCustomModes()
3054+
for (const customMode of customModes) {
3055+
await provider.providerSettingsManager.setModeConfig(customMode.slug, message.configId)
3056+
}
3057+
3058+
// Update the global state with the new mode configs
3059+
const providerProfiles = await provider.providerSettingsManager.export()
3060+
await updateGlobalState("modeApiConfigs", providerProfiles.modeApiConfigs)
3061+
3062+
// Show success message
3063+
vscode.window.showInformationMessage(t("common:info.api_config_applied_to_all_modes"))
3064+
3065+
// Update the webview state
3066+
await provider.postStateToWebview()
3067+
} catch (error) {
3068+
provider.log(
3069+
`Error applying config to all modes: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
3070+
)
3071+
vscode.window.showErrorMessage(t("common:errors.apply_config_to_all_modes_failed"))
3072+
}
3073+
}
3074+
break
3075+
}
30413076
}
30423077
}

src/i18n/locales/en/common.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"delete_api_config": "Failed to delete api configuration",
4444
"list_api_config": "Failed to get list api configuration",
4545
"update_server_timeout": "Failed to update server timeout",
46+
"apply_config_to_all_modes_failed": "Failed to apply configuration to all modes",
4647
"hmr_not_running": "Local development server is not running, HMR will not work. Please run 'npm run dev' before launching the extension to enable HMR.",
4748
"retrieve_current_mode": "Error: failed to retrieve current mode from state.",
4849
"failed_delete_repo": "Failed to delete associated shadow repository or branch: {{error}}",
@@ -142,7 +143,8 @@
142143
"image_copied_to_clipboard": "Image data URI copied to clipboard",
143144
"image_saved": "Image saved to {{path}}",
144145
"mode_exported": "Mode '{{mode}}' exported successfully",
145-
"mode_imported": "Mode imported successfully"
146+
"mode_imported": "Mode imported successfully",
147+
"api_config_applied_to_all_modes": "API configuration applied to all modes successfully"
146148
},
147149
"answers": {
148150
"yes": "Yes",

src/shared/WebviewMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ export interface WebviewMessage {
225225
| "editQueuedMessage"
226226
| "dismissUpsell"
227227
| "getDismissedUpsells"
228+
| "applyConfigToAllModes"
228229
text?: string
229230
editedMessageContent?: string
230231
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
@@ -272,6 +273,7 @@ export interface WebviewMessage {
272273
checkOnly?: boolean // For deleteCustomMode check
273274
upsellId?: string // For dismissUpsell
274275
list?: string[] // For dismissedUpsells response
276+
configId?: string // For applyConfigToAllModes
275277
codeIndexSettings?: {
276278
// Global state settings
277279
codebaseIndexEnabled: boolean

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

Lines changed: 143 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/comp
88
import { useAppTranslation } from "@/i18n/TranslationContext"
99
import { vscode } from "@/utils/vscode"
1010
import { Button } from "@/components/ui"
11+
import {
12+
AlertDialog,
13+
AlertDialogAction,
14+
AlertDialogCancel,
15+
AlertDialogContent,
16+
AlertDialogDescription,
17+
AlertDialogFooter,
18+
AlertDialogHeader,
19+
AlertDialogTitle,
20+
} from "@/components/ui/alert-dialog"
1121

1222
import { IconButton } from "./IconButton"
1323

@@ -37,6 +47,7 @@ export const ApiConfigSelector = ({
3747
const { t } = useAppTranslation()
3848
const [open, setOpen] = useState(false)
3949
const [searchValue, setSearchValue] = useState("")
50+
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
4051
const portalContainer = useRooPortal("roo-portal")
4152

4253
// Create searchable items for fuzzy search.
@@ -86,6 +97,16 @@ export const ApiConfigSelector = ({
8697
setOpen(false)
8798
}, [])
8899

100+
const handleApplyToAllModes = useCallback(() => {
101+
setOpen(false)
102+
setShowConfirmDialog(true)
103+
}, [])
104+
105+
const handleConfirmApplyToAllModes = useCallback(() => {
106+
vscode.postMessage({ type: "applyConfigToAllModes", configId: value })
107+
setShowConfirmDialog(false)
108+
}, [value])
109+
89110
const renderConfigItem = useCallback(
90111
(config: { id: string; name: string; modelId?: string }, isPinned: boolean) => {
91112
const isCurrentConfig = config.id === value
@@ -143,110 +164,139 @@ export const ApiConfigSelector = ({
143164
)
144165

145166
return (
146-
<Popover open={open} onOpenChange={setOpen} data-testid="api-config-selector-root">
147-
<StandardTooltip content={title}>
148-
<PopoverTrigger
149-
disabled={disabled}
150-
data-testid="dropdown-trigger"
151-
className={cn(
152-
"w-full min-w-0 max-w-full inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
153-
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
154-
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
155-
disabled
156-
? "opacity-50 cursor-not-allowed"
157-
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
158-
triggerClassName,
159-
)}>
160-
<ChevronUp
167+
<>
168+
<Popover open={open} onOpenChange={setOpen} data-testid="api-config-selector-root">
169+
<StandardTooltip content={title}>
170+
<PopoverTrigger
171+
disabled={disabled}
172+
data-testid="dropdown-trigger"
161173
className={cn(
162-
"pointer-events-none opacity-80 flex-shrink-0 size-3 transition-transform duration-200",
163-
open && "rotate-180",
174+
"w-full min-w-0 max-w-full inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
175+
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
176+
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
177+
disabled
178+
? "opacity-50 cursor-not-allowed"
179+
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
180+
triggerClassName,
181+
)}>
182+
<ChevronUp
183+
className={cn(
184+
"pointer-events-none opacity-80 flex-shrink-0 size-3 transition-transform duration-200",
185+
open && "rotate-180",
186+
)}
187+
/>
188+
<span className="truncate">{displayName}</span>
189+
</PopoverTrigger>
190+
</StandardTooltip>
191+
<PopoverContent
192+
align="start"
193+
sideOffset={4}
194+
container={portalContainer}
195+
className="p-0 overflow-hidden w-[300px]">
196+
<div className="flex flex-col w-full">
197+
{/* Search input or info blurb */}
198+
{listApiConfigMeta.length > 6 ? (
199+
<div className="relative p-2 border-b border-vscode-dropdown-border">
200+
<input
201+
aria-label={t("common:ui.search_placeholder")}
202+
value={searchValue}
203+
onChange={(e) => setSearchValue(e.target.value)}
204+
placeholder={t("common:ui.search_placeholder")}
205+
className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0"
206+
autoFocus
207+
/>
208+
{searchValue.length > 0 && (
209+
<div className="absolute right-4 top-0 bottom-0 flex items-center justify-center">
210+
<span
211+
className="codicon codicon-close text-vscode-input-foreground opacity-50 hover:opacity-100 text-xs cursor-pointer"
212+
onClick={() => setSearchValue("")}
213+
/>
214+
</div>
215+
)}
216+
</div>
217+
) : (
218+
<div className="p-3 border-b border-vscode-dropdown-border">
219+
<p className="text-xs text-vscode-descriptionForeground m-0">
220+
{t("prompts:apiConfiguration.select")}
221+
</p>
222+
</div>
164223
)}
165-
/>
166-
<span className="truncate">{displayName}</span>
167-
</PopoverTrigger>
168-
</StandardTooltip>
169-
<PopoverContent
170-
align="start"
171-
sideOffset={4}
172-
container={portalContainer}
173-
className="p-0 overflow-hidden w-[300px]">
174-
<div className="flex flex-col w-full">
175-
{/* Search input or info blurb */}
176-
{listApiConfigMeta.length > 6 ? (
177-
<div className="relative p-2 border-b border-vscode-dropdown-border">
178-
<input
179-
aria-label={t("common:ui.search_placeholder")}
180-
value={searchValue}
181-
onChange={(e) => setSearchValue(e.target.value)}
182-
placeholder={t("common:ui.search_placeholder")}
183-
className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0"
184-
autoFocus
185-
/>
186-
{searchValue.length > 0 && (
187-
<div className="absolute right-4 top-0 bottom-0 flex items-center justify-center">
188-
<span
189-
className="codicon codicon-close text-vscode-input-foreground opacity-50 hover:opacity-100 text-xs cursor-pointer"
190-
onClick={() => setSearchValue("")}
191-
/>
224+
225+
{/* Config list */}
226+
<div className="max-h-[300px] overflow-y-auto">
227+
{filteredConfigs.length === 0 && searchValue ? (
228+
<div className="py-2 px-3 text-sm text-vscode-foreground/70">
229+
{t("common:ui.no_results")}
230+
</div>
231+
) : (
232+
<div className="py-1">
233+
{/* Pinned configs */}
234+
{pinnedConfigs.map((config) => renderConfigItem(config, true))}
235+
236+
{/* Separator between pinned and unpinned */}
237+
{pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 && (
238+
<div className="mx-1 my-1 h-px bg-vscode-dropdown-foreground/10" />
239+
)}
240+
241+
{/* Unpinned configs */}
242+
{unpinnedConfigs.map((config) => renderConfigItem(config, false))}
192243
</div>
193244
)}
194245
</div>
195-
) : (
196-
<div className="p-3 border-b border-vscode-dropdown-border">
197-
<p className="text-xs text-vscode-descriptionForeground m-0">
198-
{t("prompts:apiConfiguration.select")}
199-
</p>
200-
</div>
201-
)}
202246

203-
{/* Config list */}
204-
<div className="max-h-[300px] overflow-y-auto">
205-
{filteredConfigs.length === 0 && searchValue ? (
206-
<div className="py-2 px-3 text-sm text-vscode-foreground/70">
207-
{t("common:ui.no_results")}
247+
{/* Bottom bar with buttons on left and title on right */}
248+
<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
249+
<div className="flex flex-row gap-1">
250+
<IconButton
251+
iconClass="codicon-settings-gear"
252+
title={t("chat:edit")}
253+
onClick={handleEditClick}
254+
tooltip={false}
255+
/>
256+
<IconButton
257+
iconClass="codicon-layers"
258+
title={t("prompts:apiConfiguration.applyToAllModes")}
259+
onClick={handleApplyToAllModes}
260+
tooltip={false}
261+
/>
208262
</div>
209-
) : (
210-
<div className="py-1">
211-
{/* Pinned configs */}
212-
{pinnedConfigs.map((config) => renderConfigItem(config, true))}
213263

214-
{/* Separator between pinned and unpinned */}
215-
{pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 && (
216-
<div className="mx-1 my-1 h-px bg-vscode-dropdown-foreground/10" />
264+
{/* Info icon and title on the right with matching spacing */}
265+
<div className="flex items-center gap-1 pr-1">
266+
{listApiConfigMeta.length > 6 && (
267+
<StandardTooltip content={t("prompts:apiConfiguration.select")}>
268+
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" />
269+
</StandardTooltip>
217270
)}
218-
219-
{/* Unpinned configs */}
220-
{unpinnedConfigs.map((config) => renderConfigItem(config, false))}
271+
<h4 className="m-0 font-medium text-sm text-vscode-descriptionForeground">
272+
{t("prompts:apiConfiguration.title")}
273+
</h4>
221274
</div>
222-
)}
223-
</div>
224-
225-
{/* Bottom bar with buttons on left and title on right */}
226-
<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
227-
<div className="flex flex-row gap-1">
228-
<IconButton
229-
iconClass="codicon-settings-gear"
230-
title={t("chat:edit")}
231-
onClick={handleEditClick}
232-
tooltip={false}
233-
/>
234-
</div>
235-
236-
{/* Info icon and title on the right with matching spacing */}
237-
<div className="flex items-center gap-1 pr-1">
238-
{listApiConfigMeta.length > 6 && (
239-
<StandardTooltip content={t("prompts:apiConfiguration.select")}>
240-
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" />
241-
</StandardTooltip>
242-
)}
243-
<h4 className="m-0 font-medium text-sm text-vscode-descriptionForeground">
244-
{t("prompts:apiConfiguration.title")}
245-
</h4>
246275
</div>
247276
</div>
248-
</div>
249-
</PopoverContent>
250-
</Popover>
277+
</PopoverContent>
278+
</Popover>
279+
280+
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
281+
<AlertDialogContent>
282+
<AlertDialogHeader>
283+
<AlertDialogTitle>
284+
{t("prompts:apiConfiguration.confirmApplyToAllModes.title")}
285+
</AlertDialogTitle>
286+
<AlertDialogDescription>
287+
{t("prompts:apiConfiguration.confirmApplyToAllModes.description")}
288+
</AlertDialogDescription>
289+
</AlertDialogHeader>
290+
<AlertDialogFooter>
291+
<AlertDialogCancel>
292+
{t("prompts:apiConfiguration.confirmApplyToAllModes.cancel")}
293+
</AlertDialogCancel>
294+
<AlertDialogAction onClick={handleConfirmApplyToAllModes}>
295+
{t("prompts:apiConfiguration.confirmApplyToAllModes.confirm")}
296+
</AlertDialogAction>
297+
</AlertDialogFooter>
298+
</AlertDialogContent>
299+
</AlertDialog>
300+
</>
251301
)
252302
}

0 commit comments

Comments
 (0)