Skip to content

fix(webview): isolate parallel provider view state#909

Open
easonLiangWorldedtech wants to merge 7 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fix/tab-mode-model-isolation
Open

fix(webview): isolate parallel provider view state#909
easonLiangWorldedtech wants to merge 7 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fix/tab-mode-model-isolation

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes multi-tab mode/model settings being overwritten by global state from other tabs. Each ClineProvider instance (sidebar, editor tab) now maintains its own isolated mode, apiConfiguration, and profile selection via a per-view local state buffer with view-specific persistence keys.

Problem

In multi-tab mode, when users switch between Roo Code tabs with different mode/API configuration selections, the active tabs settings get overwritten by the global state from another tab. This happens because all tabs share the same ContextProxy singleton — any handleModeSwitch() or activateProviderProfile() call in one tab updates shared global state that other tabs then read.

Solution

Implemented view-local state isolation using a per-provider-instance buffer with view-specific persistence keys:

Architecture

  • Added nextViewId static counter for stable, monotonically increasing view IDs ({renderContext}-{counter})
  • Implemented viewLocalState: Partial<ExtensionState> buffer per provider instance
  • Introduced view-specific ContextProxy keys — each tab persists its state under __view_state_{viewId}_{key} so recreated views restore their own values instead of the last writer shared state
  • Modified getState() to merge viewLocalState on top of global state with proper precedence

Key Changes in src/core/webview/ClineProvider.ts:

Method Change
loadViewState() Reads from view-specific keys first (__view_state_{id}_mode), falls back to shared keys for backward compatibility
saveViewState(key, value) Persists mode/currentApiConfigName/apiConfiguration to view-specific ContextProxy key instead of global key
_updateViewLocalStateFromMutation(values) New helper — syncs viewLocalState when ContextProxy is mutated via setValues(), setValue(), profile upsert/activation/deletion, or resetState()
setupGlobalStateListener() Listens for VSCode config changes to reload viewLocalState when other views modify global state
handleModeSwitch() Calls _updateViewLocalStateFromMutation + reads currentApiConfigName from getState() (not direct global read) before updateGlobalState()
activateProviderProfile() / deleteProviderProfile() Uses _updateViewLocalStateFromMutation for consistent sync
createTaskWithHistoryItem() Preserves restored task mode in both global state AND viewLocalState; reads currentApiConfigName from merged state
setValue() / setValues() Delegates to ContextProxy then calls _updateViewLocalStateFromMutation
resetState() Calls _clearViewLocalState() after resetting ContextProxy

View-Specific Persistence Keys:

// Each tab persists under its own key so recreation restores correct values
__view_state_sidebar-0_mode
__view_state_sidebar-0_currentApiConfigName
__view_state_editor-1_apiConfiguration
// ...etc

Merge Precedence in getState():

const mergedStateValues = { ...stateValues, ...this.viewLocalState }
// apiConfiguration: { ...providerSettings, ...mergedStateValues.apiConfiguration }
// All other fields use mergedStateValues for local override support

Additional Changes (beyond ClineProvider)

File Change
src/extension/api.ts API.setConfiguration() now calls sidebarProvider.setValues(values) instead of directly calling contextProxy.setValues(), ensuring _updateViewLocalStateFromMutation is triggered
webview-ui/src/utils/vscode.ts Added persistent viewStateId to webview launch handshake — each tab gets a unique ID stored in ContextProxy for cross-session persistence
webview-ui/src/App.tsx / ExtensionStateContext.tsx Pass and consume viewStateId from extension host context

Testing

22+ new unit tests across multiple test files:

  • ClineProvider.parallelMode.spec.ts — viewId uniqueness, mode/apiConfig isolation, saveViewState/loadViewState with undefined clearing, getState merge precedence (local overrides global), GlobalState listener handling, handleModeSwitch/activateProviderProfile integration, multi-instance isolation (3+ concurrent providers)
  • api-set-configuration.spec.ts — verifies API.setConfiguration() triggers _updateViewLocalStateFromMutation and propagates to view-local state

