mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-23 00:43:14 +00:00
Merge c7d1230913 into e921f9d21e
This commit is contained in:
commit
b315ebb4ca
3 changed files with 382 additions and 0 deletions
|
|
@ -38,6 +38,8 @@ import {
|
|||
ImageGenerationProvider,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import { cn } from "@src/lib/utils"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
|
|
@ -201,6 +203,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
includeCurrentTime,
|
||||
includeCurrentCost,
|
||||
maxGitStatusFiles,
|
||||
lockApiConfigAcrossModes,
|
||||
} = cachedState
|
||||
|
||||
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
|
||||
|
|
@ -416,6 +419,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
// by the `updateSettings` message.
|
||||
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
|
||||
vscode.postMessage({ type: "debugSetting", bool: cachedState.debug })
|
||||
vscode.postMessage({ type: "lockApiConfigAcrossModes", bool: lockApiConfigAcrossModes ?? false })
|
||||
|
||||
setChangeDetected(false)
|
||||
}
|
||||
|
|
@ -752,6 +756,21 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
})
|
||||
}
|
||||
/>
|
||||
<div className="mt-2 mb-3">
|
||||
<VSCodeCheckbox
|
||||
checked={lockApiConfigAcrossModes ?? false}
|
||||
onChange={(e: any) =>
|
||||
setCachedStateField("lockApiConfigAcrossModes", e.target.checked)
|
||||
}
|
||||
data-testid="lock-api-config-across-modes-checkbox">
|
||||
<span className="font-medium">
|
||||
{t("settings:providers.lockApiConfigAcrossModes")}
|
||||
</span>
|
||||
</VSCodeCheckbox>
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1 ml-5">
|
||||
{t("settings:providers.lockApiConfigAcrossModesDescription")}
|
||||
</div>
|
||||
</div>
|
||||
<ApiOptions
|
||||
uriScheme={uriScheme}
|
||||
apiConfiguration={apiConfiguration}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,361 @@
|
|||
// pnpm --filter @roo-code/vscode-webview test src/components/settings/__tests__/SettingsView.lockApiConfig.spec.tsx
|
||||
|
||||
import { render, screen, fireEvent, within } from "@/utils/test-utils"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext"
|
||||
|
||||
import SettingsView from "../SettingsView"
|
||||
|
||||
vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: vi.fn() } }))
|
||||
|
||||
vi.mock("../ApiConfigManager", () => ({
|
||||
__esModule: true,
|
||||
default: ({ currentApiConfigName }: any) => (
|
||||
<div data-testid="api-config-management">
|
||||
<span>Current config: {currentApiConfigName}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
|
||||
VSCodeButton: ({ children, onClick, appearance, "data-testid": dataTestId }: any) =>
|
||||
appearance === "icon" ? (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="codicon codicon-close"
|
||||
aria-label="Remove command"
|
||||
data-testid={dataTestId}>
|
||||
<span className="codicon codicon-close" />
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={onClick} data-appearance={appearance} data-testid={dataTestId}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
VSCodeCheckbox: ({ children, onChange, checked, "data-testid": dataTestId }: any) => (
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(e) => onChange({ target: { checked: e.target.checked } })}
|
||||
aria-label={typeof children === "string" ? children : undefined}
|
||||
data-testid={dataTestId}
|
||||
/>
|
||||
{children}
|
||||
</label>
|
||||
),
|
||||
VSCodeTextField: ({ value, onInput, placeholder, "data-testid": dataTestId }: any) => (
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onInput?.({ target: { value: e.target.value } })}
|
||||
placeholder={placeholder}
|
||||
data-testid={dataTestId}
|
||||
/>
|
||||
),
|
||||
VSCodeLink: ({ children, href }: any) => <a href={href || "#"}>{children}</a>,
|
||||
VSCodeRadio: ({ value, checked, onChange }: any) => (
|
||||
<input type="radio" value={value} checked={checked} onChange={onChange} />
|
||||
),
|
||||
VSCodeRadioGroup: ({ children, onChange }: any) => <div onChange={onChange}>{children}</div>,
|
||||
VSCodeTextArea: ({ value, onChange, rows, className, "data-testid": dataTestId }: any) => (
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
rows={rows}
|
||||
className={className}
|
||||
data-testid={dataTestId}
|
||||
role="textbox"
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("../../../components/common/Tab", () => ({
|
||||
...vi.importActual("../../../components/common/Tab"),
|
||||
Tab: ({ children }: any) => <div data-testid="tab-container">{children}</div>,
|
||||
TabHeader: ({ children }: any) => <div data-testid="tab-header">{children}</div>,
|
||||
TabContent: ({ children, "data-testid": dataTestId }: any) => (
|
||||
<div data-testid={dataTestId || "tab-content"}>{children}</div>
|
||||
),
|
||||
TabList: ({ children, value, onValueChange, "data-testid": dataTestId }: any) => {
|
||||
;(window as any).__onValueChange = onValueChange
|
||||
return (
|
||||
<div data-testid={dataTestId} data-value={value}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
TabTrigger: ({ children, value, "data-testid": dataTestId, onClick, isSelected }: any) => {
|
||||
const handleClick = () => {
|
||||
if (onClick) onClick()
|
||||
const onValueChange = (window as any).__onValueChange
|
||||
if (onValueChange) onValueChange(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<button data-testid={dataTestId} data-value={value} data-selected={isSelected} onClick={handleClick}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@/components/ui", async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual as any),
|
||||
ToggleSwitch: ({ checked, onChange, "aria-label": ariaLabel, "data-testid": dataTestId }: any) => (
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={dataTestId}
|
||||
onClick={onChange}>
|
||||
Toggle
|
||||
</button>
|
||||
),
|
||||
Checkbox: ({ checked, onCheckedChange, id, className, ...props }: any) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(e) => onCheckedChange?.(e.target.checked)}
|
||||
id={id}
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
Textarea: ({ value, onChange, placeholder, id, className, ...props }: any) => (
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
id={id}
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
StandardTooltip: ({ children }: any) => <>{children}</>,
|
||||
Tooltip: ({ children }: any) => <>{children}</>,
|
||||
TooltipContent: ({ children }: any) => <>{children}</>,
|
||||
TooltipProvider: ({ children }: any) => <>{children}</>,
|
||||
TooltipTrigger: ({ children }: any) => <>{children}</>,
|
||||
Input: ({ value, onChange, placeholder, "data-testid": dataTestId }: any) => (
|
||||
<input type="text" value={value} onChange={onChange} placeholder={placeholder} data-testid={dataTestId} />
|
||||
),
|
||||
Button: ({ children, onClick, ...props }: any) => (
|
||||
<button onClick={onClick} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
SearchableSelect: ({ value, onValueChange, options, placeholder }: any) => (
|
||||
<select value={value} onChange={(e) => onValueChange(e.target.value)} data-testid="searchable-select">
|
||||
{placeholder && <option value="">{placeholder}</option>}
|
||||
{options?.map((opt: any) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
),
|
||||
AlertDialog: ({ children, open }: any) => (
|
||||
<div data-testid="alert-dialog" data-open={open}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
AlertDialogContent: ({ children }: any) => <div data-testid="alert-dialog-content">{children}</div>,
|
||||
AlertDialogHeader: ({ children }: any) => <div data-testid="alert-dialog-header">{children}</div>,
|
||||
AlertDialogTitle: ({ children }: any) => <div data-testid="alert-dialog-title">{children}</div>,
|
||||
AlertDialogDescription: ({ children }: any) => <div data-testid="alert-dialog-description">{children}</div>,
|
||||
AlertDialogFooter: ({ children }: any) => <div data-testid="alert-dialog-footer">{children}</div>,
|
||||
AlertDialogAction: ({ children, onClick }: any) => (
|
||||
<button data-testid="alert-dialog-action" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
AlertDialogCancel: ({ children, onClick }: any) => (
|
||||
<button data-testid="alert-dialog-cancel" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
Collapsible: ({ children, open }: any) => (
|
||||
<div className="collapsible-mock" data-open={open}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CollapsibleTrigger: ({ children, className, onClick }: any) => (
|
||||
<div className={`collapsible-trigger-mock ${className || ""}`} onClick={onClick}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
CollapsibleContent: ({ children, className }: any) => (
|
||||
<div className={`collapsible-content-mock ${className || ""}`}>{children}</div>
|
||||
),
|
||||
Dialog: ({ children, ...props }: any) => (
|
||||
<div data-testid="dialog" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogContent: ({ children, ...props }: any) => (
|
||||
<div data-testid="dialog-content" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogHeader: ({ children, ...props }: any) => (
|
||||
<div data-testid="dialog-header" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogTitle: ({ children, ...props }: any) => (
|
||||
<div data-testid="dialog-title" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogDescription: ({ children, ...props }: any) => (
|
||||
<div data-testid="dialog-description" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogFooter: ({ children, ...props }: any) => (
|
||||
<div data-testid="dialog-footer" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("vscrui", () => ({
|
||||
Checkbox: ({ children, checked, onChange }: any) => (
|
||||
<label>
|
||||
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
||||
{children}
|
||||
</label>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock("../ApiOptions", () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="api-options">ApiOptions</div>,
|
||||
}))
|
||||
|
||||
const mockPostMessage = (state: any) => {
|
||||
window.postMessage(
|
||||
{
|
||||
type: "state",
|
||||
state: {
|
||||
version: "1.0.0",
|
||||
clineMessages: [],
|
||||
taskHistory: [],
|
||||
shouldShowAnnouncement: false,
|
||||
allowedCommands: [],
|
||||
alwaysAllowExecute: false,
|
||||
ttsEnabled: false,
|
||||
ttsSpeed: 1,
|
||||
soundEnabled: false,
|
||||
soundVolume: 0.5,
|
||||
lockApiConfigAcrossModes: false,
|
||||
...state,
|
||||
},
|
||||
},
|
||||
"*",
|
||||
)
|
||||
}
|
||||
|
||||
const renderSettingsView = (initialState: any = {}) => {
|
||||
const onDone = vi.fn()
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const result = render(
|
||||
<ExtensionStateContextProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SettingsView onDone={onDone} targetSection="providers" />
|
||||
</QueryClientProvider>
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
|
||||
// Hydrate initial state
|
||||
mockPostMessage(initialState)
|
||||
|
||||
const getSettingsContent = () => screen.getByTestId("settings-content")
|
||||
|
||||
return { onDone, getSettingsContent, result, queryClient }
|
||||
}
|
||||
|
||||
describe("SettingsView - Lock API Config Across Modes", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("renders the lock API config checkbox in the providers tab", () => {
|
||||
const { getSettingsContent } = renderSettingsView()
|
||||
const content = getSettingsContent()
|
||||
const checkbox = within(content).getByTestId("lock-api-config-across-modes-checkbox")
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("initializes with lockApiConfigAcrossModes unchecked by default", () => {
|
||||
const { getSettingsContent } = renderSettingsView()
|
||||
const content = getSettingsContent()
|
||||
const checkbox = within(content).getByTestId("lock-api-config-across-modes-checkbox")
|
||||
expect(checkbox).not.toBeChecked()
|
||||
})
|
||||
|
||||
it("toggles the checkbox when clicked", () => {
|
||||
const { getSettingsContent } = renderSettingsView()
|
||||
const content = getSettingsContent()
|
||||
const checkbox = within(content).getByTestId("lock-api-config-across-modes-checkbox")
|
||||
|
||||
// Initially unchecked
|
||||
expect(checkbox).not.toBeChecked()
|
||||
|
||||
// Click to enable
|
||||
fireEvent.click(checkbox)
|
||||
expect(checkbox).toBeChecked()
|
||||
|
||||
// Click to disable
|
||||
fireEvent.click(checkbox)
|
||||
expect(checkbox).not.toBeChecked()
|
||||
})
|
||||
|
||||
it("sends lockApiConfigAcrossModes=true message when saving with checkbox enabled", () => {
|
||||
const { getSettingsContent } = renderSettingsView()
|
||||
const content = getSettingsContent()
|
||||
|
||||
// Enable the lock checkbox
|
||||
const checkbox = within(content).getByTestId("lock-api-config-across-modes-checkbox")
|
||||
fireEvent.click(checkbox)
|
||||
|
||||
// Click Save
|
||||
const saveButton = screen.getByTestId("save-button")
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "lockApiConfigAcrossModes",
|
||||
bool: true,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("sends lockApiConfigAcrossModes=false message when saving with checkbox disabled", () => {
|
||||
const { getSettingsContent } = renderSettingsView()
|
||||
const content = getSettingsContent()
|
||||
|
||||
// Enable then disable the lock checkbox
|
||||
const checkbox = within(content).getByTestId("lock-api-config-across-modes-checkbox")
|
||||
fireEvent.click(checkbox) // enable
|
||||
fireEvent.click(checkbox) // disable
|
||||
|
||||
// Click Save
|
||||
const saveButton = screen.getByTestId("save-button")
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "lockApiConfigAcrossModes",
|
||||
bool: false,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -345,6 +345,8 @@
|
|||
"providerDocumentation": "{{provider}} documentation",
|
||||
"configProfile": "Configuration Profile",
|
||||
"description": "Save different API configurations to quickly switch between providers and settings.",
|
||||
"lockApiConfigAcrossModes": "Use the same API configuration for all modes",
|
||||
"lockApiConfigAcrossModesDescription": "When enabled, switching modes (e.g. Ask, Code, Architect) will not change the selected provider or model. The lock icon in the API Configuration popover does the same thing.",
|
||||
"apiProvider": "API Provider",
|
||||
"apiProviderDocs": "Provider Docs",
|
||||
"model": "Model",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue