Repository navigation
fix(webview): send stable view-state id on launch and re-pin per-view state - #1552
Open
easonLiangWorldedtech wants to merge 93 commits into
Open
easonLiangWorldedtech wants to merge 93 commits into
easonLiangWorldedtech wants to merge 93 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.
Part of the vps2 durable per-view state series — tracked in fork issue 41 (cross-repo: the displayed vs-main diff includes the F1a/F1b content this unit stacks on until those merge).
Issue (created at PR-open time): #1551
What this unit lands at this head
Fix unit F1c (3/3 of the F1 split). F1a landed per-view identity and the durable
viewStatespipeline, F1b merges view-local state intogetState(); this unit closes the loop on the webview side:viewStateId(VSCodeAPIWrapper.getViewStateId: reuse the id persisted in webview state, else create one viacrypto.randomUUIDwith a timestamp+random fallback and persist viasetState; in-memory fallback when storage is unavailable). It is posted withwebviewDidLaunchand persisted on the provider viasetViewStateId, re-keying the pre-launch temporary entry to the launching webview. Best-effort: the launch message carriesviewStateId: undefinedwhen the helper is unavailable and the provider degrades to the shared-global path.saveViewState); the legacy global repair (global write +activateProviderProfile) runs only when the shared global selection is also invalid. Validation order: merged (view-local) name, then shared global, then first listed profile.updateSettingsrouted throughprovider.setValueso the view-local buffer/pin sync path (_saveViewLocalStateFromMutation) runs for settings edits too.wroteUnderTemporaryViewStateId), shared-write rollback insetValue/setValues, null-prototypegetPersistedViewStates, plus the ported regression tests.Content source
Hand-ported from CS
e9a44b2fa(fork PR 43 head): the launch-identity hunks inwebviewMessageHandler.ts/webview-ui/src/utils/vscode.tsand their specs; the F1a/F1b snapshot on this branch carries the same review-round fixes as the lower units' heads (ported this round, with two tests adapted to the ownership semantics).Budget
a+d vs merge-base
fafad20b0: 6234 (+5814/−420) — includes the stacked F1a/F1b content until those merge; the F1c-specific delta over the F1b unit is ~545 a+d (launch identity + re-pin + specs). Split decision belongs to maintainers.How to test
pnpm --dir src exec vitest run core/webview/__tests__/ClineProvider.spec.ts core/webview/__tests__/ClineProvider.sticky-mode.spec.ts core/webview/__tests__/webviewMessageHandler.spec.ts activate/__tests__/registerCommands.spec.tspnpm --dir webview-ui exec vitest run src/utils/__tests__/vscode.spec.ts src/context/__tests__/ExtensionStateContext.spec.tsxVerification numbers
Updated this round
webviewFocusTrackerargument).Pre-merge ERROR-row pre-check (head 0657506)
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)._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.--max-warnings=0clean; tsc clean except the two known local telemetry-dist noise lines; prettier merge-ref gate ALL-MATCHED.CR round: non-activating-save refresh + overlay completeness (head 16a9ba2)
This unit already implemented both behaviors the review round named on the get-state unit (non-activating saves refresh views pinned to the saved profile; the pinned overlay is used as a complete profile snapshot with only an
apiProviderfallback). The round's two regression tests now land here as a test-only commit; the cloud instance doubles gainedon/offhandlers because provider dispose detaches the settings listener against the getter those tests leave in place.--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 aa1fb11)
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 7b9f12b)
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 8f6749a)
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.Updated this round (CodeRabbit follow-up, head 279e9da)
taskApiConfigName === saved profile; pinned-view overlays still refresh. Regression tests cover both sites (negative control: removing either guard reddens exactly its test).viewStatesthrough the durable write path so their assertions can fail; the webview state-payload test renamed to describe what it asserts.Updated this round (CodeRabbit follow-up 2, head 23838ac)
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).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
*InTabcommands keep a target when the tracked panel closes while another survives in a different group. Two regression tests added.@ts-ignorereadonly-manager swaps in the provider spec replaced withvi.spyOnon the real instances (customModesManager x2, providerSettingsManager x3, launch re-pin test).mockClineProvidermembers in its ownafterEachvia a module-scope snapshot; the telemetry suite's partial snapshot and 3-of-6 restore removed.CR round 2026-10-15 (b)
setValuesnow validatesmodeagainst custom modes carried in the same payload (createTask/setConfiguration register them only after the call), so a brand-new custom mode in the payload is no longer dropped. Regression test added; the identical block in the sibling units received the same fix.CR round 2026-10-15 (c)
ClineProvider.dispose()now deregisters (activeInstances removal, listener teardown, MCP provider unregistration) in afinallypath, so a throwing MCP disposable cannot leave the provider registered while_disposedblocks a retry; the tab-init failure cleanup nests the dispose await so a cleanup rejection cannot replace the initialization error. Two regression tests added; the identical dispose tail in the sibling units received the same fix.CR round 2026-10-15 (e)
createTaskrestores the previous mode selection when custom-mode registration rejects (identical flagged block; regression test added). The widerdispose()finally was not ported here: this unit's method body differs outside the flagged block.CR round 2026-10-15 (f)
Row-9 update (head d48f192)
upsertProviderProfileread-back feeds every post-save consumer; CR Security Boundaries).viewStatesmap, recovering entries the pruning evicted (CR Persistence Integrity).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 d48f192)
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.Review round 6 - the launch re-pin seeds the view's settings (aa6037c)
The live row on this head was right about the mechanism: the launch repair re-pinned the view to the still-valid shared profile and cleared the apiConfiguration buffer, leaving a view with a name in its overlay and no settings of its own.
getStatemergesviewLocalStateover the shared values (ClineProvider.ts:4913) and a mutation carryingundefineddeletes the key (:5298-5304), so the pinned view resolvedapiConfigurationfrom whatever the shared store names next - one sibling activation is enough - while displaying its own pin. The repair now seeds the pinned profile's stored record without its name, the wayloadViewStatepairs a name with its settings (:1137-1140).Two deliberate deviations from the suggested snippet: the seed is read before the pin write, so a failed seed leaves the pin unwritten and the invalid selection retryable (the property the replaced comment was protecting), and the read is wrapped so a record that vanishes between
hasConfigandgetProfilefalls back to an empty overlay instead of costing the view the whole repair.One part of the finding is not what the code does: nothing writes
apiConfigurationinto the durable view entry -_persistViewLocalStateFromMutation(:5322-5332) persistsmodeandcurrentApiConfigNameonly - so the seed is an in-memory overlay write and the pin write stays the only durable part of the repair.Control: head handler with the new spec gives 3 failed | 173 passed (two cases with
getProfilecalled zero times, one oncalls[0]equal to["apiConfiguration", undefined]); with the seed in place it is 176 passed, exit 0, andcore/webviewas a whole is 34 files / 1073 tests green.srctsc exit 0; eslint clean on both touched files with suppressed counts unchanged (5 and 35, total 3298); the compile-job command list is 0,0,0,0,1,1,0,0,0,0,0,0,0 with only the two box-local solheim record.sh reds (ffprobe, POSIX separators) - format:check, lint and check-types are green in this worktree.