Skip to content

feat(api): add task control and global state APIs - #1185

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

feat(api): add task control and global state APIs#1185
easonLiangWorldedtech wants to merge 3 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:feat/view-local-state-base-3

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Add task control and global state APIs at the Extension API layer, allowing external tools to control tasks and read view-local state. This is the API surface exposed by per-view isolation.

Changes

  • API task control methods — New approveAsk(), selectFollowUpSuggestion() task control methods
  • Global state API — Provide safe global state read/write interface
  • View-local value exposure — API can read current view's local state

Files Changed (4 files, +422 / -5)

File Change Description
src/extension/api.ts +74/-3 Core API implementation: task control + global state
api-task-control.spec.ts +263 New — Complete task control tests
api-set-configuration.spec.ts +73 setConfiguration view-local sync tests
packages/types/src/api.ts +17/-1 API type definitions

Design Notes

  • API methods automatically detect current viewStateId, ensuring operations target the correct view context
  • setConfiguration view-local state sync mechanism is integrated
  • Type-safe API surface via updated types

Test Notes

This PR includes commits from base-1 and base-2 (linear dependency chain), so tests pass. If reviewed in isolation without base-2's setValues() method, the persistence layer would fail.

Related

  • Depends on base-2 (viewStates persistence)
  • Prerequisite for base-4 (e2e tests)

- Export VSCodeAPIWrapper class and add getViewStateId() method
- Generate unique viewStateId using crypto.randomUUID() with fallback
- Persist viewStateId in localStorage for dev server compatibility
- Add viewStateSchema to global-settings.ts for type safety
- Send viewStateId during webviewDidLaunch handshake
- Add setValues method to update view-local state without affecting global settings
- Persist mode selection per viewStateId for tab isolation
- Update webviewMessageHandler to support new view state flow
- Add parallel mode switching tests for sidebar and tab panel
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds durable per-view state for parallel webviews, propagates stable view identifiers through launch messages, adds task-control APIs, improves provider synchronization, and strengthens message-handler typing and error handling with expanded test coverage.

Changes

Per-view state and launch flow

