mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
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
This commit is contained in:
parent
8fee3127ff
commit
f7b2952bb1
6 changed files with 264 additions and 95 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Popover open={open} onOpenChange={setOpen} data-testid="api-config-selector-root">
|
||||
<StandardTooltip content={title}>
|
||||
<PopoverTrigger
|
||||
disabled={disabled}
|
||||
data-testid="dropdown-trigger"
|
||||
className={cn(
|
||||
"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",
|
||||
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
|
||||
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
|
||||
disabled
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
|
||||
triggerClassName,
|
||||
)}>
|
||||
<ChevronUp
|
||||
<>
|
||||
<Popover open={open} onOpenChange={setOpen} data-testid="api-config-selector-root">
|
||||
<StandardTooltip content={title}>
|
||||
<PopoverTrigger
|
||||
disabled={disabled}
|
||||
data-testid="dropdown-trigger"
|
||||
className={cn(
|
||||
"pointer-events-none opacity-80 flex-shrink-0 size-3 transition-transform duration-200",
|
||||
open && "rotate-180",
|
||||
"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",
|
||||
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
|
||||
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
|
||||
disabled
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
|
||||
triggerClassName,
|
||||
)}>
|
||||
<ChevronUp
|
||||
className={cn(
|
||||
"pointer-events-none opacity-80 flex-shrink-0 size-3 transition-transform duration-200",
|
||||
open && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">{displayName}</span>
|
||||
</PopoverTrigger>
|
||||
</StandardTooltip>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
container={portalContainer}
|
||||
className="p-0 overflow-hidden w-[300px]">
|
||||
<div className="flex flex-col w-full">
|
||||
{/* Search input or info blurb */}
|
||||
{listApiConfigMeta.length > 6 ? (
|
||||
<div className="relative p-2 border-b border-vscode-dropdown-border">
|
||||
<input
|
||||
aria-label={t("common:ui.search_placeholder")}
|
||||
value={searchValue}
|
||||
onChange={(e) => 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 && (
|
||||
<div className="absolute right-4 top-0 bottom-0 flex items-center justify-center">
|
||||
<span
|
||||
className="codicon codicon-close text-vscode-input-foreground opacity-50 hover:opacity-100 text-xs cursor-pointer"
|
||||
onClick={() => setSearchValue("")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-3 border-b border-vscode-dropdown-border">
|
||||
<p className="text-xs text-vscode-descriptionForeground m-0">
|
||||
{t("prompts:apiConfiguration.select")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">{displayName}</span>
|
||||
</PopoverTrigger>
|
||||
</StandardTooltip>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
container={portalContainer}
|
||||
className="p-0 overflow-hidden w-[300px]">
|
||||
<div className="flex flex-col w-full">
|
||||
{/* Search input or info blurb */}
|
||||
{listApiConfigMeta.length > 6 ? (
|
||||
<div className="relative p-2 border-b border-vscode-dropdown-border">
|
||||
<input
|
||||
aria-label={t("common:ui.search_placeholder")}
|
||||
value={searchValue}
|
||||
onChange={(e) => 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 && (
|
||||
<div className="absolute right-4 top-0 bottom-0 flex items-center justify-center">
|
||||
<span
|
||||
className="codicon codicon-close text-vscode-input-foreground opacity-50 hover:opacity-100 text-xs cursor-pointer"
|
||||
onClick={() => setSearchValue("")}
|
||||
/>
|
||||
|
||||
{/* Config list */}
|
||||
<div className="max-h-[300px] overflow-y-auto">
|
||||
{filteredConfigs.length === 0 && searchValue ? (
|
||||
<div className="py-2 px-3 text-sm text-vscode-foreground/70">
|
||||
{t("common:ui.no_results")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1">
|
||||
{/* Pinned configs */}
|
||||
{pinnedConfigs.map((config) => renderConfigItem(config, true))}
|
||||
|
||||
{/* Separator between pinned and unpinned */}
|
||||
{pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 && (
|
||||
<div className="mx-1 my-1 h-px bg-vscode-dropdown-foreground/10" />
|
||||
)}
|
||||
|
||||
{/* Unpinned configs */}
|
||||
{unpinnedConfigs.map((config) => renderConfigItem(config, false))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-3 border-b border-vscode-dropdown-border">
|
||||
<p className="text-xs text-vscode-descriptionForeground m-0">
|
||||
{t("prompts:apiConfiguration.select")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Config list */}
|
||||
<div className="max-h-[300px] overflow-y-auto">
|
||||
{filteredConfigs.length === 0 && searchValue ? (
|
||||
<div className="py-2 px-3 text-sm text-vscode-foreground/70">
|
||||
{t("common:ui.no_results")}
|
||||
{/* Bottom bar with buttons on left and title on right */}
|
||||
<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
|
||||
<div className="flex flex-row gap-1">
|
||||
<IconButton
|
||||
iconClass="codicon-settings-gear"
|
||||
title={t("chat:edit")}
|
||||
onClick={handleEditClick}
|
||||
tooltip={false}
|
||||
/>
|
||||
<IconButton
|
||||
iconClass="codicon-layers"
|
||||
title={t("prompts:apiConfiguration.applyToAllModes")}
|
||||
onClick={handleApplyToAllModes}
|
||||
tooltip={false}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1">
|
||||
{/* Pinned configs */}
|
||||
{pinnedConfigs.map((config) => renderConfigItem(config, true))}
|
||||
|
||||
{/* Separator between pinned and unpinned */}
|
||||
{pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 && (
|
||||
<div className="mx-1 my-1 h-px bg-vscode-dropdown-foreground/10" />
|
||||
{/* Info icon and title on the right with matching spacing */}
|
||||
<div className="flex items-center gap-1 pr-1">
|
||||
{listApiConfigMeta.length > 6 && (
|
||||
<StandardTooltip content={t("prompts:apiConfiguration.select")}>
|
||||
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" />
|
||||
</StandardTooltip>
|
||||
)}
|
||||
|
||||
{/* Unpinned configs */}
|
||||
{unpinnedConfigs.map((config) => renderConfigItem(config, false))}
|
||||
<h4 className="m-0 font-medium text-sm text-vscode-descriptionForeground">
|
||||
{t("prompts:apiConfiguration.title")}
|
||||
</h4>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom bar with buttons on left and title on right */}
|
||||
<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
|
||||
<div className="flex flex-row gap-1">
|
||||
<IconButton
|
||||
iconClass="codicon-settings-gear"
|
||||
title={t("chat:edit")}
|
||||
onClick={handleEditClick}
|
||||
tooltip={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Info icon and title on the right with matching spacing */}
|
||||
<div className="flex items-center gap-1 pr-1">
|
||||
{listApiConfigMeta.length > 6 && (
|
||||
<StandardTooltip content={t("prompts:apiConfiguration.select")}>
|
||||
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" />
|
||||
</StandardTooltip>
|
||||
)}
|
||||
<h4 className="m-0 font-medium text-sm text-vscode-descriptionForeground">
|
||||
{t("prompts:apiConfiguration.title")}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("prompts:apiConfiguration.confirmApplyToAllModes.title")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("prompts:apiConfiguration.confirmApplyToAllModes.description")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t("prompts:apiConfiguration.confirmApplyToAllModes.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmApplyToAllModes}>
|
||||
{t("prompts:apiConfiguration.confirmApplyToAllModes.confirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@ vi.mock("@/components/ui", () => ({
|
|||
{children}
|
||||
</button>
|
||||
),
|
||||
AlertDialog: ({ children, open }: any) => (open ? <div data-testid="alert-dialog">{children}</div> : null),
|
||||
AlertDialogContent: ({ children }: any) => <div data-testid="alert-dialog-content">{children}</div>,
|
||||
AlertDialogHeader: ({ children }: any) => <div>{children}</div>,
|
||||
AlertDialogTitle: ({ children }: any) => <h2>{children}</h2>,
|
||||
AlertDialogDescription: ({ children }: any) => <p>{children}</p>,
|
||||
AlertDialogFooter: ({ children }: any) => <div>{children}</div>,
|
||||
AlertDialogCancel: ({ children, onClick }: any) => <button onClick={onClick}>{children}</button>,
|
||||
AlertDialogAction: ({ children, onClick }: any) => <button onClick={onClick}>{children}</button>,
|
||||
}))
|
||||
|
||||
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(<ApiConfigSelector {...defaultProps} />)
|
||||
|
||||
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(<ApiConfigSelector {...defaultProps} />)
|
||||
|
||||
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",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue