Skip to content

Commit 3519c67

Browse files
committed
fix(webview): isolate view-local state for parallel mode instances
1 parent 5319dfd commit 3519c67

3 files changed

Lines changed: 211 additions & 64 deletions

File tree

src/core/webview/ClineProvider.ts

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export class ClineProvider
161161
public static readonly sideBarId = `${Package.name}.SidebarProvider`
162162
public static readonly tabPanelId = `${Package.name}.TabPanelProvider`
163163
private static activeInstances: Set<ClineProvider> = new Set()
164+
private static nextViewId = 0
164165
private disposables: vscode.Disposable[] = []
165166
private webviewDisposables: vscode.Disposable[] = []
166167
private view?: vscode.WebviewView | vscode.WebviewPanel
@@ -238,8 +239,9 @@ export class ClineProvider
238239
mdmService?: MdmService,
239240
) {
240241
super()
241-
// Initialize viewId based on renderContext and instance counter for uniqueness
242-
this.viewId = `${renderContext}-${ClineProvider.activeInstances.size}`
242+
// Initialize viewId based on renderContext and monotonically increasing instance identifier for uniqueness.
243+
// activeInstances is used for visibility/iteration checks, so we keep tracking instances separately.
244+
this.viewId = `${renderContext}-${ClineProvider.nextViewId++}`
243245
ClineProvider.activeInstances.add(this)
244246
this.currentWorkspacePath = getWorkspacePath()
245247
this.pendingEditOperations = new PendingEditOperationStore(
@@ -418,18 +420,33 @@ export class ClineProvider
418420
}
419421
}
420422

423+
/**
424+
* Derive a view-specific ContextProxy key for persisting view-local state.
425+
* Uses the current viewId so each parallel tab restores its own values on recreation.
426+
*/
427+
private viewStateKeyFor(key: "mode" | "currentApiConfigName" | "apiConfiguration"): string {
428+
return `__view_state_${this.viewId}_${key}`
429+
}
430+
421431
/**
422432
* Loads initial state from global state into the view-local state buffer.
423-
* This allows each provider instance to have its own isolated state for fields like mode,
424-
* apiConfiguration, etc., while still sharing the same ContextProxy singleton.
433+
* For mode, currentApiConfigName, and apiConfiguration, reads from the view-specific key first;
434+
* falls back to the shared key for backward compatibility with existing persisted state.
425435
*/
426436
private async loadViewState(): Promise<void> {
427437
try {
428438
const stateValues = this.contextProxy.getValues()
429439
const providerSettings = this.contextProxy.getProviderSettings()
440+
441+
// Try view-specific keys first, then fall back to shared keys for backward compatibility.
442+
const getViewSpecificValue = (sharedKey: "mode" | "currentApiConfigName") => {
443+
const viewKey = this.viewStateKeyFor(sharedKey)
444+
return (this.contextProxy.getValue(viewKey as any) as any) ?? stateValues[sharedKey]
445+
}
446+
430447
this.viewLocalState = {
431-
mode: stateValues.mode,
432-
currentApiConfigName: stateValues.currentApiConfigName,
448+
mode: getViewSpecificValue("mode"),
449+
currentApiConfigName: getViewSpecificValue("currentApiConfigName"),
433450
apiConfiguration: providerSettings,
434451
customModePrompts: stateValues.customModePrompts,
435452
modeApiConfigs: stateValues.modeApiConfigs,
@@ -443,7 +460,7 @@ export class ClineProvider
443460
}
444461

445462
/**
446-
* Save a single view-local state value and sync to global state.
463+
* Save a single view-local state value and sync to global state using a view-specific key.
447464
* This allows each Provider instance to have its own mode/apiConfig for parallel mode support.
448465
*/
449466
private async saveViewState(key: keyof ExtensionState, value: any): Promise<void> {
@@ -454,8 +471,12 @@ export class ClineProvider
454471
this.viewLocalState[key] = value
455472
}
456473

457-
// Also write to ContextProxy so other Provider instances can see it when they sync
458-
await this.contextProxy.setValue(key as any, value)
474+
// Persist to view-specific ContextProxy key for mode/currentApiConfigName/apiConfiguration,
475+
// so recreated views restore their own values instead of the last writer's shared state.
476+
if (key === "mode" || key === "currentApiConfigName" || key === "apiConfiguration") {
477+
const viewKey = this.viewStateKeyFor(key as "mode" | "currentApiConfigName" | "apiConfiguration")
478+
await this.contextProxy.setValue(viewKey as any, value)
479+
}
459480

460481
this.log(`[saveViewState] Saved ${String(key)} for viewId ${this.viewId}`)
461482
}
@@ -1642,7 +1663,9 @@ export class ClineProvider
16421663
}
16431664
} else {
16441665
// If no saved config for this mode, save current config as default.
1645-
const currentApiConfigNameAfter = this.getGlobalState("currentApiConfigName")
1666+
// Use view-local state (via getState()) so another tab's global write doesn't supply
1667+
// this view's configuration when running in parallel mode.
1668+
const { currentApiConfigName: currentApiConfigNameAfter } = await this.getState()
16461669

16471670
if (currentApiConfigNameAfter) {
16481671
const config = listApiConfig.find((c) => c.name === currentApiConfigNameAfter)
@@ -1741,6 +1764,10 @@ export class ClineProvider
17411764
this.contextProxy.setProviderSettings(providerSettings),
17421765
])
17431766

