Skip to content

fix(webview): add view-local state base - #1138

Draft
easonLiangWorldedtech wants to merge 4 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/view-local-state-core
Draft

fix(webview): add view-local state base#1138
easonLiangWorldedtech wants to merge 4 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/view-local-state-core

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Add the foundational per-view state infrastructure for parallel mode. This is Branch 1 of 3 split from PR #977 for easier review.

Changes

  • feat(webview): add view-local state base (+1,364 / -97)
  • fix(webview): persist per-view selections (+175 / -49)
  • fix(webview): route mode switches (+43 / -15)
  • chore: remove invisible chars (+1 / -1)

~1,583 lines changed

Related

Summary by CodeRabbit

  • New Features

    • Added durable, per-view state for mode and API configuration selections.
    • Preserved independent settings across multiple simultaneous views.
    • Restored view-specific settings when reopening a view.
    • Added automatic view-state identification and persistence.
  • Bug Fixes

    • Improved initialization to use the correct view-specific API configuration.
    • Safely handles unavailable browser storage.
  • Tests

    • Expanded coverage for parallel views, persistence, restoration, switching, and state cleanup.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds durable, bounded per-view state for parallel webviews. It propagates stable view identifiers from the webview, persists non-secret mode and API configuration selections, hydrates and merges local state, and updates related tests.

Changes

Per-view state persistence

Layer / File(s) Summary
State contracts and identifiers
packages/types/src/global-settings.ts, packages/types/src/vscode-extension-host.ts, packages/types/src/__tests__/index.test.ts, webview-ui/src/utils/vscode.ts
Defines validated viewStates, registers the durable key, adds viewStateId to launch messages, and persists generated identifiers with safe browser-storage guards.
Provider state persistence and merging
src/core/webview/ClineProvider.ts
Adds per-provider view identity, bounded persistence and pruning, state hydration, local/global state merging, provider-setting synchronization, and local-state clearing.
Launch handshake and configuration initialization
webview-ui/src/context/ExtensionStateContext.tsx, webview-ui/src/App.tsx, src/core/webview/webviewMessageHandler.ts, webview-ui/src/__tests__/App.spec.tsx
Moves launch notification into the extension state flow, sends the view identifier, records it in ClineProvider, and reads the view-local API configuration during initialization.
Persistence and isolation validation
src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts, src/core/webview/__tests__/ClineProvider.spec.ts, src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts, src/core/webview/__tests__/webviewMessageHandler.spec.ts
Tests unique identifiers, isolated selections, hydration, pruning, merging, clearing, launch validation, and per-view mode persistence.

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

Sequence Diagram(s)

sequenceDiagram
  participant Webview
  participant VSCodeAPIWrapper
  participant webviewMessageHandler
  participant ClineProvider
  participant ContextProxy
  participant ProviderSettingsManager

  Webview->>VSCodeAPIWrapper: getViewStateId()
  VSCodeAPIWrapper-->>Webview: stable viewStateId
  Webview->>webviewMessageHandler: webviewDidLaunch(viewStateId)
  webviewMessageHandler->>ClineProvider: setViewStateId(viewStateId)
  ClineProvider->>ContextProxy: load persisted viewStates
  ClineProvider->>ProviderSettingsManager: resolve API configuration
  ProviderSettingsManager-->>ClineProvider: provider configuration
  ClineProvider-->>webviewMessageHandler: merged extension state
Loading

Possibly related issues

Possibly related PRs

  • Zoo-Code-Org/Zoo-Code#909 — Implements related per-view state isolation in ClineProvider and webview state management.
  • Zoo-Code-Org/Zoo-Code#928 — Shares view-local state changes across the provider, handler, context, and VS Code wrapper.
  • Zoo-Code-Org/Zoo-Code#977 — Implements related durable per-view state infrastructure across schemas, provider logic, handlers, and tests.

Suggested labels: awaiting-review

Suggested reviewers: edelauna, navedmerchant, taltas

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description summarizes the changes and links issue #984, but it omits the required test procedure, checklist, and documentation sections. Add the required template sections, testing steps, checklist status, and documentation impact statement.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: adding foundational view-local state infrastructure for webviews.
Linked Issues check ✅ Passed The implementation satisfies issue #984 by registering bounded non-secret view states, hydrating them, resolving profiles, and persisting per-view selections.
Out of Scope Changes check ✅ Passed The changes remain focused on foundational per-view persistence, mode routing, and related cleanup required for parallel webview state.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/types/src/__tests__/index.test.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

packages/types/src/global-settings.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

packages/types/src/vscode-extension-host.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 9 others

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.

@easonLiangWorldedtech
easonLiangWorldedtech marked this pull request as draft August 4, 2026 16:17

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/webview/webviewMessageHandler.ts (1)

612-626: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear the view-local currentApiConfigName when the fallback profile is activated.

currentConfigName now comes from provider.getState(), so it can be the view-local value hydrated from viewStates. When hasConfig(currentConfigName) returns false, this block repairs only shared state: updateGlobalState calls provider.contextProxy.setValue and activateProviderProfile also writes through contextProxy.setValue. Neither path runs _updateViewLocalStateFromMutation, so viewLocalState.currentApiConfigName keeps the invalid name. getState() overlays viewLocalState on shared state, so the view continues to report a profile that does not exist.

Route the repair through provider.setValue so the view-local buffer is updated as well.

🛠️ Proposed fix
 					if (currentConfigName) {
 						if (!(await provider.providerSettingsManager.hasConfig(currentConfigName))) {
 							// Current config name not valid, get first config in list.
 							const name = listApiConfig[0]?.name
-							await updateGlobalState("currentApiConfigName", name)
+							// Use provider.setValue so the view-local override is updated too,
+							// otherwise getState() keeps reporting the invalid profile name.
+							await provider.setValue("currentApiConfigName", name)
 
 							if (name) {
 								await provider.activateProviderProfile({ name })
 								return
 							}
 						}
 					}
🤖 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 612 - 626, Route the
fallback current-config repair in the invalid-currentConfigName branch through
provider.setValue so it updates both shared state and the view-local buffer.
Replace the updateGlobalState path for currentApiConfigName while preserving the
existing fallback activation and return behavior.
🧹 Nitpick comments (3)
src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts (1)

694-695: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use bracket notation instead of as any to reach private members.

The spec accesses private members through (provider as any) in roughly thirty places, for example saveViewState, loadViewState, prunePersistedViewStates, setViewStateId, viewLocalState, and _clearViewLocalState. Bracket notation keeps the assertions type-checked against the real member names, so a rename breaks the test at compile time instead of silently returning undefined.

As per coding guidelines: "Avoid as any; use typed APIs, bracket notation for private members where appropriate, or precise test doubles and unknown with type guards."

♻️ Example conversion
-			const saveViewState1 = (provider1 as any).saveViewState.bind(provider1)
-			const saveViewState2 = (provider2 as any).saveViewState.bind(provider2)
+			const saveViewState1 = provider1["saveViewState"].bind(provider1)
+			const saveViewState2 = provider2["saveViewState"].bind(provider2)
-			const pruned = (provider as any).prunePersistedViewStates(states)
+			const pruned = provider["prunePersistedViewStates"](states)

Also applies to: 745-745, 936-936, 1038-1038

