Skip to content

test(e2e): port view-state suite with reload and rehydration coverage (vps2 F6) - #1565

Open
easonLiangWorldedtech wants to merge 439 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:vps2/f6-e2e-viewstate
Open

easonLiangWorldedtech wants to merge 439 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:vps2/f6-e2e-viewstate

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Draft PR — vps2 unit F6 (e2e view-state suite + fresh-tab API surface).

Tracking: fork issue 41 (vps2 series ledger). Upstream issue: #1564 (this series' gap record; the review lineage is #929, A5 re-review minor 2). Port source: the CS worktree apps/vscode-e2e (e9a44b2, the A5 re-review state) — 3 files byte-identical to CS; the reload test is F6-authored.

What this unit does at head 56264d1

  1. apps/vscode-e2e (the F6 port, no production code) — modes.json debug-mode fixture entry; runTest.ts addViewStateFixtures wiring + model-agnostic post-switch predicates; fixtures/view-state.ts per-task marker fixtures + the three-panel ten-rounds isolation plan; suite/view-state.test.ts (2 CS tests byte-identical + the F6-authored sidebar webview reload/rehydration test).
  2. src/activate/registerCommands.ts — the exported createClineTabPanel (fresh tab, never reuses the tracked panel) with the view-state listener that re-tracks the active panel; this round it was reconciled with main's typed options + shared WebviewFocusTracker (openClineInNewTab keeps the serialized flow; createTabPanelUnlocked delegates the fresh-creation body to createClineTabPanel).
  3. src/extension/api.ts — startNewTask keeps the fresh-tab createClineTabPanel call (wired with the sidebar provider's tracker) instead of main's factory.createInNewTab, so API tabs never evict a tracked tab's task; the required ClineProviderFactory constructor parameter is kept; the context field the auto-merge dropped is restored; the readiness race (viewStateReadiness + isDisposed) members are restored.
  4. src/core/webview/ClineProvider.ts — loadViewState mutation-marker load-merge (this unit's reviewed design: a completed mutation, including a clear, wins over stale persisted values), the setValue mode validation, and the corrupt-entry pruning with its exact drop log. This round the F1a view-identity review fixes are ported on top (temporary-id authorship flag, ownsTargetEntry re-key gate, null-prototype viewStates merged with the pruning loop, loadViewState ownership guard, setValue/setValues rollback) and the per-view mergedStateValues reads keep main's new alwaysDenyUnapprovedCommands.
  5. Specs — every ClineProvider construction passes the shared WebviewFocusTracker (93 cp.spec + 49 parallelMode sites); the api specs pass the required ClineProviderFactory (never called; tab creation runs through the mocked createClineTabPanel); createTask option expectations include main's toolRepetitionSoftLimit: 0; the ported main-side panel test pins the MdmService double locally; the sticky-mode persisted-view-state-keys suite, rollback tests and viewStates global-setting tests are ported, with the re-key tests authoring entries through the real write path.
  6. Upstream main 793b65b merged (this round): 7 conflicts reconciled with both-sides review (registerCommands + spec, api.ts, ClineProvider + spec, Task.spec, ExtensionStateContext.spec.tsx).

Budget

Unit delta vs the stack base stays within budget (the e2e port is 4 files, +543/-0; the review-fix layer is production-hardening carried from the pre-merge review). The PR diff vs upstream/main is larger because the branch is stacked on the unmerged F1-F5 series; the series merges bottom-up, at which point each PR's CI gate measures its own unit. The CI mutation-diff changed-line advisory is a by-construction artifact of the stacked diff.

How to test

  • pnpm --dir apps/vscode-e2e test (mocked e2e) — the "Roo Code View State" suite incl. the sidebar reload/rehydration test.
  • vitest run src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts — pruning, retention, profile-mutation and multi-instance describes.
  • vitest run src/activate/__tests__/registerCommands.spec.ts — fresh-tab creation, tracked-tab reuse, init-failure cleanup, panel title test.
  • vitest run src/extension/__tests__/api-task-control.spec.ts — startNewTask fresh-tab wiring without evicting a tracked tab's task.

Verification numbers (head 56264d1)

  • vitest: ClineProvider.spec 294/294, Task.spec 190/190, full core batch (cp.spec + sticky + parallelMode + registerCommands + Task + 3 api specs + webviewMessageHandler + flicker-free-cancel) 742/742, api-start-new-task 6/6, remaining factory-based api/extension specs 71/71; webview-ui ExtensionStateContext 25/25; packages/types global-settings 9/9. (Local donor types dist was refreshed from source mid-round, which retired the previously-known local-only failures.)
  • eslint --max-warnings=0 clean on all touched files; eslint-suppressions ClineProvider.spec.ts 196 -> 194 (decrease), no count increased.
  • tsc: only 2 local-only errors remain (donor @roo-code/telemetry dist predates the 4-arg captureConsecutiveMistakeError signature; CI builds from source).
  • Negative controls: forcing the re-key ownership check to true reddens sticky-mode "does not merge a stale temporary-id entry"; reverting startNewTask to the factory design reddens api-start-new-task routing + tab-failure tests; both restored byte-exact (sha256 match).
  • Prettier: full-repo check passes on the PR merge tree (re-verified at the current head).

Updated this round (b2333b1 -> d5a5895)

  • Merged upstream/main 793b65b; resolved 7 conflicts with both-sides review.
  • Reconciled the fresh-tab createClineTabPanel surface with main's typed options + shared tracker; api.ts keeps fresh-tab semantics and the new required factory parameter; restored the auto-merge-dropped context field and viewStateReadiness/isDisposed members.
  • Ported the F1a view-identity review fixes into this unit's ClineProvider copy while keeping this unit's setValue mode validation, corrupt-entry pruning log and mutation-marker load-merge semantics; adapted the re-key tests to author entries through the real write path; ported the sticky-mode persisted-view-state-keys suite, rollback tests and viewStates global-setting tests.
  • Added the required WebviewFocusTracker constructor argument to 93 ClineProvider.spec and 49 parallelMode.spec constructions; api specs pass the required ClineProviderFactory; createTask expectations include toolRepetitionSoftLimit: 0; the ported main-side panel test pins the MdmService double locally.
  • Suppression count ClineProvider.spec.ts 196 -> 194.
  • (Second commit 56264d1) Adapted main's api-start-new-task.spec.ts to this unit's fresh-tab design: the spec now mocks the registerCommands createClineTabPanel module function, asserts the exact {context, outputChannel, webviewFocusTracker} payload and pins that the factory is not used for tab creation (CI caught the unadapted spec failing 3 tests at the first head).

Mutation gate (carried)

  • Unit-delta local Stryker preflight was certified at the pre-merge head; the stacked CI mutation-diff reports a changed-line cap artifact by construction (inherited shared content).

Series mechanics

  • PR base is main; the branch is stacked on the F5 head; merge order F1a -> F1b -> F1c -> F2 -> F3 -> F4 -> F5 -> F6 -> F7.
  • CS not-ported register (for consistency): kimi-code OAuth try/catch + routerModels.spec +29; ApiConfigManager min-w-0->grow; ApiConfigManager.visual.tsx deletion + 2 PNG baselines; mojibake comment; providers/, .coderabbit.yaml, .github/, CONTRIBUTING.md, .gitignore churn — none of these touch the files in this PR.

Pre-merge ERROR-row pre-check (head 599844c)

  • Security Boundaries / view-state id rows: addressed at 599844c. setViewStateId now rejects non-string ids (crafted postMessage), bounds the key length (MAX_VIEW_STATE_ID_LENGTH), and refuses ids already owned by another live instance; a refused view stays on its temporary id against shared state. Regression tests: non-string, oversized, claimed-by-another-view. Negative controls: disabling the ownership check reddens exactly the claim test; widening the length bound reddens exactly the oversized test; restores byte-exact (hash match).
  • All three refusal paths resolve viewStateReadiness, so API-driven task creation never falls through to its timeout when a registration is refused.
  • The spec gained an afterEach clearing the static instance registry (suites construct providers directly and never dispose most of them; leaked instances would claim ids ahead of later tests).
  • Persistence Integrity setValues/setValue ordering rows: verified already addressed by the pushed code: _saveViewLocalStateFromMutation persists the non-secret subset before updating the buffer; the re-key is a single queued write (no delete-then-write window; authorship clears only after the write lands); the mutation hook keeps the buffer synced across setValue/setValues and profile upsert/activation/deletion.
  • Proofs at 599844c: sticky-mode 33/33; eslint --max-warnings=0 clean; tsc clean except the two known local telemetry-dist noise lines; prettier merge-ref gate ALL-MATCHED.
  • Non-activating saves (upsertProviderProfile with activate=false, and the zoo-gateway callback's non-active path) rewrote the profile on disk but left pinned views serving the pre-save snapshot; the non-activating branch now refreshes pinned sibling views (and this view when it holds the pin), and the callback routes through upsertProviderProfile so the refresh cannot be forgotten.
  • getState() no longer spreads the pinned overlay over the shared provider settings (a key the profile never defined leaked in from the shared profile: model id, region, tuning). The overlay is a complete snapshot: used in place, with only apiProvider falling back to shared state.
  • Regression tests: "refreshes views pinned to a profile saved without activation"; "does not leak shared provider keys into a pinned view's api configuration; the zoo-gateway callback test asserts the upsert path.
  • Negative controls: dropping the else-branch refresh reddens exactly the refresh test, reverting the merge semantics reddens exactly the leak test; restores byte-exact (hash match).
  • Proofs at fbad9cc: ClineProvider.spec green, sticky-mode and webviewMessageHandler green, eslint --max-warnings=0 clean, tsc clean except the two known local telemetry-dist noise lines, prettier merge-ref gate ALL-MATCHED.

Follow-up: overlay read from the view buffer (required-lane fix)

The previous round's overlay-completeness change read the overlay from the merged state values, which also carry the raw apiConfiguration state key. That key is a partial patch written by shared-store paths, not a view snapshot: with no overlay present and the raw key partial, getState() served only the partial object and dropped the shared provider settings the previous merge produced (the parallel-mode activation tests caught exactly this - the activated profile's model id vanished; required lane red on the units carrying that spec). The overlay is now read from the view buffer itself, where a present value is by contract a complete profile and an absent one means the shared store serves the settings.

Proofs: ClineProvider.parallelMode.spec green where present (the six synchronization tests that regressed), ClineProvider.spec green including the overlay-completeness tests, sticky-mode and webviewMessageHandler green; negative control: reverting the overlay to a shallow merge over the shared settings still reddens exactly the leak test; restores byte-exact (hash match); eslint --max-warnings=0 clean; tsc clean except the two known local telemetry-dist noise lines; prettier merge-ref gate ALL-MATCHED.

Follow-up: active task handlers rebuilt on non-activating saves (head eb27fac)

A review round on the view-identity unit found that the non-activating-save refresh updated pinned views' buffers but not their active tasks' API handlers, and the posted state prefers the task's own apiConfiguration: a conversation running in a view pinned to the saved profile kept sending the old token (e.g. after the Zoo Gateway callback rotated it) even though the view's buffer had caught up. The refresh now rebuilds the affected tasks' handlers in the pinned views, and the non-activating branch rebuilds this view's own handler when it holds the pin.

Proofs: ClineProvider.spec green (the pinned-sibling refresh test now asserts the active task's handler is rebuilt with the saved settings, not just the private buffer); negative control: dropping the rebuild from the refresh reddens exactly that assertion, restores byte-exact (hash match); parallelMode/sticky-mode/webviewMessageHandler green; eslint --max-warnings=0 clean; tsc clean except the two known local telemetry-dist noise lines; prettier merge-ref gate ALL-MATCHED.

Follow-up: test-quality round from the review (head 320fd43)

The review round on the api-wiring unit named four test-quality issues; the ones present in this unit are fixed here: the viewStates describe block moved out of the alwaysDenyUnapprovedCommands block (test names carried the wrong subject), the prune test's toBeDefined() survival checks replaced with exact-shape toEqual assertions (they pass even if pruning strips fields), the stale-temporary-entry test's fixed 10 ms wait replaced with awaiting the load itself (the negative assertion could pass vacuously on a slow load), and the readiness-cancel test's fake timers restored in a finally block (a failed assertion left fake timers active and hung a later real-timer test).

Proofs: global-settings 9/9 where present, ClineProvider.spec green, sticky-mode green where present, api-task-control green where present; negative controls: pruning that strips updatedAt reddens exactly the prune test, a loadViewState that adopts the unowned temporary-id entry reddens exactly the stale-entry test, restores byte-exact (hash match); eslint --max-warnings=0 clean (src and types); prettier merge-ref gate ALL-MATCHED.

Follow-up: review-round fixes from the series (head b6341af)

A review round on this unit produced three fixes. The view-state readiness bound no longer rejects headless callers whose webview never launched (it only rejects when the webview has launched and readiness failed), and the IPC start-task listener catches rejections since the IPC server does not await async listeners (new headless-fallback and launched-stuck tests; negative control: an unconditional timeout reddens exactly the fallback test). The readiness-cancel test now pins the bound's own timer via fake timers and a zero timer count instead of a global clearTimeout spy, and the mode-ordering test asserts the switch has not been called before the pending initialization is released (negative controls: dropping the timer clear / the initialization await reddens exactly the respective assertion).

Proofs: Task.spec 190/190, api-task-control 26/26; negative controls: dropping the initialization await reddens exactly the ordering test's in-flight assertion, dropping the readiness timer's clear reddens exactly the timer-count assertion, an unconditional timeout reddens exactly the headless-fallback test, restores byte-exact (hash match); eslint --max-warnings=0 clean; tsc clean except the two known local telemetry-dist noise lines; prettier merge-ref gate ALL-MATCHED.


Updated this round (CodeRabbit follow-up, head e27816f)

  • Mode-switch atomicity (ported from the sibling unit): handleModeSwitchUnlocked now captures the pre-switch task mode with a task-write rollback and reconciles every write it owns (shared mode, durable view pin, task mode, task history) when a failure lands after the task-mode write - including the mode-linked profile activation - so submitUserMessage delivers on the pre-switch mode and profile instead of a half-applied switch; regression test included.
  • Non-activating profile saves no longer force-rebuild the API handler of a view whose active task follows a different sticky profile (both rebuild sites guarded by taskApiConfigName); regression tests cover both sites.
  • Test hygiene: prune assertions compare full kept entries; the two re-key storage tests seed through the durable write path so their assertions can fail; sticky-mode afterEach disposes the per-test provider before resetting the instance registry; the webview state-payload test renamed.
  • Verified: ClineProvider.spec 298 green, sticky-mode 34 green, parallelMode 40 green, eslint clean, tsc real=0, prettier clean; negative controls each exactly one red.

Updated this round (CodeRabbit follow-up 3, head ed668c4)

  • Mode-switch reconcile (this unit's thread): the trailing postStateToWebview now runs outside the reconciled region - a UI-refresh failure after a fully committed switch (ModeChanged emitted, activation landed) is logged, not rolled back. The catch also skips the rollback once the mode-linked activation has started writing shared state (a mode-only rollback would pair the old mode with the new profile) and when the mutation signal has aborted (the queue has advanced; a newer mutation may own the mode stores). Three regression tests included (negative controls: removing either guard or re-throwing the post reddens exactly one test each). Ported series-wide.
  • Verified: sticky-mode 37 green, ClineProvider.spec 298 green, parallelMode 40 green, eslint --max-warnings=0 clean, tsc real=0, prettier clean.

Updated this round (CodeRabbit follow-up 4, head 03913bf)

  • Mode-switch events now match the stores: ModeChanged is emitted only after the switch commits (a reconciled failure no longer leaves listeners on the rolled-back mode), and the rollback re-emits TaskModeSwitched with the restored mode for API/IPC consumers. The lock path no longer posts inside the reconciled region (it falls through to the committed-switch tail, where a post failure is logged). The mode-default recording reads the view's effective profile (getValues overlay) instead of the shared slot, so a sibling view's activation cannot become this mode's default. Three regression tests included (negative controls: each mutation reddens exactly its test).
  • Verified: sticky-mode 40 green, ClineProvider.spec 298 green, parallelMode 40 green, eslint --max-warnings=0 clean (suppressions pruned: cp spec no-explicit-any count reduced), tsc real=0, prettier clean.

Updated this round (CodeRabbit follow-up 5, head 1759c8e)

  • The committed-switch guard now flips only when the activation has actually written the activated profile (onSharedWriteStart callback), so a mode-linked activation whose own profile lookup fails (deleted profile) still reconciles the switch. The catch that keeps a switch (activation had written, no abort) now emits ModeChanged and posts state before rethrowing, so listeners and the webview learn the committed mode; the abort branch intentionally stays silent (a newer mutation may own the fields - documented in code). In-try abort checkpoints after the mode writes no longer return silently: they fall through to the emit/post tail so the committed mode reaches listeners and the webview. Three regression tests; negative controls: each mutation reddens exactly its test. The activation-failure test double now mirrors the onSharedWriteStart flip.
  • Verified: sticky-mode 43 green, ClineProvider.spec 298 green, parallelMode 40 green, eslint --max-warnings=0 clean, tsc real=0, prettier clean.

Updated this round (CodeRabbit follow-up 6, head 9e20cc9)

  • Abort-silence rule settled: the emit/post tail announces a switch only while its committed mode is still current (after an abort, the tail checks the effective mode store; a superseded commit stays silent because the newer mutation already announced its own mode). This keeps both invariants: a committed switch is never left un-notified while it is current, and no stale announce clobbers a newer owner. Regression tests cover both branches (still-current -> emitted; superseded -> silent); negative controls: dropping the guard reddens the superseded test, always-silencing reddens the still-current test.
  • Verified: sticky-mode 44 green, ClineProvider.spec 298 green, parallelMode 40 green, eslint --max-warnings=0 clean, tsc real=0, prettier clean.

Updated this round (CodeRabbit follow-up 7, head 786a1ce)

  • The lookup-failure regression test now exercises the REAL activateProviderProfileUnlocked (providerSettingsManager.activateProfile rejects) instead of a stub, so the production placement of the committed-switch flip (only after activateProfile resolves) is covered; a matching test keeps the switch when activateProfile resolves and a later shared write (setModeConfig) rejects. Negative controls: moving the flip above the activateProfile await reddens exactly the rollback test; removing it reddens exactly the kept-switch test.
  • Verified: sticky-mode 45 green, ClineProvider.spec 298 green, parallelMode 40 green, eslint --max-warnings=0 clean, tsc real=0, prettier clean.

Updated this round (CodeRabbit follow-up 8, head 0e9337f)

  • The kept-switch regression test now also pins the task-side state on the committed mode: the task's in-memory mode stays architect and the last task-history write carries mode architect (a trailing rollback of the task-side writes after the committed-switch point would now be caught). Negative control: an extra history write with a different mode in the kept branch reddens exactly this test.
  • Verified: sticky-mode 45 green, ClineProvider.spec 298 green, parallelMode 40 green, eslint --max-warnings=0 clean, tsc real=0, prettier clean.

Stryker suppression comments removed

Per the AGENTS.md policy (never add Stryker directives; remove them when a file is touched), all // Stryker disable directives were dropped from this PR. Equivalence rationale for the removed suppressions:

  • Stryker disable next-line ConditionalExpression: defensive clobber guard, unreachable per the ordering argument above.
  • Stryker disable next-line StringLiteral: setPanel branches only on type === "sidebar", so any other literal routes to the identical tab-ref assignment
  • Stryker disable next-line StringLiteral: setPanel only distinguishes "sidebar"; any other value routes to the tab-ref assignment
  • Stryker disable next-line ConditionalExpression: at the catch, this call's own state-change handler only ever re-points the tracked ref at newPanel itself, so tabPanel === newPanel is the only reachable state here; a concurrent re-pointing race is not representable in this spec.
  • Stryker disable next-line ConditionalExpression: only tool_use and mcp_tool_use entries ever carry an id — the text block pushed in the text case has no id field and the start events reaching this guard always carry the string id locked in by processRawChunk — so the type check cannot change the outcome of entry.id === event.id for any entry this pipeline produces; the false replacement is additionally killed by the rejection tests.
  • Stryker disable next-line ConditionalExpression: event.name is always a string on this path — the only delta events reaching this handler are emitted by processRawChunk with the tracked start name (never undefined), so the legacy nameless branch below is unreachable defensive code and the conditional always takes the named side.
  • Stryker disable next-line ConditionalExpression: streamingKey can never be undefined here — makeStreamingKey always returns a string and the legacy lookup branch above is unreachable (see the directive on the event.name check) — so the null side is unreachable defensive code.
  • Stryker disable next-line ConditionalExpression, EqualityOperator: streamingKey is always a defined string on this path (makeStreamingKey never returns undefined; the legacy branch is unreachable per the directives above), and processStreamingChunk returns a partial ToolUse for a known key, so both defensive conditions reduce to the partialToolUse check and cannot change the observed behavior.
  • Stryker disable next-line LogicalOperator: event.name is always a string on this path — finalizeRawChunks emits end events with the tracked start name (a string, set before the tracker can start), so the ?? event.id fallback guards a nameless end event this pipeline cannot produce.
  • Stryker disable next-line ConditionalExpression, LogicalOperator, EqualityOperator: the both-miss case is unreachable via the raw-chunk pipeline — end events carry the locked start name so the compound key resolves the registered entry, and a same-ID rejected-start end is always processed after the accepted start's end has already removed that entry; this defensive path protects against a cross-name end event the pipeline cannot produce.
  • Stryker disable next-line ConditionalExpression: unreachable defensive region — getStreamingToolCallById can only resolve when the both-miss case above is reachable, which the raw-chunk pipeline cannot produce (see the directive there).
  • Stryker disable next-line CallExpression,ObjectLiteral: _updateViewLocalStateFromMutation applies only mode, currentApiConfigName, apiConfiguration and provider-settings keys, so a listApiConfigMeta-only payload (deleted call or empty object) is a behavior-preserving no-op.
  • Stryker disable next-line ConditionalExpression: in this fallback the view does not pin the deleted profile, so when the deleted profile was not the shared selection profileToActivate equals the shared selection itself; forcing the branch only rewrites that same value back into the store.
  • Stryker disable next-line ConditionalExpression,BlockStatement: hostile non-stabilizing encodings are unreachable from the webview (its posts are plain link targets); the bound is defensive
  • Stryker disable next-line CallExpression: hostile non-stabilizing encodings are pinned by the direct decodeUntrustedPathToStable bound test; the webview never posts such values
  • Stryker disable next-line ConditionalExpression,OptionalChaining: equivalent mutant - when localStorage is unavailable the guard-false path and the throwing body both return this.fallbackState from this catch
  • Stryker disable next-line ConditionalExpression,OptionalChaining: equivalent mutant - when localStorage is unavailable the guard-false path and the throwing body both mark the write failed and return newState

CR round 2026-10-15 (b)

  • Ported the payload-customModes fix for setValues mode validation (identical flagged block; regression test added) so the units stay byte-identical at merge.

CR round 2026-10-15 (c)

CR round 2026-10-15 (d)

  • Ported: post-flatten shared-provider fallback for explicitly-undefined nested apiProvider (identical flagged block; regression test added).

CR round 2026-10-15 (e)

CR round 2026-10-15 (f)

  • The createTask registration-failure rollback now restores the shared mode and the view's own mode pin independently (unset pin stays unset); divergent-pin regression test added. Same fix in all six units carrying the flagged block.

Row-9 update (head 44d7a56)

  • setGlobalState persists parsed.data instead of the raw payload: non-strict schemas strip unknown fields only in the parsed value, so credential-shaped extras cannot reach durable global state (CR Security Boundaries).
  • Activation writes from the stored record (read-back feeds every post-save consumer; CR Security Boundaries).
  • Clear/re-point cache divergence is guarded at the ContextProxy#updateGlobalState layer on this unit; the new tests pin that guard.

Verification: new tests are focused negative controls (reverting each guard reddens exactly the matching test); ClineProvider.spec, webviewMessageHandler.spec spot-checks, api specs, spec-inclusive tsc --noEmit, eslint --max-warnings=0 (suppression counts unchanged) and prettier all green on this head.

Upstream refresh (head 44d7a56)

Merged the current upstream/main (merge commit; the row-9 fix commit is unchanged and is the first parent) to clear the review gate's mergeability hold. Upstream refactored upsertProviderProfile into a new persistProviderProfile method and added a runToCompletion queue option; this unit's transactional body was ported into the new method unchanged and upstream's Promise.allSettled activation fix was adopted in it. ClineProvider.spec, webviewMessageHandler.spec, tsc --noEmit, eslint (suppression counts unchanged) and prettier re-verified green at this head. The first refreshed head's windows unit-test lane caught that upstream's new updateProfileModel tests in ClineProvider.apiHandlerRebuild.spec.ts mock the settings manager with a static getProfile, which starves the post-save read-back; the mock now repoints getProfile at what saveConfig wrote (the real manager's behavior), and the file's 38 tests pass locally alongside the rest of the verification set.

@coderabbitai

coderabbitai Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

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

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
Summary

Summary by CodeRabbit

  • New Features
    • Added support for keeping mode and API profile selections separate across sidebar and editor-tab views, with view-specific settings preserved when switching tasks or reopening the sidebar.
    • Added editor-tab actions for starting tasks and opening settings, marketplace, and history.
    • Added an option to start a task without closing open editors.
    • Improved handling of mode switches so they apply to the task that requested them.
  • Bug Fixes
    • Improved recovery when switching modes or profiles fails, and when restoring or importing settings.
    • Excluded view-specific state and sensitive credentials from portable settings exports.

The change adds durable per-view mode and profile state, updates profile and mode operations to use provider-level handling, and expands task-control APIs. It also separates sidebar and editor-tab command routing and adds unit and end-to-end coverage for these flows.

Changes: Per-view state and profile handling

Layer / File(s) Summary
View-state contract and webview identity
packages/types/src/global-settings.ts, packages/types/src/vscode-extension-host.ts, webview-ui/src/utils/vscode.ts, webview-ui/src/context/ExtensionStateContext.tsx
Adds a persisted view-state schema and sends a view-state ID at webview launch. The browser wrapper validates and persists IDs and uses in-memory state when browser storage fails.
Durable state and settings portability
src/core/config/ContextProxy.ts, src/core/config/importExport.ts, src/core/config/ProviderSettingsManager.ts, src/core/config/__tests__/*, packages/types/src/global-settings.ts
Migrates two legacy provider keys to secret storage, excludes view-local state from settings import and export, and adds cross-instance profile-manager operations.
View-local profile and mode mutations
src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts, src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts, src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts
Adds coverage for persisted view state, profile changes, mode-switch commit and rollback behavior, multi-instance isolation, and temporary view-ID ownership.
Configuration reads and writes
src/extension/api.ts, src/extension/__tests__/api-configuration.spec.ts, packages/types/src/api.ts
Flattens configuration reads while excluding view-state and credential-bearing fields. Configuration writes save profiles and mappings with rollback handling.
Launch validation and profile operations
src/core/webview/webviewMessageHandler.ts, src/core/webview/__tests__/webviewMessageHandler.spec.ts
Registers view IDs at launch and repairs invalid view profile pins. Profile, custom-mode, and sign-out updates use provider-level operations; profile renames include compensation paths.

Task mode and extension API

Layer / File(s) Summary
Task-scoped mode switching
src/core/task/Task.ts, src/core/tools/SwitchModeTool.ts, src/core/task/__tests__/Task.spec.ts, src/core/tools/__tests__/switchModeTool.spec.ts
Task submissions wait for mode initialization and delegate switches to the provider. The switch tool passes its task and reports a missing provider as a tool failure.
Task creation and test-only API
src/extension/api.ts, packages/types/src/api.ts, src/extension/__tests__/api-task-control.spec.ts, apps/vscode-e2e/src/suite/index.ts, apps/vscode-e2e/src/types/global.d.ts
Adds task ask and follow-up controls, validated global-state accessors, optional editor preservation, and view-readiness handling for task creation.
View-isolation E2E coverage
apps/vscode-e2e/fixtures/modes.json, apps/vscode-e2e/src/fixtures/view-state.ts, apps/vscode-e2e/src/runTest.ts, apps/vscode-e2e/src/suite/view-state.test.ts
Adds fixtures and E2E tests for mode isolation across views, multi-panel follow-up sequences, persisted state, and sidebar reload behavior.

Sidebar and editor-tab command routing

Layer / File(s) Summary
Tab command contributions and routing
packages/types/src/vscode.ts, src/package.json, src/activate/registerCommands.ts
Adds tab-specific commands and routes sidebar and tab actions to their respective providers.
Tab creation and panel lifecycle
src/activate/registerCommands.ts, src/activate/__tests__/registerCommands.spec.ts
Tracks live tab panels, reuses available providers, serializes overlapping creation, and cleans up failed initialization.
Task creation with fresh tab panels
src/extension/api.ts, src/extension/__tests__/api-start-new-task.spec.ts
Routes new-tab task creation through createClineTabPanel.

Test and repository maintenance

Layer / File(s) Summary
Test synchronization and repository settings
webview-ui/src/i18n/__tests__/TranslationContext.locale-loading.spec.tsx, src/eslint-suppressions.json, .gitignore
Locale tests await language-loading promises. Lint-suppression counts and ignore rules are updated.

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

Change: Other

Sequence Diagram(s)

sequenceDiagram
  participant VSCodeAPIWrapper
  participant ExtensionStateContext
  participant webviewMessageHandler
  participant ClineProvider
  ExtensionStateContext->>VSCodeAPIWrapper: Get view-state ID
  ExtensionStateContext->>webviewMessageHandler: Send launch message with viewStateId
  webviewMessageHandler->>ClineProvider: Register view-state ID
  ClineProvider->>ClineProvider: Persist view-local state
Loading

Possibly related issues

  • Zoo-Code-Org/Zoo-Code#2314 — The PR changes persisted view-state handling, but does not add the issue’s cross-host coordination.
  • Zoo-Code-Org/Zoo-Code#2152 — The PR filters credential-bearing configuration and adds view-state registration and isolation tests, which overlap with the issue’s secret-exposure and loading objectives.
  • Zoo-Code-Org/Zoo-Code#2151 — The PR changes view-ID validation and persisted-state handling; the issue’s proposed claim-token mechanism is not shown in the supplied change summary.
  • Zoo-Code-Org/Zoo-Code#2303 — The PR adds per-view mode and profile handling and tests for isolation across panels.

Possibly related PRs

Security Architecture Review


Security architecture risk: 🟡 Moderate · up to b2333

Per-view isolation improves command targeting, but profile identity and effective credentials can diverge after a lookup failure or overlapping profile switches. A subsequent task could use a different provider or account than the selected profile indicates. The demonstrated scope is within one extension host and its configured profiles; broader privilege escalation is not established.

Retained concerns

  • Medium · security · inferred: The new rehydration path installs the persisted profile name before resolving its credential-bearing settings. If resolution fails, the name remains locally authoritative while getState falls back to shared provider settings. Subsequent task creation can therefore consume another profile's configuration under the pinned profile identity. Launch repair checks profile existence, so it does not necessarily repair a transient settings-resolution failure for an existing profile. This introduces a profile-ownership failure absent from the base's shared-only state model.
  • Medium · security · inferred: Profile activation is queued per provider instance, although it snapshots sibling overlays and replaces shared credential settings. Two activations can both finish their snapshots before either shared write, then each clear its own overlay. The final shared configuration can consequently belong to profile Q while another view remains durably pinned to profile P. Sibling refresh selects only matching profile names and does not repair that mismatch. Existing tasks receive resolved activation settings directly, but later tasks consume the potentially mispaired view state.

Security review details


Security Blast Radius

  • inferred — The demonstrated exposure involves sibling views, shared settings, and configured profiles within one extension host. Mispaired configuration can affect subsequent tasks' provider or credential selection. Cross-tenant consequences depend on the configured profiles; exposure to other installations or environments is not established.

Security Findings and Attack Paths

  • inferred — A failed lookup or overlapping profile activation can leave a view displaying or persisting one profile while supplying another profile's settings to a new task. Sensitive task content could then be processed using the unintended provider or account. The inconsistent-state paths are source-supported; an executed exploit or unauthorized remote entrypoint was not demonstrated.

Trust Boundaries and Controls

  • observed — Stable identity is supplied by the renderer and normalized by the extension, without an ownership proof in setViewStateId. This is a new state-lookup mechanism, but arbitrary profile activation was already available through renderer messages at base. Identity lookup alone therefore does not establish newly expanded credential authority.
  • observed — Mode switching still validates the target mode and requests approval, now passes the originating task explicitly, and reports failure when its provider is unavailable. New-task creation retains organization allow-list validation of the effective configuration.

Resilience and Maintainability Implications

  • observed — Successful settings import awaits cross-view invalidation before posting the initiating view's state. Broadcast failures are logged without failing the import, so completed import does not itself prove every live view was invalidated.

Hardening Proposals

  • proposed — Treat profile identity and resolved configuration as one view-owned snapshot. Publish them together only after successful resolution, and serialize cross-view activation as a whole transaction or retain independent configuration snapshots for each pinned view. Failed recovery should expose an explicit unavailable-profile state rather than silently substitute shared credentials.

Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore (reviewers only)

Failed Failed checks: 2 errors · 2 warnings

Check name Status Explanation Resolution
Security Boundaries Error A failed profile lookup can send private task content to the wrong provider. In src/core/webview/ClineProvider.ts:1177-1189, loadViewState keeps the pinned profile name when getProfile fails, bu… Fail closed when a pinned profile cannot be resolved. Do not start tasks with shared provider settings until the pinned profile is loaded or the user explicitly selects a replacement. Apply the same rule to the reapplied-profile lookup, and…
Persistence Integrity Error The prior failure remains. In src/core/webview/ClineProvider.ts:3494-3555, the profile save completes before the newly added sibling snapshot. If a sibling profile read fails, the snapshot throws an… Make profile save and activation atomic, or compensate completed writes without overwriting concurrent changes. Report compensation failures. Add regression tests for sibling snapshot and activation-write failures.
Regression Evidence Warning Fresh-task inheritance of a restored view-local mode lacks focused coverage. ClineProvider.getState() now prefers view-local state, and Task.initializeTaskMode() reads that state. Provider tests m… Add a focused test using a real provider and task. Restore a non-default view-local mode that differs from the shared mode, create a fresh task without an explicit mode, and assert the inherited mode. Dispose both objects.
Lifecycle Resource Cleanup Warning View-state registration can continue after disposal. In ClineProvider.ts:957-979, a blocked re-key can write after disposal. Registration then starts loadViewState at line 1107, and a pending prof… Invalidate registration and load work when disposal starts. Check disposal before queued writes and after awaited lookups, while preserving consistency for writes already started. Stop launch handling and profile synchronization after dispo…
Passed Passed checks: 4 passed

Check name Status Explanation
Title check Passed The title names the primary change: a new e2e test suite for view-state with reload and rehydration coverage. The changeset includes that e2e port (modes.json fixture, view-state fixtures, runTest wir…
Description check Passed The description covers most template sections. It has a Description section that explains the unit, a Test Procedure section with concrete commands, and a Pre-Submission Checklist. It states that the …
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.
Full details: Title check

Explanation

The title names the primary change: a new e2e test suite for view-state with reload and rehydration coverage. The changeset includes that e2e port (modes.json fixture, view-state fixtures, runTest wiring, and view-state.test.ts with the sidebar reload test). The title omits the larger production work (fresh-tab creation, view-state persistence, profile and mode-switch handling, API surface), so it is partially related in scope. It is specific and readable, and it contains no file lists or vague terms. The 'vps2 F6' tag is internal series jargon, which a teammate scanning history may not recognize.

Full details: Description check

Explanation

The description covers most template sections. It has a Description section that explains the unit, a Test Procedure section with concrete commands, and a Pre-Submission Checklist. It states that the e2e suite was compile-checked but not run. Gaps: the 'Related GitHub Issue' section does not contain a 'Closes: #' line. It links only to a fork issue (41) and an upstream issue (#1564) in prose, and the template requires every PR to link to an approved issue. The description is also very long, with many follow-up rounds, and it does not use the template's Simplified Technical English style in several places (for example, long sentences and a dense list of symbols). Several checklist items are unticked, and the Documentation Updates section has no selection. The description is mostly complete, so it passes.

Full details: Security Boundaries

Explanation

A failed profile lookup can send private task content to the wrong provider. In src/core/webview/ClineProvider.ts:1177-1189, loadViewState keeps the pinned profile name when getProfile fails, but does not load its settings. getState then uses shared provider settings at lines 5373-5379, and createTask passes them to the new task at lines 6523-6554. A transient secret-storage failure can therefore route a task intended for a private provider to another provider. The launch repair in src/core/webview/webviewMessageHandler.ts:658-662 does not repair this case if the later existence check succeeds.

Resolution

Fail closed when a pinned profile cannot be resolved. Do not start tasks with shared provider settings until the pinned profile is loaded or the user explicitly selects a replacement. Apply the same rule to the reapplied-profile lookup, and add regression tests for transient lookup failures.

Full details: Persistence Integrity

Explanation

The prior failure remains. In src/core/webview/ClineProvider.ts:3494-3555, the profile save completes before the newly added sibling snapshot. If a sibling profile read fails, the snapshot throws and the saved profile is not restored. At lines 3558-3576, an activation write can also fail after setValue has persisted the new view pin; the successful writes are not compensated. upsertProviderProfile then reports failure. The read-back compensation only covers failure at lines 3509-3535.

Resolution

Make profile save and activation atomic, or compensate completed writes without overwriting concurrent changes. Report compensation failures. Add regression tests for sibling snapshot and activation-write failures.

Full details: Regression Evidence

Explanation

Fresh-task inheritance of a restored view-local mode lacks focused coverage. ClineProvider.getState() now prefers view-local state, and Task.initializeTaskMode() reads that state. Provider tests mock Task; task tests do not restore a view-local mode. The reload test explicitly skips fresh-task inheritance, and the isolation test starts tasks with an explicit mode.

Resolution

Add a focused test using a real provider and task. Restore a non-default view-local mode that differs from the shared mode, create a fresh task without an explicit mode, and assert the inherited mode. Dispose both objects.

Full details: Lifecycle Resource Cleanup

Explanation

View-state registration can continue after disposal. In ClineProvider.ts:957-979, a blocked re-key can write after disposal. Registration then starts loadViewState at line 1107, and a pending profile lookup can update the disposed provider at lines 1181-1192 and 1292. The changed launch handler continues after registration and can repeat custom-mode and profile synchronization for a replacement view (`webviewMessageHandler.ts:603-612, 631-705). Disposal does not invalidate this work.

Resolution

Invalidate registration and load work when disposal starts. Check disposal before queued writes and after awaited lookups, while preserving consistency for writes already started. Stop launch handling and profile synchronization after disposal. Add regression tests that block the re-key queue and profile lookup, dispose the provider, and verify that no new work starts after release.

Finishing Touches<