1767+
// Update view-local state for parallel mode support.
1768+
this._updateViewLocalStateFromMutation({ currentApiConfigName: name } as Partial<RooCodeSettings>)
1769+
this.viewLocalState.apiConfiguration = providerSettings
1770+
17441771
// Change the provider for the current task.
17451772
// TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`).
17461773
this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true })
@@ -1783,6 +1810,9 @@ export class ClineProvider
17831810
listApiConfigMeta: entries,
17841811
})
17851812

1813+
// Update view-local state for parallel mode support.
1814+
this._updateViewLocalStateFromMutation({ currentApiConfigName: profileToActivate })
1815+
17861816
await this.postStateToWebview()
17871817
}
17881818

@@ -1830,8 +1860,8 @@ export class ClineProvider
18301860
this.contextProxy.setProviderSettings(providerSettings),
18311861
])
18321862

1833-
// Save to view-local state for parallel mode support.
1834-
await this.saveViewState("currentApiConfigName", name)
1863+
// Update view-local state for parallel mode support.
1864+
this._updateViewLocalStateFromMutation({ currentApiConfigName: name } as Partial<RooCodeSettings>)
18351865
this.viewLocalState.apiConfiguration = providerSettings
18361866

18371867
const { mode } = await this.getState()
@@ -2966,6 +2996,7 @@ export class ClineProvider
29662996

29672997
public async setValue<K extends keyof RooCodeSettings>(key: K, value: RooCodeSettings[K]) {
29682998
await this.contextProxy.setValue(key, value)
2999+
this._updateViewLocalStateFromMutation({ [key]: value })
29693000
}
29703001

29713002
public getValue<K extends keyof RooCodeSettings>(key: K) {
@@ -2978,6 +3009,48 @@ export class ClineProvider
29783009

29793010
public async setValues(values: RooCodeSettings) {
29803011
await this.contextProxy.setValues(values)
3012+
this._updateViewLocalStateFromMutation(values)
3013+
}
3014+
3015+
/**
3016+
* Update or invalidate viewLocalState when ContextProxy is mutated via setValues, setValue,
3017+
* profile upsert/activation/deletion, or resetState. This ensures the local cache stays in
3018+
* sync with global state changes that would otherwise be invisible behind mergedStateValues.
3019+
*/
3020+
private _updateViewLocalStateFromMutation(values: Partial<RooCodeSettings>): void {
3021+
if ("mode" in values) {
3022+
const val = values.mode
3023+
if (val === undefined || val === null) {
3024+
delete this.viewLocalState.mode
3025+
} else {
3026+
this.viewLocalState.mode = val as any
3027+
}
3028+
}
3029+
3030+
if ("currentApiConfigName" in values) {
3031+
const val = values.currentApiConfigName
3032+
if (val === undefined || val === null) {
3033+
delete this.viewLocalState.currentApiConfigName
3034+
} else {
3035+
this.viewLocalState.currentApiConfigName = val as any
3036+
}
3037+
}
3038+
3039+
if ("apiConfiguration" in values) {
3040+
const val = (values as any).apiConfiguration
3041+
if (val === undefined || val === null) {
3042+
delete this.viewLocalState.apiConfiguration
3043+
} else {
3044+
this.viewLocalState.apiConfiguration = val
3045+
}
3046+
}
3047+
}
3048+
3049+
/**
3050+
* Clear view-local state cache so that getState() falls back to ContextProxy defaults.
3051+
*/
3052+
private _clearViewLocalState(): void {
3053+
this.viewLocalState = {}
29813054
}
29823055

29833056
// dev
@@ -3006,6 +3079,8 @@ export class ClineProvider
30063079
}
30073080

30083081
await this.contextProxy.resetAllState()
3082+
this._clearViewLocalState()
3083+
30093084
await this.providerSettingsManager.resetAllConfigs()
30103085
await this.customModesManager.resetCustomModes()
30113086
await this.removeClineFromStack()

0 commit comments

Comments
 (0)