mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-22 13:21:07 -05:00
merge: resolve conflicts after upstream merge
This commit is contained in:
225
webview-ui/src/components/settings/ApiConfigManager.tsx
Normal file
225
webview-ui/src/components/settings/ApiConfigManager.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, useEffect, useRef, useState } from "react"
|
||||
import { ApiConfigMeta } from "../../../../src/shared/ExtensionMessage"
|
||||
|
||||
interface ApiConfigManagerProps {
|
||||
currentApiConfigName?: string
|
||||
listApiConfigMeta?: ApiConfigMeta[]
|
||||
onSelectConfig: (configName: string) => void
|
||||
onDeleteConfig: (configName: string) => void
|
||||
onRenameConfig: (oldName: string, newName: string) => void
|
||||
onUpsertConfig: (configName: string) => void
|
||||
}
|
||||
|
||||
const ApiConfigManager = ({
|
||||
currentApiConfigName = "",
|
||||
listApiConfigMeta = [],
|
||||
onSelectConfig,
|
||||
onDeleteConfig,
|
||||
onRenameConfig,
|
||||
onUpsertConfig,
|
||||
}: ApiConfigManagerProps) => {
|
||||
const [editState, setEditState] = useState<'new' | 'rename' | null>(null);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
|
||||
// Focus input when entering edit mode
|
||||
useEffect(() => {
|
||||
if (editState) {
|
||||
setTimeout(() => inputRef.current?.focus(), 0);
|
||||
}
|
||||
}, [editState]);
|
||||
|
||||
// Reset edit state when current profile changes
|
||||
useEffect(() => {
|
||||
setEditState(null);
|
||||
setInputValue("");
|
||||
}, [currentApiConfigName]);
|
||||
|
||||
const handleAdd = () => {
|
||||
const newConfigName = currentApiConfigName + " (copy)";
|
||||
onUpsertConfig(newConfigName);
|
||||
};
|
||||
|
||||
const handleStartRename = () => {
|
||||
setEditState('rename');
|
||||
setInputValue(currentApiConfigName || "");
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditState(null);
|
||||
setInputValue("");
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const trimmedValue = inputValue.trim();
|
||||
if (!trimmedValue) return;
|
||||
|
||||
if (editState === 'new') {
|
||||
onUpsertConfig(trimmedValue);
|
||||
} else if (editState === 'rename' && currentApiConfigName) {
|
||||
onRenameConfig(currentApiConfigName, trimmedValue);
|
||||
}
|
||||
|
||||
setEditState(null);
|
||||
setInputValue("");
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!currentApiConfigName || !listApiConfigMeta || listApiConfigMeta.length <= 1) return;
|
||||
|
||||
// Let the extension handle both deletion and selection
|
||||
onDeleteConfig(currentApiConfigName);
|
||||
};
|
||||
|
||||
const isOnlyProfile = listApiConfigMeta?.length === 1;
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "2px"
|
||||
}}>
|
||||
<label htmlFor="config-profile">
|
||||
<span style={{ fontWeight: "500" }}>Configuration Profile</span>
|
||||
</label>
|
||||
|
||||
{editState ? (
|
||||
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
|
||||
<VSCodeTextField
|
||||
ref={inputRef as any}
|
||||
value={inputValue}
|
||||
onInput={(e: any) => setInputValue(e.target.value)}
|
||||
placeholder={editState === 'new' ? "Enter profile name" : "Enter new name"}
|
||||
style={{ flexGrow: 1 }}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === 'Enter' && inputValue.trim()) {
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
handleCancel();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
disabled={!inputValue.trim()}
|
||||
onClick={handleSave}
|
||||
title="Save"
|
||||
style={{
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
height: '28px',
|
||||
width: '28px',
|
||||
minWidth: '28px'
|
||||
}}
|
||||
>
|
||||
<span className="codicon codicon-check" />
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={handleCancel}
|
||||
title="Cancel"
|
||||
style={{
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
height: '28px',
|
||||
width: '28px',
|
||||
minWidth: '28px'
|
||||
}}
|
||||
>
|
||||
<span className="codicon codicon-close" />
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
|
||||
<select
|
||||
id="config-profile"
|
||||
value={currentApiConfigName}
|
||||
onChange={(e) => onSelectConfig(e.target.value)}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
padding: "4px 8px",
|
||||
paddingRight: "24px",
|
||||
backgroundColor: "var(--vscode-dropdown-background)",
|
||||
color: "var(--vscode-dropdown-foreground)",
|
||||
border: "1px solid var(--vscode-dropdown-border)",
|
||||
borderRadius: "2px",
|
||||
height: "28px",
|
||||
cursor: "pointer",
|
||||
outline: "none"
|
||||
}}
|
||||
>
|
||||
{listApiConfigMeta?.map((config) => (
|
||||
<option
|
||||
key={config.name}
|
||||
value={config.name}
|
||||
>
|
||||
{config.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={handleAdd}
|
||||
title="Add profile"
|
||||
style={{
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
height: '28px',
|
||||
width: '28px',
|
||||
minWidth: '28px'
|
||||
}}
|
||||
>
|
||||
<span className="codicon codicon-add" />
|
||||
</VSCodeButton>
|
||||
{currentApiConfigName && (
|
||||
<>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={handleStartRename}
|
||||
title="Rename profile"
|
||||
style={{
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
height: '28px',
|
||||
width: '28px',
|
||||
minWidth: '28px'
|
||||
}}
|
||||
>
|
||||
<span className="codicon codicon-edit" />
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={handleDelete}
|
||||
title={isOnlyProfile ? "Cannot delete the only profile" : "Delete profile"}
|
||||
disabled={isOnlyProfile}
|
||||
style={{
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
height: '28px',
|
||||
width: '28px',
|
||||
minWidth: '28px'
|
||||
}}
|
||||
>
|
||||
<span className="codicon codicon-trash" />
|
||||
</VSCodeButton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p style={{
|
||||
fontSize: "12px",
|
||||
margin: "5px 0 12px",
|
||||
color: "var(--vscode-descriptionForeground)"
|
||||
}}>
|
||||
Save different API configurations to quickly switch between providers and settings
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ApiConfigManager)
|
||||
@@ -44,13 +44,12 @@ import OpenAiModelPicker from "./OpenAiModelPicker"
|
||||
import GlamaModelPicker from "./GlamaModelPicker"
|
||||
|
||||
interface ApiOptionsProps {
|
||||
showModelOptions: boolean
|
||||
apiErrorMessage?: string
|
||||
modelIdErrorMessage?: string
|
||||
}
|
||||
|
||||
const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) => {
|
||||
const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState()
|
||||
const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) => {
|
||||
const { apiConfiguration, setApiConfiguration, uriScheme, onUpdateApiConfig } = useExtensionState()
|
||||
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
||||
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
|
||||
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
|
||||
@@ -59,7 +58,9 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
|
||||
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
|
||||
setApiConfiguration({ ...apiConfiguration, [field]: event.target.value })
|
||||
const apiConfig = { ...apiConfiguration, [field]: event.target.value }
|
||||
onUpdateApiConfig(apiConfig)
|
||||
setApiConfiguration(apiConfig)
|
||||
}
|
||||
|
||||
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
|
||||
@@ -743,16 +744,15 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }:
|
||||
</p>
|
||||
)}
|
||||
|
||||
{selectedProvider === "glama" && showModelOptions && <GlamaModelPicker />}
|
||||
{selectedProvider === "glama" && <GlamaModelPicker />}
|
||||
|
||||
{selectedProvider === "openrouter" && showModelOptions && <OpenRouterModelPicker />}
|
||||
{selectedProvider === "openrouter" && <OpenRouterModelPicker />}
|
||||
|
||||
{selectedProvider !== "glama" &&
|
||||
selectedProvider !== "openrouter" &&
|
||||
selectedProvider !== "openai" &&
|
||||
selectedProvider !== "ollama" &&
|
||||
selectedProvider !== "lmstudio" &&
|
||||
showModelOptions && (
|
||||
selectedProvider !== "lmstudio" && (
|
||||
<>
|
||||
<div className="dropdown-container">
|
||||
<label htmlFor="model-id">
|
||||
|
||||
@@ -11,7 +11,7 @@ import { highlight } from "../history/HistoryView"
|
||||
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
||||
|
||||
const GlamaModelPicker: React.FC = () => {
|
||||
const { apiConfiguration, setApiConfiguration, glamaModels } = useExtensionState()
|
||||
const { apiConfiguration, setApiConfiguration, glamaModels, onUpdateApiConfig } = useExtensionState()
|
||||
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.glamaModelId || glamaDefaultModelId)
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
@@ -22,11 +22,14 @@ const GlamaModelPicker: React.FC = () => {
|
||||
|
||||
const handleModelChange = (newModelId: string) => {
|
||||
// could be setting invalid model id/undefined info but validation will catch it
|
||||
setApiConfiguration({
|
||||
const apiConfig = {
|
||||
...apiConfiguration,
|
||||
glamaModelId: newModelId,
|
||||
glamaModelInfo: glamaModels[newModelId],
|
||||
})
|
||||
}
|
||||
setApiConfiguration(apiConfig)
|
||||
onUpdateApiConfig(apiConfig)
|
||||
|
||||
setSearchTerm(newModelId)
|
||||
}
|
||||
|
||||
@@ -34,6 +37,13 @@ const GlamaModelPicker: React.FC = () => {
|
||||
return normalizeApiConfiguration(apiConfiguration)
|
||||
}, [apiConfiguration])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (apiConfiguration?.glamaModelId && apiConfiguration?.glamaModelId !== searchTerm) {
|
||||
setSearchTerm(apiConfiguration?.glamaModelId)
|
||||
}
|
||||
}, [apiConfiguration, searchTerm])
|
||||
|
||||
useMount(() => {
|
||||
vscode.postMessage({ type: "refreshGlamaModels" })
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import { vscode } from "../../utils/vscode"
|
||||
import { highlight } from "../history/HistoryView"
|
||||
|
||||
const OpenAiModelPicker: React.FC = () => {
|
||||
const { apiConfiguration, setApiConfiguration, openAiModels } = useExtensionState()
|
||||
const { apiConfiguration, setApiConfiguration, openAiModels, onUpdateApiConfig } = useExtensionState()
|
||||
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openAiModelId || "")
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
@@ -18,13 +18,22 @@ const OpenAiModelPicker: React.FC = () => {
|
||||
|
||||
const handleModelChange = (newModelId: string) => {
|
||||
// could be setting invalid model id/undefined info but validation will catch it
|
||||
setApiConfiguration({
|
||||
const apiConfig = {
|
||||
...apiConfiguration,
|
||||
openAiModelId: newModelId,
|
||||
})
|
||||
}
|
||||
setApiConfiguration(apiConfig)
|
||||
onUpdateApiConfig(apiConfig)
|
||||
|
||||
setSearchTerm(newModelId)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (apiConfiguration?.openAiModelId && apiConfiguration?.openAiModelId !== searchTerm) {
|
||||
setSearchTerm(apiConfiguration?.openAiModelId)
|
||||
}
|
||||
}, [apiConfiguration, searchTerm])
|
||||
|
||||
useEffect(() => {
|
||||
if (!apiConfiguration?.openAiBaseUrl || !apiConfiguration?.openAiApiKey) {
|
||||
return
|
||||
|
||||
@@ -11,7 +11,7 @@ import { highlight } from "../history/HistoryView"
|
||||
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
||||
|
||||
const OpenRouterModelPicker: React.FC = () => {
|
||||
const { apiConfiguration, setApiConfiguration, openRouterModels } = useExtensionState()
|
||||
const { apiConfiguration, setApiConfiguration, openRouterModels, onUpdateApiConfig } = useExtensionState()
|
||||
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openRouterModelId || openRouterDefaultModelId)
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
@@ -22,11 +22,14 @@ const OpenRouterModelPicker: React.FC = () => {
|
||||
|
||||
const handleModelChange = (newModelId: string) => {
|
||||
// could be setting invalid model id/undefined info but validation will catch it
|
||||
setApiConfiguration({
|
||||
const apiConfig = {
|
||||
...apiConfiguration,
|
||||
openRouterModelId: newModelId,
|
||||
openRouterModelInfo: openRouterModels[newModelId],
|
||||
})
|
||||
}
|
||||
|
||||
setApiConfiguration(apiConfig)
|
||||
onUpdateApiConfig(apiConfig)
|
||||
setSearchTerm(newModelId)
|
||||
}
|
||||
|
||||
@@ -34,6 +37,12 @@ const OpenRouterModelPicker: React.FC = () => {
|
||||
return normalizeApiConfiguration(apiConfiguration)
|
||||
}, [apiConfiguration])
|
||||
|
||||
useEffect(() => {
|
||||
if (apiConfiguration?.openRouterModelId && apiConfiguration?.openRouterModelId !== searchTerm) {
|
||||
setSearchTerm(apiConfiguration?.openRouterModelId)
|
||||
}
|
||||
}, [apiConfiguration, searchTerm])
|
||||
|
||||
useMount(() => {
|
||||
vscode.postMessage({ type: "refreshOpenRouterModels" })
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import { validateApiConfiguration, validateModelId } from "../../utils/validate"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import ApiOptions from "./ApiOptions"
|
||||
import McpEnabledToggle from "../mcp/McpEnabledToggle"
|
||||
import ApiConfigManager from "./ApiConfigManager"
|
||||
|
||||
const IS_DEV = false // FIXME: use flags when packaging
|
||||
|
||||
@@ -51,10 +52,17 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
terminalOutputLineLimit,
|
||||
setTerminalOutputLineLimit,
|
||||
mcpEnabled,
|
||||
alwaysApproveResubmit,
|
||||
setAlwaysApproveResubmit,
|
||||
requestDelaySeconds,
|
||||
setRequestDelaySeconds,
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
} = useExtensionState()
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [commandInput, setCommandInput] = useState("")
|
||||
|
||||
const handleSubmit = () => {
|
||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||
const modelIdValidationResult = validateModelId(apiConfiguration, glamaModels, openRouterModels)
|
||||
@@ -83,6 +91,15 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
vscode.postMessage({ type: "screenshotQuality", value: screenshotQuality ?? 75 })
|
||||
vscode.postMessage({ type: "terminalOutputLineLimit", value: terminalOutputLineLimit ?? 500 })
|
||||
vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
|
||||
vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
|
||||
vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
|
||||
vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName })
|
||||
vscode.postMessage({
|
||||
type: "upsertApiConfiguration",
|
||||
text: currentApiConfigName,
|
||||
apiConfiguration
|
||||
})
|
||||
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
@@ -146,8 +163,37 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Provider Settings</h3>
|
||||
<ApiConfigManager
|
||||
currentApiConfigName={currentApiConfigName}
|
||||
listApiConfigMeta={listApiConfigMeta}
|
||||
onSelectConfig={(configName: string) => {
|
||||
vscode.postMessage({
|
||||
type: "loadApiConfiguration",
|
||||
text: configName
|
||||
})
|
||||
}}
|
||||
onDeleteConfig={(configName: string) => {
|
||||
vscode.postMessage({
|
||||
type: "deleteApiConfiguration",
|
||||
text: configName
|
||||
})
|
||||
}}
|
||||
onRenameConfig={(oldName: string, newName: string) => {
|
||||
vscode.postMessage({
|
||||
type: "renameApiConfiguration",
|
||||
values: { oldName, newName },
|
||||
apiConfiguration
|
||||
})
|
||||
}}
|
||||
onUpsertConfig={(configName: string) => {
|
||||
vscode.postMessage({
|
||||
type: "upsertApiConfiguration",
|
||||
text: configName,
|
||||
apiConfiguration
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ApiOptions
|
||||
showModelOptions={true}
|
||||
apiErrorMessage={apiErrorMessage}
|
||||
modelIdErrorMessage={modelIdErrorMessage}
|
||||
/>
|
||||
@@ -355,18 +401,51 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
<span style={{ fontWeight: "500" }}>Always approve browser actions</span>
|
||||
</VSCodeCheckbox>
|
||||
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
Automatically perform browser actions without requiring approval<br/>
|
||||
Automatically perform browser actions without requiring approval<br />
|
||||
Note: Only applies when the model supports computer use
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={alwaysApproveResubmit}
|
||||
onChange={(e: any) => setAlwaysApproveResubmit(e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Always retry failed API requests</span>
|
||||
</VSCodeCheckbox>
|
||||
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
Automatically retry failed API requests when server returns an error response
|
||||
</p>
|
||||
{alwaysApproveResubmit && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value={requestDelaySeconds}
|
||||
onChange={(e) => setRequestDelaySeconds(parseInt(e.target.value))}
|
||||
style={{
|
||||
flex: 1,
|
||||
accentColor: 'var(--vscode-button-background)',
|
||||
height: '2px'
|
||||
}}
|
||||
/>
|
||||
<span style={{ minWidth: '45px', textAlign: 'left' }}>
|
||||
{requestDelaySeconds}s
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
Delay before retrying the request
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={alwaysAllowMcp}
|
||||
onChange={(e: any) => {
|
||||
setAlwaysAllowMcp(e.target.checked)
|
||||
vscode.postMessage({ type: "alwaysAllowMcp", bool: e.target.checked })
|
||||
}}>
|
||||
onChange={(e: any) => setAlwaysAllowMcp(e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Always approve MCP tools</span>
|
||||
</VSCodeCheckbox>
|
||||
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
|
||||
@@ -525,7 +604,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Notification Settings</h3>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Notification Settings</h3>
|
||||
<VSCodeCheckbox checked={soundEnabled} onChange={(e: any) => setSoundEnabled(e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Enable sound effects</span>
|
||||
</VSCodeCheckbox>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import ApiConfigManager from '../ApiConfigManager';
|
||||
|
||||
// Mock VSCode components
|
||||
jest.mock('@vscode/webview-ui-toolkit/react', () => ({
|
||||
VSCodeButton: ({ children, onClick, title, disabled }: any) => (
|
||||
<button onClick={onClick} title={title} disabled={disabled}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
VSCodeTextField: ({ value, onInput, placeholder }: any) => (
|
||||
<input
|
||||
value={value}
|
||||
onChange={e => onInput(e)}
|
||||
placeholder={placeholder}
|
||||
ref={undefined} // Explicitly set ref to undefined to avoid warning
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('ApiConfigManager', () => {
|
||||
const mockOnSelectConfig = jest.fn();
|
||||
const mockOnDeleteConfig = jest.fn();
|
||||
const mockOnRenameConfig = jest.fn();
|
||||
const mockOnUpsertConfig = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
currentApiConfigName: 'Default Config',
|
||||
listApiConfigMeta: [
|
||||
{ name: 'Default Config' },
|
||||
{ name: 'Another Config' }
|
||||
],
|
||||
onSelectConfig: mockOnSelectConfig,
|
||||
onDeleteConfig: mockOnDeleteConfig,
|
||||
onRenameConfig: mockOnRenameConfig,
|
||||
onUpsertConfig: mockOnUpsertConfig,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('immediately creates a copy when clicking add button', () => {
|
||||
render(<ApiConfigManager {...defaultProps} />);
|
||||
|
||||
// Find and click the add button
|
||||
const addButton = screen.getByTitle('Add profile');
|
||||
fireEvent.click(addButton);
|
||||
|
||||
// Verify that onUpsertConfig was called with the correct name
|
||||
expect(mockOnUpsertConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnUpsertConfig).toHaveBeenCalledWith('Default Config (copy)');
|
||||
});
|
||||
|
||||
it('creates copy with correct name when current config has spaces', () => {
|
||||
render(
|
||||
<ApiConfigManager
|
||||
{...defaultProps}
|
||||
currentApiConfigName="My Test Config"
|
||||
/>
|
||||
);
|
||||
|
||||
const addButton = screen.getByTitle('Add profile');
|
||||
fireEvent.click(addButton);
|
||||
|
||||
expect(mockOnUpsertConfig).toHaveBeenCalledWith('My Test Config (copy)');
|
||||
});
|
||||
|
||||
it('handles empty current config name gracefully', () => {
|
||||
render(
|
||||
<ApiConfigManager
|
||||
{...defaultProps}
|
||||
currentApiConfigName=""
|
||||
/>
|
||||
);
|
||||
|
||||
const addButton = screen.getByTitle('Add profile');
|
||||
fireEvent.click(addButton);
|
||||
|
||||
expect(mockOnUpsertConfig).toHaveBeenCalledWith(' (copy)');
|
||||
});
|
||||
|
||||
it('allows renaming the current config', () => {
|
||||
render(<ApiConfigManager {...defaultProps} />);
|
||||
|
||||
// Start rename
|
||||
const renameButton = screen.getByTitle('Rename profile');
|
||||
fireEvent.click(renameButton);
|
||||
|
||||
// Find input and enter new name
|
||||
const input = screen.getByDisplayValue('Default Config');
|
||||
fireEvent.input(input, { target: { value: 'New Name' } });
|
||||
|
||||
// Save
|
||||
const saveButton = screen.getByTitle('Save');
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
expect(mockOnRenameConfig).toHaveBeenCalledWith('Default Config', 'New Name');
|
||||
});
|
||||
|
||||
it('allows selecting a different config', () => {
|
||||
render(<ApiConfigManager {...defaultProps} />);
|
||||
|
||||
const select = screen.getByRole('combobox');
|
||||
fireEvent.change(select, { target: { value: 'Another Config' } });
|
||||
|
||||
expect(mockOnSelectConfig).toHaveBeenCalledWith('Another Config');
|
||||
});
|
||||
|
||||
it('allows deleting the current config when not the only one', () => {
|
||||
render(<ApiConfigManager {...defaultProps} />);
|
||||
|
||||
const deleteButton = screen.getByTitle('Delete profile');
|
||||
expect(deleteButton).not.toBeDisabled();
|
||||
|
||||
fireEvent.click(deleteButton);
|
||||
expect(mockOnDeleteConfig).toHaveBeenCalledWith('Default Config');
|
||||
});
|
||||
|
||||
it('disables delete button when only one config exists', () => {
|
||||
render(
|
||||
<ApiConfigManager
|
||||
{...defaultProps}
|
||||
listApiConfigMeta={[{ name: 'Default Config' }]}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteButton = screen.getByTitle('Cannot delete the only profile');
|
||||
expect(deleteButton).toHaveAttribute('disabled');
|
||||
});
|
||||
|
||||
it('cancels rename operation when clicking cancel', () => {
|
||||
render(<ApiConfigManager {...defaultProps} />);
|
||||
|
||||
// Start rename
|
||||
const renameButton = screen.getByTitle('Rename profile');
|
||||
fireEvent.click(renameButton);
|
||||
|
||||
// Find input and enter new name
|
||||
const input = screen.getByDisplayValue('Default Config');
|
||||
fireEvent.input(input, { target: { value: 'New Name' } });
|
||||
|
||||
// Cancel
|
||||
const cancelButton = screen.getByTitle('Cancel');
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
// Verify rename was not called
|
||||
expect(mockOnRenameConfig).not.toHaveBeenCalled();
|
||||
|
||||
// Verify we're back to normal view
|
||||
expect(screen.queryByDisplayValue('New Name')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,16 @@ jest.mock('../../../utils/vscode', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock ApiConfigManager component
|
||||
jest.mock('../ApiConfigManager', () => ({
|
||||
__esModule: true,
|
||||
default: ({ currentApiConfigName, listApiConfigMeta, onSelectConfig, onDeleteConfig, onRenameConfig, onUpsertConfig }: any) => (
|
||||
<div data-testid="api-config-management">
|
||||
<span>Current config: {currentApiConfigName}</span>
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
|
||||
// Mock VSCode components
|
||||
jest.mock('@vscode/webview-ui-toolkit/react', () => ({
|
||||
VSCodeButton: ({ children, onClick, appearance }: any) => (
|
||||
@@ -185,6 +195,18 @@ describe('SettingsView - Sound Settings', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('SettingsView - API Configuration', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders ApiConfigManagement with correct props', () => {
|
||||
renderSettingsView()
|
||||
|
||||
expect(screen.getByTestId('api-config-management')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('SettingsView - Allowed Commands', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
Reference in New Issue
Block a user