Skip to content

feat(provider): persist per-view view-state identity and durable viewStates - #1546

Draft
easonLiangWorldedtech wants to merge 1 commit into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:vps2/f1a-view-identity
Draft

feat(provider): persist per-view view-state identity and durable viewStates#1546
easonLiangWorldedtech wants to merge 1 commit into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:vps2/f1a-view-identity

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Part of the vps2 durable per-view state series — tracked in easonLiangWorldedtech#41 (cross-repo: this PR is standalone against upstream/main @ 0d937c0).

Issue (created at PR-open time): #1547

What

At the base commit ClineProvider has no per-view state identity: every view shares the same global mode / profile / apiConfiguration keys, nothing is persisted per view, postMessageToWebview awaits an ack a remounted page never sends, and reset / history-restore writes leak across views. This PR lands the durable per-view core (fix unit F1a, 1/3): per-view identity, the viewStates persistence pipeline, and the view-local state buffer. The getState() merging of hydrated per-view values and the webview-side identity / launch wiring land in the follow-ups (F1b / F1c).

Design decisions

  • Per-view identity: viewId = renderContext plus a monotonic counter (unique per instance for its lifetime). viewStateId is the stable durable key (registered by the webview launch flow in F1c); rekeyPersistedViewStateEntry moves the temporary-id entry to the stable id, stable id winning on a collision.
  • Durable writes go through a serialized write queue (savePersistedViewState) so concurrent provider instances merge without lost updates; viewStates is pruned to the newest 50 entries (missing updatedAt sorts oldest).
  • setViewStateId sanitizes ids and rejects __proto__: a per-view entry must never be keyable through the Object.prototype setter. The fresh-read guard treats a corrupted non-object storage value as an empty map.
  • View-local buffer (viewLocalState): mode / currentApiConfigName / apiConfiguration (non-secret subset) live per view in memory. saveViewState awaits the durable write before logging success. loadViewState hydrates at registration, keeps the profile name and logs when the profile lookup fails, and discards a stale load when the viewStateId changes mid-lookup.
  • setValues / setValue validate mode against getModeBySlug (unknown → log and ignore; non-string passes through) and keep or clear the matching buffer fields; undefined / null values delete the buffer field rather than storing it. getValues merges context values with the buffer (buffer wins).
  • postMessageToWebview no longer awaits the webview ack (a remounted or disposed page never acknowledges; awaiting would wedge task-critical callers).
  • resetState clears viewLocalState and the view's persisted entry (after the customModesManager.resetCustomModes modal confirm).
  • History restore (sticky-mode spec) writes the restored mode view-locally via saveViewState("mode", ...) instead of the shared global mode.

Measurements

  • a+d vs upstream/main @ 0d937c0: 999 (976+/23−) — over the 400 soft budget; measured at cut (git diff --numstat 0d937c050..HEAD); under the 1000 hard cap. Composition: impl + types + adapted history-restore tests ≈ 424 a+d (ClineProvider.ts 391, sticky-mode spec 15, packages/types 16, suppressions 2); the remainder is the new view state persistence edge cases describe (17 focused tests) plus spec fixture adaptation.
  • src executable lines (mutation preflight): ClineProvider.ts 391 a+d (382+/9−) — under the 500-line cap; the gate run produced 179 raw mutants (under the 400 cap).

Gates

  • eslint --prune-suppressions: pass (suppression counts unchanged: ClineProvider.spec.ts no-explicit-any 198; prune-only reindent reverted)
  • check-types: pass (11 packages)
  • vitest: ClineProvider.spec.ts + ClineProvider.sticky-mode.spec.ts 204 pass
  • stryker-diff ci @ 0d937c0: 179/179 killed, 0 surviving, 0 uncovered, 0 blocking (ClineProvider.ts)
  • e2e / i18n / visual: n/a (zero new i18n strings; no webview-ui changes)

Parked / documented

From the gap-review parked-items register (F1a scope, all bounded):

  1. Dev/prod viewStateId divergence — inherent to the browser mock.
  2. Pre-launch write-queue / rekey orphan window + cross-session temp-id collision — narrow window, prune-bounded, non-secret.
  3. Cross-process write-queue interleave — pre-existing memento semantics.
  4. Redundant repoint branch — cosmetic.
  5. Launch-repair queue sync — transient, self-healing.
  6. Same-provider two-writer interleave test — cross-instance interleave already covered.
  7. Flat-mutation apiConfiguration replace — coherent via the getState() re-merge (lands F1b).

Porting notes

All F1a content is re-implemented against the base by hand-porting hunks from CS e9a44b2fa (#977 head): the durable core (getPersistedViewStates fresh-read guard, savePersistedViewState queued merge + prune, clearPersistedViewState, prunePersistedViewStates, rekeyPersistedViewStateEntry, setViewStateId, loadViewState, saveViewState), the view-local buffer with the setValue / setValues / getValues mutation handlers, the postMessageToWebview void-ack, the resetState clear, the viewStates record in GLOBAL_STATE_KEYS + types (global-settings.ts / vscode-extension-host.ts / index.test.ts), and the two history-restore tests adapted in ClineProvider.sticky-mode.spec.ts. The CS F1 spec (1790-line parallelMode.spec.ts) is NOT ported as one file: the F1-series describes are rewritten into the existing ClineProvider.spec.ts fixture (drops the 588-line mock preamble).

  • Only intentional delta from CS: setViewStateId gains the 5-line __proto__ rejection (A1 review hardening); the CS stryker-ignore comment is dropped — the guard is covered by the mutation gate instead.
  • The six-item CS-hunks-not-ported register is observed (kimi-code OAuth try/catch; ApiConfigManager className tweak; ApiConfigManager.visual.tsx deletion + baselines; mojibake comment hunk; unused defaultModeSlug import — F3 re-adds it; providers/* + repo-config churn) — none ported here.

…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).
@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: 353c3cb1-f2e4-44fa-bda7-fef7aa70e0f6

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: Mark the PR ready. Required CI must pass before CodeRabbit starts.

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 93.02326% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/webview/ClineProvider.ts 93.02% 3 Missing and 6 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