Regression: All 2179 existing core tests pass ✅

Related Issue

Closes #908

Summary by CodeRabbit

  • New Features

    • Added per-view “parallel mode” isolation using unique view identifiers to keep mode/profile and API configuration scoped per webview instance.
    • Extended the webview launch handshake with an optional persistent viewStateId so view state can be reliably correlated.
  • Bug Fixes

    • Prevented cross-view interference by ensuring view-local overrides take priority and remain synchronized during mode switches and configuration updates.
  • Tests

    • Added/updated automated coverage for parallel-mode behavior, viewStateId propagation, and configuration-state merging.

Add view-local state loading and syncing for parallel ClineProvider instances,
with safe initialization and configuration listener handling for test/shim
environments.

Key changes:
- Add viewId property for unique provider instance identification
- Implement viewLocalState buffer to isolate mode, profile, and apiConfig per view
- Load view-local state after provider dependencies are initialized
- Merge viewLocalState in getState() with local apiConfiguration taking precedence
- Clear local overrides when saveViewState receives undefined or null values
- Sync mode/profile changes through saveViewState(), loadViewState(), and profile activation
- Preserve restored task mode in both global state and viewLocalState
- Guard global configuration listener setup when VS Code workspace events are unavailable
- Keep sticky-mode task restore compatible with view-local state isolation

Tests:
- Add 22 parallel mode cases covering viewId uniqueness, state isolation,
  save/load behavior, merge precedence, local override clearing, config listener
  handling, mode switching, profile activation, and multi-instance isolation
- Verify sticky-mode restore compatibility
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

ClineProvider now isolates mode and API profile state per view instance, synchronizes local state with shared context changes, and merges local overrides into ExtensionState. View-state identifiers flow through webview launch messages, and API configuration updates use the provider mutation path.

Changes

Parallel view state isolation

Layer / File(s) Summary
View identity and initialization
src/core/webview/ClineProvider.ts, webview-ui/src/..., src/core/webview/webviewMessageHandler.ts, packages/types/src/vscode-extension-host.ts, src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts
Providers receive unique identifiers, initialize view-local state, load persisted values, and receive stable view-state identifiers from the webview launch handshake.
View state persistence and synchronization
src/core/webview/ClineProvider.ts, src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts, src/core/webview/__tests__/ClineProvider.spec.ts
Mode, profile, task restoration, and mutation paths update local state; configuration changes reload local values and repost state.
Local-first state projection
src/core/webview/ClineProvider.ts, src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts
getState() overlays view-local values on global state for mode, API configuration, profiles, and related fields, with multi-instance isolation coverage.
API configuration integration
src/extension/api.ts, src/extension/__tests__/api-set-configuration.spec.ts
setConfiguration() updates through ClineProvider.setValues(), persists the profile, posts state, and verifies the resulting API configuration.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Webview
  participant webviewMessageHandler
  participant ClineProvider
  participant ContextProxy
  Webview->>webviewMessageHandler: webviewDidLaunch(viewStateId)
  webviewMessageHandler->>ClineProvider: setViewStateId(viewStateId)
  ClineProvider->>ContextProxy: loadViewState()
  ContextProxy-->>ClineProvider: return persisted view-local state
  Webview->>ClineProvider: switch mode or activate provider profile
  ClineProvider->>ClineProvider: saveViewState()
  ClineProvider->>ContextProxy: update shared state
  ClineProvider-->>Webview: post merged ExtensionState
Loading

