Skip to content

Commit 8885969

Browse files
committed
fix(webview): sync view local state after profile mutations
1 parent 6655bb1 commit 8885969

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
@@ -1767,13 +1767,21 @@ export class ClineProvider
17671767
// this.contextProxy.setValues({ ...providerSettings, listApiConfigMeta: ..., currentApiConfigName: ... })
17681768
// We should probably switch to that and verify that it works.
17691769
// I left the original implementation in just to be safe.
1770+
const listApiConfigMeta = await this.providerSettingsManager.listConfig()
1771+
17701772
await Promise.all([
1771-
this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
1773+
this.updateGlobalState("listApiConfigMeta", listApiConfigMeta),
17721774
this.updateGlobalState("currentApiConfigName", name),
17731775
this.providerSettingsManager.setModeConfig(mode, id),
17741776
this.contextProxy.setProviderSettings(providerSettings),
17751777
])
17761778

1779+
this._updateViewLocalStateFromMutation({
1780+
listApiConfigMeta,
1781+
currentApiConfigName: name,
1782+
apiConfiguration: providerSettings,
1783+
})
1784+
17771785
// Change the provider for the current task.
17781786
// TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`).
17791787
this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true })
@@ -1816,6 +1824,11 @@ export class ClineProvider
18161824
listApiConfigMeta: entries,
18171825
})
18181826

1827+
this._updateViewLocalStateFromMutation({
1828+
currentApiConfigName: profileToActivate,
1829+
listApiConfigMeta: entries,
1830+
})
1831+
18191832
await this.postStateToWebview()
18201833
}
18211834

@@ -1857,12 +1870,20 @@ export class ClineProvider
18571870
const persistTaskHistory = options?.persistTaskHistory ?? true
18581871

18591872
// See `upsertProviderProfile` for a description of what this is doing.
1873+
const listApiConfigMeta = await this.providerSettingsManager.listConfig()
1874+
18601875
await Promise.all([
1861-
this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
1876+
this.contextProxy.setValue("listApiConfigMeta", listApiConfigMeta),
18621877
this.contextProxy.setValue("currentApiConfigName", name),
18631878
this.contextProxy.setProviderSettings(providerSettings),
18641879
])
18651880

1881+
this._updateViewLocalStateFromMutation({
1882+
listApiConfigMeta,
1883+
currentApiConfigName: name,
1884+
apiConfiguration: providerSettings,
1885+
})
1886+
18661887
const { mode } = await this.getState()
18671888

18681889
if (id && persistModeConfig) {
@@ -3007,7 +3028,7 @@ export class ClineProvider
30073028
* profile upsert/activation/deletion, or resetState. This ensures the local cache stays in
30083029
* sync with global state changes that would otherwise be invisible behind mergedStateValues.
30093030
*/
3010-
private _updateViewLocalStateFromMutation(values: Partial<RooCodeSettings>): void {
3031+
private _updateViewLocalStateFromMutation(values: Partial<RooCodeSettings> & Partial<ExtensionState>): void {
30113032
if ("mode" in values) {
30123033
const val = values.mode
30133034
if (val === undefined || val === null) {
@@ -3083,6 +3104,9 @@ export class ClineProvider
30833104

30843105
await this.contextProxy.resetAllState()
30853106

3107+
// Clear view-local state cache so getState() falls back to ContextProxy defaults.
3108+
this._clearViewLocalState()
3109+
30863110
await this.providerSettingsManager.resetAllConfigs()
30873111
await this.customModesManager.resetCustomModes()
30883112
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)