mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
feat: config manager using secret store
This commit is contained in:
165
webview-ui/src/components/settings/ApiConfigManager.tsx
Normal file
165
webview-ui/src/components/settings/ApiConfigManager.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { VSCodeButton, VSCodeDivider, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { memo, 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
|
||||
// setDraftNewConfig: (mode: boolean) => void
|
||||
}
|
||||
|
||||
const ApiConfigManager = ({
|
||||
currentApiConfigName,
|
||||
listApiConfigMeta,
|
||||
onSelectConfig,
|
||||
onDeleteConfig,
|
||||
onRenameConfig,
|
||||
onUpsertConfig,
|
||||
// setDraftNewConfig,
|
||||
}: ApiConfigManagerProps) => {
|
||||
const [isNewMode, setIsNewMode] = useState(false);
|
||||
const [isRenameMode, setIsRenameMode] = useState(false);
|
||||
const [newConfigName, setNewConfigName] = useState("");
|
||||
const [renamedConfigName, setRenamedConfigName] = useState("");
|
||||
|
||||
const handleNewConfig = () => {
|
||||
setIsNewMode(true);
|
||||
setNewConfigName("");
|
||||
// setDraftNewConfig(true)
|
||||
};
|
||||
|
||||
const handleSaveNewConfig = () => {
|
||||
if (newConfigName.trim()) {
|
||||
onUpsertConfig(newConfigName.trim());
|
||||
setIsNewMode(false);
|
||||
setNewConfigName("");
|
||||
// setDraftNewConfig(false)
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelNewConfig = () => {
|
||||
setIsNewMode(false);
|
||||
setNewConfigName("");
|
||||
// setDraftNewConfig(false)
|
||||
};
|
||||
|
||||
const handleStartRename = () => {
|
||||
setIsRenameMode(true);
|
||||
setRenamedConfigName(currentApiConfigName || "");
|
||||
};
|
||||
|
||||
const handleSaveRename = () => {
|
||||
if (renamedConfigName.trim() && currentApiConfigName) {
|
||||
onRenameConfig(currentApiConfigName, renamedConfigName.trim());
|
||||
setIsRenameMode(false);
|
||||
setRenamedConfigName("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelRename = () => {
|
||||
setIsRenameMode(false);
|
||||
setRenamedConfigName("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
|
||||
API Configuration
|
||||
</label>
|
||||
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
|
||||
{isNewMode ? (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={newConfigName}
|
||||
onInput={(e: any) => setNewConfigName(e.target.value)}
|
||||
placeholder="Enter configuration name"
|
||||
style={{ flexGrow: 1 }}
|
||||
/>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
disabled={!newConfigName.trim()}
|
||||
onClick={handleSaveNewConfig}
|
||||
>
|
||||
<span className="codicon codicon-check" /> Save
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={handleCancelNewConfig}
|
||||
>
|
||||
<span className="codicon codicon-close" /> Cancel
|
||||
</VSCodeButton>
|
||||
</>
|
||||
) : isRenameMode ? (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={renamedConfigName}
|
||||
onInput={(e: any) => setRenamedConfigName(e.target.value)}
|
||||
placeholder="Enter new name"
|
||||
style={{ flexGrow: 1 }}
|
||||
/>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
disabled={!renamedConfigName.trim()}
|
||||
onClick={handleSaveRename}
|
||||
>
|
||||
<span className="codicon codicon-check" /> Save
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={handleCancelRename}
|
||||
>
|
||||
<span className="codicon codicon-close" /> Cancel
|
||||
</VSCodeButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<select
|
||||
value={currentApiConfigName}
|
||||
onChange={(e) => onSelectConfig(e.target.value)}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
padding: "4px 8px",
|
||||
backgroundColor: "var(--vscode-input-background)",
|
||||
color: "var(--vscode-input-foreground)",
|
||||
border: "1px solid var(--vscode-input-border)",
|
||||
borderRadius: "2px",
|
||||
height: "28px"
|
||||
}}>
|
||||
{listApiConfigMeta?.map((config) => (
|
||||
<option key={config.name} value={config.name}>{config.name} {config.apiProvider ? `(${config.apiProvider})` : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={handleNewConfig}
|
||||
>
|
||||
<span className="codicon codicon-add" /> New
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
disabled={!currentApiConfigName}
|
||||
onClick={handleStartRename}
|
||||
>
|
||||
<span className="codicon codicon-edit" /> Rename
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
disabled={!currentApiConfigName}
|
||||
onClick={() => onDeleteConfig(currentApiConfigName!)}
|
||||
>
|
||||
<span className="codicon codicon-trash" /> Delete
|
||||
</VSCodeButton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<VSCodeDivider style={{ margin: "15px 0" }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ApiConfigManager)
|
||||
@@ -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
|
||||
|
||||
@@ -55,10 +56,15 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
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 [draftNewMode, setDraftNewMode] = useState(false)
|
||||
|
||||
|
||||
const handleSubmit = () => {
|
||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||
const modelIdValidationResult = validateModelId(apiConfiguration, glamaModels, openRouterModels)
|
||||
@@ -89,6 +95,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -150,6 +163,42 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
</div>
|
||||
<div
|
||||
style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<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
|
||||
})
|
||||
}}
|
||||
// setDraftNewConfig={(mode: boolean) => {
|
||||
// setDraftNewMode(mode)
|
||||
// }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>Provider Settings</h3>
|
||||
<ApiOptions
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
|
||||
import { ApiConfigMeta, ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
|
||||
import {
|
||||
ApiConfiguration,
|
||||
ModelInfo,
|
||||
@@ -13,6 +13,9 @@ import { vscode } from "../utils/vscode"
|
||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||
import { findLastIndex } from "../../../src/shared/array"
|
||||
import { McpServer } from "../../../src/shared/mcp"
|
||||
import {
|
||||
checkExistKey
|
||||
} from "../../../src/shared/checkExistApiConfig"
|
||||
|
||||
export interface ExtensionStateContextType extends ExtensionState {
|
||||
didHydrateState: boolean
|
||||
@@ -50,6 +53,8 @@ export interface ExtensionStateContextType extends ExtensionState {
|
||||
setAlwaysApproveResubmit: (value: boolean) => void
|
||||
requestDelaySeconds: number
|
||||
setRequestDelaySeconds: (value: number) => void
|
||||
setCurrentApiConfigName: (value: string) => void
|
||||
setListApiConfigMeta: (value: ApiConfigMeta[]) => void
|
||||
}
|
||||
|
||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
@@ -72,7 +77,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
terminalOutputLineLimit: 500,
|
||||
mcpEnabled: true,
|
||||
alwaysApproveResubmit: false,
|
||||
requestDelaySeconds: 5
|
||||
requestDelaySeconds: 5,
|
||||
currentApiConfigName: 'default',
|
||||
listApiConfigMeta: [],
|
||||
})
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
const [showWelcome, setShowWelcome] = useState(false)
|
||||
@@ -88,27 +95,16 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
const [openAiModels, setOpenAiModels] = useState<string[]>([])
|
||||
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
|
||||
|
||||
|
||||
const setListApiConfigMeta = useCallback((value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), [setState])
|
||||
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
switch (message.type) {
|
||||
case "state": {
|
||||
setState(message.state!)
|
||||
const config = message.state?.apiConfiguration
|
||||
const hasKey = config
|
||||
? [
|
||||
config.apiKey,
|
||||
config.glamaApiKey,
|
||||
config.openRouterApiKey,
|
||||
config.awsRegion,
|
||||
config.vertexProjectId,
|
||||
config.openAiApiKey,
|
||||
config.ollamaModelId,
|
||||
config.lmStudioModelId,
|
||||
config.geminiApiKey,
|
||||
config.openAiNativeApiKey,
|
||||
config.deepSeekApiKey,
|
||||
].some((key) => key !== undefined)
|
||||
: false
|
||||
const hasKey = checkExistKey(config)
|
||||
setShowWelcome(!hasKey)
|
||||
setDidHydrateState(true)
|
||||
break
|
||||
@@ -162,8 +158,12 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
setMcpServers(message.mcpServers ?? [])
|
||||
break
|
||||
}
|
||||
case "listApiConfig": {
|
||||
setListApiConfigMeta(message.listApiConfig ?? [])
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}, [setListApiConfigMeta])
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
@@ -208,7 +208,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
setTerminalOutputLineLimit: (value) => setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
|
||||
setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
|
||||
setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
|
||||
setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value }))
|
||||
setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
|
||||
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
|
||||
setListApiConfigMeta
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
||||
Reference in New Issue
Block a user