Suggested reviewers: taltas

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement per-view state isolation, preserve backward compatibility, and address the tab-overwrite bug in #908.
Out of Scope Changes check ✅ Passed The additional API, message, and test updates support the same view-state isolation work and do not appear unrelated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title is concise and accurately summarizes the main change: isolating parallel webview provider state.
Description check ✅ Passed It covers the issue link, problem, solution, and testing, though several optional template sections are not filled out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.54971% with 23 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/webview/ClineProvider.ts 88.59% 9 Missing and 8 partials ⚠️
webview-ui/src/utils/vscode.ts 66.66% 2 Missing and 3 partials ⚠️
webview-ui/src/App.tsx 66.66% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts`:
- Around line 1118-1143: Update the mode-switch section of the test “should
handle mode switch in one instance without affecting others” to call
provider1.handleModeSwitch("architect") instead of the private saveViewState
method, while preserving the existing state assertions for both providers.
- Around line 829-857: Update the configuration-change test around ClineProvider
and setupGlobalStateListener to capture the registered onDidChangeConfiguration
handler, spy on loadViewState(), invoke the handler with configChangeEvent, and
assert that loadViewState() is called. Remove the unused event-only setup and
the weak disposables.length assertion.

In `@src/core/webview/ClineProvider.ts`:
- Around line 449-458: Update saveViewState() to persist each value through a
view-specific ContextProxy key derived from the current viewId, rather than the
shared ExtensionState key. Keep the existing viewLocalState cache update
behavior, and ensure corresponding view-state reads use the same derived key so
recreated views restore their own mode, profile, and configuration.
- Around line 1600-1602: Update the mode-switch flow around
saveViewState("mode", newMode) so the no-saved-configuration path reads the
profile configuration name from this view’s getState() result rather than shared
currentApiConfigName. Preserve the existing mode-switch behavior while ensuring
another tab’s global state cannot supply this view’s configuration.
- Around line 2674-2676: Update the state mutation paths around setMode(),
setValues(), profile upsert/deletion, and resetState() so local-key changes also
update or invalidate viewLocalState instead of being hidden by
mergedStateValues. Centralize the local-key mutation handling, and explicitly
clear viewLocalState during resetState() so reset returns current persisted
defaults rather than stale mode, profile, or configuration values.
- Around line 241-243: Update the viewId initialization in the ClineProvider
constructor to use a monotonically increasing instance identifier rather than
ClineProvider.activeInstances.size, ensuring IDs remain unique after instances
are disposed; keep activeInstances tracking unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b3c9918-8ed9-4d37-8d1e-80201da59ec5

📥 Commits

Reviewing files that changed from the base of the PR and between c39535e and dcce266.

📒 Files selected for processing (2)
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts

Comment thread src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts Outdated
Comment thread src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts
Comment thread src/core/webview/ClineProvider.ts Outdated
Comment thread src/core/webview/ClineProvider.ts Outdated
Comment thread src/core/webview/ClineProvider.ts
Comment thread src/core/webview/ClineProvider.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts`:
- Around line 750-752: Update the view-state key construction used by
saveViewState and the related test expectation so it uses a stable per-view
identifier rather than the construction-order suffix from viewId. Ensure
restored tabs consistently read and write their own persisted mode and API
configuration, and remove reliance on the shared-key fallback for this state.
- Around line 862-877: Update loadViewState in ClineProvider to read
apiConfiguration from the view-specific persisted state key rather than shared
provider settings, preserving the tab’s model/provider configuration across
reloads. Add or adjust coverage in the parallel-mode tests by seeding per-view
apiConfiguration and asserting it is restored after loading, while keeping
existing failure behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 530ae001-d224-4694-b647-70aef27ee0f4

📥 Commits

Reviewing files that changed from the base of the PR and between dcce266 and a75d707.

📒 Files selected for processing (5)
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts
  • src/core/webview/__tests__/ClineProvider.spec.ts
  • src/extension/__tests__/api-set-configuration.spec.ts
  • src/extension/api.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/webview/ClineProvider.ts

Comment thread src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts Outdated
Comment thread src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/webview/webviewMessageHandler.ts`:
- Around line 561-562: Update downstream state access in the webview message
handler to use view-local state after setViewStateId initializes the provider.
Replace the currentApiConfigName read near line 607 with the current value from
await provider.getState(), and replace both updateGlobalState("mode", ...) calls
near lines 2200 and 2296 with await provider.handleModeSwitch(...) so mode
changes apply to the active tab.

In `@webview-ui/src/App.tsx`:
- Line 194: Remove the duplicate webviewDidLaunch useEffect from App, keeping
the launch handshake in ExtensionStateContextProvider; update
webview-ui/src/App.tsx lines 194-194 and leave
webview-ui/src/context/ExtensionStateContext.tsx lines 492-492 unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 045a32de-dd60-4dec-9b66-4f1e3da5467f

📥 Commits

Reviewing files that changed from the base of the PR and between 1922745 and e77ca7e.

📒 Files selected for processing (8)
  • packages/types/src/vscode-extension-host.ts
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts
  • src/core/webview/webviewMessageHandler.ts
  • webview-ui/src/App.tsx
  • webview-ui/src/__tests__/App.spec.tsx
  • webview-ui/src/context/ExtensionStateContext.tsx
  • webview-ui/src/utils/vscode.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/core/webview/ClineProvider.ts
  • src/core/webview/tests/ClineProvider.parallelMode.spec.ts

Comment on lines +561 to +562
await provider.setViewStateId(message.viewStateId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Update downstream accesses to use view-local state.

While setViewStateId correctly initializes the view-local buffer, several downstream handlers in this file still bypass it by using getGlobalState or updateGlobalState for isolated fields (mode and currentApiConfigName). This causes them to interact with the shared fallback state rather than the current tab's isolated state.

Please update the following locations to use view-local methods:

  • Line 607: const currentConfigName = getGlobalState("currentApiConfigName") should use (await provider.getState()).currentApiConfigName so it validates the local profile.
  • Lines 2200 & 2296: updateGlobalState("mode", ...) should use await provider.handleModeSwitch(...) so the current tab actually switches into the created/default mode.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/webview/webviewMessageHandler.ts` around lines 561 - 562, Update
downstream state access in the webview message handler to use view-local state
after setViewStateId initializes the provider. Replace the currentApiConfigName
read near line 607 with the current value from await provider.getState(), and
replace both updateGlobalState("mode", ...) calls near lines 2200 and 2296 with
await provider.handleModeSwitch(...) so mode changes apply to the active tab.

Comment thread webview-ui/src/App.tsx Outdated

// Tell the extension that we are ready to receive messages.
useEffect(() => vscode.postMessage({ type: "webviewDidLaunch" }), [])
useEffect(() => vscode.postMessage({ type: "webviewDidLaunch", viewStateId: vscode.getViewStateId() }), [])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Duplicate webviewDidLaunch messages on startup.

Both App and ExtensionStateContextProvider broadcast the webviewDidLaunch message when they mount. Since App is wrapped by this context provider, the backend receives two concurrent launch messages on every reload, causing its full initialization sequence (fetching custom modes, themes, API profiles, MCP servers) to execute twice.

Please remove the duplicate call from one of the files:

  • webview-ui/src/App.tsx#L194-L194: Remove this useEffect if the context provider is responsible for the launch handshake.
  • webview-ui/src/context/ExtensionStateContext.tsx#L492-L492: Alternatively, remove this useEffect if App should drive the handshake.
📍 Affects 2 files
  • webview-ui/src/App.tsx#L194-L194 (this comment)
  • webview-ui/src/context/ExtensionStateContext.tsx#L492-L492
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webview-ui/src/App.tsx` at line 194, Remove the duplicate webviewDidLaunch
useEffect from App, keeping the launch handshake in
ExtensionStateContextProvider; update webview-ui/src/App.tsx lines 194-194 and
leave webview-ui/src/context/ExtensionStateContext.tsx lines 492-492 unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Multi-tab mode/model settings get overwritten by latest task

2 participants