🤖 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/__tests__/ClineProvider.parallelMode.spec.ts` around lines
694 - 695, Replace all `(provider as any)` private-member accesses in the
parallel-mode spec with bracket notation using the actual member names,
including saveViewState, loadViewState, prunePersistedViewStates,
setViewStateId, viewLocalState, and _clearViewLocalState. Apply this
consistently to the roughly thirty affected accesses while preserving the
existing test behavior and avoiding any `as any` casts.

Source: Coding guidelines

src/core/webview/ClineProvider.ts (1)

554-559: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the any casts with typed keys.

saveViewState takes value: any, and _updateViewLocalStateFromMutation casts with val as any at Line 3084 and Line 3093 and reads (values as any).apiConfiguration at Line 3098. The coding guidelines require typed APIs instead of as any, and require a comment when a cast is unavoidable. Typed assignments here also catch a mismatch between RooCodeSettings values and ExtensionState fields at compile time.

As per coding guidelines: "Avoid as any; use typed APIs, bracket notation for private members where appropriate, or precise test doubles and unknown with type guards."

♻️ Proposed typing changes
-	private async saveViewState(key: keyof ExtensionState, value: any): Promise<void> {
+	private async saveViewState<K extends keyof ExtensionState>(
+		key: K,
+		value: ExtensionState[K] | undefined | null,
+	): Promise<void> {
 		if (value === undefined || value === null) {
 			delete this.viewLocalState[key]
 		} else {
 			this.viewLocalState[key] = value
 		}
 		if ("mode" in values) {
 			const val = values.mode
 			if (val === undefined || val === null) {
 				delete this.viewLocalState.mode
 			} else {
-				this.viewLocalState.mode = val as any
+				this.viewLocalState.mode = val
 			}
 		}
 
 		if ("currentApiConfigName" in values) {
 			const val = values.currentApiConfigName
 			if (val === undefined || val === null) {
 				delete this.viewLocalState.currentApiConfigName
 			} else {
-				this.viewLocalState.currentApiConfigName = val as any
+				this.viewLocalState.currentApiConfigName = val
 			}
 		}
 
-		if ("apiConfiguration" in values) {
-			const val = (values as any).apiConfiguration
+		if ("apiConfiguration" in values) {
+			const val = (values as { apiConfiguration?: ProviderSettings }).apiConfiguration

Also applies to: 3078-3118

🤖 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/ClineProvider.ts` around lines 554 - 559, Replace the
any-typed value and casts in saveViewState and _updateViewLocalStateFromMutation
with typed key/value relationships between ExtensionState and RooCodeSettings.
Use keyed generic parameters or an explicit type guard for mutation values,
access apiConfiguration through its typed shape, and preserve compile-time
validation that each RooCodeSettings value matches the corresponding
ExtensionState field; only retain a cast if unavoidable and document its
justification.

Source: Coding guidelines

src/core/webview/__tests__/webviewMessageHandler.spec.ts (1)

233-239: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wait for the assertion instead of flushing one tick.

webviewDidLaunch starts providerSettingsManager.listConfig() as a fire-and-forget chain. A single setImmediate flush is sufficient for the current chain length only. If another await is added to that chain, the test fails intermittently. vi.waitFor removes that coupling.

♻️ Proposed change
 	it("validates the view-local currentApiConfigName on launch", async () => {
 		await webviewMessageHandler(mockClineProvider, { type: "webviewDidLaunch", viewStateId: "view-1" })
-		await new Promise((resolve) => setImmediate(resolve))
 
-		expect((mockClineProvider as any).providerSettingsManager.hasConfig).toHaveBeenCalledWith("view-local-profile")
+		await vi.waitFor(() => {
+			expect((mockClineProvider as any).providerSettingsManager.hasConfig).toHaveBeenCalledWith(
+				"view-local-profile",
+			)
+		})
 		expect((mockClineProvider as any).providerSettingsManager.hasConfig).not.toHaveBeenCalledWith("shared-profile")
 	})
🤖 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/__tests__/webviewMessageHandler.spec.ts` around lines 233 -
239, Update the “validates the view-local currentApiConfigName on launch” test
to wait with vi.waitFor until providerSettingsManager.hasConfig has been called
with “view-local-profile”, instead of relying on a single setImmediate flush.
Keep the existing assertion that “shared-profile” is not checked.
🤖 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/ClineProvider.ts`:
- Around line 518-549: The loadViewState method needs three changes to guard
against concurrent execution and prevent loss of in-memory state. First, capture
the current viewStateId at the start of the method and compare it against the
current viewStateId after the async getProfile operation completes - if the id
has changed during the async operations, discard the result without updating.
Second, instead of replacing this.viewLocalState entirely with the loadedState
object, merge the newly loaded state properties into the existing
this.viewLocalState to preserve any values that were previously set by other
code paths (such as mode set elsewhere or apiConfiguration updated by
_updateViewLocalStateFromMutation). This ensures that missing persisted entries
do not reset previously established state values.

---

Outside diff comments:
In `@src/core/webview/webviewMessageHandler.ts`:
- Around line 612-626: Route the fallback current-config repair in the
invalid-currentConfigName branch through provider.setValue so it updates both
shared state and the view-local buffer. Replace the updateGlobalState path for
currentApiConfigName while preserving the existing fallback activation and
return behavior.

---

