Skip to content

Commit 2940c14

Browse files
committed
fix(webview): sync view local state after profile mutations
1 parent 3a46a09 commit 2940c14

2 files changed

Lines changed: 133 additions & 3 deletions

File tree

src/core/webview/ClineProvider.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,13 +1800,21 @@ export class ClineProvider
18001800
// this.contextProxy.setValues({ ...providerSettings, listApiConfigMeta: ..., currentApiConfigName: ... })
18011801
// We should probably switch to that and verify that it works.
18021802
// I left the original implementation in just to be safe.
1803+
const listApiConfigMeta = await this.providerSettingsManager.listConfig()
1804+
18031805
await Promise.all([
1804-
this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
1806+
this.updateGlobalState("listApiConfigMeta", listApiConfigMeta),
18051807
this.updateGlobalState("currentApiConfigName", name),
18061808
this.providerSettingsManager.setModeConfig(mode, id),
18071809
this.contextProxy.setProviderSettings(providerSettings),
18081810
])
18091811

1812+
this._updateViewLocalStateFromMutation({
1813+
listApiConfigMeta,
1814+
currentApiConfigName: name,
1815+
apiConfiguration: providerSettings,
1816+
})
1817+
18101818
// Change the provider for the current task.
18111819
// TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`).
18121820
this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true })
@@ -1849,6 +1857,11 @@ export class ClineProvider
18491857
listApiConfigMeta: entries,
18501858
})
18511859

1860+
this._updateViewLocalStateFromMutation({
1861+
currentApiConfigName: profileToActivate,
1862+
listApiConfigMeta: entries,
1863+
})
1864+
18521865
await this.postStateToWebview()
18531866
}
18541867

@@ -1890,12 +1903,20 @@ export class ClineProvider
18901903
const persistTaskHistory = options?.persistTaskHistory ?? true
18911904

18921905
// See `upsertProviderProfile` for a description of what this is doing.
1906+
const listApiConfigMeta = await this.providerSettingsManager.listConfig()
1907+
18931908
await Promise.all([
1894-
this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
1909+
this.contextProxy.setValue("listApiConfigMeta", listApiConfigMeta),
18951910
this.contextProxy.setValue("currentApiConfigName", name),
18961911
this.contextProxy.setProviderSettings(providerSettings),
18971912
])
18981913

1914+
this._updateViewLocalStateFromMutation({
1915+
listApiConfigMeta,
1916+
currentApiConfigName: name,
1917+
apiConfiguration: providerSettings,
1918+
})
1919+
18991920
const { mode } = await this.getState()
19001921

19011922
if (id && persistModeConfig) {
@@ -3040,7 +3061,7 @@ export class ClineProvider
30403061
* profile upsert/activation/deletion, or resetState. This ensures the local cache stays in
30413062
* sync with global state changes that would otherwise be invisible behind mergedStateValues.
30423063
*/
3043-
private _updateViewLocalStateFromMutation(values: Partial<RooCodeSettings>): void {
3064+
private _updateViewLocalStateFromMutation(values: Partial<RooCodeSettings> & Partial<ExtensionState>): void {
30443065
if ("mode" in values) {
30453066
const val = values.mode
30463067
if (val === undefined || val === null) {
@@ -3116,6 +3137,9 @@ export class ClineProvider
31163137

31173138
await this.contextProxy.resetAllState()
31183139

3140+
// Clear view-local state cache so getState() falls back to ContextProxy defaults.
3141+
this._clearViewLocalState()
3142+
31193143
await this.providerSettingsManager.resetAllConfigs()
31203144
await this.customModesManager.resetCustomModes()
31213145
await this.removeClineFromStack()

src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,10 @@ vi.mock("../../config/ContextProxy", () => {
290290
)
291291
})
292292
setProviderSettings = vi.fn().mockImplementation((settings: Record<string, any>) => this.setValues(settings))
293+
resetAllState = vi.fn().mockImplementation(() => {
294+
const keys = this.context?.globalState?.keys?.() ?? []
295+
return Promise.all(keys.map((key: string) => this.setValue(key, undefined))).then(() => undefined)
296+
})
293297
}
294298
return { ContextProxy: MockContextProxy }
295299
})
@@ -482,6 +486,7 @@ vi.mock("../../config/ProviderSettingsManager", () => ({
482486
})),
483487
setModeConfig: vi.fn().mockResolvedValue(undefined),
484488
getModeConfigId: vi.fn().mockResolvedValue(undefined),
489+
resetAllConfigs: vi.fn().mockResolvedValue(undefined),
485490
}
486491
}),
487492
}))
@@ -492,6 +497,7 @@ vi.mock("../../config/CustomModesManager", () => ({
492497
return {
493498
updateCustomMode: vi.fn().mockResolvedValue(undefined),
494499
getCustomModes: vi.fn().mockResolvedValue([]),
500+
resetCustomModes: vi.fn().mockResolvedValue(undefined),
495501
dispose: vi.fn(),
496502
}
497503
}),
@@ -1038,6 +1044,106 @@ describe("ClineProvider - Parallel Mode Support", () => {
10381044
})
10391045
})
10401046

