Skip to content

Commit 17be7c1

Browse files
authored
Merge pull request #979 from System233/patch-input-boxes
Fix input box revert issue and configuration loss during profile switch #955
2 parents f3d4c37 + 7316f82 commit 17be7c1

File tree

13 files changed

+275
-232
lines changed

13 files changed

+275
-232
lines changed

src/core/config/ConfigManager.ts

Lines changed: 90 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -33,29 +33,38 @@ export class ConfigManager {
3333
return Math.random().toString(36).substring(2, 15)
3434
}
3535

36+
// Synchronize readConfig/writeConfig operations to avoid data loss.
37+
private _lock = Promise.resolve()
38+
private lock<T>(cb: () => Promise<T>) {
39+
const next = this._lock.then(cb)
40+
this._lock = next.catch(() => {}) as Promise<void>
41+
return next
42+
}
3643
/**
3744
* Initialize config if it doesn't exist
3845
*/
3946
async initConfig(): Promise<void> {
4047
try {
41-
const config = await this.readConfig()
42-
if (!config) {
43-
await this.writeConfig(this.defaultConfig)
44-
return
45-
}
48+
return await this.lock(async () => {
49+
const config = await this.readConfig()
50+
if (!config) {
51+
await this.writeConfig(this.defaultConfig)
52+
return
53+
}
4654

47-
// Migrate: ensure all configs have IDs
48-
let needsMigration = false
49-
for (const [name, apiConfig] of Object.entries(config.apiConfigs)) {
50-
if (!apiConfig.id) {
51-
apiConfig.id = this.generateId()
52-
needsMigration = true
55+
// Migrate: ensure all configs have IDs
56+
let needsMigration = false
57+
for (const [name, apiConfig] of Object.entries(config.apiConfigs)) {
58+
if (!apiConfig.id) {
59+
apiConfig.id = this.generateId()
60+
needsMigration = true
61+
}
5362
}
54-
}
5563

56-
if (needsMigration) {
57-
await this.writeConfig(config)
58-
}
64+
if (needsMigration) {
65+
await this.writeConfig(config)
66+
}
67+
})
5968
} catch (error) {
6069
throw new Error(`Failed to initialize config: ${error}`)
6170
}
@@ -66,12 +75,14 @@ export class ConfigManager {
6675
*/
6776
async listConfig(): Promise<ApiConfigMeta[]> {
6877
try {
69-
const config = await this.readConfig()
70-
return Object.entries(config.apiConfigs).map(([name, apiConfig]) => ({
71-
name,
72-
id: apiConfig.id || "",
73-
apiProvider: apiConfig.apiProvider,
74-
}))
78+
return await this.lock(async () => {
79+
const config = await this.readConfig()
80+
return Object.entries(config.apiConfigs).map(([name, apiConfig]) => ({
81+
name,
82+
id: apiConfig.id || "",
83+
apiProvider: apiConfig.apiProvider,
84+
}))
85+
})
7586
} catch (error) {
7687
throw new Error(`Failed to list configs: ${error}`)
7788
}
@@ -82,13 +93,15 @@ export class ConfigManager {
8293
*/
8394
async saveConfig(name: string, config: ApiConfiguration): Promise<void> {
8495
try {
85-
const currentConfig = await this.readConfig()
86-
const existingConfig = currentConfig.apiConfigs[name]
87-
currentConfig.apiConfigs[name] = {
88-
...config,
89-
id: existingConfig?.id || this.generateId(),
90-
}
91-
await this.writeConfig(currentConfig)
96+
return await this.lock(async () => {
97+
const currentConfig = await this.readConfig()
98+
const existingConfig = currentConfig.apiConfigs[name]
99+
currentConfig.apiConfigs[name] = {
100+
...config,
101+
id: existingConfig?.id || this.generateId(),
102+
}
103+
await this.writeConfig(currentConfig)
104+
})
92105
} catch (error) {
93106
throw new Error(`Failed to save config: ${error}`)
94107
}
@@ -99,17 +112,19 @@ export class ConfigManager {
99112
*/
100113
async loadConfig(name: string): Promise<ApiConfiguration> {
101114
try {
102-
const config = await this.readConfig()
103-
const apiConfig = config.apiConfigs[name]
115+
return await this.lock(async () => {
116+
const config = await this.readConfig()
117+
const apiConfig = config.apiConfigs[name]
104118

105-
if (!apiConfig) {
106-
throw new Error(`Config '${name}' not found`)
107-
}
119+
if (!apiConfig) {
120+
throw new Error(`Config '${name}' not found`)
121+
}
108122

109-
config.currentApiConfigName = name
110-
await this.writeConfig(config)
123+
config.currentApiConfigName = name
124+
await this.writeConfig(config)
111125

112-
return apiConfig
126+
return apiConfig
127+
})
113128
} catch (error) {
114129
throw new Error(`Failed to load config: ${error}`)
115130
}
@@ -120,18 +135,20 @@ export class ConfigManager {
120135
*/
121136
async deleteConfig(name: string): Promise<void> {
122137
try {
123-
const currentConfig = await this.readConfig()
124-
if (!currentConfig.apiConfigs[name]) {
125-
throw new Error(`Config '${name}' not found`)
126-
}
138+
return await this.lock(async () => {
139+
const currentConfig = await this.readConfig()
140+
if (!currentConfig.apiConfigs[name]) {
141+
throw new Error(`Config '${name}' not found`)
142+
}
127143

128-
// Don't allow deleting the default config
129-
if (Object.keys(currentConfig.apiConfigs).length === 1) {
130-
throw new Error(`Cannot delete the last remaining configuration.`)
131-
}
144+
// Don't allow deleting the default config
145+
if (Object.keys(currentConfig.apiConfigs).length === 1) {
146+
throw new Error(`Cannot delete the last remaining configuration.`)
147+
}
132148

133-
delete currentConfig.apiConfigs[name]
134-
await this.writeConfig(currentConfig)
149+
delete currentConfig.apiConfigs[name]
150+
await this.writeConfig(currentConfig)
151+
})
135152
} catch (error) {
136153
throw new Error(`Failed to delete config: ${error}`)
137154
}
@@ -142,13 +159,15 @@ export class ConfigManager {
142159
*/
143160
async setCurrentConfig(name: string): Promise<void> {
144161
try {
145-
const currentConfig = await this.readConfig()
146-
if (!currentConfig.apiConfigs[name]) {
147-
throw new Error(`Config '${name}' not found`)
148-
}
162+
return await this.lock(async () => {
163+
const currentConfig = await this.readConfig()
164+
if (!currentConfig.apiConfigs[name]) {
165+
throw new Error(`Config '${name}' not found`)
166+
}
149167

150-
currentConfig.currentApiConfigName = name
151-
await this.writeConfig(currentConfig)
168+
currentConfig.currentApiConfigName = name
169+
await this.writeConfig(currentConfig)
170+
})
152171
} catch (error) {
153172
throw new Error(`Failed to set current config: ${error}`)
154173
}
@@ -159,8 +178,10 @@ export class ConfigManager {
159178
*/
160179
async hasConfig(name: string): Promise<boolean> {
161180
try {
162-
const config = await this.readConfig()
163-
return name in config.apiConfigs
181+
return await this.lock(async () => {
182+
const config = await this.readConfig()
183+
return name in config.apiConfigs
184+
})
164185
} catch (error) {
165186
throw new Error(`Failed to check config existence: ${error}`)
166187
}
@@ -171,12 +192,14 @@ export class ConfigManager {
171192
*/
172193
async setModeConfig(mode: Mode, configId: string): Promise<void> {
173194
try {
174-
const currentConfig = await this.readConfig()
175-
if (!currentConfig.modeApiConfigs) {
176-
currentConfig.modeApiConfigs = {}
177-
}
178-
currentConfig.modeApiConfigs[mode] = configId
179-
await this.writeConfig(currentConfig)
195+
return await this.lock(async () => {
196+
const currentConfig = await this.readConfig()
197+
if (!currentConfig.modeApiConfigs) {
198+
currentConfig.modeApiConfigs = {}
199+
}
200+
currentConfig.modeApiConfigs[mode] = configId
201+
await this.writeConfig(currentConfig)
202+
})
180203
} catch (error) {
181204
throw new Error(`Failed to set mode config: ${error}`)
182205
}
@@ -187,8 +210,10 @@ export class ConfigManager {
187210
*/
188211
async getModeConfigId(mode: Mode): Promise<string | undefined> {
189212
try {
190-
const config = await this.readConfig()
191-
return config.modeApiConfigs?.[mode]
213+
return await this.lock(async () => {
214+
const config = await this.readConfig()
215+
return config.modeApiConfigs?.[mode]
216+
})
192217
} catch (error) {
193218
throw new Error(`Failed to get mode config: ${error}`)
194219
}
@@ -205,7 +230,9 @@ export class ConfigManager {
205230
* Reset all configuration by deleting the stored config from secrets
206231
*/
207232
public async resetAllConfigs(): Promise<void> {
208-
await this.context.secrets.delete(this.getConfigKey())
233+
return await this.lock(async () => {
234+
await this.context.secrets.delete(this.getConfigKey())
235+
})
209236
}
210237

211238
private async readConfig(): Promise<ApiConfigData> {

src/core/webview/ClineProvider.ts

Lines changed: 63 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
13171317
}
13181318
break
13191319
}
1320+
case "saveApiConfiguration":
1321+
if (message.text && message.apiConfiguration) {
1322+
try {
1323+
await this.configManager.saveConfig(message.text, message.apiConfiguration)
1324+
const listApiConfig = await this.configManager.listConfig()
1325+
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
1326+
} catch (error) {
1327+
this.outputChannel.appendLine(
1328+
`Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
1329+
)
1330+
vscode.window.showErrorMessage("Failed to save api configuration")
1331+
}
1332+
}
1333+
break
13201334
case "upsertApiConfiguration":
13211335
if (message.text && message.apiConfiguration) {
13221336
try {
@@ -1361,9 +1375,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
13611375
await this.postStateToWebview()
13621376
} catch (error) {
13631377
this.outputChannel.appendLine(
1364-
`Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
1378+
`Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
13651379
)
1366-
vscode.window.showErrorMessage("Failed to create api configuration")
1380+
vscode.window.showErrorMessage("Failed to rename api configuration")
13671381
}
13681382
}
13691383
break
@@ -1647,51 +1661,53 @@ export class ClineProvider implements vscode.WebviewViewProvider {
16471661
requestyModelInfo,
16481662
modelTemperature,
16491663
} = apiConfiguration
1650-
await this.updateGlobalState("apiProvider", apiProvider)
1651-
await this.updateGlobalState("apiModelId", apiModelId)
1652-
await this.storeSecret("apiKey", apiKey)
1653-
await this.updateGlobalState("glamaModelId", glamaModelId)
1654-
await this.updateGlobalState("glamaModelInfo", glamaModelInfo)
1655-
await this.storeSecret("glamaApiKey", glamaApiKey)
1656-
await this.storeSecret("openRouterApiKey", openRouterApiKey)
1657-
await this.storeSecret("awsAccessKey", awsAccessKey)
1658-
await this.storeSecret("awsSecretKey", awsSecretKey)
1659-
await this.storeSecret("awsSessionToken", awsSessionToken)
1660-
await this.updateGlobalState("awsRegion", awsRegion)
1661-
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
1662-
await this.updateGlobalState("awsProfile", awsProfile)
1663-
await this.updateGlobalState("awsUseProfile", awsUseProfile)
1664-
await this.updateGlobalState("vertexProjectId", vertexProjectId)
1665-
await this.updateGlobalState("vertexRegion", vertexRegion)
1666-
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
1667-
await this.storeSecret("openAiApiKey", openAiApiKey)
1668-
await this.updateGlobalState("openAiModelId", openAiModelId)
1669-
await this.updateGlobalState("openAiCustomModelInfo", openAiCustomModelInfo)
1670-
await this.updateGlobalState("openAiUseAzure", openAiUseAzure)
1671-
await this.updateGlobalState("ollamaModelId", ollamaModelId)
1672-
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
1673-
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
1674-
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
1675-
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
1676-
await this.storeSecret("geminiApiKey", geminiApiKey)
1677-
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
1678-
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
1679-
await this.updateGlobalState("azureApiVersion", azureApiVersion)
1680-
await this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled)
1681-
await this.updateGlobalState("openRouterModelId", openRouterModelId)
1682-
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
1683-
await this.updateGlobalState("openRouterBaseUrl", openRouterBaseUrl)
1684-
await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
1685-
await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector)
1686-
await this.storeSecret("mistralApiKey", mistralApiKey)
1687-
await this.updateGlobalState("mistralCodestralUrl", mistralCodestralUrl)
1688-
await this.storeSecret("unboundApiKey", unboundApiKey)
1689-
await this.updateGlobalState("unboundModelId", unboundModelId)
1690-
await this.updateGlobalState("unboundModelInfo", unboundModelInfo)
1691-
await this.storeSecret("requestyApiKey", requestyApiKey)
1692-
await this.updateGlobalState("requestyModelId", requestyModelId)
1693-
await this.updateGlobalState("requestyModelInfo", requestyModelInfo)
1694-
await this.updateGlobalState("modelTemperature", modelTemperature)
1664+
await Promise.all([
1665+
this.updateGlobalState("apiProvider", apiProvider),
1666+
this.updateGlobalState("apiModelId", apiModelId),
1667+
this.storeSecret("apiKey", apiKey),
1668+
this.updateGlobalState("glamaModelId", glamaModelId),
1669+
this.updateGlobalState("glamaModelInfo", glamaModelInfo),
1670+
this.storeSecret("glamaApiKey", glamaApiKey),
1671+
this.storeSecret("openRouterApiKey", openRouterApiKey),
1672+
this.storeSecret("awsAccessKey", awsAccessKey),
1673+
this.storeSecret("awsSecretKey", awsSecretKey),
1674+
this.storeSecret("awsSessionToken", awsSessionToken),
1675+
this.updateGlobalState("awsRegion", awsRegion),
1676+
this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference),
1677+
this.updateGlobalState("awsProfile", awsProfile),
1678+
this.updateGlobalState("awsUseProfile", awsUseProfile),
1679+
this.updateGlobalState("vertexProjectId", vertexProjectId),
1680+
this.updateGlobalState("vertexRegion", vertexRegion),
1681+
this.updateGlobalState("openAiBaseUrl", openAiBaseUrl),
1682+
this.storeSecret("openAiApiKey", openAiApiKey),
1683+
this.updateGlobalState("openAiModelId", openAiModelId),
1684+
this.updateGlobalState("openAiCustomModelInfo", openAiCustomModelInfo),
1685+
this.updateGlobalState("openAiUseAzure", openAiUseAzure),
1686+
this.updateGlobalState("ollamaModelId", ollamaModelId),
1687+
this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl),
1688+
this.updateGlobalState("lmStudioModelId", lmStudioModelId),
1689+
this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
1690+
this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
1691+
this.storeSecret("geminiApiKey", geminiApiKey),
1692+
this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
1693+
this.storeSecret("deepSeekApiKey", deepSeekApiKey),
1694+
this.updateGlobalState("azureApiVersion", azureApiVersion),
1695+
this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled),
1696+
this.updateGlobalState("openRouterModelId", openRouterModelId),
1697+
this.updateGlobalState("openRouterModelInfo", openRouterModelInfo),
1698+
this.updateGlobalState("openRouterBaseUrl", openRouterBaseUrl),
1699+
this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform),
1700+
this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector),
1701+
this.storeSecret("mistralApiKey", mistralApiKey),
1702+
this.updateGlobalState("mistralCodestralUrl", mistralCodestralUrl),
1703+
this.storeSecret("unboundApiKey", unboundApiKey),
1704+
this.updateGlobalState("unboundModelId", unboundModelId),
1705+
this.updateGlobalState("unboundModelInfo", unboundModelInfo),
1706+
this.storeSecret("requestyApiKey", requestyApiKey),
1707+
this.updateGlobalState("requestyModelId", requestyModelId),
1708+
this.updateGlobalState("requestyModelInfo", requestyModelInfo),
1709+
this.updateGlobalState("modelTemperature", modelTemperature),
1710+
])
16951711
if (this.cline) {
16961712
this.cline.api = buildApiHandler(apiConfiguration)
16971713
}

0 commit comments

Comments
 (0)