Skip to content

feat(webview): persist viewStates in ClineProvider with setValues method - #1184

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

feat(webview): persist viewStates in ClineProvider with setValues method#1184
easonLiangWorldedtech wants to merge 2 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/view-local-state-base-2

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Add setValues() method in ClineProvider to implement per-view state persistence. Each tab/sidebar panel can independently save mode, API profile selections without interfering with each other.

Changes

  • setValues() method — Update view-local state without affecting global settings
  • Per-view mode persistence — Each viewStateId independently saves mode selection (tab isolation)
  • webviewMessageHandler update — Support new view state flow
  • Parallel mode switching tests — Sidebar and tab panel parallel mode switching tests

Files Changed (8 files, +1930 / -120)

File Change Description
ClineProvider.parallelMode.spec.ts +1605 New — Complete parallel mode test suite
webviewMessageHandler.spec.ts +292/-0 Extend message handler tests
ExtensionStateContext.spec.tsx (merged) Context tests
ClineProvider.spec.ts +20/-5 Provider unit tests
ClineProvider.sticky-mode.spec.ts +37/-9 Sticky mode tests
webviewMessageHandler.routerModels.spec.ts +33 Router model tests
webviewMessageHandler.ts +52/-15 Handler logic update
src/eslint-suppressions.json -10 Clean up suppressions

Key Design

  • setValues() only updates state for the current viewStateId, does not trigger global state changes
  • webviewMessageHandler routes to correct state layer based on viewStateId
  • All tests cover both sidebar + tab panel scenarios

Test Notes

  • base-1 alone: Tests will fail because base-2 provides the setValues() method that base-1's persistence logic depends on. The infrastructure is in place but not yet wired to actual state updates.
  • base-1 + base-2: All tests pass — persistence is fully functional with setValues integration.

Related

  • Depends on base-1 (viewStateId infrastructure)
  • Prerequisite for base-3 (API task control)

- Export VSCodeAPIWrapper class and add getViewStateId() method
- Generate unique viewStateId using crypto.randomUUID() with fallback
- Persist viewStateId in localStorage for dev server compatibility
- Add viewStateSchema to global-settings.ts for type safety
- Send viewStateId during webviewDidLaunch handshake
- Add setValues method to update view-local state without affecting global settings
- Persist mode selection per viewStateId for tab isolation
- Update webviewMessageHandler to support new view state flow
- Add parallel mode switching tests for sidebar and tab panel
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds durable per-view webview state for modes and provider profiles. It introduces view-state schemas and identifiers, isolates and persists provider state, updates launch and message handling, improves typing, and adds extensive persistence and multi-instance tests.

Changes

Per-view state isolation and persistence

Layer / File(s) Summary
State contracts and view identity
packages/types/src/*, webview-ui/src/utils/*, webview-ui/src/context/*
Defines persisted view-state schemas. The webview wrapper generates or reuses view identifiers and includes them in launch messages.
Durable view-state persistence
src/core/webview/ClineProvider.ts, src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts
Adds view registration, hydration, serialized writes, pruning to 50 entries, sanitization, and persistence failure handling.
View-local state integration
src/core/webview/ClineProvider.ts, src/core/webview/__tests__/ClineProvider*.spec.ts
Overlays view-local values on global settings and persists mode, provider settings, profile changes, resets, and delegation state.
Launch and message flows
src/core/webview/webviewMessageHandler.ts, src/core/webview/__tests__/webviewMessageHandler*.spec.ts, src/eslint-suppressions.json
Stores launch view identifiers, reads view-local profile state, improves payload typing, and handles Kimi Code credential failures without aborting router-model responses.

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

Possibly related issues

Possibly related PRs

Suggested labels: awaiting-review

Suggested reviewers: edelauna, taltas

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the implementation and tests but omits the required linked issue, checklist, and template sections. Add the approved GitHub issue number, complete the pre-submission checklist, and include the required template sections for testing and documentation impact.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: per-view state persistence through ClineProvider.setValues().
✨ 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.

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

🧹 Nitpick comments (6)
src/core/webview/__tests__/webviewMessageHandler.spec.ts (1)

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

Use typed test fixtures or document the double assertions.

The changed tests use as unknown as without documenting why a typed fixture cannot represent the value.

  • src/core/webview/__tests__/webviewMessageHandler.spec.ts#L233-L242: Use a typed provider-access fixture. Document any required double assertion.
  • src/core/webview/__tests__/webviewMessageHandler.spec.ts#L344-L344: Use a typed task fixture. Document any required double assertion.
  • src/core/webview/__tests__/webviewMessageHandler.spec.ts#L1065-L1095: Use a shared typed task fixture. Document any required double assertion.
  • src/core/webview/__tests__/webviewMessageHandler.spec.ts#L1316-L1316: Document that the cast intentionally sends malformed runtime input.
  • src/core/webview/__tests__/webviewMessageHandler.spec.ts#L1601-L1604: Use a typed diagnostics-task fixture. Document any required double assertion.
🤖 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 -
242, Replace the untyped double assertions in webviewMessageHandler.spec.ts with
typed fixtures: create a typed provider-access fixture at lines 233-242, typed
task fixtures at lines 344 and 1065-1095, and a typed diagnostics-task fixture
at lines 1601-1604; document any unavoidable double assertions. At line 1316,
retain the cast only if it intentionally supplies malformed runtime input and
add a comment documenting that purpose.

Source: Coding guidelines

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

1927-1943: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Remove the duplicated view-state persistence in upsertProviderProfile.

saveViewState("currentApiConfigName", name) and saveViewState("apiConfiguration", providerSettings) already call _saveViewLocalStateFromMutation. Line 1938 then calls the same helper with the same fields. One profile activation therefore enqueues the durable write twice and updates the local cache three times. Keep only the explicit _saveViewLocalStateFromMutation call and drop the two saveViewState calls from the Promise.all. Note that the test at src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts line 1307 asserts saveViewState is called with "currentApiConfigName", so update that assertion together with this change.

🤖 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 1927 - 1943, Remove the two
saveViewState calls for "currentApiConfigName" and "apiConfiguration" from the
Promise.all in upsertProviderProfile, retaining the explicit
_saveViewLocalStateFromMutation call. Update the related parallelMode test
assertion to reflect that saveViewState is no longer called with
"currentApiConfigName".

642-648: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the as any casts on the on/off overrides.

Both overrides use (super.on as any) and (super.off as any) with inline ESLint suppressions. Cast the listener to the emitter's expected signature instead, so the suppressions are not needed.

As per coding guidelines: "Avoid as any; use typed APIs, bracket notation for private members, or precise test doubles and type guards. Use double assertions only as a last resort and document them." and "Fix lint violations in new TypeScript code rather than suppressing them."

♻️ Proposed typed forwarding
 	override on<K extends keyof TaskProviderEvents>(
 		event: K,
 		listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
 	): this {
-		// eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-		return (super.on as any)(event, listener)
+		return super.on(event, listener as (...args: TaskProviderEvents[K]) => void)
 	}

Apply the same change to off.

Also applies to: 657-658

🤖 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 642 - 648, Update the on and
off overrides in ClineProvider to remove the inline ESLint suppressions and
`(super.on as any)`/`(super.off as any)` casts. Cast each listener to the
emitter’s expected typed listener signature before forwarding to the superclass,
preserving the existing event and listener behavior.

Source: Coding guidelines


3269-3285: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Build the provider-settings delta without a spread inside reduce.

The reducer allocates a new object for every matching key. PROVIDER_SETTINGS_KEYS holds a large number of entries, and this runs on every setValue and setValues call. Mutate a single accumulator instead.

♻️ Proposed refactor
-			const providerSettingsUpdate = PROVIDER_SETTINGS_KEYS.reduce((acc, key) => {
-				if (key in values) {
-					return { ...acc, [key]: values[key as keyof RooCodeSettings] }
-				}
-
-				return acc
-			}, {} as ProviderSettings)
+			const providerSettingsUpdate: ProviderSettings = {}
+			for (const key of PROVIDER_SETTINGS_KEYS) {
+				if (key in values) {
+					Object.assign(providerSettingsUpdate, { [key]: values[key as keyof RooCodeSettings] })
+				}
+			}
🤖 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 3269 - 3285, Update the
providerSettingsUpdate construction in the PROVIDER_SETTINGS_KEYS.reduce
callback to assign matching values directly onto the existing accumulator
instead of returning a newly spread object for each key. Preserve the current
filtering and resulting ProviderSettings shape.

519-573: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove unreachable clearPersistedViewState.

No tracked file references clearPersistedViewState, including TypeScript source, and no clearPersistedViewState(...) call exists. If disposal should not delete persisted view states, keep only savePersistedViewState, which already removes an entry when both mode and currentApiConfigName are empty.

🤖 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 519 - 573, Remove the unused
clearPersistedViewState method from ClineProvider. Preserve
savePersistedViewState’s existing behavior of deleting the persisted entry when
both mode and currentApiConfigName are absent.
src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts (1)

890-891: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicated assertion.

Lines 890 and 891 assert the same condition. Delete one of them.

♻️ Proposed fix
 			expect(asProviderAccess(provider).viewLocalState).not.toHaveProperty("mode")
-			expect(asProviderAccess(provider).viewLocalState).not.toHaveProperty("mode")
 			expect(provider.contextProxy.getValue("viewStates")).toBeUndefined()
🤖 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
890 - 891, Remove the duplicated expect assertion in the relevant parallel-mode
test, keeping one assertion that viewLocalState does not have the "mode"
property.
🤖 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 623-637: Update saveViewState so persistence errors from
_saveViewLocalStateFromMutation propagate instead of being swallowed, allowing
handleModeSwitch to avoid emitting ModeChanged when the view state write fails.
Preserve the existing success logging and error context while ensuring the
in-memory state and task mode remain consistent.
- Line 1315: Persist both view-local mutations: in
src/core/webview/ClineProvider.ts lines 1315-1315, replace the direct mode
assignment with await saveViewState("mode", historyItem.mode); in lines
1987-1991, replace _updateViewLocalStateFromMutation with await
_saveViewLocalStateFromMutation so the deleted profile is removed from the
durable view state.

In `@src/core/webview/webviewMessageHandler.ts`:
- Around line 616-617: In the invalid-profile recovery flow around currentState
and currentConfigName, replace the contextProxy.updateGlobalState write for
currentApiConfigName with provider.setValues({ currentApiConfigName: name })
before calling activateProviderProfile, keeping the fallback change local to the
current view.

---

Nitpick comments:
In `@src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts`:
- Around line 890-891: Remove the duplicated expect assertion in the relevant
parallel-mode test, keeping one assertion that viewLocalState does not have the
"mode" property.

In `@src/core/webview/__tests__/webviewMessageHandler.spec.ts`:
- Around line 233-242: Replace the untyped double assertions in
webviewMessageHandler.spec.ts with typed fixtures: create a typed
provider-access fixture at lines 233-242, typed task fixtures at lines 344 and
1065-1095, and a typed diagnostics-task fixture at lines 1601-1604; document any
unavoidable double assertions. At line 1316, retain the cast only if it
intentionally supplies malformed runtime input and add a comment documenting
that purpose.

In `@src/core/webview/ClineProvider.ts`:
- Around line 1927-1943: Remove the two saveViewState calls for
"currentApiConfigName" and "apiConfiguration" from the Promise.all in
upsertProviderProfile, retaining the explicit _saveViewLocalStateFromMutation
call. Update the related parallelMode test assertion to reflect that
saveViewState is no longer called with "currentApiConfigName".
- Around line 642-648: Update the on and off overrides in ClineProvider to
remove the inline ESLint suppressions and `(super.on as any)`/`(super.off as
any)` casts. Cast each listener to the emitter’s expected typed listener
signature before forwarding to the superclass, preserving the existing event and
listener behavior.
- Around line 3269-3285: Update the providerSettingsUpdate construction in the
PROVIDER_SETTINGS_KEYS.reduce callback to assign matching values directly onto
the existing accumulator instead of returning a newly spread object for each
key. Preserve the current filtering and resulting ProviderSettings shape.
- Around line 519-573: Remove the unused clearPersistedViewState method from
ClineProvider. Preserve savePersistedViewState’s existing behavior of deleting
the persisted entry when both mode and currentApiConfigName are absent.
🪄 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: 9877f071-bddd-4931-ad13-cd96ebd9c575

📥 Commits

Reviewing files that changed from the base of the PR and between 276e425 and f77fd55.

📒 Files selected for processing (14)
  • 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.routerModels.spec.ts
  • src/core/webview/__tests__/webviewMessageHandler.spec.ts
  • src/core/webview/webviewMessageHandler.ts
  • src/eslint-suppressions.json
  • webview-ui/src/context/ExtensionStateContext.tsx
  • webview-ui/src/utils/__tests__/vscode.spec.ts
  • webview-ui/src/utils/vscode.ts
💤 Files with no reviewable changes (1)
  • src/eslint-suppressions.json

Comment on lines +623 to +637
/**
* Save a single view-local state value. Only non-secret selections are persisted durably.
*/
private async saveViewState(key: keyof ExtensionState, value: unknown): Promise<void> {
try {
await this._saveViewLocalStateFromMutation({ [key]: value } as Partial<RooCodeSettings> &
Partial<ExtensionState>)

this.log(`[saveViewState] Saved ${String(key)} for viewId ${this.viewId}`)
} catch (error) {
this.log(
`[saveViewState] Error saving 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 | ⚡ Quick win

saveViewState swallows persistence failures, so handleModeSwitch can diverge.

saveViewState catches every error and only logs it. _saveViewLocalStateFromMutation persists first and updates viewLocalState second, so a failed write leaves the in-memory mode unchanged. handleModeSwitch (line 1777) depends on this call to apply the new mode. When the write fails, the task's _taskMode was already set at line 1764 and ModeChanged is still emitted at line 1779, but getState().mode keeps the old value. Propagate the failure to handleModeSwitch, or emit ModeChanged only after the view state is applied.

🤖 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 623 - 637, Update
saveViewState so persistence errors from _saveViewLocalStateFromMutation
propagate instead of being swallowed, allowing handleModeSwitch to avoid
emitting ModeChanged when the view state write fails. Preserve the existing
success logging and error context while ensuring the in-memory state and task
mode remain consistent.

}

await this.updateGlobalState("mode", historyItem.mode)
this.viewLocalState.mode = historyItem.mode

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Two mutation sites update the view-local cache without a durable write. Every other mode and profile mutation routes through saveViewState or _saveViewLocalStateFromMutation, which persists mode and currentApiConfigName into the viewStates map. These two sites write only the in-memory cache, so the persisted entry for this view keeps the previous value and a reload restores a stale selection.

  • src/core/webview/ClineProvider.ts#L1315-L1315: replace the direct this.viewLocalState.mode = historyItem.mode assignment with await this.saveViewState("mode", historyItem.mode) so the restored mode is durable for this view.
  • src/core/webview/ClineProvider.ts#L1987-L1991: replace _updateViewLocalStateFromMutation with await this._saveViewLocalStateFromMutation, otherwise the persisted entry still names the deleted profile and loadViewState fails to resolve it on the next launch.
📍 Affects 1 file
  • src/core/webview/ClineProvider.ts#L1315-L1315 (this comment)
  • src/core/webview/ClineProvider.ts#L1987-L1991
🤖 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` at line 1315, Persist both view-local
mutations: in src/core/webview/ClineProvider.ts lines 1315-1315, replace the
direct mode assignment with await saveViewState("mode", historyItem.mode); in
lines 1987-1991, replace _updateViewLocalStateFromMutation with await
_saveViewLocalStateFromMutation so the deleted profile is removed from the
durable view state.

Comment on lines +616 to +617
const currentState = await provider.getState()
const currentConfigName = currentState.currentApiConfigName

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep invalid-profile recovery view-local.

Line 616 reads currentApiConfigName from the view-local provider state. Line 623 then writes the fallback name through updateGlobalState. One view with a stale profile can therefore change the selected profile for other views.

Use provider.setValues({ currentApiConfigName: name }) before activateProviderProfile instead of writing currentApiConfigName through contextProxy.

Proposed fix
- await updateGlobalState("currentApiConfigName", name)
+ await provider.setValues({ currentApiConfigName: name })
🤖 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 616 - 617, In the
invalid-profile recovery flow around currentState and currentConfigName, replace
the contextProxy.updateGlobalState write for currentApiConfigName with
provider.setValues({ currentApiConfigName: name }) before calling
activateProviderProfile, keeping the fallback change local to the current view.

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.71930% with 28 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/webview/ClineProvider.ts 90.90% 11 Missing and 6 partials ⚠️
src/core/webview/webviewMessageHandler.ts 64.70% 3 Missing and 3 partials ⚠️
webview-ui/src/utils/vscode.ts 77.27% 2 Missing and 3 partials ⚠️

📢 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.

2 participants