Repository navigation
fix(webview): show modes skeleton until view state loads (vps2 F7) - #1567
Open
easonLiangWorldedtech wants to merge 547 commits into
Open
easonLiangWorldedtech wants to merge 547 commits into
easonLiangWorldedtech wants to merge 547 commits into
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft PR — vps2 unit F7 (modes skeleton until view state loads).
Tracking: fork issue 41 (vps2 series ledger). Upstream issue: #1566 (this series' gap record; the original upstream bug is #915). Port source: upstream PR #928 (fix(webview): add loading skeleton for view state initialization) — closed/superseded; the fork issue 41 ledger names it as the F7 port source (webview-ui sections only).
What this unit does at head 109f821
Budget
Unit delta vs the stack base: 6 webview-ui files, +96/-5 (plus the src-side coverage carried from earlier rounds of this unit). The PR diff vs upstream/main is larger because the branch is stacked on the unmerged F1-F6 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 webview-ui test src/components/modes/__tests__/ModesView.spec.tsx— the skeleton test.pnpm --dir webview-ui test src/context/__tests__/ExtensionStateContext.spec.tsx— the viewStateLoaded lifecycle tests.pnpm --dir webview-ui test src/__tests__/App.spec.tsx— launch/focus message wiring.pnpm --dir src test src/activate/__tests__/registerCommands.spec.ts— the focusInput tab-priority guard tests.Verification numbers (head 109f821)
Updated this round (9859261 -> 1e87d0b)
Series mechanics
Temp-id test hygiene (head 7537805)
finally(constructor registers into the staticactiveInstances), restore the rejectingglobalState.updatestub infinally, and await the constructor'sloadViewStatepromise instead of a fixed 10 ms sleep; theCloudServicedouble gainedon/offfor dispose. Proofs: sticky-mode 30/30, eslint clean, prettier gate ALL-MATCHED; test-only change, no mutation control applies.webviewDidLaunchawaitssetViewStateId->loadViewStatebefore the first state post, soviewStateLoadedflipping withdidHydrateStateis accurate and no wrong-mode frame exists; pre-gating the skeleton would flash it on every boot regardless of the active tab. Details in the thread reply.CI re-trigger: head fa68982 is an empty commit over 7537805 (no content change) to re-run the flaky e2e-mock lane (known 30s-timeout flake); all verification numbers above apply unchanged.
Pre-merge ERROR-row pre-check (head 3fd9e42)
setViewStateIdnow 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).viewStateReadiness, so API-driven task creation never falls through to its timeout when a registration is refused._saveViewLocalStateFromMutationpersists 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.loadViewStateresolves the profile into the in-memory buffer, which reaches the webview through the existing ExtensionState apiConfiguration channel, and logs name the profile only. No secret is written to workspace storage.--max-warnings=0clean; tsc clean except the two known local telemetry-dist noise lines; prettier merge-ref gate ALL-MATCHED.upsertProviderProfilewithactivate=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 throughupsertProviderProfileso 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 onlyapiProviderfalling back to shared state.--max-warnings=0clean, 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
apiConfigurationstate 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=0clean; 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 268bb11)
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=0clean; 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 726e230)
The review round on the api-wiring unit named four test-quality issues; the ones present in this unit are fixed here: the
viewStatesdescribe block moved out of thealwaysDenyUnapprovedCommandsblock (test names carried the wrong subject), the prune test'stoBeDefined()survival checks replaced with exact-shapetoEqualassertions (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 afinallyblock (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
updatedAtreddens exactly the prune test, aloadViewStatethat adopts the unowned temporary-id entry reddens exactly the stale-entry test, restores byte-exact (hash match); eslint--max-warnings=0clean (src and types); prettier merge-ref gate ALL-MATCHED.Follow-up: review-round fixes from the series (head f528dcc)
A review round on the mode-profile-writes and e2e-view-state units found fixes this unit carries. When the view's durable mode pin write rejected inside the mode-switch handler, the task-history write and the in-memory task mode had already landed and the caller delivered the submitted message with the new task mode against the view's unchanged mode and mode-linked API configuration - the failure now restores the pre-switch task mode and history entry before rethrowing (new sticky-mode test; negative control: dropping the rollback reddens exactly that test).
A review round on the mode-profile-writes unit found that the profile-deletion re-pin updated each sibling view's buffer but not its running task: the posted state prefers the task's own
apiConfiguration, so a conversation in a view pinned to the deleted profile kept sending the deleted profile's settings. The re-pin now rebuilds the sibling's API handler with the replacement's settings and re-points the task's sticky profile name, mirroring the activation path.Proofs: ClineProvider.spec and sticky-mode green; eslint
--max-warnings=0clean; tsc clean except the two known local telemetry-dist noise lines; prettier merge-ref gate ALL-MATCHED. The rebuild's negative control (dropping it reddens exactly the sibling re-pin test, byte-exact restore) runs in the units that own that test.In addition: 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
clearTimeoutspy, 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: ClineProvider.spec 299/299, sticky-mode 34/34, parallel-mode 40/40, Task.spec 190/190, api-task-control 26/26; every negative control reddens exactly its target test and restores byte-exact (hash match); eslint
--max-warnings=0clean; tsc clean except the two known local telemetry-dist noise lines; prettier merge-ref gate ALL-MATCHED.Updated this round (CodeRabbit follow-up, head 78c21d1)
taskApiConfigName === saved profile. Pinned-view overlays still refresh; regression tests cover both sites (negative control: removing either guard reddens exactly its test).afterEachnow disposes the per-test provider before resetting the instance registry, so providers leaveMcpServerManager's static provider set between tests.Updated this round (CodeRabbit follow-up 2, head 41bbc27)
taskApiConfigName === deleted profile(the view-buffer re-pin stays unconditional), so a pinned view whose task follows another sticky profile keeps its conversation's settings and persisted profile name. Regression test included (negative control: removing the guard reddens exactly that test); the existing sibling re-pin test's task double now names the deleted profile as its sticky profile, matching what the test asserts.Stryker suppression comments removed
Per the AGENTS.md policy (never add Stryker directives; remove them when a file is touched), all
// Stryker disabledirectives were dropped from this PR. Equivalence rationale for the removed suppressions:CR round 2026-10-15 (b)
setValuesmode validation (identical flagged block; regression test added) so the units stay byte-identical at merge.CR round 2026-10-15 (c)
apiProviderfallback ingetConfigurationand thedispose()finally-path deregistration (identical flagged blocks; regression tests added).CR round 2026-10-15 (d)
getConfigurationre-applies the shared-provider fallback after flattening, so an explicitapiProvider: undefinedin the nested view-local profile can no longer clobber it (matchinggetState()'s??semantics). Regression test added; the identical block in fix(api): wire task controls and sidebar-targeted configuration (vps2 F5) #1557/test(e2e): port view-state suite with reload and rehydration coverage (vps2 F6) #1565 received the same fix.CR round 2026-10-15 (e)
finallyindispose()andcreateTaskmode restore on custom-mode registration failure (identical flagged blocks; regression tests added).CR round 2026-10-15 (f)
Row-9 update (head 92ae27e)
viewStatesmap and the rollback restores it through the serialized queue, recovering sibling entries the activation's pruning evicted (CR Persistence Integrity).setGlobalStatepersistsparsed.datainstead of the raw payload (CR Security Boundaries).ContextProxy#updateGlobalStatelayer 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 92ae27e)
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 refactoredupsertProviderProfileinto a newpersistProviderProfilemethod and added arunToCompletionqueue option; this unit's transactional body was ported into the new method unchanged and upstream'sPromise.allSettledactivation 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 newupdateProfileModeltests inClineProvider.apiHandlerRebuild.spec.tsmock the settings manager with a staticgetProfile, which starves the post-save read-back; the mock now repointsgetProfileat whatsaveConfigwrote (the real manager's behavior), and the file's 38 tests pass locally alongside the rest of the verification set.