Skip to content

fix(webview): add durable per-view state base - #977

Open
easonLiangWorldedtech wants to merge 18 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/view-local-state-base
Open

fix(webview): add durable per-view state base#977
easonLiangWorldedtech wants to merge 18 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/view-local-state-base

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Add the foundational per-view state infrastructure for parallel mode. This is the root PR that all subsequent parallel-mode PRs depend on.

Addresses old Issue #908 (root mechanism). Fixes persistence limitation by introducing registered global viewStates with bounded cleanup and proper ContextProxy hydration.

Changes

  • Per-view identity: Assign stable IDs to each webview instance via webview_state_id
  • viewLocalState buffer: Transient per-view state that merges with global state via getState() layer
  • Durable viewStates persistence: Registered global setting key storing only non-secret selections (mode, currentApiConfigName, updatedAt) — bounded to most recent 50 entries
  • Stable webview state ID plumbing: Ensures persistence lookups are consistent across reloads
  • No-op compatibility: Existing single-tab behavior unchanged

Key Design Decisions

Durable per-view persistence via registered global viewStates

Previously, persistence relied on scattered dynamic __view_state_* keys that were not formally registered. Now:

Tests

  • Targeted tests passed
  • Full core run: 129 files passed / 2184 passed / 9 skipped
  • Types test: 6 passed

Related

Summary by CodeRabbit

  • New Features

    • Added durable, per-webview state persistence via a new viewStates setting, including isolated mode and API profile selection by viewStateId.
    • Updated launch flow to hydrate state (including current API configuration) from the latest provider state.
    • Added task-specific API controls (approve asks, select follow-up suggestions) and preserveOpenTabs support for new tasks.
    • Added Kimi Code sign-in/sign-out and “abandon subtask by ID” messaging support.
  • Bug Fixes

    • Prevented secret-state keys (including API and Kimi Code keys) from being persisted; improved resilience when storage is unavailable.
  • Tests

    • Expanded provider, webview context, and end-to-end coverage for per-view isolation, pruning, concurrency, and launch-time behavior.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds durable per-view state schemas, identifiers, persistence, restoration, and state merging across webview and extension layers. It also adds task-specific API controls, browser storage fallbacks, and parallel-view integration coverage.

Changes

Per-view state and task control

