Skip to content

fix(webview): sync cachedState on mode changes in SettingsView#925

Open
easonLiangWorldedtech wants to merge 2 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fix/914-settingsview-stale-mode-apiconfig
Open

fix(webview): sync cachedState on mode changes in SettingsView#925
easonLiangWorldedtech wants to merge 2 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fix/914-settingsview-stale-mode-apiconfig

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Problem

In parallel mode, SettingsView shows stale mode and apiConfiguration values because its internal cachedState only updates via useEffect when currentApiConfigName changes, but doesn't refresh when mode changes through handleModeSwitch().

Fix

Add mode to the dependency array of the useEffect that updates cachedState:

Modified Files

Changes

  1. Destructure mode from extensionState (line 133)
  2. Add prevMode ref to track previous mode value (line 150)
  3. Update useEffect condition to check both currentApiConfigName and mode (line 226)
  4. Add mode to the dependency array (line 234)
// Before
if (prevApiConfigName.current === currentApiConfigName) {
    return
}

// After
if (prevApiConfigName.current === currentApiConfigName && prevMode.current === mode) {
    return
}

Closes #914

Summary by CodeRabbit

  • Bug Fixes
    • Settings now stay synchronized when the application mode changes.
    • Unsaved-change indicators and save-button state reset correctly after mode-triggered synchronization.
  • Tests
    • Expanded and refactored SettingsView change-detection tests, including new coverage to confirm state sync occurs on mode changes and not on re-renders with unchanged mode.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5acb5fec-1e42-4df0-93c7-586d09a3ba50

📥 Commits

Reviewing files that changed from the base of the PR and between 13fe3ad and 188b53c.

📒 Files selected for processing (2)
  • webview-ui/src/components/settings/SettingsView.tsx
  • webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • webview-ui/src/components/settings/SettingsView.tsx

📝 Walkthrough

Walkthrough

SettingsView now refreshes cached settings when the view-local mode changes, alongside API configuration changes. Test setup is expanded to isolate settings rendering, and tests cover synchronization and unchanged-mode behavior.

Changes

Settings cache synchronization

Layer / File(s) Summary
Track mode changes during cache synchronization
webview-ui/src/components/settings/SettingsView.tsx
SettingsView tracks the previous mode and re-syncs cached state when either mode or currentApiConfigName changes, resetting the change-detected flag.
Validate mode synchronization behavior
webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx
Tests add isolated UI and search mocks, mode-aware state setup, adjusted async timeouts, and coverage for synchronization after mode changes and no synchronization when mode is unchanged.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested labels: awaiting-review

Suggested reviewers: navedmerchant

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the problem and fix, but it omits several required template sections such as Test Procedure and the checklist. Fill out the full repository template, including Related GitHub Issue, Test Procedure, checklist, screenshots/videos, docs updates, and additional notes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: syncing SettingsView cached state when mode changes.
Linked Issues check ✅ Passed The changes implement #914 by refreshing cachedState when mode or currentApiConfigName changes and adding tests for the new sync behavior.
Out of Scope Changes check ✅ Passed The added test mocks and refactors support the new mode-sync coverage and do not appear unrelated to the issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 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.

🧹 Nitpick comments (1)
webview-ui/src/components/settings/SettingsView.tsx (1)

224-234: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update Vitest coverage for mode synchronization.

As per coding guidelines, please ensure that the local Vitest test (webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx) is updated to cover this new React hook behavior. The test should verify that when the mode changes in extensionState, the component's dirty state is correctly reset and the new mode settings are synchronized to cachedState.

🤖 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/components/settings/SettingsView.tsx` around lines 224 - 234,
The SettingsView tests do not cover the new mode-synchronization behavior in the
effect guarding on prevApiConfigName and prevMode. Update the Vitest cases in
SettingsView.spec.tsx to change extensionState.mode after the component is
dirty, then verify changeDetected resets and cachedState reflects the new mode
settings.

Source: Coding guidelines

🤖 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 `@webview-ui/src/components/settings/SettingsView.tsx`:
- Around line 224-234: The SettingsView tests do not cover the new
mode-synchronization behavior in the effect guarding on prevApiConfigName and
prevMode. Update the Vitest cases in SettingsView.spec.tsx to change
extensionState.mode after the component is dirty, then verify changeDetected
resets and cachedState reflects the new mode settings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: baa04223-c09f-4573-8b66-24b3973d3b3c

📥 Commits

Reviewing files that changed from the base of the PR and between 367013f and 6372c82.

📒 Files selected for processing (1)
  • webview-ui/src/components/settings/SettingsView.tsx

@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the fix/914-settingsview-stale-mode-apiconfig branch from 6372c82 to 9d21d45 Compare July 17, 2026 15:10

@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
`@webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx`:
- Around line 607-640: Update the “does not trigger sync when mode has not
changed” test to create a dirty local provider state before rerendering, while
keeping the mocked extensionState provider as “openai.” Assert that the locally
modified provider value remains after rerender, so an unintended synchronization
would be detected rather than masked by identical values.
🪄 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: 5414c876-564b-4c0e-88e1-a770dd725d2b

📥 Commits

Reviewing files that changed from the base of the PR and between 6372c82 and 9d21d45.

📒 Files selected for processing (2)
  • webview-ui/src/components/settings/SettingsView.tsx
  • webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • webview-ui/src/components/settings/SettingsView.tsx

@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the fix/914-settingsview-stale-mode-apiconfig branch from 9d21d45 to 13fe3ad Compare July 17, 2026 15:17
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 17, 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.

Nice ! Thanks for taking this on - is a nice little fix. Had some comments about increasing the test robustness, otherwise looks good.

Comment on lines +607 to +644
it("does not trigger sync when mode has not changed", async () => {
const onDone = vi.fn()
const extensionState = createExtensionState({
mode: "code",
apiConfiguration: {
apiProvider: "openai",
apiModelId: "gpt-4.1",
},
})

;(useExtensionState as any).mockImplementation(() => extensionState)

const { rerender } = render(
<QueryClientProvider client={queryClient}>
<SettingsView onDone={onDone} />
</QueryClientProvider>,
)

await waitFor(() => {
expect(screen.getByTestId("provider-value")).toHaveTextContent("openai")
})

// Make a dirty change so we can verify it isn't overwritten by a sync
fireEvent.click(screen.getByTestId("set-provider-baseten"))
expect(screen.getByTestId("provider-value")).toHaveTextContent("baseten")

// Re-render with same mode - should not trigger sync
await act(async () => {
rerender(
<QueryClientProvider client={queryClient}>
<SettingsView onDone={onDone} />
</QueryClientProvider>,
)
})

// Provider value should remain unchanged from the dirty state
expect(screen.getByTestId("provider-value")).toHaveTextContent("baseten")
}, 20000)

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.

It looks like this test would pass even if the whole mode fix were reverted. Since extensionState is a const (line 609) and the rerender at line 635 reuses the same object reference, React sees no dependency change — the sync effect never re-runs under either the old or new code, so provider-value stays baseten purely from the dirty click above.

Would reassigning extensionState to a new object (same values, new identity) before the rerender make this guard load-bearing? As written, I think this catches a regression of nothing.

Comment on lines +574 to +605
// Now change the mode - this should trigger the mode sync effect
await act(async () => {
extensionState = createExtensionState({
mode: "ask",
apiConfiguration: {
apiProvider: "openrouter",
apiModelId: "claude-3.5-sonnet",
},
})
;(useExtensionState as any).mockImplementation(() => extensionState)

rerender(
<QueryClientProvider client={queryClient}>
<SettingsView onDone={onDone} />
</QueryClientProvider>,
)
})

// Let the mode sync effect run
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0))
})

// Verify cachedState reflects the new mode's settings
await waitFor(() => {
expect(screen.getByTestId("provider-value")).toHaveTextContent("openrouter")
})

// Verify changeDetected is reset (save button should be disabled)
const updatedSaveButton = screen.getByTestId("save-button") as HTMLButtonElement
expect(updatedSaveButton.disabled).toBe(true)
}, 20000)

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.

Two mutations of the fix that this test seems to miss:

  • If mode were dropped from the dependency array, the effect would still fire here because extensionState is reassigned to a new object at line 576 — so the mode dep entry (part of this PR) is effectively untested.
  • If the prevMode.current = mode assignment were removed, the single code → ask transition would still produce correct state once; the stale ref is never re-exercised without a second transition.

Would it be worth adding a rerender where only extensionState identity changes (mode + apiConfigName unchanged) to pin the dep array, and a second same-mode rerender to expose a stale prevMode?

@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 labels Jul 19, 2026
@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the fix/914-settingsview-stale-mode-apiconfig branch from 13fe3ad to 188b53c Compare July 19, 2026 20:59
@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 19, 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.

[UX] SettingsView shows stale mode/apiConfig in multi-tab mode

3 participants