diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index a6e8e73a6a..53602ac69c 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -3038,5 +3038,40 @@ export const webviewMessageHandler = async ( }) break } + case "applyConfigToAllModes": { + if (message.configId) { + try { + // Get all available modes + const { modes } = await import("../../shared/modes") + + // Apply the config to all modes + for (const mode of modes) { + await provider.providerSettingsManager.setModeConfig(mode.slug, message.configId) + } + + // Also apply to custom modes + const customModes = await provider.customModesManager.getCustomModes() + for (const customMode of customModes) { + await provider.providerSettingsManager.setModeConfig(customMode.slug, message.configId) + } + + // Update the global state with the new mode configs + const providerProfiles = await provider.providerSettingsManager.export() + await updateGlobalState("modeApiConfigs", providerProfiles.modeApiConfigs) + + // Show success message + vscode.window.showInformationMessage(t("common:info.api_config_applied_to_all_modes")) + + // Update the webview state + await provider.postStateToWebview() + } catch (error) { + provider.log( + `Error applying config to all modes: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.apply_config_to_all_modes_failed")) + } + } + break + } } } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 18fe939442..69394caf67 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -43,6 +43,7 @@ "delete_api_config": "Failed to delete api configuration", "list_api_config": "Failed to get list api configuration", "update_server_timeout": "Failed to update server timeout", + "apply_config_to_all_modes_failed": "Failed to apply configuration to all modes", "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.", "retrieve_current_mode": "Error: failed to retrieve current mode from state.", "failed_delete_repo": "Failed to delete associated shadow repository or branch: {{error}}", @@ -142,7 +143,8 @@ "image_copied_to_clipboard": "Image data URI copied to clipboard", "image_saved": "Image saved to {{path}}", "mode_exported": "Mode '{{mode}}' exported successfully", - "mode_imported": "Mode imported successfully" + "mode_imported": "Mode imported successfully", + "api_config_applied_to_all_modes": "API configuration applied to all modes successfully" }, "answers": { "yes": "Yes", diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 93d0b9bc45..337e3cfe2a 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -225,6 +225,7 @@ export interface WebviewMessage { | "editQueuedMessage" | "dismissUpsell" | "getDismissedUpsells" + | "applyConfigToAllModes" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" @@ -272,6 +273,7 @@ export interface WebviewMessage { checkOnly?: boolean // For deleteCustomMode check upsellId?: string // For dismissUpsell list?: string[] // For dismissedUpsells response + configId?: string // For applyConfigToAllModes codeIndexSettings?: { // Global state settings codebaseIndexEnabled: boolean diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx index 6d3f458b34..3b4087af62 100644 --- a/webview-ui/src/components/chat/ApiConfigSelector.tsx +++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx @@ -8,6 +8,16 @@ import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/comp import { useAppTranslation } from "@/i18n/TranslationContext" import { vscode } from "@/utils/vscode" import { Button } from "@/components/ui" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" import { IconButton } from "./IconButton" @@ -37,6 +47,7 @@ export const ApiConfigSelector = ({ const { t } = useAppTranslation() const [open, setOpen] = useState(false) const [searchValue, setSearchValue] = useState("") + const [showConfirmDialog, setShowConfirmDialog] = useState(false) const portalContainer = useRooPortal("roo-portal") // Create searchable items for fuzzy search. @@ -86,6 +97,16 @@ export const ApiConfigSelector = ({ setOpen(false) }, []) + const handleApplyToAllModes = useCallback(() => { + setOpen(false) + setShowConfirmDialog(true) + }, []) + + const handleConfirmApplyToAllModes = useCallback(() => { + vscode.postMessage({ type: "applyConfigToAllModes", configId: value }) + setShowConfirmDialog(false) + }, [value]) + const renderConfigItem = useCallback( (config: { id: string; name: string; modelId?: string }, isPinned: boolean) => { const isCurrentConfig = config.id === value @@ -143,110 +164,139 @@ export const ApiConfigSelector = ({ ) return ( - - - - + + + + + {displayName} + + + +
+ {/* Search input or info blurb */} + {listApiConfigMeta.length > 6 ? ( +
+ setSearchValue(e.target.value)} + placeholder={t("common:ui.search_placeholder")} + 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" + autoFocus + /> + {searchValue.length > 0 && ( +
+ setSearchValue("")} + /> +
+ )} +
+ ) : ( +
+

+ {t("prompts:apiConfiguration.select")} +

+
)} - /> - {displayName} - - - -
- {/* Search input or info blurb */} - {listApiConfigMeta.length > 6 ? ( -
- setSearchValue(e.target.value)} - placeholder={t("common:ui.search_placeholder")} - 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" - autoFocus - /> - {searchValue.length > 0 && ( -
- setSearchValue("")} - /> + + {/* Config list */} +
+ {filteredConfigs.length === 0 && searchValue ? ( +
+ {t("common:ui.no_results")} +
+ ) : ( +
+ {/* Pinned configs */} + {pinnedConfigs.map((config) => renderConfigItem(config, true))} + + {/* Separator between pinned and unpinned */} + {pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 && ( +
+ )} + + {/* Unpinned configs */} + {unpinnedConfigs.map((config) => renderConfigItem(config, false))}
)}
- ) : ( -
-

- {t("prompts:apiConfiguration.select")} -

-
- )} - {/* Config list */} -
- {filteredConfigs.length === 0 && searchValue ? ( -
- {t("common:ui.no_results")} + {/* Bottom bar with buttons on left and title on right */} +
+
+ +
- ) : ( -
- {/* Pinned configs */} - {pinnedConfigs.map((config) => renderConfigItem(config, true))} - {/* Separator between pinned and unpinned */} - {pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 && ( -
+ {/* Info icon and title on the right with matching spacing */} +
+ {listApiConfigMeta.length > 6 && ( + + + )} - - {/* Unpinned configs */} - {unpinnedConfigs.map((config) => renderConfigItem(config, false))} +

+ {t("prompts:apiConfiguration.title")} +

- )} -
- - {/* Bottom bar with buttons on left and title on right */} -
-
- -
- - {/* Info icon and title on the right with matching spacing */} -
- {listApiConfigMeta.length > 6 && ( - - - - )} -

- {t("prompts:apiConfiguration.title")} -

-
- - + + + + + + + + {t("prompts:apiConfiguration.confirmApplyToAllModes.title")} + + + {t("prompts:apiConfiguration.confirmApplyToAllModes.description")} + + + + + {t("prompts:apiConfiguration.confirmApplyToAllModes.cancel")} + + + {t("prompts:apiConfiguration.confirmApplyToAllModes.confirm")} + + + + + ) } diff --git a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx index bac66255e4..289ccc8916 100644 --- a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx @@ -54,6 +54,14 @@ vi.mock("@/components/ui", () => ({ {children} ), + AlertDialog: ({ children, open }: any) => (open ?
{children}
: null), + AlertDialogContent: ({ children }: any) =>
{children}
, + AlertDialogHeader: ({ children }: any) =>
{children}
, + AlertDialogTitle: ({ children }: any) =>

{children}

, + AlertDialogDescription: ({ children }: any) =>

{children}

, + AlertDialogFooter: ({ children }: any) =>
{children}
, + AlertDialogCancel: ({ children, onClick }: any) => , + AlertDialogAction: ({ children, onClick }: any) => , })) describe("ApiConfigSelector", () => { @@ -444,4 +452,69 @@ describe("ApiConfigSelector", () => { // Search value should be maintained expect(searchInput.value).toBe("Config") }) + + test("shows Apply to All Modes button and handles click with confirmation", async () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + // Find the Apply to All Modes button by its aria-label + const popoverContent = screen.getByTestId("popover-content") + const applyToAllButton = popoverContent.querySelector('[aria-label="prompts:apiConfiguration.applyToAllModes"]') + expect(applyToAllButton).toBeInTheDocument() + + // Click the button + if (applyToAllButton) { + fireEvent.click(applyToAllButton) + } + + // Check that confirmation dialog appears + await waitFor(() => { + expect(screen.getByText("prompts:apiConfiguration.confirmApplyToAllModes.title")).toBeInTheDocument() + expect(screen.getByText("prompts:apiConfiguration.confirmApplyToAllModes.description")).toBeInTheDocument() + }) + + // Find and click the confirm button + const confirmButton = screen.getByText("prompts:apiConfiguration.confirmApplyToAllModes.confirm") + fireEvent.click(confirmButton) + + // Verify the message was posted + await waitFor(() => { + expect(vi.mocked(vscode.postMessage)).toHaveBeenCalledWith({ + type: "applyConfigToAllModes", + configId: "config1", + }) + }) + }) + + test("cancels Apply to All Modes when cancel is clicked in confirmation dialog", async () => { + render() + + const trigger = screen.getByTestId("dropdown-trigger") + fireEvent.click(trigger) + + // Find and click the Apply to All Modes button by its aria-label + const popoverContent = screen.getByTestId("popover-content") + const applyToAllButton = popoverContent.querySelector('[aria-label="prompts:apiConfiguration.applyToAllModes"]') + + if (applyToAllButton) { + fireEvent.click(applyToAllButton) + } + + // Wait for confirmation dialog + await waitFor(() => { + expect(screen.getByText("prompts:apiConfiguration.confirmApplyToAllModes.title")).toBeInTheDocument() + }) + + // Find and click the cancel button + const cancelButton = screen.getByText("prompts:apiConfiguration.confirmApplyToAllModes.cancel") + fireEvent.click(cancelButton) + + // Verify the message was NOT posted + expect(vi.mocked(vscode.postMessage)).not.toHaveBeenCalledWith({ + type: "applyConfigToAllModes", + configId: "config1", + }) + }) }) diff --git a/webview-ui/src/i18n/locales/en/prompts.json b/webview-ui/src/i18n/locales/en/prompts.json index 19837c6b2c..8fa96d1340 100644 --- a/webview-ui/src/i18n/locales/en/prompts.json +++ b/webview-ui/src/i18n/locales/en/prompts.json @@ -14,7 +14,14 @@ }, "apiConfiguration": { "title": "API Configuration", - "select": "Select which API configuration to use for this mode" + "select": "Select which API configuration to use for this mode", + "applyToAllModes": "Apply to all modes", + "confirmApplyToAllModes": { + "title": "Apply Configuration to All Modes", + "description": "This will apply the currently selected API configuration to all modes (built-in and custom). Are you sure you want to continue?", + "confirm": "Apply to All", + "cancel": "Cancel" + } }, "tools": { "title": "Available Tools",