Nitpick comments:
In `@src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts`:
- Around line 694-695: Replace all `(provider as any)` private-member accesses
in the parallel-mode spec with bracket notation using the actual member names,
including saveViewState, loadViewState, prunePersistedViewStates,
setViewStateId, viewLocalState, and _clearViewLocalState. Apply this
consistently to the roughly thirty affected accesses while preserving the
existing test behavior and avoiding any `as any` casts.

In `@src/core/webview/__tests__/webviewMessageHandler.spec.ts`:
- Around line 233-239: Update the “validates the view-local currentApiConfigName
on launch” test to wait with vi.waitFor until providerSettingsManager.hasConfig
has been called with “view-local-profile”, instead of relying on a single
setImmediate flush. Keep the existing assertion that “shared-profile” is not
checked.

In `@src/core/webview/ClineProvider.ts`:
- Around line 554-559: Replace the any-typed value and casts in saveViewState
and _updateViewLocalStateFromMutation with typed key/value relationships between
ExtensionState and RooCodeSettings. Use keyed generic parameters or an explicit
type guard for mutation values, access apiConfiguration through its typed shape,
and preserve compile-time validation that each RooCodeSettings value matches the
corresponding ExtensionState field; only retain a cast if unavoidable and
document its justification.
🪄 Autofix

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: b53a3839-bb65-4b0b-bdfd-14dc9a7c4822

📥 Commits

Reviewing files that changed from the base of the PR and between 7918f6b and c24b7ba.

📒 Files selected for processing (13)
  • packages/types/src/__tests__/index.test.ts
  • packages/types/src/global-settings.ts
  • packages/types/src/vscode-extension-host.ts
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts
  • src/core/webview/__tests__/ClineProvider.spec.ts
  • src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts
  • src/core/webview/__tests__/webviewMessageHandler.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 with no reviewable changes (1)
  • webview-ui/src/App.tsx

Comment on lines +518 to +549
private async loadViewState(): Promise<void> {
try {
const persisted = this.getPersistedViewStates()[this.viewStateId]
const loadedState: Partial<ExtensionState> = {}

if (persisted?.mode) {
loadedState.mode = persisted.mode as Mode
}

if (persisted?.currentApiConfigName) {
loadedState.currentApiConfigName = persisted.currentApiConfigName

try {
const { name: _name, ...apiConfiguration } = await this.providerSettingsManager.getProfile({
name: persisted.currentApiConfigName,
})
loadedState.apiConfiguration = apiConfiguration as ProviderSettings
} catch (error) {
this.log(
`[loadViewState] Unable to resolve API profile '${persisted.currentApiConfigName}' for viewId ${this.viewId}: ${error instanceof Error ? error.message : String(error)}`,
)
}
}

this.viewLocalState = loadedState
this.log(`[loadViewState] Loaded state for viewId ${this.viewId}`)
} catch (error) {
this.log(
`[loadViewState] Error loading state for viewId ${this.viewId}: ${error instanceof Error ? error.message : String(error)}`,
)
}
}

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Guard loadViewState against a changed viewStateId and against replacing in-memory overrides.

Two hazards exist in this method:

  1. Ordering hazard. The constructor calls loadViewState() at Line 296 with viewStateId === viewId (for example sidebar-0). setViewStateId() later calls loadViewState() again with the stable webview id. Both runs await providerSettingsManager.getProfile(...), then assign this.viewLocalState = loadedState. If the first run resolves last, it overwrites the correctly hydrated state. This is reachable because savePersistedViewState() persists under viewId whenever a mode switch happens before the webview reports its id, and nextViewId restarts at 0 in every session, so a stale sidebar-0 entry can exist and force the slow getProfile path.

  2. Replacement instead of merge. The final assignment discards any existing viewLocalState entries, including apiConfiguration recorded by _updateViewLocalStateFromMutation and mode set at Line 1244. When the persisted entry is missing, the buffer is reset to {}.

Capture the id at entry, discard the result if the id changed, and merge into the existing buffer.

🛠️ Proposed fix
 	private async loadViewState(): Promise<void> {
+		const requestedViewStateId = this.viewStateId
+
 		try {
-			const persisted = this.getPersistedViewStates()[this.viewStateId]
+			const persisted = this.getPersistedViewStates()[requestedViewStateId]
 			const loadedState: Partial<ExtensionState> = {}
 
 			if (persisted?.mode) {
 				loadedState.mode = persisted.mode as Mode
 			}
 
 			if (persisted?.currentApiConfigName) {
 				loadedState.currentApiConfigName = persisted.currentApiConfigName
 
 				try {
 					const { name: _name, ...apiConfiguration } = await this.providerSettingsManager.getProfile({
 						name: persisted.currentApiConfigName,
 					})
 					loadedState.apiConfiguration = apiConfiguration as ProviderSettings
 				} catch (error) {
 					this.log(
 						`[loadViewState] Unable to resolve API profile '${persisted.currentApiConfigName}' for viewId ${this.viewId}: ${error instanceof Error ? error.message : String(error)}`,
 					)
 				}
 			}
 
-			this.viewLocalState = loadedState
+			// A newer setViewStateId() call may have superseded this load.
+			if (this.viewStateId !== requestedViewStateId) {
+				this.log(`[loadViewState] Discarding stale load for ${requestedViewStateId}`)
+				return
+			}
+
+			this.viewLocalState = { ...this.viewLocalState, ...loadedState }
 			this.log(`[loadViewState] Loaded state for viewId ${this.viewId}`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private async loadViewState(): Promise<void> {
try {
const persisted = this.getPersistedViewStates()[this.viewStateId]
const loadedState: Partial<ExtensionState> = {}
if (persisted?.mode) {
loadedState.mode = persisted.mode as Mode
}
if (persisted?.currentApiConfigName) {
loadedState.currentApiConfigName = persisted.currentApiConfigName
try {
const { name: _name, ...apiConfiguration } = await this.providerSettingsManager.getProfile({
name: persisted.currentApiConfigName,
})
loadedState.apiConfiguration = apiConfiguration as ProviderSettings
} catch (error) {
this.log(
`[loadViewState] Unable to resolve API profile '${persisted.currentApiConfigName}' for viewId ${this.viewId}: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
this.viewLocalState = loadedState
this.log(`[loadViewState] Loaded state for viewId ${this.viewId}`)
} catch (error) {
this.log(
`[loadViewState] Error loading state for viewId ${this.viewId}: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
private async loadViewState(): Promise<void> {
const requestedViewStateId = this.viewStateId
try {
const persisted = this.getPersistedViewStates()[requestedViewStateId]
const loadedState: Partial<ExtensionState> = {}
if (persisted?.mode) {
loadedState.mode = persisted.mode as Mode
}
if (persisted?.currentApiConfigName) {
loadedState.currentApiConfigName = persisted.currentApiConfigName
try {
const { name: _name, ...apiConfiguration } = await this.providerSettingsManager.getProfile({
name: persisted.currentApiConfigName,
})
loadedState.apiConfiguration = apiConfiguration as ProviderSettings
} catch (error) {
this.log(
`[loadViewState] Unable to resolve API profile '${persisted.currentApiConfigName}' for viewId ${this.viewId}: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
// A newer setViewStateId() call may have superseded this load.
if (this.viewStateId !== requestedViewStateId) {
this.log(`[loadViewState] Discarding stale load for ${requestedViewStateId}`)
return
}
this.viewLocalState = { ...this.viewLocalState, ...loadedState }
this.log(`[loadViewState] Loaded state for viewId ${this.viewId}`)
} catch (error) {
this.log(
`[loadViewState] Error loading state for viewId ${this.viewId}: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
🤖 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/ClineProvider.ts` around lines 518 - 549, The loadViewState
method needs three changes to guard against concurrent execution and prevent
loss of in-memory state. First, capture the current viewStateId at the start of
the method and compare it against the current viewStateId after the async
getProfile operation completes - if the id has changed during the async
operations, discard the result without updating. Second, instead of replacing
this.viewLocalState entirely with the loadedState object, merge the newly loaded
state properties into the existing this.viewLocalState to preserve any values
that were previously set by other code paths (such as mode set elsewhere or
apiConfiguration updated by _updateViewLocalStateFromMutation). This ensures
that missing persisted entries do not reset previously established state values.

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Add durable per-view state persistence for parallel tabs

2 participants