1047+
describe("profile mutations", () => {
1048+
it("should synchronize viewLocalState when activateProviderProfile mutates ContextProxy", async () => {
1049+
const provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
1050+
vi.spyOn(provider.providerSettingsManager, "activateProfile").mockResolvedValueOnce({
1051+
name: "new-profile",
1052+
id: "new-profile-id",
1053+
apiProvider: "openrouter",
1054+
openRouterModelId: "openrouter/new-model",
1055+
} as any)
1056+
vi.spyOn(provider.providerSettingsManager, "listConfig").mockResolvedValueOnce([
1057+
{ id: "new-profile-id", name: "new-profile", apiProvider: "openrouter" },
1058+
] as any)
1059+
;(provider as any).viewLocalState = {
1060+
currentApiConfigName: "stale-profile",
1061+
apiConfiguration: { apiProvider: "anthropic" },
1062+
}
1063+
1064+
await provider.activateProviderProfile({ name: "new-profile" })
1065+
const state = await provider.getState()
1066+
1067+
expect(state.currentApiConfigName).toBe("new-profile")
1068+
expect(state.apiConfiguration).toMatchObject({
1069+
apiProvider: "openrouter",
1070+
openRouterModelId: "openrouter/new-model",
1071+
})
1072+
1073+
await provider.dispose()
1074+
})
1075+
1076+
it("should synchronize viewLocalState when upsertProviderProfile activates a saved profile", async () => {
1077+
const provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
1078+
vi.spyOn(provider.providerSettingsManager, "listConfig").mockResolvedValue([
1079+
{ id: "test-id", name: "saved-profile", apiProvider: "bedrock" },
1080+
] as any)
1081+
;(provider as any).viewLocalState = {
1082+
currentApiConfigName: "stale-profile",
1083+
apiConfiguration: { apiProvider: "anthropic" },
1084+
}
1085+
1086+
await provider.upsertProviderProfile("saved-profile", {
1087+
apiProvider: "bedrock",
1088+
awsRegion: "us-east-1",
1089+
} as any)
1090+
const state = await provider.getState()
1091+
1092+
expect(state.currentApiConfigName).toBe("saved-profile")
1093+
expect(state.apiConfiguration).toMatchObject({
1094+
apiProvider: "bedrock",
1095+
awsRegion: "us-east-1",
1096+
})
1097+
1098+
await provider.dispose()
1099+
})
1100+
1101+
it("should synchronize viewLocalState when deleteProviderProfile selects a replacement profile", async () => {
1102+
const provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
1103+
await provider.contextProxy.setValue("currentApiConfigName" as any, "deleted-profile")
1104+
await provider.contextProxy.setValue("listApiConfigMeta" as any, [
1105+
{ id: "deleted-id", name: "deleted-profile", apiProvider: "anthropic" },
1106+
{ id: "replacement-id", name: "replacement-profile", apiProvider: "openrouter" },
1107+
])
1108+
;(provider as any).viewLocalState = {
1109+
currentApiConfigName: "deleted-profile",
1110+
apiConfiguration: { apiProvider: "anthropic" },
1111+
}
1112+
1113+
await provider.deleteProviderProfile({
1114+
id: "deleted-id",
1115+
name: "deleted-profile",
1116+
apiProvider: "anthropic",
1117+
} as any)
1118+
const state = await provider.getState()
1119+
1120+
expect(state.currentApiConfigName).toBe("replacement-profile")
1121+
expect(state.listApiConfigMeta).toEqual([
1122+
{ id: "replacement-id", name: "replacement-profile", apiProvider: "openrouter" },
1123+
])
1124+
1125+
await provider.dispose()
1126+
})
1127+
1128+
it("should clear viewLocalState when resetState resets ContextProxy", async () => {
1129+
vi.mocked(vscode.window.showInformationMessage).mockImplementationOnce(
1130+
async (_message: string, _options: unknown, confirm: unknown) => confirm as any,
1131+
)
1132+
const provider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", new ContextProxy(mockContext))
1133+
;(provider as any).viewLocalState = {
1134+
mode: "architect",
1135+
currentApiConfigName: "stale-profile",
1136+
apiConfiguration: { apiProvider: "openrouter" },
1137+
}
1138+
1139+
await provider.resetState()
1140+
1141+
expect((provider as any).viewLocalState).toEqual({})
1142+
1143+
await provider.dispose()
1144+
})
1145+
})
1146+
10411147
describe("handleModeSwitch integration", () => {
10421148
it("should update viewLocalState.mode when handleModeSwitch is called", async () => {
10431149
const postMessage = vi.fn()

0 commit comments

Comments
 (0)