fix(webview): send stable view-state id on launch and re-pin per-view state - #1552
fix(webview): send stable view-state id on launch and re-pin per-view state#1552easonLiangWorldedtech wants to merge 3 commits into
Conversation
…States Each ClineProvider instance now owns a unique viewId (renderContext plus a monotonic counter) and registers a stable viewStateId for durable persistence. - Per-view state buffer (viewLocalState) holds mode / currentApiConfigName / apiConfiguration overrides in memory; saveViewState persists the non-secret subset durably under the active view id, rekeyed to the stable id on registration. - viewStates is stored as a map pruned to the newest 50 entries; writes go through a serialized queue so concurrent provider instances merge without lost updates. - setViewStateId sanitizes ids and rejects "__proto__" so a per-view entry can never be keyed through the Object.prototype setter. - postMessageToWebview no longer awaits the webview ack: a remounted or disposed page never acknowledges, and awaiting would wedge task-critical callers. - History restore falls back to the default mode view-locally instead of writing the shared global mode. - GlobalState gains the "viewStates" key and GLOBAL_STATE_KEYS tracks it. Adds F1a coverage in ClineProvider.spec.ts (viewId uniqueness, saveViewState persistence semantics, loadViewState fallback and failure, pruning, the __proto__ guard) and adapts the two history-restore tests in ClineProvider.sticky-mode.spec.ts to the view-local restore. getState() merging of hydrated per-view values and the remaining view-state suites land in the follow-up (F1b).
…overrides Fold ClineProvider viewLocalState on top of ContextProxy values in getState() (mode, apiConfiguration, and all per-view fields) so each webview reports its own selections while falling back to shared global state for everything else. Ports the getState-merging and local-state-isolation spec coverage from the superseded vps2 source. Also pins the full default surface of the merged read path, including the apiConfiguration provider fill-in when provider settings sanitize the raw value away (mutation-diff gate).
… state WMH webviewDidLaunch persists the webview view-state id via provider.setViewStateId and re-pins the view-local currentApiConfigName through provider.saveViewState when the view-local profile is missing but the shared global selection is still valid. updateSettings is routed through provider.setValue so view-local buffer and pin sync stay consistent with the other mutation paths. The webview VSCodeAPIWrapper gains a stable getViewStateId persisted via setState (with an in-memory fallback) and the launch effect posts the id with the webviewDidLaunch message.
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
Review statusThanks for contributing. This comment tracks the review sequence and the next action. Current step: Fix the failing required CI checks; awaiting-maintainer requires CI and automated review completion. Review-state labels are managed by this workflow; do not edit them manually. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Part of the vps2 durable per-view state series — tracked in easonLiangWorldedtech#41 (cross-repo: standalone a+d measured against the stack base; the displayed vs-main diff includes lower units until they merge).
Issue (created at PR-open time): #1551
What
Persists each webview's stable state identity at launch and makes launch-time per-view state re-pin correctly. The webview now carries a
viewStateId(created and persisted via the webview state API, with an in-memory fallback) that is posted withwebviewDidLaunchand persisted on the provider viasetViewStateId, re-keying the pre-launch temporary state entry to the launching webview. When the view-local API profile is invalid at launch, the view is re-pinned to the still-valid shared global selection with a view-local write only — the shared global is repaired only when its own selection is also invalid.updateSettingsis routed throughprovider.setValueso view-local buffer/pin sync stays consistent with the other mutation paths.Design decisions
VSCodeAPIWrapper.getViewStateId): it reuses the id persisted in webview state, creates one (crypto.randomUUID, with a timestamp+random fallback) and persists it viasetState; when storage is unavailable it falls back to an in-memory field. The id is best-effort — the launch message carriesviewStateId: undefinedwhen the helper is unavailable, and the provider degrades to the shared-global path.provider.saveViewState("currentApiConfigName", name)writes the view's buffer/pin without touching the shared global selection; the legacy global repair (global write +activateProviderProfile) runs only when the shared global selection is also invalid.getState()semantics from F1b.updateSettingsdelegates toprovider.setValuerather thancontextProxy.setValueso the view-local buffer/pin sync path (_saveViewLocalStateFromMutation) runs for settings edits too.Measurements
git diff --numstat 43b52aa11(stack base, F1b head) — a+d total: 545 (528 insertions, 17 deletions):src/core/webview/webviewMessageHandler.tssrc/core/webview/__tests__/webviewMessageHandler.spec.tswebview-ui/src/utils/vscode.tswebview-ui/src/utils/__tests__/vscode.spec.ts(new)webview-ui/src/context/ExtensionStateContext.tsxwebview-ui/src/context/__tests__/ExtensionStateContext.spec.tsxComposition note: 430 of the 528 inserted lines are tests (spec files 117 + 97 + 216); production additions are 98 lines. a+d is above the 400 soft budget because the unit ships both webview-side and extension-side behavior with unit + integration tests at each layer; it is well under the 1000 hard cap.
Changed executable lines (stryker-diff): 65 (32 extension + 33 webview changed lines) — cap ≤500.
Raw mutants (stryker-diff): 65 — cap ≤400.
Gates
eslint --prune-suppressions --max-warnings=0exit 0 per touched file (src:webviewMessageHandler.ts+ spec; webview-ui:vscode.ts,vscode.spec.ts,ExtensionStateContext.tsx+ spec).src/eslint-suppressions.jsonunchanged — suppression counts did not increase (a prune run that only re-indented the file with zero count change was reverted).srcexit 0;webview-uiexit 0.webviewMessageHandler.spec.ts85/85 pass (81 base + 4 new launch tests);vscode.spec.ts9/9 (new spec);ExtensionStateContext.spec.tsx24/24 (21 base + 3 new).ClineProvider.spec.tsnot affected (noClineProvider.tschanges in this unit).--checkexit 0 on all six touched files (CRLF checkout normalized via--write).43b52aa11, head090d2c87e): 65 raw mutants — 59 Killed, 0 Survived, 0 NoCoverage (6 Ignored equivalent mutants, the documented CSStryker disablecomments invscode.tsL91/L122). Caps: 0 Survived / 0 NoCoverage in changed code, 65 changed executable lines ≤ 500, 65 raw mutants ≤ 400.Parked / documented
Observed in the CS diff but not ported (register items, to be tracked by the series ledger):
—?corruption in therequestRouterModelsopencode-go comment): base comment// Deliberately no opencodeGoApiKey — the endpoint is public.kept as-is.defaultModeSlugimport in the WMH spec: not ported (F3 re-adds it with its use).try/catchhunk inrequestRouterModels+ itswebviewMessageHandler.routerModels.spec.tsadditions: not ported (review-hardening hunk outside F1c scope).ApiConfigManager.tsxclassName tweak: not ported (not part of the F1c row).ApiConfigManager.visual.tsxdeletion + screenshot baselines: not ported (visual-suite churn outside F1c scope)..coderabbit.yaml,label-pr-review-state.yml,.gitignore,CONTRIBUTING.md,ClineProvider.tschanges, parallel-mode/sticky-mode specs, etc.): not ported (belong to the other series units).Porting notes
Hand-ported from CS commit
e9a44b2fa(base of record0d937c050), hunk by hunk; no cherry-pick.Ported:
src/core/webview/webviewMessageHandler.ts:webviewDidLaunchhandler —await provider.setViewStateId(message.viewStateId); launch-time re-pin block (validate merged view-local name, then shared global, re-pin the view viaprovider.saveViewStatewhen the global is still valid, else legacy global repair);updateSettingsrouted throughprovider.setValue.webview-ui/src/utils/vscode.ts:VSCodeAPIWrapperfallback state,createViewStateId/getViewStateId, browser-fallbackgetState/setStatewith the CSStryker disablecomments.webview-ui/src/context/ExtensionStateContext.tsx: launch effect postswebviewDidLaunchwithviewStateId.webview-ui/src/utils/__tests__/vscode.spec.ts: new spec, all 9 tests (id reuse, create+persist, in-memory fallback, id shape, stored-state edge cases).src/core/webview/__tests__/webviewMessageHandler.spec.ts:RooCodeSettingsimport,saveViewStatemock,setValuemock delegating tocontextProxy.setValue, and thewebviewDidLaunchdescribe (CS verbatim, plus one CS deviation below).CS deviation (mutation coverage): the CS launch tests as-is leave 2 mutants alive in the re-pin block — the
StringLiteralon thegetGlobalState("currentApiConfigName")key (the CS mockgetValuereturns the canned value for any key, so a mutated key is unobservable) and theConditionalExpressiononif (name)(every CS test leavesnametruthy). To satisfy the 0-survived stryker-diff gate without an escape hatch, thegetValuemock in the launch describe is key-aware ("currentApiConfigName"→"shared-profile", anything else →undefined), and one additional test covers the falsy-namelegacy repair (selection recorded, no profile activation). This matches the CS commit's own "harden viewStateId mutation coverage" intent.webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx:@src/utils/vscodemock,ViewLocalStateTestComponent, and its 3 tests (launch post with/without id; view-local reseed contract).Not ported (per the register above): the six parked items — verified by full-file diff against the CS final state: every ported file is byte-identical to the CS tree (modulo the intentionally skipped hunks).