fix(webview): add durable per-view state base - #977
fix(webview): add durable per-view state base#977easonLiangWorldedtech wants to merge 23 commits into
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds durable per-view state schemas, identifiers, persistence, restoration, and state merging across webview and extension layers. It also adds task-specific API controls, browser storage fallbacks, and parallel-view integration coverage. ChangesPer-view state and task control
Estimated code review effort: 5 (Critical) | ~90 minutes Sequence Diagram(s)sequenceDiagram
participant Webview
participant ExtensionStateContext
participant webviewMessageHandler
participant ClineProvider
participant globalState
Webview->>ExtensionStateContext: obtain stable viewStateId
ExtensionStateContext->>webviewMessageHandler: webviewDidLaunch with viewStateId
webviewMessageHandler->>ClineProvider: setViewStateId(viewStateId)
ClineProvider->>globalState: load or save viewStates
ClineProvider-->>Webview: merged view-local state
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 3005-3056: Update resetState(), activateProviderProfile(),
upsertProviderProfile(), and deleteProviderProfile() to clear or synchronize the
affected viewLocalState fields after mutating contextProxy. Reuse
_clearViewLocalState() for resetState() and _updateViewLocalStateFromMutation()
or equivalent targeted invalidation for profile changes, ensuring stale
currentApiConfigName and apiConfiguration values cannot mask the updated global
state.
🪄 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: 58c5e801-f818-415c-b0de-9f69e7498604
📒 Files selected for processing (13)
packages/types/src/__tests__/index.test.tspackages/types/src/global-settings.tspackages/types/src/vscode-extension-host.tssrc/core/webview/ClineProvider.tssrc/core/webview/__tests__/ClineProvider.parallelMode.spec.tssrc/core/webview/__tests__/ClineProvider.spec.tssrc/core/webview/__tests__/ClineProvider.sticky-mode.spec.tssrc/core/webview/__tests__/webviewMessageHandler.spec.tssrc/core/webview/webviewMessageHandler.tswebview-ui/src/App.tsxwebview-ui/src/__tests__/App.spec.tsxwebview-ui/src/context/ExtensionStateContext.tsxwebview-ui/src/utils/vscode.ts
💤 Files with no reviewable changes (1)
- webview-ui/src/App.tsx
edelauna
left a comment
There was a problem hiding this comment.
Exciting to see this work come together! Had some implementation comments, and can we also add some ui testing:
We can leverage the UI testing setup established in McpServerRestriction.spec.tsx and webview-ui/src/utils/test-utils.tsx:
-
ExtensionStateContext.ProviderWrapper Pattern:
Re-use therenderWithStatepattern to mount webview components with specificviewStateIdprops and verify that UI components respond correctly to view-localmodeandcurrentApiConfigNamestate without global bleed. -
Reseed & Identity Tests:
Similar to the slug-change reseed tests inMcpServerRestriction.spec.tsx, add UI-level tests inExtensionStateContext.spec.tsxorApp.spec.tsxto verify that whenviewStateIdchanges or a webview reloads, local React state reseeds properly from the new view'sviewStateIdpayload. -
vscode.getViewStateId& Messaging Spies:
Ensure UI tests verifyVSCodeAPIWrapper.getViewStateId()fallback behavior whensessionStorage/localStorageare restricted or cleared.
| return viewStates | ||
| } | ||
|
|
||
| private async savePersistedViewState(values: Partial<PersistedViewState>): Promise<void> { |
There was a problem hiding this comment.
savePersistedViewState reads the global viewStates dictionary from contextProxy, mutates states[this.viewStateId] in memory, and writes it back asynchronously via await contextProxy.setValue("viewStates", ...). When concurrent webview instances update mode or API profile selections simultaneously, one instance reads stale global state before the other's write completes, causing a lost update on viewStates.
| @@ -2864,6 +3082,7 @@ export class ClineProvider | |||
| } | |||
|
|
|||
| await this.contextProxy.resetAllState() | |||
There was a problem hiding this comment.
resetState() clears global settings in contextProxy but does not clear this.viewLocalState (e.g., via _clearViewLocalState()). When getState(viewStateId) runs, it merges stale viewLocalState overrides over the reset contextProxy defaults, causing pre-reset or deleted profile settings to persist in active webview instances.
| * profile upsert/activation/deletion, or resetState. This ensures the local cache stays in | ||
| * sync with global state changes that would otherwise be invisible behind mergedStateValues. | ||
| */ | ||
| private _updateViewLocalStateFromMutation(values: Partial<RooCodeSettings>): void { |
There was a problem hiding this comment.
_updateViewLocalStateFromMutation updates in-memory viewLocalState in response to setValue/setValues calls, but never invokes savePersistedViewState. Mutations made via setValue/setValues are held only in memory and lost when the webview reloads or VS Code restarts.
|
|
||
| describe("local state isolation", () => { | ||
| it("should isolate mode state between instances", async () => { | ||
| const provider1 = new ClineProvider( |
There was a problem hiding this comment.
The test 'should isolate mode state between instances' reads the initial mode of two provider instances without mutating mode in either, making it incapable of verifying whether mode changes in one instance leak to other instances.
| vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue("shared-profile") | ||
| vi.mocked(mockClineProvider.contextProxy.setValue).mockResolvedValue(undefined) | ||
| }) | ||
|
|
There was a problem hiding this comment.
The webviewDidLaunch handler test passes viewStateId: 'view-1' but omits an assertion verifying that provider.setViewStateId was called with 'view-1'.
82f9f23 to
d724948
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts (1)
1098-1195: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding coverage for the two uncovered profile-mutation paths.
None of these tests exercise
upsertProviderProfile(..., false)(non-activating save) or adeleteProviderProfilecase whereviewLocalState.currentApiConfigNamediverges from the global value - both are the exact gaps flagged insrc/core/webview/ClineProvider.ts(upsertProviderProfile/deleteProviderProfile). Adding cases here would catch regressions on those fixes.🤖 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 1098 - 1195, The profile-mutation tests cover only activating upserts and matching delete state; add coverage for the two missing branches. In the “profile mutations” suite, add a test for upsertProviderProfile(..., false) that verifies the saved profile does not activate or incorrectly synchronize current state, and a deleteProviderProfile test where viewLocalState.currentApiConfigName differs from the global ContextProxy value, asserting the intended local-state behavior after deletion.
🤖 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.
Nitpick comments:
In `@src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts`:
- Around line 1098-1195: The profile-mutation tests cover only activating
upserts and matching delete state; add coverage for the two missing branches. In
the “profile mutations” suite, add a test for upsertProviderProfile(..., false)
that verifies the saved profile does not activate or incorrectly synchronize
current state, and a deleteProviderProfile test where
viewLocalState.currentApiConfigName differs from the global ContextProxy value,
asserting the intended local-state behavior after deletion.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 1c33c4bc-cef3-4dc5-a6f1-07927265c1e5
📒 Files selected for processing (6)
src/core/webview/ClineProvider.tssrc/core/webview/__tests__/ClineProvider.parallelMode.spec.tssrc/core/webview/__tests__/webviewMessageHandler.spec.tswebview-ui/src/context/__tests__/ExtensionStateContext.spec.tsxwebview-ui/src/utils/__tests__/vscode.spec.tswebview-ui/src/utils/vscode.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/core/webview/tests/webviewMessageHandler.spec.ts
edelauna
left a comment
There was a problem hiding this comment.
Couple more comments - thanks for continuing to iterate on this.
2ef16bb to
37a5dd1
Compare
A just-resolved globalState write can momentarily lag a synchronous globalState.get in the extension host. The per-view writes are already awaited through the serialized view-state write queue before the tasks complete, so poll until both the sidebar and tab persisted selections are visible before asserting, instead of reading globalState once.
Address the CodeRabbit docstring coverage warning on the durable per-view state PR by documenting the new view-state persistence/merge helpers in ClineProvider, the task-scoped API controls in the extension API, and the viewStateId generation/restoration helpers in the webview wrapper.
# Conflicts: # src/core/webview/ClineProvider.ts # src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts # src/core/webview/__tests__/webviewMessageHandler.spec.ts # src/core/webview/webviewMessageHandler.ts
…sted view-local secrets - selectTaskFollowupSuggestion() now passes the registered task explicitly to handleModeSwitch(), so answering a follow-up on task B no longer switches the mode of the provider's currently focused task A (resolves review comment on src/extension/api.ts). - getConfiguration() flattens the nested view-local apiConfiguration onto the top level before the isSecretStateKey() filter, so nested provider secrets (apiKey, openRouterApiKey, ...) cannot leak through the API — a regression introduced by the per-view state base's nested apiConfiguration shape. - Consolidate the duplicate kimi-code oauth vi.mock in the routerModels spec and replace raw provider identifier literals flagged by the merged zoo/no-raw-provider-identifiers rule with providerIdentifiers.* constants. Validated: 112 targeted vitest tests pass, pnpm --dir src run check-types clean, eslint --max-warnings=0 clean on all touched files.
Related GitHub Issue
Closes: #984
Description
Add the foundational per-view state infrastructure for parallel mode. This is the root PR that all subsequent parallel-mode PRs depend on.
How:
webview-ui/src/utils/vscode.tsviagetViewStateId(), sent during launch).ClineProvider.setViewStateId()sanitizes it into a safe object key.viewLocalStatebuffer: transient per-view state that merges on top of the sharedContextProxyvalues ingetState()(themergedStateValueslayer), so a tab's mode never overwrites the sidebar's.viewStatespersistence: registered global setting key storing only non-secret selections (mode,currentApiConfigName,updatedAt), bounded to the most recent 50 entries byupdatedAtordering.viewStatesmutation goes through a static write queue (persistedViewStateWriteQueue) that re-reads the map fresh fromglobalStateon each write, so concurrent sidebar/tab providers cannot clobber each other.Reviewers should pay attention to:
mode,currentApiConfigName). FullapiConfiguration(API keys, Kimi Code keys) is never written to globalState; e2e asserts no secret paths leak into persisted entries.dispose()deliberately preserves the persisted entry — retention is handled by the 50-entry pruning cap, not deletion (tracked in follow-up Preserve durable editor view state across provider disposal #1065).approveTaskAsk,selectTaskFollowupSuggestion) andpreserveOpenTabsfor new tasks. These are required so parallel views can be driven per-task by the orchestrator e2e foundation (test(vscode-e2e): add orchestrator E2E foundation for parallel-mode coverage #1064), which is why they stay in this root PR rather than being split out.Test Procedure
Unit / integration (all green in CI):
pnpm --dir src test— full core suite (129 files / 2184 passed / 9 skipped), including the newClineProvider.parallelMode.spec.tscovering persistence, restoration, isolation, pruning, and concurrent-write serializationpnpm --dir packages/types test(6 passed) andpnpm --dir webview-ui test(viewStateId generation/restoration)pnpm --dir src run check-types, pluspackages/types,webview-ui, andapps/vscode-e2epnpm --dir src exec eslint --prune-suppressions --max-warnings=0 <touched files>— suppression counts unchangedE2E (real VS Code extension host, mock API):
USE_MOCK=true TEST_FILE=view-state.test VSCODE_VERSION=1.100.0 pnpm --dir apps/vscode-e2e run test:runviewStatesentries are visible viaapi.getGlobalState("viewStates")and no secret keys appear in persisted entriesManual verification:
codemode and a new tab task indebugmodecurrentApiConfigNameis unaffectedPre-Submission Checklist
viewStatesis an internal registered setting.Visual Snapshots
Not applicable — no user-visible rendered state changes.
Videos (interaction / animation only)
Not applicable.
Summary by CodeRabbit
New Features
viewStatessetting, including isolated mode and API profile selection byviewStateId.preserveOpenTabssupport for new tasks.Bug Fixes
Tests