mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Fix input box revert issue and configuration loss during profile switch #955
This commit is contained in:
parent
2dcde02839
commit
2943d655cf
8 changed files with 224 additions and 163 deletions
|
|
@ -33,29 +33,38 @@ export class ConfigManager {
|
|||
return Math.random().toString(36).substring(2, 15)
|
||||
}
|
||||
|
||||
// Synchronize readConfig/writeConfig operations to avoid data loss.
|
||||
private _lock = Promise.resolve()
|
||||
private lock<T>(cb: () => Promise<T>) {
|
||||
const next = this._lock.then(cb)
|
||||
this._lock = next.catch(() => {}) as Promise<void>
|
||||
return next
|
||||
}
|
||||
/**
|
||||
* Initialize config if it doesn't exist
|
||||
*/
|
||||
async initConfig(): Promise<void> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
if (!config) {
|
||||
await this.writeConfig(this.defaultConfig)
|
||||
return
|
||||
}
|
||||
|
||||
// Migrate: ensure all configs have IDs
|
||||
let needsMigration = false
|
||||
for (const [name, apiConfig] of Object.entries(config.apiConfigs)) {
|
||||
if (!apiConfig.id) {
|
||||
apiConfig.id = this.generateId()
|
||||
needsMigration = true
|
||||
return await this.lock(async () => {
|
||||
const config = await this.readConfig()
|
||||
if (!config) {
|
||||
await this.writeConfig(this.defaultConfig)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMigration) {
|
||||
await this.writeConfig(config)
|
||||
}
|
||||
// Migrate: ensure all configs have IDs
|
||||
let needsMigration = false
|
||||
for (const [name, apiConfig] of Object.entries(config.apiConfigs)) {
|
||||
if (!apiConfig.id) {
|
||||
apiConfig.id = this.generateId()
|
||||
needsMigration = true
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMigration) {
|
||||
await this.writeConfig(config)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to initialize config: ${error}`)
|
||||
}
|
||||
|
|
@ -66,12 +75,14 @@ export class ConfigManager {
|
|||
*/
|
||||
async listConfig(): Promise<ApiConfigMeta[]> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
return Object.entries(config.apiConfigs).map(([name, apiConfig]) => ({
|
||||
name,
|
||||
id: apiConfig.id || "",
|
||||
apiProvider: apiConfig.apiProvider,
|
||||
}))
|
||||
return await this.lock(async () => {
|
||||
const config = await this.readConfig()
|
||||
return Object.entries(config.apiConfigs).map(([name, apiConfig]) => ({
|
||||
name,
|
||||
id: apiConfig.id || "",
|
||||
apiProvider: apiConfig.apiProvider,
|
||||
}))
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list configs: ${error}`)
|
||||
}
|
||||
|
|
@ -82,13 +93,15 @@ export class ConfigManager {
|
|||
*/
|
||||
async saveConfig(name: string, config: ApiConfiguration): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await this.readConfig()
|
||||
const existingConfig = currentConfig.apiConfigs[name]
|
||||
currentConfig.apiConfigs[name] = {
|
||||
...config,
|
||||
id: existingConfig?.id || this.generateId(),
|
||||
}
|
||||
await this.writeConfig(currentConfig)
|
||||
return await this.lock(async () => {
|
||||
const currentConfig = await this.readConfig()
|
||||
const existingConfig = currentConfig.apiConfigs[name]
|
||||
currentConfig.apiConfigs[name] = {
|
||||
...config,
|
||||
id: existingConfig?.id || this.generateId(),
|
||||
}
|
||||
await this.writeConfig(currentConfig)
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to save config: ${error}`)
|
||||
}
|
||||
|
|
@ -99,17 +112,19 @@ export class ConfigManager {
|
|||
*/
|
||||
async loadConfig(name: string): Promise<ApiConfiguration> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
const apiConfig = config.apiConfigs[name]
|
||||
return await this.lock(async () => {
|
||||
const config = await this.readConfig()
|
||||
const apiConfig = config.apiConfigs[name]
|
||||
|
||||
if (!apiConfig) {
|
||||
throw new Error(`Config '${name}' not found`)
|
||||
}
|
||||
if (!apiConfig) {
|
||||
throw new Error(`Config '${name}' not found`)
|
||||
}
|
||||
|
||||
config.currentApiConfigName = name
|
||||
await this.writeConfig(config)
|
||||
config.currentApiConfigName = name
|
||||
await this.writeConfig(config)
|
||||
|
||||
return apiConfig
|
||||
return apiConfig
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load config: ${error}`)
|
||||
}
|
||||
|
|
@ -120,18 +135,20 @@ export class ConfigManager {
|
|||
*/
|
||||
async deleteConfig(name: string): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await this.readConfig()
|
||||
if (!currentConfig.apiConfigs[name]) {
|
||||
throw new Error(`Config '${name}' not found`)
|
||||
}
|
||||
return await this.lock(async () => {
|
||||
const currentConfig = await this.readConfig()
|
||||
if (!currentConfig.apiConfigs[name]) {
|
||||
throw new Error(`Config '${name}' not found`)
|
||||
}
|
||||
|
||||
// Don't allow deleting the default config
|
||||
if (Object.keys(currentConfig.apiConfigs).length === 1) {
|
||||
throw new Error(`Cannot delete the last remaining configuration.`)
|
||||
}
|
||||
// Don't allow deleting the default config
|
||||
if (Object.keys(currentConfig.apiConfigs).length === 1) {
|
||||
throw new Error(`Cannot delete the last remaining configuration.`)
|
||||
}
|
||||
|
||||
delete currentConfig.apiConfigs[name]
|
||||
await this.writeConfig(currentConfig)
|
||||
delete currentConfig.apiConfigs[name]
|
||||
await this.writeConfig(currentConfig)
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete config: ${error}`)
|
||||
}
|
||||
|
|
@ -142,13 +159,15 @@ export class ConfigManager {
|
|||
*/
|
||||
async setCurrentConfig(name: string): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await this.readConfig()
|
||||
if (!currentConfig.apiConfigs[name]) {
|
||||
throw new Error(`Config '${name}' not found`)
|
||||
}
|
||||
return await this.lock(async () => {
|
||||
const currentConfig = await this.readConfig()
|
||||
if (!currentConfig.apiConfigs[name]) {
|
||||
throw new Error(`Config '${name}' not found`)
|
||||
}
|
||||
|
||||
currentConfig.currentApiConfigName = name
|
||||
await this.writeConfig(currentConfig)
|
||||
currentConfig.currentApiConfigName = name
|
||||
await this.writeConfig(currentConfig)
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set current config: ${error}`)
|
||||
}
|
||||
|
|
@ -159,8 +178,10 @@ export class ConfigManager {
|
|||
*/
|
||||
async hasConfig(name: string): Promise<boolean> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
return name in config.apiConfigs
|
||||
return await this.lock(async () => {
|
||||
const config = await this.readConfig()
|
||||
return name in config.apiConfigs
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to check config existence: ${error}`)
|
||||
}
|
||||
|
|
@ -171,12 +192,14 @@ export class ConfigManager {
|
|||
*/
|
||||
async setModeConfig(mode: Mode, configId: string): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await this.readConfig()
|
||||
if (!currentConfig.modeApiConfigs) {
|
||||
currentConfig.modeApiConfigs = {}
|
||||
}
|
||||
currentConfig.modeApiConfigs[mode] = configId
|
||||
await this.writeConfig(currentConfig)
|
||||
return await this.lock(async () => {
|
||||
const currentConfig = await this.readConfig()
|
||||
if (!currentConfig.modeApiConfigs) {
|
||||
currentConfig.modeApiConfigs = {}
|
||||
}
|
||||
currentConfig.modeApiConfigs[mode] = configId
|
||||
await this.writeConfig(currentConfig)
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set mode config: ${error}`)
|
||||
}
|
||||
|
|
@ -187,8 +210,10 @@ export class ConfigManager {
|
|||
*/
|
||||
async getModeConfigId(mode: Mode): Promise<string | undefined> {
|
||||
try {
|
||||
const config = await this.readConfig()
|
||||
return config.modeApiConfigs?.[mode]
|
||||
return await this.lock(async () => {
|
||||
const config = await this.readConfig()
|
||||
return config.modeApiConfigs?.[mode]
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get mode config: ${error}`)
|
||||
}
|
||||
|
|
@ -205,7 +230,9 @@ export class ConfigManager {
|
|||
* Reset all configuration by deleting the stored config from secrets
|
||||
*/
|
||||
public async resetAllConfigs(): Promise<void> {
|
||||
await this.context.secrets.delete(this.getConfigKey())
|
||||
return await this.lock(async () => {
|
||||
await this.context.secrets.delete(this.getConfigKey())
|
||||
})
|
||||
}
|
||||
|
||||
private async readConfig(): Promise<ApiConfigData> {
|
||||
|
|
|
|||
|
|
@ -1317,6 +1317,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
}
|
||||
break
|
||||
}
|
||||
case "saveApiConfiguration":
|
||||
if (message.text && message.apiConfiguration) {
|
||||
try {
|
||||
await this.configManager.saveConfig(message.text, message.apiConfiguration)
|
||||
const listApiConfig = await this.configManager.listConfig()
|
||||
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
|
||||
} catch (error) {
|
||||
this.outputChannel.appendLine(
|
||||
`Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
|
||||
)
|
||||
vscode.window.showErrorMessage("Failed to save api configuration")
|
||||
}
|
||||
}
|
||||
break
|
||||
case "upsertApiConfiguration":
|
||||
if (message.text && message.apiConfiguration) {
|
||||
try {
|
||||
|
|
@ -1361,9 +1375,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
await this.postStateToWebview()
|
||||
} catch (error) {
|
||||
this.outputChannel.appendLine(
|
||||
`Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
|
||||
`Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
|
||||
)
|
||||
vscode.window.showErrorMessage("Failed to create api configuration")
|
||||
vscode.window.showErrorMessage("Failed to rename api configuration")
|
||||
}
|
||||
}
|
||||
break
|
||||
|
|
@ -1647,51 +1661,53 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
requestyModelInfo,
|
||||
modelTemperature,
|
||||
} = apiConfiguration
|
||||
await this.updateGlobalState("apiProvider", apiProvider)
|
||||
await this.updateGlobalState("apiModelId", apiModelId)
|
||||
await this.storeSecret("apiKey", apiKey)
|
||||
await this.updateGlobalState("glamaModelId", glamaModelId)
|
||||
await this.updateGlobalState("glamaModelInfo", glamaModelInfo)
|
||||
await this.storeSecret("glamaApiKey", glamaApiKey)
|
||||
await this.storeSecret("openRouterApiKey", openRouterApiKey)
|
||||
await this.storeSecret("awsAccessKey", awsAccessKey)
|
||||
await this.storeSecret("awsSecretKey", awsSecretKey)
|
||||
await this.storeSecret("awsSessionToken", awsSessionToken)
|
||||
await this.updateGlobalState("awsRegion", awsRegion)
|
||||
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
|
||||
await this.updateGlobalState("awsProfile", awsProfile)
|
||||
await this.updateGlobalState("awsUseProfile", awsUseProfile)
|
||||
await this.updateGlobalState("vertexProjectId", vertexProjectId)
|
||||
await this.updateGlobalState("vertexRegion", vertexRegion)
|
||||
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
|
||||
await this.storeSecret("openAiApiKey", openAiApiKey)
|
||||
await this.updateGlobalState("openAiModelId", openAiModelId)
|
||||
await this.updateGlobalState("openAiCustomModelInfo", openAiCustomModelInfo)
|
||||
await this.updateGlobalState("openAiUseAzure", openAiUseAzure)
|
||||
await this.updateGlobalState("ollamaModelId", ollamaModelId)
|
||||
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
|
||||
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
|
||||
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
|
||||
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
|
||||
await this.storeSecret("geminiApiKey", geminiApiKey)
|
||||
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
|
||||
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
|
||||
await this.updateGlobalState("azureApiVersion", azureApiVersion)
|
||||
await this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled)
|
||||
await this.updateGlobalState("openRouterModelId", openRouterModelId)
|
||||
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
|
||||
await this.updateGlobalState("openRouterBaseUrl", openRouterBaseUrl)
|
||||
await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
|
||||
await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector)
|
||||
await this.storeSecret("mistralApiKey", mistralApiKey)
|
||||
await this.updateGlobalState("mistralCodestralUrl", mistralCodestralUrl)
|
||||
await this.storeSecret("unboundApiKey", unboundApiKey)
|
||||
await this.updateGlobalState("unboundModelId", unboundModelId)
|
||||
await this.updateGlobalState("unboundModelInfo", unboundModelInfo)
|
||||
await this.storeSecret("requestyApiKey", requestyApiKey)
|
||||
await this.updateGlobalState("requestyModelId", requestyModelId)
|
||||
await this.updateGlobalState("requestyModelInfo", requestyModelInfo)
|
||||
await this.updateGlobalState("modelTemperature", modelTemperature)
|
||||
await Promise.all([
|
||||
this.updateGlobalState("apiProvider", apiProvider),
|
||||
this.updateGlobalState("apiModelId", apiModelId),
|
||||
this.storeSecret("apiKey", apiKey),
|
||||
this.updateGlobalState("glamaModelId", glamaModelId),
|
||||
this.updateGlobalState("glamaModelInfo", glamaModelInfo),
|
||||
this.storeSecret("glamaApiKey", glamaApiKey),
|
||||
this.storeSecret("openRouterApiKey", openRouterApiKey),
|
||||
this.storeSecret("awsAccessKey", awsAccessKey),
|
||||
this.storeSecret("awsSecretKey", awsSecretKey),
|
||||
this.storeSecret("awsSessionToken", awsSessionToken),
|
||||
this.updateGlobalState("awsRegion", awsRegion),
|
||||
this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference),
|
||||
this.updateGlobalState("awsProfile", awsProfile),
|
||||
this.updateGlobalState("awsUseProfile", awsUseProfile),
|
||||
this.updateGlobalState("vertexProjectId", vertexProjectId),
|
||||
this.updateGlobalState("vertexRegion", vertexRegion),
|
||||
this.updateGlobalState("openAiBaseUrl", openAiBaseUrl),
|
||||
this.storeSecret("openAiApiKey", openAiApiKey),
|
||||
this.updateGlobalState("openAiModelId", openAiModelId),
|
||||
this.updateGlobalState("openAiCustomModelInfo", openAiCustomModelInfo),
|
||||
this.updateGlobalState("openAiUseAzure", openAiUseAzure),
|
||||
this.updateGlobalState("ollamaModelId", ollamaModelId),
|
||||
this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl),
|
||||
this.updateGlobalState("lmStudioModelId", lmStudioModelId),
|
||||
this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
|
||||
this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
|
||||
this.storeSecret("geminiApiKey", geminiApiKey),
|
||||
this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
|
||||
this.storeSecret("deepSeekApiKey", deepSeekApiKey),
|
||||
this.updateGlobalState("azureApiVersion", azureApiVersion),
|
||||
this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled),
|
||||
this.updateGlobalState("openRouterModelId", openRouterModelId),
|
||||
this.updateGlobalState("openRouterModelInfo", openRouterModelInfo),
|
||||
this.updateGlobalState("openRouterBaseUrl", openRouterBaseUrl),
|
||||
this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform),
|
||||
this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector),
|
||||
this.storeSecret("mistralApiKey", mistralApiKey),
|
||||
this.updateGlobalState("mistralCodestralUrl", mistralCodestralUrl),
|
||||
this.storeSecret("unboundApiKey", unboundApiKey),
|
||||
this.updateGlobalState("unboundModelId", unboundModelId),
|
||||
this.updateGlobalState("unboundModelInfo", unboundModelInfo),
|
||||
this.storeSecret("requestyApiKey", requestyApiKey),
|
||||
this.updateGlobalState("requestyModelId", requestyModelId),
|
||||
this.updateGlobalState("requestyModelInfo", requestyModelInfo),
|
||||
this.updateGlobalState("modelTemperature", modelTemperature),
|
||||
])
|
||||
if (this.cline) {
|
||||
this.cline.api = buildApiHandler(apiConfiguration)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1405,5 +1405,38 @@ describe("ClineProvider", () => {
|
|||
])
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
|
||||
})
|
||||
|
||||
test("handles successful saveApiConfiguration", async () => {
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
|
||||
// Mock ConfigManager methods
|
||||
provider.configManager = {
|
||||
saveConfig: jest.fn().mockResolvedValue(undefined),
|
||||
listConfig: jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
|
||||
} as any
|
||||
|
||||
const testApiConfig = {
|
||||
apiProvider: "anthropic" as const,
|
||||
apiKey: "test-key",
|
||||
}
|
||||
|
||||
// Trigger upsertApiConfiguration
|
||||
await messageHandler({
|
||||
type: "saveApiConfiguration",
|
||||
text: "test-config",
|
||||
apiConfiguration: testApiConfig,
|
||||
})
|
||||
|
||||
// Verify config was saved
|
||||
expect(provider.configManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
|
||||
|
||||
// Verify state updates
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
|
||||
{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export interface WebviewMessage {
|
|||
type:
|
||||
| "apiConfiguration"
|
||||
| "currentApiConfigName"
|
||||
| "saveApiConfiguration"
|
||||
| "upsertApiConfiguration"
|
||||
| "deleteApiConfiguration"
|
||||
| "loadApiConfiguration"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { memo, useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useEvent, useInterval } from "react-use"
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui"
|
||||
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { TemperatureControl } from "./TemperatureControl"
|
||||
|
|
@ -65,7 +65,8 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
return normalizeApiConfiguration(apiConfiguration)
|
||||
}, [apiConfiguration])
|
||||
|
||||
// Poll ollama/lmstudio models
|
||||
const requestLocalModelsTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
// Pull ollama/lmstudio models
|
||||
const requestLocalModels = useCallback(() => {
|
||||
if (selectedProvider === "ollama") {
|
||||
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
|
||||
|
|
@ -75,34 +76,29 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
vscode.postMessage({ type: "requestVsCodeLmModels" })
|
||||
}
|
||||
}, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl])
|
||||
// Debounced model updates, only executed 250ms after the user stops typing
|
||||
useEffect(() => {
|
||||
if (selectedProvider === "ollama" || selectedProvider === "lmstudio" || selectedProvider === "vscode-lm") {
|
||||
requestLocalModels()
|
||||
if (requestLocalModelsTimeoutRef.current) {
|
||||
clearTimeout(requestLocalModelsTimeoutRef.current)
|
||||
}
|
||||
}, [selectedProvider, requestLocalModels])
|
||||
useInterval(
|
||||
requestLocalModels,
|
||||
selectedProvider === "ollama" || selectedProvider === "lmstudio" || selectedProvider === "vscode-lm"
|
||||
? 2000
|
||||
: null,
|
||||
)
|
||||
requestLocalModelsTimeoutRef.current = setTimeout(requestLocalModels, 250)
|
||||
return () => {
|
||||
if (requestLocalModelsTimeoutRef.current) {
|
||||
clearTimeout(requestLocalModelsTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [requestLocalModels])
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
if (message.type === "ollamaModels" && Array.isArray(message.ollamaModels)) {
|
||||
const newModels = message.ollamaModels
|
||||
setOllamaModels((prevModels) => {
|
||||
return JSON.stringify(prevModels) === JSON.stringify(newModels) ? prevModels : newModels
|
||||
})
|
||||
setOllamaModels(newModels)
|
||||
} else if (message.type === "lmStudioModels" && Array.isArray(message.lmStudioModels)) {
|
||||
const newModels = message.lmStudioModels
|
||||
setLmStudioModels((prevModels) => {
|
||||
return JSON.stringify(prevModels) === JSON.stringify(newModels) ? prevModels : newModels
|
||||
})
|
||||
setLmStudioModels(newModels)
|
||||
} else if (message.type === "vsCodeLmModels" && Array.isArray(message.vsCodeLmModels)) {
|
||||
const newModels = message.vsCodeLmModels
|
||||
setVsCodeLmModels((prevModels) => {
|
||||
return JSON.stringify(prevModels) === JSON.stringify(newModels) ? prevModels : newModels
|
||||
})
|
||||
setVsCodeLmModels(newModels)
|
||||
}
|
||||
}, [])
|
||||
useEvent("message", handleMessage)
|
||||
|
|
@ -142,10 +138,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
id="api-provider"
|
||||
value={selectedProvider}
|
||||
onChange={(value: unknown) => {
|
||||
handleInputChange(
|
||||
"apiProvider",
|
||||
true,
|
||||
)({
|
||||
handleInputChange("apiProvider")({
|
||||
target: {
|
||||
value: (value as DropdownOption).value,
|
||||
},
|
||||
|
|
@ -261,7 +254,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
value={apiConfiguration?.requestyApiKey || ""}
|
||||
style={{ width: "100%" }}
|
||||
type="password"
|
||||
onBlur={handleInputChange("requestyApiKey")}
|
||||
onInput={handleInputChange("requestyApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>Requesty API Key</span>
|
||||
</VSCodeTextField>
|
||||
|
|
@ -341,7 +334,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
value={apiConfiguration?.mistralCodestralUrl || ""}
|
||||
style={{ width: "100%", marginTop: "10px" }}
|
||||
type="url"
|
||||
onBlur={handleInputChange("mistralCodestralUrl")}
|
||||
onInput={handleInputChange("mistralCodestralUrl")}
|
||||
placeholder="Default: https://codestral.mistral.ai">
|
||||
<span style={{ fontWeight: 500 }}>Codestral Base URL (Optional)</span>
|
||||
</VSCodeTextField>
|
||||
|
|
@ -408,7 +401,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
|
|||
value={apiConfiguration?.openRouterBaseUrl || ""}
|
||||
style={{ width: "100%", marginTop: 3 }}
|
||||
type="url"
|
||||
onBlur={handleInputChange("openRouterBaseUrl")}
|
||||
onInput={handleInputChange("openRouterBaseUrl")}
|
||||
placeholder="Default: https://openrouter.ai/api/v1"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -77,10 +77,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
setApiErrorMessage(apiValidationResult)
|
||||
setModelIdErrorMessage(modelIdValidationResult)
|
||||
if (!apiValidationResult && !modelIdValidationResult) {
|
||||
vscode.postMessage({
|
||||
type: "apiConfiguration",
|
||||
apiConfiguration,
|
||||
})
|
||||
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
|
||||
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
|
||||
vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
|
||||
|
|
@ -195,6 +191,11 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
currentApiConfigName={currentApiConfigName}
|
||||
listApiConfigMeta={listApiConfigMeta}
|
||||
onSelectConfig={(configName: string) => {
|
||||
vscode.postMessage({
|
||||
type: "saveApiConfiguration",
|
||||
text: currentApiConfigName,
|
||||
apiConfiguration,
|
||||
})
|
||||
vscode.postMessage({
|
||||
type: "loadApiConfiguration",
|
||||
text: configName,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { vscode } from "../../utils/vscode"
|
|||
import ApiOptions from "../settings/ApiOptions"
|
||||
|
||||
const WelcomeView = () => {
|
||||
const { apiConfiguration } = useExtensionState()
|
||||
const { apiConfiguration, currentApiConfigName } = useExtensionState()
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
|
||||
|
||||
|
|
@ -17,7 +17,11 @@ const WelcomeView = () => {
|
|||
return
|
||||
}
|
||||
setErrorMessage(undefined)
|
||||
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
|
||||
vscode.postMessage({
|
||||
type: "upsertApiConfiguration",
|
||||
text: currentApiConfigName,
|
||||
apiConfiguration,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -162,26 +162,12 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
const handleInputChange = useCallback(
|
||||
// Returns a function that handles an input change event for a specific API configuration field.
|
||||
// The optional "softUpdate" flag determines whether to immediately update local state or send an external update.
|
||||
(field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => {
|
||||
(field: keyof ApiConfiguration) => (event: any) => {
|
||||
// Use the functional form of setState to ensure the latest state is used in the update logic.
|
||||
setState((currentState) => {
|
||||
if (softUpdate) {
|
||||
// Return a new state object with the updated apiConfiguration.
|
||||
// This will trigger a re-render with the new configuration value.
|
||||
return {
|
||||
...currentState,
|
||||
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
|
||||
}
|
||||
} else {
|
||||
// For non-soft updates, send a message to the VS Code extension with the updated config.
|
||||
// This side effect communicates the change without updating local React state.
|
||||
vscode.postMessage({
|
||||
type: "upsertApiConfiguration",
|
||||
text: currentState.currentApiConfigName,
|
||||
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
|
||||
})
|
||||
// Return the unchanged state as no local state update is intended in this branch.
|
||||
return currentState
|
||||
return {
|
||||
...currentState,
|
||||
apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue