Skip to content

fix(webview): send stable view-state id on launch and re-pin per-view state - #1552

Draft
easonLiangWorldedtech wants to merge 3 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:vps2/f1c-webview-identity
Draft

fix(webview): send stable view-state id on launch and re-pin per-view state#1552
easonLiangWorldedtech wants to merge 3 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:vps2/f1c-webview-identity

Conversation

@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor

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 with webviewDidLaunch and persisted on the provider via setViewStateId, 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. updateSettings is routed through provider.setValue so view-local buffer/pin sync stays consistent with the other mutation paths.

Design decisions

  • The view-state id is owned by the webview (VSCodeAPIWrapper.getViewStateId): it reuses the id persisted in webview state, creates one (crypto.randomUUID, with a timestamp+random fallback) and persists it via setState; when storage is unavailable it falls back to an in-memory field. The id is best-effort — the launch message carries viewStateId: undefined when the helper is unavailable, and the provider degrades to the shared-global path.
  • Re-pin is view-local: 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.
  • Validation order on launch: merged (view-local) name first, then the shared global selection, then the first listed profile — matching the merged getState() semantics from F1b.
  • updateSettings delegates to provider.setValue rather than contextProxy.setValue so 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):

file +
src/core/webview/webviewMessageHandler.ts 32 8
src/core/webview/__tests__/webviewMessageHandler.spec.ts 117 1
webview-ui/src/utils/vscode.ts 62 7
webview-ui/src/utils/__tests__/vscode.spec.ts (new) 216 0
webview-ui/src/context/ExtensionStateContext.tsx 4 1
webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx 97 0

Composition 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: eslint --prune-suppressions --max-warnings=0 exit 0 per touched file (src: webviewMessageHandler.ts + spec; webview-ui: vscode.ts, vscode.spec.ts, ExtensionStateContext.tsx + spec). src/eslint-suppressions.json unchanged — suppression counts did not increase (a prune run that only re-indented the file with zero count change was reverted).
  • check-types: src exit 0; webview-ui exit 0.
  • vitest: webviewMessageHandler.spec.ts 85/85 pass (81 base + 4 new launch tests); vscode.spec.ts 9/9 (new spec); ExtensionStateContext.spec.tsx 24/24 (21 base + 3 new). ClineProvider.spec.ts not affected (no ClineProvider.ts changes in this unit).
  • prettier: --check exit 0 on all six touched files (CRLF checkout normalized via --write).
  • stryker-diff (base 43b52aa11, head 090d2c87e): 65 raw mutants — 59 Killed, 0 Survived, 0 NoCoverage (6 Ignored equivalent mutants, the documented CS Stryker disable comments in vscode.ts L91/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):

  • Mojibake comment hunk in the WMH diff (—? corruption in the requestRouterModels opencode-go comment): base comment // Deliberately no opencodeGoApiKey — the endpoint is public. kept as-is.
  • Unused defaultModeSlug import in the WMH spec: not ported (F3 re-adds it with its use).
  • kimi-code OAuth try/catch hunk in requestRouterModels + its webviewMessageHandler.routerModels.spec.ts additions: not ported (review-hardening hunk outside F1c scope).
  • ApiConfigManager.tsx className tweak: not ported (not part of the F1c row).
  • ApiConfigManager.visual.tsx deletion + screenshot baselines: not ported (visual-suite churn outside F1c scope).
  • providers/*, types, fetchers, e2e fixtures, and repo-config churn (.coderabbit.yaml, label-pr-review-state.yml, .gitignore, CONTRIBUTING.md, ClineProvider.ts changes, parallel-mode/sticky-mode specs, etc.): not ported (belong to the other series units).

Porting notes

Hand-ported from CS commit e9a44b2fa (base of record 0d937c050), hunk by hunk; no cherry-pick.

Ported:

  • src/core/webview/webviewMessageHandler.ts: webviewDidLaunch handler — await provider.setViewStateId(message.viewStateId); launch-time re-pin block (validate merged view-local name, then shared global, re-pin the view via provider.saveViewState when the global is still valid, else legacy global repair); updateSettings routed through provider.setValue.
  • webview-ui/src/utils/vscode.ts: VSCodeAPIWrapper fallback state, createViewStateId/getViewStateId, browser-fallback getState/setState with the CS Stryker disable comments.
  • webview-ui/src/context/ExtensionStateContext.tsx: launch effect posts webviewDidLaunch with viewStateId.
  • 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: RooCodeSettings import, saveViewState mock, setValue mock delegating to contextProxy.setValue, and the webviewDidLaunch describe (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 StringLiteral on the getGlobalState("currentApiConfigName") key (the CS mock getValue returns the canned value for any key, so a mutated key is unobservable) and the ConditionalExpression on if (name) (every CS test leaves name truthy). To satisfy the 0-survived stryker-diff gate without an escape hatch, the getValue mock in the launch describe is key-aware ("currentApiConfigName""shared-profile", anything else → undefined), and one additional test covers the falsy-name legacy 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/vscode mock, 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).

…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.
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • coderabbit-review-active

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Team

Run ID: 49c0719b-6c69-4e25-9e9f-2b4b921bc41a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Review status

Thanks 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

codecov Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 13 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/webview/ClineProvider.ts 94.59% 3 Missing and 7 partials ⚠️
webview-ui/src/utils/vscode.ts 86.36% 1 Missing and 2 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