Layer / File(s) Summary
State contracts and public API
packages/types/src/*
Defines persisted viewStates, secret-key coverage, view-state launch fields, Kimi Code state, and task-control API methods.
Webview identity and launch wiring
webview-ui/src/utils/*, webview-ui/src/context/*, src/core/webview/webviewMessageHandler.ts
Generates or restores view identifiers, sends them during launch, applies them to providers, and reads selections from provider state.
Provider persistence and state merging
src/core/webview/ClineProvider.ts
Adds per-view overlays, persistence and restoration, pruning, mutation synchronization, profile handling, and view-local state precedence.
Task registry and API controls
src/extension/api.ts, src/extension/__tests__/*
Tracks active tasks, adds task-specific approval and follow-up actions, preserves open tabs when requested, and routes configuration through provider state.
Isolation and integration validation
src/core/webview/__tests__/*, apps/vscode-e2e/*, webview-ui/src/utils/__tests__/*
Tests persistence, restoration, isolation, mode switching, storage fallback, task controls, and multi-panel end-to-end behavior.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Webview
  participant ExtensionStateContext
  participant webviewMessageHandler
  participant ClineProvider
  participant globalState

  Webview->>ExtensionStateContext: obtain stable viewStateId
  ExtensionStateContext->>webviewMessageHandler: webviewDidLaunch with viewStateId
  webviewMessageHandler->>ClineProvider: setViewStateId(viewStateId)
  ClineProvider->>globalState: load or save viewStates
  ClineProvider-->>Webview: merged view-local state
Loading

Possibly related issues

Possibly related PRs

Suggested labels: awaiting-review

Suggested reviewers: navedmerchant, taltas, edelauna

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description doesn't follow the required template and omits the linked issue, test procedure, and checklist sections. Add the required template sections: Related GitHub Issue with Closes #984, Description, Test Procedure, and the pre-submission checklist.
Out of Scope Changes check ⚠️ Warning Several added APIs and message-handling changes (e.g. Kimi Code and task controls) are outside the durable viewStates scope. Move unrelated task-control/Kimi Code feature work and supporting API changes to separate PRs, keeping this one focused on view state persistence.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: durable per-view state infrastructure for webviews.
Linked Issues check ✅ Passed The implementation covers #984: registered viewStates, startup hydration, profile resolution, secret exclusion, and 50-entry pruning.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.40678% with 25 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/webview/ClineProvider.ts 91.95% 9 Missing and 5 partials ⚠️
src/core/webview/webviewMessageHandler.ts 58.33% 2 Missing and 3 partials ⚠️
webview-ui/src/utils/vscode.ts 77.27% 2 Missing and 3 partials ⚠️
src/extension/api.ts 96.15% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/webview/ClineProvider.ts`:
- Around line 3005-3056: Update resetState(), activateProviderProfile(),
upsertProviderProfile(), and deleteProviderProfile() to clear or synchronize the
affected viewLocalState fields after mutating contextProxy. Reuse
_clearViewLocalState() for resetState() and _updateViewLocalStateFromMutation()
or equivalent targeted invalidation for profile changes, ensuring stale
currentApiConfigName and apiConfiguration values cannot mask the updated global
state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 58c5e801-f818-415c-b0de-9f69e7498604

📥 Commits

Reviewing files that changed from the base of the PR and between f2bdcb6 and 605976b.

📒 Files selected for processing (13)
  • packages/types/src/__tests__/index.test.ts
  • packages/types/src/global-settings.ts
  • packages/types/src/vscode-extension-host.ts
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts
  • src/core/webview/__tests__/ClineProvider.spec.ts
  • src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts
  • src/core/webview/__tests__/webviewMessageHandler.spec.ts
  • src/core/webview/webviewMessageHandler.ts
  • webview-ui/src/App.tsx
  • webview-ui/src/__tests__/App.spec.tsx
  • webview-ui/src/context/ExtensionStateContext.tsx
  • webview-ui/src/utils/vscode.ts
💤 Files with no reviewable changes (1)
  • webview-ui/src/App.tsx

Comment thread src/core/webview/ClineProvider.ts
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 21, 2026

@edelauna edelauna left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exciting to see this work come together! Had some implementation comments, and can we also add some ui testing:

We can leverage the UI testing setup established in McpServerRestriction.spec.tsx and webview-ui/src/utils/test-utils.tsx:

  • ExtensionStateContext.Provider Wrapper Pattern:
    Re-use the renderWithState pattern to mount webview components with specific viewStateId props and verify that UI components respond correctly to view-local mode and currentApiConfigName state without global bleed.

  • Reseed & Identity Tests:
    Similar to the slug-change reseed tests in McpServerRestriction.spec.tsx, add UI-level tests in ExtensionStateContext.spec.tsx or App.spec.tsx to verify that when viewStateId changes or a webview reloads, local React state reseeds properly from the new view's viewStateId payload.

  • vscode.getViewStateId & Messaging Spies:
    Ensure UI tests verify VSCodeAPIWrapper.getViewStateId() fallback behavior when sessionStorage / localStorage are restricted or cleared.

return viewStates
}

private async savePersistedViewState(values: Partial<PersistedViewState>): Promise<void> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

savePersistedViewState reads the global viewStates dictionary from contextProxy, mutates states[this.viewStateId] in memory, and writes it back asynchronously via await contextProxy.setValue("viewStates", ...). When concurrent webview instances update mode or API profile selections simultaneously, one instance reads stale global state before the other's write completes, causing a lost update on viewStates.

@@ -2864,6 +3082,7 @@ export class ClineProvider
}

await this.contextProxy.resetAllState()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resetState() clears global settings in contextProxy but does not clear this.viewLocalState (e.g., via _clearViewLocalState()). When getState(viewStateId) runs, it merges stale viewLocalState overrides over the reset contextProxy defaults, causing pre-reset or deleted profile settings to persist in active webview instances.

Comment thread src/core/webview/ClineProvider.ts Outdated
* profile upsert/activation/deletion, or resetState. This ensures the local cache stays in
* sync with global state changes that would otherwise be invisible behind mergedStateValues.
*/
private _updateViewLocalStateFromMutation(values: Partial<RooCodeSettings>): void {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_updateViewLocalStateFromMutation updates in-memory viewLocalState in response to setValue/setValues calls, but never invokes savePersistedViewState. Mutations made via setValue/setValues are held only in memory and lost when the webview reloads or VS Code restarts.


describe("local state isolation", () => {
it("should isolate mode state between instances", async () => {
const provider1 = new ClineProvider(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test 'should isolate mode state between instances' reads the initial mode of two provider instances without mutating mode in either, making it incapable of verifying whether mode changes in one instance leak to other instances.

vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue("shared-profile")
vi.mocked(mockClineProvider.contextProxy.setValue).mockResolvedValue(undefined)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The webviewDidLaunch handler test passes viewStateId: 'view-1' but omits an assertion verifying that provider.setViewStateId was called with 'view-1'.

@github-actions github-actions Bot added awaiting-author PR is waiting for the author to address requested changes and removed awaiting-review PR changes are ready and waiting for maintainer re-review awaiting-author PR is waiting for the author to address requested changes labels Jul 23, 2026
@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the feat/view-local-state-base branch from 82f9f23 to d724948 Compare July 23, 2026 20:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts (1)

1098-1195: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding coverage for the two uncovered profile-mutation paths.

None of these tests exercise upsertProviderProfile(..., false) (non-activating save) or a deleteProviderProfile case where viewLocalState.currentApiConfigName diverges from the global value - both are the exact gaps flagged in src/core/webview/ClineProvider.ts (upsertProviderProfile/deleteProviderProfile). Adding cases here would catch regressions on those fixes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts` around lines
1098 - 1195, The profile-mutation tests cover only activating upserts and
matching delete state; add coverage for the two missing branches. In the
“profile mutations” suite, add a test for upsertProviderProfile(..., false) that
verifies the saved profile does not activate or incorrectly synchronize current
state, and a deleteProviderProfile test where
viewLocalState.currentApiConfigName differs from the global ContextProxy value,
asserting the intended local-state behavior after deletion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts`:
- Around line 1098-1195: The profile-mutation tests cover only activating
upserts and matching delete state; add coverage for the two missing branches. In
the “profile mutations” suite, add a test for upsertProviderProfile(..., false)
that verifies the saved profile does not activate or incorrectly synchronize
current state, and a deleteProviderProfile test where
viewLocalState.currentApiConfigName differs from the global ContextProxy value,
asserting the intended local-state behavior after deletion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c33c4bc-cef3-4dc5-a6f1-07927265c1e5

📥 Commits

Reviewing files that changed from the base of the PR and between 6655bb1 and 82f9f23.

📒 Files selected for processing (6)
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts
  • src/core/webview/__tests__/webviewMessageHandler.spec.ts
  • webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx
  • webview-ui/src/utils/__tests__/vscode.spec.ts
  • webview-ui/src/utils/vscode.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/webview/tests/webviewMessageHandler.spec.ts

@github-actions github-actions Bot added the awaiting-author PR is waiting for the author to address requested changes label Jul 24, 2026
@github-actions github-actions Bot added awaiting-review PR changes are ready and waiting for maintainer re-review and removed awaiting-author PR is waiting for the author to address requested changes labels Jul 24, 2026

@edelauna edelauna left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple more comments - thanks for continuing to iterate on this.

const state2 = await provider2.getState()

expect(state1.mode).toBe("architect")
expect(state2.mode).toBe("code")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this actually prove isolation? provider2 starts at "code" (the global default) and is never mutated, so this assertion passes whether or not the two instances share state. Would it be more meaningful to first set provider2 to a distinct non-default mode (e.g. "debugger"), then mutate provider1, then assert provider2 still reads "debugger"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed — the isolation test now sets the second provider to a distinct non-default mode before mutating the first provider. Specifically, provider2 is set to debugger, then provider1 is set to architect. The assertions then verify provider1 reads architect while provider2 still reads debugger.

So the test now proves instance isolation with two distinct non-default/local states instead of relying on the global default.


expect(state.apiConfiguration.apiProvider).toBe("bedrock")
expect(state.apiConfiguration.awsBedrockEndpoint).toBe("http://127.0.0.1:4567")
expect((provider as any).viewLocalState.apiConfiguration.apiProvider).toBe("bedrock")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test confirms the new bedrock keys are present, but the PROVIDER_SETTINGS_KEYS merge at line 3118 of ClineProvider.ts spreads onto the existing viewLocalState.apiConfiguration — so stale keys from the previous provider (e.g. openRouterModelId) could survive. Worth adding expect((provider as any).viewLocalState.apiConfiguration).not.toHaveProperty("openRouterModelId") here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added the stale-key assertion on line 1068, so the test now verifies that switching from OpenRouter to Bedrock removes the previous OpenRouter model key. The implementation already handles this on line 3143: when a flat provider-settings update includes the provider field, it replaces the local API configuration instead of merging into the existing one, so stale provider-specific keys cannot survive. Verified with the parallel-mode spec: 42 tests passed.

const wrapper = new VSCodeAPIWrapper()

expect(wrapper.getViewStateId()).toBe("memory-view")
expect(wrapper.getViewStateId()).toBe("memory-view")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto.randomUUID is mocked to always return "memory-view", so calling getViewStateId() twice will return the same string regardless of whether the early-return guard (that reads the stored ID) is present or not. What if the mock returned a different value on the second call — would the second getViewStateId() still return "memory-view"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the UUID mock now returns different values on subsequent calls (line 64). The assertions on lines 83-85 verify that the second call still returns "memory-view" and that UUID generation only happens once, so the test now proves the stored/in-memory ID is reused rather than regenerated. Verified: 3 tests passed.


vi.mock("../../task-persistence", () => ({
readApiMessages: vi.fn().mockResolvedValue([]),
saveApiMessages: vi.fn().mockResolvedValue(undefined),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The production code replaces non-alphanumeric/dash characters with underscores: .replace(/[^A-Za-z0-9_-]/g, "_"). All calls here use already-clean IDs — is there a test somewhere that passes a raw ID containing spaces, dots, or slashes to verify the sanitization actually runs and that the sanitized form is the key used in viewStates?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — there is coverage for this now. The test at ClineProvider.parallelMode.spec.ts passes a raw view state ID containing a slash, dot, and spaces: tab panel/with.dots and spaces. It then verifies that the persisted viewStates key is the sanitized form on ClineProvider.parallelMode.spec.ts, and also verifies the raw unsanitized key is not present on ClineProvider.parallelMode.spec.ts. The parallel-mode spec was verified passing with 42 tests.

Comment thread src/core/webview/ClineProvider.ts Outdated
private async savePersistedViewState(values: Partial<PersistedViewState>): Promise<void> {
const write = ClineProvider.persistedViewStateWriteQueue.then(async () => {
const states = this.getPersistedViewStates({ fresh: true })
const current = states[this.viewStateId] ?? {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.viewStateId is read inside the .then() closure at execution time. If setViewStateId() fires between when this write is enqueued and when the queue runs it, the write would land under the new ID rather than the one that was active when the change was made. Should viewStateId be captured before the .then() (e.g. const viewStateId = this.viewStateId at the top of savePersistedViewState)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — fixed. savePersistedViewState now captures the active viewStateId before the queued write is created on line 454, and the queued closure uses that captured ID on lines 457-458 instead of reading this.viewStateId later: ClineProvider.ts.

There is also regression coverage starting on line 1115: ClineProvider.parallelMode.spec.ts. The test starts a save under view-a on line 1139, switches to view-b before the queued write completes on line 1141, then verifies the write lands under view-a on lines 1145-1148: ClineProvider.parallelMode.spec.ts.

Comment thread src/core/webview/ClineProvider.ts Outdated
if (value === undefined || value === null) {
delete this.viewLocalState[key]
} else {
this.viewLocalState[key] = value

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

viewLocalState is updated here before savePersistedViewState is awaited. If the durable write fails, the in-memory state is ahead of what was persisted — on next reload viewLocalState would be re-hydrated from the old globalState value. Is that acceptable, or should the in-memory update only happen after the persist succeeds?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — I changed the ordering so the in-memory viewLocalState update only happens after durable persistence succeeds. The shared helper now awaits persistence first on line 3091, then updates in-memory state on line 3092: ClineProvider.ts.

I also added regression coverage starting on line 847: ClineProvider.parallelMode.spec.ts. The test forces the persisted viewStates write to fail on line 854, verifies saveViewState(...) rejects on line 858, and confirms viewLocalState was not advanced on line 859: ClineProvider.parallelMode.spec.ts.

Comment thread src/core/webview/ClineProvider.ts Outdated
this.saveViewState("apiConfiguration", providerSettings),
])

this._updateViewLocalStateFromMutation({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saveViewState("currentApiConfigName", name) at line 1823 already writes both keys into viewLocalState (line 561). Does _updateViewLocalStateFromMutation here need to repeat that write, or is it a no-op? The setValue/setValues path only calls _updateViewLocalStateFromMutation + _persistViewLocalStateFromMutation without the preceding saveViewState — seems like these two paths could be consolidated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. I consolidated the view-local state update/persist flow so profile activation no longer calls saveViewState(...) and then repeats the same local-state writes through the mutation sync path. Both activateProviderProfile and activated upsertProviderProfile now use the shared mutation helper directly, while setValue / setValues and saveViewState also route through the same helper.

I also added assertions that profile activation does not call saveViewState(...), which catches the duplicate path if it comes back.

await write
}

private async clearPersistedViewState(viewStateId = this.viewStateId): Promise<void> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearPersistedViewState is defined here but I cannot find any call site for it. When a tab-panel ClineProvider is disposed, its viewStates entry is never cleaned up — it lives until the 50-entry prune evicts it by age. Should this be called from dispose()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — fixed. dispose() now clears persisted view-local state for editor/tab providers on line 891: ClineProvider.ts. It is guarded to editor render contexts starting on line 889, so sidebar providers keep their stable persisted state: ClineProvider.ts.

I also added test coverage starting on line 1153: ClineProvider.parallelMode.spec.ts. The test seeds a viewStates entry, disposes the editor provider on line 1160, and verifies the persisted key is removed on line 1162: ClineProvider.parallelMode.spec.ts.

@github-actions github-actions Bot added awaiting-author PR is waiting for the author to address requested changes has-conflicts PR has merge conflicts with the base branch and removed awaiting-review PR changes are ready and waiting for maintainer re-review awaiting-author PR is waiting for the author to address requested changes labels Jul 25, 2026
@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the feat/view-local-state-base branch from 2ef16bb to 37a5dd1 Compare July 27, 2026 12:55
@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/extension/api.ts (1)

386-411: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear tasksById when tasks are removed beyond completion/abort.

ClineProvider.dispose() and removeClineFromStack() remove task instances from the provider registry via TaskRegistry.remove() without emitting TaskCompleted/TaskAborted, so API.tasksById can keep live RegisteredTask entries holding strong references to Task and ClineProvider. Clear the API map on task/remove/evict paths, or key the provider cleanup off registry removal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/extension/api.ts` around lines 386 - 411, The tasksById entries are only
deleted on TaskCompleted and TaskAborted, leaving stale strong references when
TaskRegistry.remove() is triggered by ClineProvider.dispose() or
removeClineFromStack(). Update registerListeners and the relevant
provider-removal/eviction flow to delete the task.taskId entry whenever a task
is removed without lifecycle completion events, including disposal and stack
removal paths.
src/core/webview/webviewMessageHandler.ts (1)

1207-1221: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard kimiCodeOAuthManager.getAccessToken() against rejection.

Every other candidate's credential lookup is a plain property read, but this one performs an OAuth token read/refresh. If it throws (expired refresh token, network failure), the whole requestRouterModels case rejects before postMessageToWebview({ type: "routerModels" }) runs, so the model picker stays in a loading state with no error surfaced.

🛡️ Proposed fix
 			if (!providerFilter || providerFilter === "kimi-code") {
-				const { kimiCodeOAuthManager } = await import("../../integrations/kimi-code/oauth")
-				const kimiCodeAuthMethod =
-					message?.values?.kimiCodeAuthMethod ?? apiConfiguration.kimiCodeAuthMethod ?? "oauth"
-				const kimiCodeApiKey =
-					kimiCodeAuthMethod === "api-key"
-						? (message?.values?.kimiCodeApiKey ?? apiConfiguration.kimiCodeApiKey)
-						: await kimiCodeOAuthManager.getAccessToken()
-				if (kimiCodeApiKey) {
-					candidates.push({
-						key: "kimi-code",
-						options: { provider: "kimi-code", apiKey: kimiCodeApiKey },
-					})
+				try {
+					const { kimiCodeOAuthManager } = await import("../../integrations/kimi-code/oauth")
+					const kimiCodeAuthMethod =
+						message?.values?.kimiCodeAuthMethod ?? apiConfiguration.kimiCodeAuthMethod ?? "oauth"
+					const kimiCodeApiKey =
+						kimiCodeAuthMethod === "api-key"
+							? (message?.values?.kimiCodeApiKey ?? apiConfiguration.kimiCodeApiKey)
+							: await kimiCodeOAuthManager.getAccessToken()
+					if (kimiCodeApiKey) {
+						candidates.push({
+							key: "kimi-code",
+							options: { provider: "kimi-code", apiKey: kimiCodeApiKey },
+						})
+					}
+				} catch (error) {
+					provider.log(
+						`[requestRouterModels] kimi-code credential lookup failed: ${error instanceof Error ? error.message : String(error)}`,
+					)
 				}
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/webview/webviewMessageHandler.ts` around lines 1207 - 1221, Update
the kimi-code candidate handling in requestRouterModels to catch rejections from
kimiCodeOAuthManager.getAccessToken(), allowing the flow to continue and always
reach postMessageToWebview({ type: "routerModels" }). Preserve API-key behavior
and only add a candidate when a credential is successfully available.
src/core/webview/__tests__/ClineProvider.spec.ts (1)

580-592: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Don’t leak the hasLoadedFullDetails override.

mockReturnValue(true) is not restored, and Vitest config only clears call counts/clear mocks here — it will remain mocked for later tests. Reset it in this test (mockReset(true)) or restore/replace it in afterEach.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/webview/__tests__/ClineProvider.spec.ts` around lines 580 - 592,
Reset the hasLoadedFullDetails mock after this test so its mockReturnValue(true)
does not affect later tests. Update the test cleanup or use the mock’s
reset/restore behavior while preserving the existing assertion that
forceFullModelDetailsLoad is not called.
🧹 Nitpick comments (7)
packages/types/src/api.ts (1)

19-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add preserveOpenTabs to the destructuring list for consistency.

The parameter pattern lists configuration, text, images, newTab but not the new field, so the declaration no longer mirrors the option type. Type-wise it's inert, but it reads as an omission.

♻️ Proposed tweak
 	startNewTask({
 		configuration,
 		text,
 		images,
 		newTab,
+		preserveOpenTabs,
 	}: {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/types/src/api.ts` around lines 19 - 30, Update the startNewTask
parameter destructuring to include preserveOpenTabs, keeping it aligned with the
accompanying options type while preserving the existing parameter behavior.
src/core/webview/webviewMessageHandler.ts (1)

565-582: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrap the webviewDidLaunch case body in a block.

Biome flags customModes (Line 565) and mcpHub (Line 579) as lint/correctness/noSwitchDeclarations — the bindings leak into sibling clauses (TDZ risk) and this is reported as an error, so lint will fail.

♻️ Proposed fix
-		case "webviewDidLaunch":
+		case "webviewDidLaunch": {
 			await provider.setViewStateId(message.viewStateId)
@@
 			provider.isViewLaunched = true
 			break
+		}

The same pattern applies to vsCodeLmModels at Line 1372.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/webview/webviewMessageHandler.ts` around lines 565 - 582, Wrap the
webviewDidLaunch switch case body in braces so its customModes and mcpHub
declarations are scoped to that case; apply the same block scoping to the case
declaring vsCodeLmModels, without changing their existing logic.

Source: Linters/SAST tools

apps/vscode-e2e/src/runTest.ts (1)

136-149: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This fixture supersedes the legacy regex fixture below (lines 166-184).

Both match the post-switch_mode→ask turn and return the same attempt_completion with id call_modes_post_switch_001. Keeping both leaves a dead fixture plus a now-stale explanatory comment; consider removing the /^<environment_details>/ one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/vscode-e2e/src/runTest.ts` around lines 136 - 149, Remove the superseded
legacy regex fixture matching the post-switch-to-Ask turn, along with its stale
explanatory comment. Keep the newer fixture using toolResultContains for
call_modes_switch_001 and its existing attempt_completion response unchanged.
apps/vscode-e2e/src/suite/view-state.test.ts (1)

120-120: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

taskNamesById is written but never read.

Drop it, or use it in the timeout diagnostics message.

Also applies to: 205-205

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/vscode-e2e/src/suite/view-state.test.ts` at line 120, Remove the unused
taskNamesById Map declaration from the affected test flows, unless the timeout
diagnostics need task-name context; in that case, use it when constructing the
timeout diagnostic message. Apply the same change at both referenced locations.
src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts (1)

684-733: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate isolation test.

should allow different modes in separate instances after saveViewState (707-733) exercises exactly the same behavior as should isolate mode state between instances (685-705) — two providers, two distinct modes, same assertions. Consider dropping one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts` around lines
684 - 733, Remove the duplicate test case “should allow different modes in
separate instances after saveViewState” from the “local state isolation” suite,
keeping the existing “should isolate mode state between instances” coverage and
its cleanup intact.
src/extension/api.ts (1)

355-365: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Silent no-op when mode is invalid.

selectTaskFollowupSuggestion still returns true after ignoring an unknown mode slug, so callers can't distinguish "switched" from "silently skipped". A log line (consistent with the rest of the class) would make this debuggable.

♻️ Suggested logging
 			if (isValidMode) {
 				await entry.provider.handleModeSwitch(mode)
+			} else {
+				this.log(`[API#selectTaskFollowupSuggestion] ignoring unknown mode "${mode}" for task ${taskId}`)
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/extension/api.ts` around lines 355 - 365, Update
selectTaskFollowupSuggestion so the invalid-mode branch logs an appropriate
warning or error using the class’s existing logging convention when isValidMode
is false, while preserving the current response handling and return value.
apps/vscode-e2e/src/fixtures/view-state.ts (1)

3-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Round count is duplicated between the fixture module and the suite. ROUNDS is defined only locally in the fixture module and never validated against MODE_SEQUENCES, so the suite repeats the literal 10; a change in one place silently breaks the other (missing rounds yield mode: undefined in fixtures).

  • apps/vscode-e2e/src/fixtures/view-state.ts#L3-L15: derive/validate ROUNDS from MODE_SEQUENCES lengths and export it.
  • apps/vscode-e2e/src/suite/view-state.test.ts#L209-L242: replace the hardcoded 10 in plan.length * 10 and the roundIndex < 10 loop with the exported round count (or taskPlan.rounds.length).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/vscode-e2e/src/fixtures/view-state.ts` around lines 3 - 15, The fixture
and suite duplicate the round count, allowing MODE_SEQUENCES and test iteration
counts to diverge. In apps/vscode-e2e/src/fixtures/view-state.ts#L3-L15, derive
and validate the exported ROUNDS from MODE_SEQUENCES lengths; in
apps/vscode-e2e/src/suite/view-state.test.ts#L209-L242, replace both hardcoded
10 values in plan.length * 10 and the roundIndex loop with the exported ROUNDS
or taskPlan.rounds.length.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/vscode-e2e/src/suite/view-state.test.ts`:
- Around line 49-50: Update the first test’s cleanup in its finally block to
unregister the TaskModeSwitched listener as well as completionHandler, using the
listener reference from globalThis.api.on. Mirror the existing cleanup pattern
in the second test so modeEvents is no longer mutated after the test completes.

In `@src/core/webview/ClineProvider.ts`:
- Around line 901-909: Update the editor cleanup in ClineProvider.dispose() so
clearPersistedViewState() runs only for a user-initiated tab close, not VS Code
shutdown, reload, or extension deactivation. Preserve the existing persisted
view state across lifecycle teardown so mode and currentApiConfigName remain
restorable.

---

Outside diff comments:
In `@src/core/webview/__tests__/ClineProvider.spec.ts`:
- Around line 580-592: Reset the hasLoadedFullDetails mock after this test so
its mockReturnValue(true) does not affect later tests. Update the test cleanup
or use the mock’s reset/restore behavior while preserving the existing assertion
that forceFullModelDetailsLoad is not called.

In `@src/core/webview/webviewMessageHandler.ts`:
- Around line 1207-1221: Update the kimi-code candidate handling in
requestRouterModels to catch rejections from
kimiCodeOAuthManager.getAccessToken(), allowing the flow to continue and always
reach postMessageToWebview({ type: "routerModels" }). Preserve API-key behavior
and only add a candidate when a credential is successfully available.

In `@src/extension/api.ts`:
- Around line 386-411: The tasksById entries are only deleted on TaskCompleted
and TaskAborted, leaving stale strong references when TaskRegistry.remove() is
triggered by ClineProvider.dispose() or removeClineFromStack(). Update
registerListeners and the relevant provider-removal/eviction flow to delete the
task.taskId entry whenever a task is removed without lifecycle completion
events, including disposal and stack removal paths.

---

Nitpick comments:
In `@apps/vscode-e2e/src/fixtures/view-state.ts`:
- Around line 3-15: The fixture and suite duplicate the round count, allowing
MODE_SEQUENCES and test iteration counts to diverge. In
apps/vscode-e2e/src/fixtures/view-state.ts#L3-L15, derive and validate the
exported ROUNDS from MODE_SEQUENCES lengths; in
apps/vscode-e2e/src/suite/view-state.test.ts#L209-L242, replace both hardcoded
10 values in plan.length * 10 and the roundIndex loop with the exported ROUNDS
or taskPlan.rounds.length.

In `@apps/vscode-e2e/src/runTest.ts`:
- Around line 136-149: Remove the superseded legacy regex fixture matching the
post-switch-to-Ask turn, along with its stale explanatory comment. Keep the
newer fixture using toolResultContains for call_modes_switch_001 and its
existing attempt_completion response unchanged.

In `@apps/vscode-e2e/src/suite/view-state.test.ts`:
- Line 120: Remove the unused taskNamesById Map declaration from the affected
test flows, unless the timeout diagnostics need task-name context; in that case,
use it when constructing the timeout diagnostic message. Apply the same change
at both referenced locations.

In `@packages/types/src/api.ts`:
- Around line 19-30: Update the startNewTask parameter destructuring to include
preserveOpenTabs, keeping it aligned with the accompanying options type while
preserving the existing parameter behavior.

In `@src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts`:
- Around line 684-733: Remove the duplicate test case “should allow different
modes in separate instances after saveViewState” from the “local state
isolation” suite, keeping the existing “should isolate mode state between
instances” coverage and its cleanup intact.

In `@src/core/webview/webviewMessageHandler.ts`:
- Around line 565-582: Wrap the webviewDidLaunch switch case body in braces so
its customModes and mcpHub declarations are scoped to that case; apply the same
block scoping to the case declaring vsCodeLmModels, without changing their
existing logic.

In `@src/extension/api.ts`:
- Around line 355-365: Update selectTaskFollowupSuggestion so the invalid-mode
branch logs an appropriate warning or error using the class’s existing logging
convention when isValidMode is false, while preserving the current response
handling and return value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 98f52931-0f0a-4b0f-980c-3c91340bce56

📥 Commits

Reviewing files that changed from the base of the PR and between 82f9f23 and a46929c.

📒 Files selected for processing (22)
  • 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
  • packages/types/src/__tests__/index.test.ts
  • packages/types/src/api.ts
  • packages/types/src/global-settings.ts
  • packages/types/src/vscode-extension-host.ts
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts
  • src/core/webview/__tests__/ClineProvider.spec.ts
  • src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts
  • src/core/webview/__tests__/webviewMessageHandler.spec.ts
  • src/core/webview/webviewMessageHandler.ts
  • src/eslint-suppressions.json
  • src/extension/__tests__/api-set-configuration.spec.ts
  • src/extension/__tests__/api-task-control.spec.ts
  • src/extension/api.ts
  • webview-ui/src/App.tsx
  • webview-ui/src/__tests__/App.spec.tsx
  • webview-ui/src/context/ExtensionStateContext.tsx
  • webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx
💤 Files with no reviewable changes (1)
  • webview-ui/src/App.tsx

Comment thread apps/vscode-e2e/src/suite/view-state.test.ts Outdated
Comment thread src/core/webview/ClineProvider.ts Outdated
Comment on lines +901 to +909
if (this.renderContext === "editor") {
try {
await this.clearPersistedViewState()
} catch (error) {
this.log(
`[dispose] Failed to clear persisted view state for ${this.viewStateId}: ${error instanceof Error ? error.message : String(error)}`,
)
}
}

@coderabbitai coderabbitai Bot Jul 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Inspect dispose wiring and any deactivate-time disposal of providers.
rg -n --type=ts -C5 'onDidDispose|deactivate\s*\(' src/core/webview/ClineProvider.ts src/extension.ts 2>/dev/null
fd -t f 'extension.ts' src --exec rg -n -C5 'dispose\(\)' {}

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 1514


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== files ====="
git ls-files 'src/core/webview/ClineProvider.ts' 'src/extension.ts' | sed -n '1,20p'

echo "===== ClineProvider dispose/viewState relevant sections ====="
wc -l src/core/webview/ClineProvider.ts
sed -n '840,925p' src/core/webview/ClineProvider.ts
sed -n '1090,1155p' src/core/webview/ClineProvider.ts
rg -n -C3 'renderContext|viewStateId|clearPersistedViewState|Persisted|getCurrentApiConfig|mode' src/core/webview/ClineProvider.ts

echo "===== extension deactivate relevant section ====="
sed -n '350,395p' src/extension.ts
rg -n -C4 'extensionContext|subscriptions|provider|ClineProvider|activate|deactivate' src/extension.ts

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 38485


🌐 Web query:

VS Code WebviewView.onDidDispose extension deactivation webview panel dispose documentations

💡 Result:

In VS Code extensions, the onDidDispose event is the standard mechanism to handle the cleanup of resources associated with a WebviewView or WebviewPanel when it is closed or destroyed [1][2]. Key Concepts and Usage: Event Purpose: The onDidDispose event fires when the webview is destroyed, either by the user closing the panel/view or by the extension programmatically calling dispose() [1][3]. It is essential for preventing memory leaks by allowing you to clean up timers, event listeners, file watchers, and other resources tied to that webview instance [1][4]. Implementation Pattern: You should register the onDidDispose listener within your extension, typically by passing your context.subscriptions to ensure it is managed correctly as the extension deactivates [1][5]. Example using WebviewPanel: panel.onDidDispose( => { // Cleanup resources here, e.g., clear intervals or timeouts clearInterval(interval); }, null, context.subscriptions); WebviewView Considerations: WebviewView instances also provide an onDidDispose event (or onDispose depending on the internal interface) to signal when the view pane is disposed [6][7][2]. Like WebviewPanel, this is the signal to clear associated disposables and state [7][5]. Important Technical Notes: 1. Asynchronous Nature: Communication between the extension host and the webview is asynchronous [8]. While onDidDispose is the correct hook for cleanup, be aware that attempting to access a webview instance immediately after an operation that triggers its disposal may occasionally lead to errors if the internal state has already been marked as disposed [8]. 2. Resource Management: Always use the context.subscriptions array when registering events or disposables to ensure everything is cleaned up when the extension is deactivated or the workbench component is destroyed [1][5]. 3. Manual Disposal: You can programmatically close a webview panel by calling the dispose() method on the panel object itself [1][3]. When you call dispose(), the onDidDispose event will still fire [1][5]. For complex scenarios, such as managing shared resources across multiple editors or views, ensure your cleanup logic is idempotent so that it handles both user-initiated closure and extension-initiated disposal gracefully [4][5].

Citations:


🌐 Web query:

VS Code WorkbenchExtension activation events extension reloading extension deactivation dispose webview persistence state

💡 Result:

Visual Studio Code extensions use a lazy-loading architecture governed by activation events, a well-defined lifecycle, and specific patterns for resource disposal and state persistence. Activation Events Extensions remain inactive until a specific trigger occurs. You must declare these in the activationEvents field of your package.json [1][2]. Common events include onLanguage (file opened), onCommand (command invoked), and workspaceContains (file exists in workspace) [1][2]. Note that as of VS Code 1.74.0, commands declared in the commands contribution section automatically trigger activation without requiring an explicit onCommand entry [3][4]. Extension Lifecycle The extension entry point typically exports two functions: activate and deactivate [3][4]. 1. activate: Called when the registered activation event occurs. This is where you initialize your extension and register commands, providers, and subscriptions [3][5]. 2. deactivate: Optional. Use this to perform explicit cleanup, such as closing external handles or deleting temporary files when the extension is disabled, uninstalled, or VS Code shuts down [3][4]. For most extensions, this is not needed if you properly use the context.subscriptions array [5]. Dispose and Resource Management The most important practice for lifecycle management is pushing disposables (e.g., event listeners, command registrations) into the context.subscriptions array provided in the activate function [5]. VS Code automatically calls.dispose on all items in this array when the extension is deactivated, ensuring clean resource management [5]. Webview Persistence and State Webview panels are destroyed when closed or disposed, but their contents are also destroyed if the panel is moved to the background [6][7]. To handle this: 1. getState/setState: Scripts inside the webview can use these methods to save and restore JSON-serializable state [6][7]. This is the preferred, high-performance method [6][7]. 2. WebviewPanelSerializer: To restore webviews after a VS Code restart, you must register a WebviewPanelSerializer in your activate function. It uses the state saved via setState to reconstruct the panel [6][7][8]. 3. retainContextWhenHidden: For complex UIs that cannot be easily serialized, you can set this property to true. It keeps the webview content alive in a hidden state, though it consumes more resources [6][7]. Extension Reloading VS Code does not natively support "hot reloading" of extension code while the host is running [9][10]. To reload an extension during development, you must reload the VS Code window (e.g., via the Developer: Reload Window command) [10]. Some developers implement file system watchers to trigger this command automatically when source files change [10].

Citations:


Avoid clearing editor-tab persisted state on every dispose().

onDidDispose is also reached when VS Code shuts down, reloads, or the extension is deactivated, and dispose() currently unconditionally deletes viewStates[viewStateId] for renderContext === "editor". This drops the restored tab’s mode/currentApiConfigName; gate this cleanup to user-initiated tab close or otherwise preserve VS Code-persisted view state for restarts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/webview/ClineProvider.ts` around lines 901 - 909, Update the editor
cleanup in ClineProvider.dispose() so clearPersistedViewState() runs only for a
user-initiated tab close, not VS Code shutdown, reload, or extension
deactivation. Preserve the existing persisted view state across lifecycle
teardown so mode and currentApiConfigName remain restorable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai onDidDispose does not reliably distinguish user tab close from reload/shutdown/deactivation.
please provide a plan to fix. this should be fix in another pr.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@easonLiangWorldedtech Agreed — disposal is not a reliable lifecycle discriminator, so the cleanup must not infer intent from onDidDispose.

Follow-up PR plan: preserve view state unless a tab is explicitly closed

  • Introduce an explicit “close this view state” flow, initiated by the webview while it is still alive (for example, a dedicated close command/message or an intentional UI action).
  • In src/core/webview/webviewMessageHandler.ts and src/core/webview/ClineProvider.ts, validate that close intent belongs to the current viewStateId, then remove only that viewStates[viewStateId] entry.
  • Remove clearPersistedViewState() from the generic ClineProvider.dispose() path. Disposal should only release runtime resources; it must preserve durable state during reload, shutdown, extension deactivation, and restoration.
  • Keep the existing bounded persistence behavior (50 most-recent entries), so tabs closed without an explicit signal are eventually eligible for timestamp-based eviction rather than immediately losing state.
  • Add focused tests covering:
    1. explicit tab-close intent removes the corresponding persisted entry;
    2. dispose() without close intent preserves state;
    3. reload/deactivation-style disposal restores the same mode and API-profile selection;
    4. a close signal from one tab cannot remove another tab’s entry;
    5. no profile configuration or secrets are persisted/introduced by the new flow.

Would you like me to open a GitHub follow-up issue for this plan and assign it to you?


✏️ Learnings added
Learnt from: easonLiangWorldedtech
Repo: Zoo-Code-Org/Zoo-Code PR: 977
File: src/core/webview/ClineProvider.ts:0-0
Timestamp: 2026-07-30T20:04:05.204Z
Learning: For `src/core/webview/ClineProvider.ts`, VS Code `onDidDispose` does not reliably distinguish an explicit user tab close from reload, shutdown, or extension deactivation. Do not use generic provider disposal as the signal to delete durable per-view `viewStates`; explicit close intent is required.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


Failed to handle agent chat message. Please try again.

You are interacting with an AI system.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai please create issue and point out the way out suggestions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Zoo-Code-Org/Zoo-Code"
body=$(cat <<'EOF'
## Summary

Follow up on the durable per-view state work from `#977`: `ClineProvider.dispose()` must not delete an editor tab's persisted `viewStates[viewStateId]` entry merely because the provider is being disposed.

`onDidDispose` / provider disposal does not reliably identify an explicit user tab close. It can also occur during VS Code reload, shutdown, or extension deactivation, where the persisted mode and selected API profile must remain available for restoration.

Requested by: `@easonLiangWorldedtech`

## Required changes

- Remove `clearPersistedViewState()` from the generic `ClineProvider.dispose()` lifecycle path.
- Make disposal runtime-resource cleanup only; retain durable per-view state by default.
- If immediate deletion for an explicitly closed tab is still desired, implement it through a separate, explicit lifecycle signal that is not inferred from `onDidDispose`.
- Preserve the existing cap of 50 most-recent persisted entries and timestamp-based pruning.

## Suggested approaches

1. **Recommended: retention-only**
   - Do not delete state on close.
   - Rely on the existing 50-entry timestamp-based pruning to bound storage.
   - This is the simplest and safest approach because it preserves restart/reload restoration without needing an unreliable close discriminator.

2. **Explicit close protocol**
   - Add a narrowly scoped command or message that records explicit close intent while the webview is alive.
   - Validate that the intent applies only to the current `viewStateId`, then remove that one entry.
   - Keep generic disposal non-destructive as a fallback, since the close signal may be unavailable during host teardown.

3. **Lifecycle-aware cleanup (only if VS Code exposes a reliable signal)**
   - Investigate whether the tab creation/management path has a close-specific signal distinct from host reload/deactivation.
   - Use it only if it is demonstrably reliable across reload/shutdown/deactivation; otherwise prefer approach 1 or 2.

## Affected areas

- `src/core/webview/ClineProvider.ts`
- Potentially `src/core/webview/webviewMessageHandler.ts` and the webview state/message types if an explicit close protocol is selected.
- Focused unit tests for per-view persistence and disposal behavior.

## Acceptance criteria

- Calling `ClineProvider.dispose()` without explicit close intent leaves `viewStates[viewStateId]` intact.
- Reload/deactivation-style teardown restores the same persisted `mode` and `currentApiConfigName`.
- Any explicit cleanup removes only the intended view-state entry and cannot remove another tab's entry.
- Persisted view state continues to contain only non-secret fields (`mode`, `currentApiConfigName`, `updatedAt`).
- The 50-entry maximum and newest-first pruning behavior remain intact.

## Backlinks

- PR: https://github.com/Zoo-Code-Org/Zoo-Code/pull/977
- Review discussion: https://github.com/Zoo-Code-Org/Zoo-Code/pull/977#discussion_r3684795957
EOF
)

gh issue create \
  --repo "$repo" \
  --title "Preserve durable editor view state across provider disposal" \
  --body "$body" \
  --assignee "easonLiangWorldedtech"

Length of output: 3382


@easonLiangWorldedtech, confirmed: the follow-up issue was created and assigned to you:

#1065 — Preserve durable editor view state across provider disposal
#1065

Correction: my prior reply listed the wrong issue number. The correct issue is #1065.

You are interacting with an AI system.

@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

Added 50541112d (fix(webview): address view-state review feedback) to address the actionable review feedback with a minimal, focused fix.

Summary:

  • Fixed the view-state E2E listener leak by unregistering the TaskModeSwitched handler in the first cross-panel isolation test cleanup.
  • Preserved editor-tab persisted viewStates during normal provider teardown so VS Code reload/shutdown/deactivation no longer drops restorable tab mode / currentApiConfigName state.
  • Added/updated coverage to assert editor provider teardown preserves persisted view state.
  • Prevented the LM Studio hasLoadedFullDetails mock from leaking into later tests by using a one-shot mock return.
  • Guarded Kimi Code OAuth credential lookup in requestRouterModels so token refresh/read failures are logged and the model picker still receives a routerModels response instead of staying in loading state.
  • Scoped switch-case declarations in webviewMessageHandler to satisfy lint and avoid TDZ leakage between cases.
  • Cleaned up tasksById on TaskUnfocused, covering task removal paths that do not emit completion/abort events.
  • Added logging for invalid follow-up suggestion mode slugs while preserving the existing response behavior.
  • Aligned the startNewTask API destructuring with the existing preserveOpenTabs option.

Validation:

  • cd src ; npx vitest run extension/__tests__/api-task-control.spec.ts core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts — passed.
  • cd src ; npx vitest run core/webview/__tests__/ClineProvider.spec.ts -t 'does not reload full model details' — passed.
  • cd src ; npx vitest run core/webview/__tests__/ClineProvider.parallelMode.spec.ts -t 'should preserve persisted viewStates entry' — passed.
  • pnpm --dir src run check-types — passed.
  • pnpm --dir packages/types run check-types — passed.
  • Targeted ESLint for touched files — passed.
  • git diff --check — passed.
  • Commit hook lint — passed.

Note: I intentionally kept the persisted view-state fix minimal. Since onDidDispose does not reliably distinguish user tab close from reload/shutdown/deactivation, this commit avoids data loss by preserving editor view state during teardown and relies on the existing pruning behavior for stale entries.

@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

Follow-up plan for the e2e coverage requested here:

I created PR #1064 as the first dedicated e2e foundation PR for this work: test(vscode-e2e): add orchestrator E2E foundation for parallel-mode coverage.

The intent is to keep #977 focused on the durable per-view state base, while using follow-up PRs to build progressively heavier real VS Code e2e coverage on top of it. PR #1064 establishes reusable orchestrator fixtures, delegation lifecycle coverage, resource diagnostics, and cleanup/convergence checks that future parallel-mode e2e tests can reuse.

Planned e2e levels:

Level 1: view state + orchestrator across 3 tabs

Use three real tab/sidebar providers and verify that each panel keeps isolated view-local state while orchestrator tasks delegate and resume independently.

This should cover:

  • 3 live providers / panels.
  • independent mode and current profile state per panel.
  • orchestrator fan-out / fan-in in each panel.
  • no cross-panel mode/config contamination.
  • persisted view state still contains only non-secret fields.
  • cleanup leaves no stale task stack or registered task resources.

Level 2: view state + orchestrator + follow-up option mode switching across tabs

Extend Level 1 by using real ask_followup_question suggestions with mode options, because this is closer to how users actually switch mode from the chat UI.

This should cover:

  • 3 panels running orchestrator workflows.
  • follow-up suggestions switching each task through different modes.
  • coordinated multi-round mode changes across panels.
  • parent/child delegation still routed to the correct provider.
  • no mode switch from one tab overwrites another tab’s view-local mode.
  • resource convergence after all tasks finish.

Level 3: reload / restoration stress

After Level 1 and Level 2 prove live isolation, the next important boundary is persistence across lifecycle events.

Suggested scenario:

  • Open 3 panels with different modes/profiles.
  • Start orchestrator workflows in each.
  • Persist view state.
  • Simulate reload/reopen or dispose/recreate providers where possible.
  • Verify each panel restores the correct mode/current profile from its own view state entry.
  • Verify no secrets are restored from persisted view state.
  • Verify orphan/stale view state entries are pruned rather than corrupting active panels.

This directly protects the durable per-view state mechanism introduced by #977.

Level 4: interrupted child / recovery across panels

Stress delegation state and view state together.

Suggested scenario:

  • Three panels each run an orchestrator task.
  • One panel completes normally.
  • One panel has a delegated child interrupted/cancelled.
  • One panel keeps switching mode through follow-up options while another is recovering.
  • Verify the interrupted child does not resume the wrong parent/provider.
  • Verify recovery returns to the correct parent task.
  • Verify other panels’ modes/configs are unchanged.

This catches the class of bugs where task lifecycle state and provider-local view state accidentally share the wrong boundary.

Final boss: mixed parallel orchestration with reload, recovery, and resource convergence

A final high-value e2e could combine the important boundaries into one long-running smoke/stress scenario:

  1. Start 3 panels with distinct initial modes and profiles.
  2. Each panel starts an orchestrator parent.
  3. Each parent delegates children in different modes.
  4. Each child uses follow-up option mode switching.
  5. One child completes normally, one is interrupted and recovered, one continues through multiple rounds.
  6. Persist and restore view state during the workflow boundary where possible.
  7. Assert final parent summaries are routed to the correct task/provider.
  8. Assert every panel ends with its expected mode/profile.
  9. Assert persisted view states contain no secret fields.
  10. Assert task registry, task stack, and event listener diagnostics converge back to baseline.

That would be the “final boss” parallel-mode e2e: real VS Code host, multiple live providers, real globalState boundary, view-local state isolation, orchestrator delegation, follow-up mode switching, cancellation/recovery, and resource leak detection in one scenario.

I do not think all of this should block #977. My proposal is:

@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the feat/view-local-state-base branch from 5054111 to 97afc41 Compare July 30, 2026 19:57
@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

rebased

@github-actions github-actions Bot added awaiting-review PR changes are ready and waiting for maintainer re-review and removed awaiting-author PR is waiting for the author to address requested changes labels Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Add durable per-view state persistence for parallel tabs

3 participants