Layer / File(s) Summary
View-state contracts and launch flow
packages/types/src/*, webview-ui/src/context/ExtensionStateContext.tsx, webview-ui/src/utils/*, src/core/webview/webviewMessageHandler.ts
Defines persisted viewStates, generates stable view-state IDs, sends IDs during launch, and restores view-local API configuration.
Durable provider state isolation
src/core/webview/ClineProvider.ts
Adds isolated per-view mode and provider-profile state with serialized persistence, pruning, loading, mutation synchronization, and typed task/message handling.
Provider state behavior validation
src/core/webview/__tests__/ClineProvider*.spec.ts
Tests persistence, recovery, pruning, concurrent updates, profile changes, mode switching, reset behavior, and multi-instance isolation.

Task control and message handling

Layer / File(s) Summary
Task-control and configuration API
src/extension/api.ts, packages/types/src/api.ts, src/extension/__tests__/api-*.spec.ts
Adds task ask approval, follow-up suggestion selection, tab-preserving task creation, typed global-state access, and provider-based configuration updates.
Message-handler typing and resilience
src/core/webview/webviewMessageHandler.ts, src/core/webview/__tests__/webviewMessageHandler*.spec.ts, src/eslint-suppressions.json
Replaces several untyped payloads, adds launch and diagnostics coverage, and keeps router-model requests running when Kimi Code credential lookup fails.

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

Possibly related PRs

Suggested labels: awaiting-review

Suggested reviewers: hannesrudolph

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided, so the required issue, implementation, testing, checklist, and documentation details are missing. Add the required pull request description, including the linked issue, implementation summary, test procedure, checklist, and documentation impact.
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 (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the added task-control and global-state APIs, which are significant changes in the pull request.
✨ 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.

@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: 4

🧹 Nitpick comments (10)
src/extension/__tests__/api-set-configuration.spec.ts (1)

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

Align the test double with the declared type.

ProviderDouble extends EventEmitter, but the object literal at Line 46-57 is a plain object cast with as ProviderDouble. The cast holds only because registerListeners calls the stubbed on. src/extension/__tests__/api-task-control.spec.ts Line 74 builds a real EventEmitter instead. Use the same pattern here so a future event-driven assertion does not fail on a missing emitter.

🤖 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/__tests__/api-set-configuration.spec.ts` around lines 26 - 57,
Update the ProviderDouble setup in the setConfiguration test to instantiate a
real EventEmitter, matching the pattern used by api-task-control.spec.ts, and
assign the mocked provider properties onto it. Preserve the existing mocked on,
setValues, contextProxy, providerSettingsManager, and postStateToWebview
behavior while ensuring the test object genuinely satisfies the EventEmitter
type.
src/extension/__tests__/api-task-control.spec.ts (1)

227-242: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for a failing mode switch.

This suite covers an unknown mode but not a rejecting handleModeSwitch. That is the failure path I raised at src/extension/api.ts Line 355-367, where the rejection currently escapes and the answer is never forwarded. Add the case together with the fix.

💚 Proposed test addition
+		it("still responds when the mode switch fails", async () => {
+			const task = createTask("task-mode-switch-error")
+			sidebarProvider.handleModeSwitch.mockRejectedValue(new Error("switch failed"))
+			sidebarProvider.emit(RooCodeEventName.TaskCreated, task)
+
+			await expect(
+				api.selectTaskFollowupSuggestion({ taskId: task.taskId, answer: "Use architect", mode: "architect" }),
+			).resolves.toBe(true)
+
+			expect(task.handleWebviewAskResponse).toHaveBeenCalledWith("messageResponse", "Use architect")
+		})
🤖 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/__tests__/api-task-control.spec.ts` around lines 227 - 242, Add
a test beside the invalid-mode case that mocks handleModeSwitch to reject, then
verify selectTaskFollowupSuggestion still resolves successfully, forwards the
answer through task.handleWebviewAskResponse, and records the failure
appropriately. Update API#selectTaskFollowupSuggestion so a rejected
handleModeSwitch is caught and does not prevent answer forwarding.
src/core/webview/ClineProvider.ts (3)

642-659: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the as any casts with a documented typed assertion.

Both overrides add an as any cast plus an ESLint suppression. The coding guidelines require avoiding as any and fixing lint violations instead of suppressing them, and they allow a double assertion only as a last resort with documentation. Cast the listener to the emitter's parameter type instead, so no suppression is needed.

♻️ Proposed refactor
 	override on<K extends keyof TaskProviderEvents>(
 		event: K,
 		listener: (...args: TaskProviderEvents[K]) => void | Promise<void>,
 	): this {
-		// eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-		return (super.on as any)(event, listener)
+		// EventEmitter listeners are typed as returning void; async listeners are
+		// intentionally allowed here, so the return type is widened for the base call.
+		return super.on(event, listener as (...args: TaskProviderEvents[K]) => void)
 	}

Apply the same change to the off override at Line 653-659.

As per coding guidelines: "Avoid as any; use typed APIs, bracket notation for private members, or precise test doubles and type guards. Use double assertions only as a last resort and document them."

🤖 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 642 - 659, Replace the `as
any` casts and ESLint suppressions in the `on` and `off` overrides with
documented typed assertions that adapt the listener to EventEmitter’s parameter
type. Preserve the existing event and listener behavior, and apply the same
precise assertion approach to both `super.on` and `super.off`.

Source: Coding guidelines


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

Document the replace-versus-merge rule for flat provider settings.

When the mutation contains apiProvider, the cached apiConfiguration is replaced by only the keys present in that mutation. When it does not, the keys are merged into the existing cache. The test at src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts Line 1159 depends on this distinction. Add a short comment so a future change does not turn the replace branch into a merge.

♻️ Proposed refactor
+			// A mutation that sets apiProvider is a provider switch: replace the cached
+			// configuration so keys from the previous provider do not leak through.
+			// A mutation without apiProvider only edits fields of the current provider.
 			this.viewLocalState.apiConfiguration =
 				"apiProvider" in providerSettingsUpdate
 					? providerSettingsUpdate
🤖 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 3269 - 3285, Add a brief
comment beside the apiConfiguration assignment in the provider settings update
branch explaining that mutations containing apiProvider replace the cached
configuration with only supplied keys, while mutations without it merge into the
existing configuration. Preserve the current conditional behavior and reference
the providerSettingsUpdate distinction clearly.

1927-1943: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate view-state writes in upsertProviderProfile and activateProviderProfileUnlocked. saveViewState already delegates to _saveViewLocalStateFromMutation, so both methods persist currentApiConfigName twice and update the view-local cache twice. Each duplicate write also enqueues an extra job on the shared static persistedViewStateWriteQueue.

  • src/core/webview/ClineProvider.ts#L1927-L1943: remove the two saveViewState calls from the Promise.all and keep only the _saveViewLocalStateFromMutation call.
  • src/core/webview/ClineProvider.ts#L2059-L2073: remove the two saveViewState calls from the Promise.all and keep only the _saveViewLocalStateFromMutation call.
🤖 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 1927 - 1943, Remove the
redundant saveViewState calls from the Promise.all blocks in
upsertProviderProfile at src/core/webview/ClineProvider.ts#L1927-L1943 and
activateProviderProfileUnlocked at
src/core/webview/ClineProvider.ts#L2059-L2073. Keep each
_saveViewLocalStateFromMutation call and all other state updates unchanged so
each profile update is persisted once.
src/extension/api.ts (1)

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

Document the double assertion and the registry lifetime.

Two points on this line:

  1. task as unknown as TaskAskController is an undocumented double assertion. The coding guidelines allow a double assertion only as a last resort and require documentation. Add a short comment stating that Task structurally satisfies TaskAskController but is not declared as implementing it.
  2. The registry removes an entry on TaskUnfocused (Line 421). A delegating parent emits TaskUnfocused when its child takes the stack, so the parent becomes unreachable through approveTaskAsk while it is only paused. Record that rule next to the registry so callers understand which tasks are addressable.

As per coding guidelines: "Use double assertions only as a last resort and document them."

🤖 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 389 - 391, Document the double assertion
in the TaskCreated handler by noting that Task structurally satisfies
TaskAskController but is not declared to implement it. Also add a concise
comment by the tasksById registry explaining that TaskUnfocused removes entries,
including the delegating-parent/paused-child behavior that makes paused parents
unreachable through approveTaskAsk.

Source: Coding guidelines

src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts (3)

925-937: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen the empty-state assertion.

viewLocalState starts as {}, so vi.waitFor succeeds on the first tick and never waits for the constructor's loadViewState() to settle. Await the load explicitly, then assert.

💚 Proposed fix
-			await vi.waitFor(() => {
-				expect(asProviderAccess(provider).viewLocalState).toEqual({})
-			})
+			await asProviderAccess(provider).loadViewState()
+			expect(asProviderAccess(provider).viewLocalState).toEqual({})
🤖 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
925 - 937, Update the “should keep viewLocalState empty when no stable per-view
values exist” test to explicitly await the provider’s constructor-triggered
loadViewState() completion before asserting viewLocalState. Remove the
vi.waitFor-based assertion and keep the empty-state and subsequent getState
expectations after loading has settled.

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

Remove the duplicated assertion.

Line 890 and Line 891 are identical. Delete one.

💚 Proposed fix
 			expect(asProviderAccess(provider).viewLocalState).not.toHaveProperty("mode")
-			expect(asProviderAccess(provider).viewLocalState).not.toHaveProperty("mode")
 			expect(provider.contextProxy.getValue("viewStates")).toBeUndefined()
🤖 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
890 - 891, Remove the duplicated expect assertion in the parallel mode test,
keeping a single assertion that viewLocalState does not have the mode property.

1316-1341: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a durable-persistence assertion for profile deletion.

This test only checks the in-memory result of getState(). It does not check the viewStates entry. That gap hides the issue I raised at src/core/webview/ClineProvider.ts Line 1987-1991, where deleteProviderProfile updates the cache but does not persist the replacement selection.

Set a stable view-state id first, then assert the persisted entry.

💚 Proposed test addition
 			const state = await provider.getState()
 
 			expect(state.currentApiConfigName).toBe("replacement-profile")
+			expect(provider.contextProxy.getValue("viewStates")).toMatchObject({
+				"stable-sidebar-view": { currentApiConfigName: "replacement-profile" },
+			})

Add await asProviderAccess(provider).setViewStateId("stable-sidebar-view") before the deletion so the entry has a stable key.

🤖 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
1316 - 1341, Add durable persistence coverage to the test around
deleteProviderProfile: call
asProviderAccess(provider).setViewStateId("stable-sidebar-view") before
deletion, then assert the persisted viewStates entry for that key contains the
replacement profile selection, in addition to the existing getState assertions.
webview-ui/src/utils/vscode.ts (1)

79-89: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Prefer the in-memory fallback when a storage write failed.

setState always assigns this.fallbackState, but the localStorage.setItem call can fail silently. getState then returns the older localStorage value and ignores the newer in-memory value. In that case the comment at Line 115-116 does not hold, because getViewStateId can return a different id than the one just written.

Track whether the last storage write succeeded, or read the in-memory value first when it is set.

♻️ Proposed refactor
 		try {
 			if (typeof localStorage?.getItem === "function") {
 				const state = localStorage.getItem("vscodeState")
-				return state ? JSON.parse(state) : this.fallbackState
+				if (this.fallbackState !== undefined) {
+					return this.fallbackState
+				}
+
+				return state ? JSON.parse(state) : undefined
 			}
 		} catch {
 			return this.fallbackState
 		}
🤖 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 `@webview-ui/src/utils/vscode.ts` around lines 79 - 89, Update setState and
getState so a failed localStorage.setItem causes subsequent reads to use the
current in-memory fallbackState rather than stale storage data. Track
storage-write success or prioritize the in-memory state when set, while
preserving the existing fallback behavior for unavailable or invalid storage.
🤖 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 3012-3013: Confirm the intended scope of
destructiveCommandGuardEnabled in the surrounding object literal: if it is
intentionally global and cannot be overridden by a view, keep the stateValues
read and add a brief comment documenting that constraint; otherwise, switch this
property to mergedStateValues to match the other fields.
- Around line 575-584: Update setViewStateId to sanitize the trimmed viewStateId
before comparing it with this.viewStateId. Return early when the sanitized value
is empty or unchanged, then assign that same sanitized value and call
loadViewState only when it differs.
- Around line 1987-1991: Update deleteProviderProfile to call
_saveViewLocalStateFromMutation instead of _updateViewLocalStateFromMutation
when applying the replacement currentApiConfigName and listApiConfigMeta,
ensuring the new profile selection and metadata are persisted to viewStates.

In `@src/extension/api.ts`:
- Around line 355-367: Update selectTaskFollowupSuggestion around
entry.provider.handleModeSwitch so a rejected mode switch is caught and logged
without propagating the error. Always continue to
entry.task.handleWebviewAskResponse("messageResponse", answer) and return true
after a valid mode switch attempt, preserving the existing unknown-mode
behavior.

---

Nitpick comments:
In `@src/core/webview/__tests__/ClineProvider.parallelMode.spec.ts`:
- Around line 925-937: Update the “should keep viewLocalState empty when no
stable per-view values exist” test to explicitly await the provider’s
constructor-triggered loadViewState() completion before asserting
viewLocalState. Remove the vi.waitFor-based assertion and keep the empty-state
and subsequent getState expectations after loading has settled.
- Around line 890-891: Remove the duplicated expect assertion in the parallel
mode test, keeping a single assertion that viewLocalState does not have the mode
property.
- Around line 1316-1341: Add durable persistence coverage to the test around
deleteProviderProfile: call
asProviderAccess(provider).setViewStateId("stable-sidebar-view") before
deletion, then assert the persisted viewStates entry for that key contains the
replacement profile selection, in addition to the existing getState assertions.

In `@src/core/webview/ClineProvider.ts`:
- Around line 642-659: Replace the `as any` casts and ESLint suppressions in the
`on` and `off` overrides with documented typed assertions that adapt the
listener to EventEmitter’s parameter type. Preserve the existing event and
listener behavior, and apply the same precise assertion approach to both
`super.on` and `super.off`.
- Around line 3269-3285: Add a brief comment beside the apiConfiguration
assignment in the provider settings update branch explaining that mutations
containing apiProvider replace the cached configuration with only supplied keys,
while mutations without it merge into the existing configuration. Preserve the
current conditional behavior and reference the providerSettingsUpdate
distinction clearly.
- Around line 1927-1943: Remove the redundant saveViewState calls from the
Promise.all blocks in upsertProviderProfile at
src/core/webview/ClineProvider.ts#L1927-L1943 and
activateProviderProfileUnlocked at
src/core/webview/ClineProvider.ts#L2059-L2073. Keep each
_saveViewLocalStateFromMutation call and all other state updates unchanged so
each profile update is persisted once.

In `@src/extension/__tests__/api-set-configuration.spec.ts`:
- Around line 26-57: Update the ProviderDouble setup in the setConfiguration
test to instantiate a real EventEmitter, matching the pattern used by
api-task-control.spec.ts, and assign the mocked provider properties onto it.
Preserve the existing mocked on, setValues, contextProxy,
providerSettingsManager, and postStateToWebview behavior while ensuring the test
object genuinely satisfies the EventEmitter type.

In `@src/extension/__tests__/api-task-control.spec.ts`:
- Around line 227-242: Add a test beside the invalid-mode case that mocks
handleModeSwitch to reject, then verify selectTaskFollowupSuggestion still
resolves successfully, forwards the answer through
task.handleWebviewAskResponse, and records the failure appropriately. Update
API#selectTaskFollowupSuggestion so a rejected handleModeSwitch is caught and
does not prevent answer forwarding.

In `@src/extension/api.ts`:
- Around line 389-391: Document the double assertion in the TaskCreated handler
by noting that Task structurally satisfies TaskAskController but is not declared
to implement it. Also add a concise comment by the tasksById registry explaining
that TaskUnfocused removes entries, including the delegating-parent/paused-child
behavior that makes paused parents unreachable through approveTaskAsk.

In `@webview-ui/src/utils/vscode.ts`:
- Around line 79-89: Update setState and getState so a failed
localStorage.setItem causes subsequent reads to use the current in-memory
fallbackState rather than stale storage data. Track storage-write success or
prioritize the in-memory state when set, while preserving the existing fallback
behavior for unavailable or invalid storage.
🪄 Autofix

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: 7be9ad30-1680-4bd1-a995-c13c5d89d706

📥 Commits

Reviewing files that changed from the base of the PR and between 276e425 and cf9437b.

📒 Files selected for processing (18)
  • 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.routerModels.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/context/ExtensionStateContext.tsx
  • webview-ui/src/utils/__tests__/vscode.spec.ts
  • webview-ui/src/utils/vscode.ts
💤 Files with no reviewable changes (1)
  • src/eslint-suppressions.json

Comment on lines +575 to +584
public async setViewStateId(viewStateId: string | undefined): Promise<void> {
const normalizedViewStateId = viewStateId?.trim()

if (!normalizedViewStateId || normalizedViewStateId === this.viewStateId) {
return
}

this.viewStateId = normalizedViewStateId.replace(/[^A-Za-z0-9_-]/g, "_")
await this.loadViewState()
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Compare the sanitized id, not the raw id.

this.viewStateId stores the sanitized value, but the early return compares the raw trimmed value. When the webview reports an id that contains a character outside [A-Za-z0-9_-], every repeated report fails the equality check and triggers another loadViewState(). Sanitize first, then compare.

🐛 Proposed fix
 	public async setViewStateId(viewStateId: string | undefined): Promise<void> {
-		const normalizedViewStateId = viewStateId?.trim()
+		const normalizedViewStateId = viewStateId?.trim().replace(/[^A-Za-z0-9_-]/g, "_")
 
 		if (!normalizedViewStateId || normalizedViewStateId === this.viewStateId) {
 			return
 		}
 
-		this.viewStateId = normalizedViewStateId.replace(/[^A-Za-z0-9_-]/g, "_")
+		this.viewStateId = normalizedViewStateId
 		await this.loadViewState()
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public async setViewStateId(viewStateId: string | undefined): Promise<void> {
const normalizedViewStateId = viewStateId?.trim()
if (!normalizedViewStateId || normalizedViewStateId === this.viewStateId) {
return
}
this.viewStateId = normalizedViewStateId.replace(/[^A-Za-z0-9_-]/g, "_")
await this.loadViewState()
}
public async setViewStateId(viewStateId: string | undefined): Promise<void> {
const normalizedViewStateId = viewStateId?.trim().replace(/[^A-Za-z0-9_-]/g, "_")
if (!normalizedViewStateId || normalizedViewStateId === this.viewStateId) {
return
}
this.viewStateId = normalizedViewStateId
await this.loadViewState()
}
🤖 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 575 - 584, Update
setViewStateId to sanitize the trimmed viewStateId before comparing it with
this.viewStateId. Return early when the sanitized value is empty or unchanged,
then assign that same sanitized value and call loadViewState only when it
differs.

Comment on lines +1987 to +1991
this._updateViewLocalStateFromMutation({
currentApiConfigName: profileToActivate,
listApiConfigMeta: entries,
})

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 | ⚡ Quick win

Persist the replacement profile selection after a deletion.

deleteProviderProfile calls _updateViewLocalStateFromMutation, which updates only the in-memory cache. upsertProviderProfile (Line 1938) and activateProviderProfileUnlocked (Line 2069) call _saveViewLocalStateFromMutation, which also writes the durable viewStates entry.

Because the deletion is not persisted, the viewStates entry keeps the deleted profile name. After a reload, loadViewState restores that name, providerSettingsManager.getProfile fails, and the view reports a profile that no longer exists.

🐛 Proposed fix
-		this._updateViewLocalStateFromMutation({
-			currentApiConfigName: profileToActivate,
-			listApiConfigMeta: entries,
-		})
+		await this._saveViewLocalStateFromMutation({
+			currentApiConfigName: profileToActivate,
+			listApiConfigMeta: entries,
+		})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this._updateViewLocalStateFromMutation({
currentApiConfigName: profileToActivate,
listApiConfigMeta: entries,
})
await this._saveViewLocalStateFromMutation({
currentApiConfigName: profileToActivate,
listApiConfigMeta: entries,
})
🤖 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 1987 - 1991, Update
deleteProviderProfile to call _saveViewLocalStateFromMutation instead of
_updateViewLocalStateFromMutation when applying the replacement
currentApiConfigName and listApiConfigMeta, ensuring the new profile selection
and metadata are persisted to viewStates.

Comment on lines 3012 to 3013
destructiveCommandGuardEnabled:
stateValues.destructiveCommandGuardEnabled ?? DEFAULT_DESTRUCTIVE_COMMAND_GUARD_ENABLED,

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Confirm that destructiveCommandGuardEnabled must stay global.

Every other field in this object literal now reads from mergedStateValues. This one still reads stateValues. If the guard must remain a global setting that a view cannot override, add a short comment stating that. If the global read is an oversight, switch it to mergedStateValues for consistency.

🤖 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 3012 - 3013, Confirm the
intended scope of destructiveCommandGuardEnabled in the surrounding object
literal: if it is intentionally global and cannot be overridden by a view, keep
the stateValues read and add a brief comment documenting that constraint;
otherwise, switch this property to mergedStateValues to match the other fields.

Comment thread src/extension/api.ts
Comment on lines +355 to +367
if (mode) {
const { customModes } = await entry.provider.getState()
const isValidMode = getAllModes(customModes).some((modeConfig) => modeConfig.slug === mode)

if (isValidMode) {
await entry.provider.handleModeSwitch(mode)
} else {
this.log(`[API#selectTaskFollowupSuggestion] ignoring unknown mode "${mode}" for task ${taskId}`)
}
}

entry.task.handleWebviewAskResponse("messageResponse", answer)
return true

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle a failing mode switch so the answer still reaches the task.

entry.provider.handleModeSwitch(mode) can reject. ClineProvider.handleModeSwitchUnlocked rethrows persistence failures, and enqueueProviderProfileMutation rejects after the 30-second mutation timeout. When that happens, selectTaskFollowupSuggestion throws before Line 366, the answer is never forwarded, and the task stays blocked on the pending ask. Every other failure path in this method returns a boolean.

🐛 Proposed fix
 			if (isValidMode) {
-				await entry.provider.handleModeSwitch(mode)
+				try {
+					await entry.provider.handleModeSwitch(mode)
+				} catch (error) {
+					const errorMessage = error instanceof Error ? error.message : String(error)
+					this.log(
+						`[API#selectTaskFollowupSuggestion] mode switch to "${mode}" failed for task ${taskId}: ${errorMessage}`,
+					)
+				}
 			} else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (mode) {
const { customModes } = await entry.provider.getState()
const isValidMode = getAllModes(customModes).some((modeConfig) => modeConfig.slug === mode)
if (isValidMode) {
await entry.provider.handleModeSwitch(mode)
} else {
this.log(`[API#selectTaskFollowupSuggestion] ignoring unknown mode "${mode}" for task ${taskId}`)
}
}
entry.task.handleWebviewAskResponse("messageResponse", answer)
return true
if (mode) {
const { customModes } = await entry.provider.getState()
const isValidMode = getAllModes(customModes).some((modeConfig) => modeConfig.slug === mode)
if (isValidMode) {
try {
await entry.provider.handleModeSwitch(mode)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.log(
`[API#selectTaskFollowupSuggestion] mode switch to "${mode}" failed for task ${taskId}: ${errorMessage}`,
)
}
} else {
this.log(`[API#selectTaskFollowupSuggestion] ignoring unknown mode "${mode}" for task ${taskId}`)
}
}
entry.task.handleWebviewAskResponse("messageResponse", answer)
return true
🤖 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 - 367, Update
selectTaskFollowupSuggestion around entry.provider.handleModeSwitch so a
rejected mode switch is caught and logged without propagating the error. Always
continue to entry.task.handleWebviewAskResponse("messageResponse", answer) and
return true after a valid mode switch attempt, preserving the existing
unknown-mode behavior.

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants