Skip to content

Keep rapid settings edits from reverting each other#4494

Open
colonelpanic8 wants to merge 1 commit into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-provider-settings-toggle
Open

Keep rapid settings edits from reverting each other#4494
colonelpanic8 wants to merge 1 commit into
pingdotgg:mainfrom
colonelpanic8:t3code/fix-provider-settings-toggle

Conversation

@colonelpanic8

@colonelpanic8 colonelpanic8 commented Jul 25, 2026

Copy link
Copy Markdown

What Changed

useUpdateSettingsTarget now retains each in-flight server settings patch and replays it over the server value (via the same applyServerSettingsPatch the server runs) until no write is outstanding for that environment.

  • New apps/web/src/hooks/pendingServerSettings.ts holds the per-environment overlay: retain on dispatch, release when the write settles, drop the overlay only when the last outstanding write settles.
  • useMergedSettings applies the overlay before merging client settings, so usePrimarySettings / useEnvironmentSettings reflect the edit immediately.
  • Unit tests for the overlay semantics in pendingServerSettings.test.ts.

Why

Server settings only change locally once the server echoes settingsUpdated back over the websocket. Until then, readers still see pre-edit settings, so a second edit issued inside that window is computed from stale state — and because keys such as providerInstances are whole-map replacements rather than deep merges (applyServerSettingsPatch), the second write silently reverts the first.

Reproduction before this change, in Settings → Providers: toggle Codex off, then toggle Claude off a moment later. Codex's switch snaps back on and its enabled: false never reaches settings.json — only the Claude entry is persisted. The same race applies to every provider-instance edit (binary path, custom models, accent color, add/delete), plus favorites and providerModelPreferences, since those patches are also built from the settings snapshot.

The doc comment on useUpdateSettingsTarget already claimed server keys were patched optimistically; this makes that true.

UI Changes

No visual changes. Behavioral only: a provider toggle now flips immediately and stays flipped instead of reverting when another edit follows quickly.

Verified in a local build (server + web from this branch, driven through the browser): toggling two providers in immediate succession leaves both switches off and persists both entries to settings.json; an off/on/other three-click sequence composes correctly. On main the same first sequence loses the first toggle.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes — n/a, no visual change
  • I included a video for animation/interaction changes — n/a, no motion change

Validation: vp test run src/hooks/pendingServerSettings.test.ts (6 passed), vp test run src/hooks src/components/settings src/providerInstances.test.ts (86 passed), vp run typecheck in apps/web, vp lint apps/web/src/hooks/, vp fmt --check.


Note

Medium Risk
Touches settings read/write paths and persistence timing; behavior is scoped to the web client overlay but incorrect release timing could briefly show stale or server-authoritative values after failures.

Overview
Fixes a race where rapid server settings edits could undo each other because the UI still read pre-echo websocket state while patches like providerInstances replace the whole map.

Adds pendingServerSettings: each dispatched server patch is retained, replayed with applyServerSettingsPatch for reads, and released when the RPC settles; the overlay clears only when no writes are in flight for that environment. useMergedSettings applies this overlay (via useSyncExternalStore) before merging client settings, and useUpdateSettingsTarget retains on dispatch and releases in finally instead of relying on atom-only optimism.

Unit tests cover stacked provider toggles, refcounted release, per-environment scoping, and subscriber notifications.

Reviewed by Cursor Bugbot for commit b27a313. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Prevent rapid server settings edits from reverting each other with an optimistic patch overlay

  • Introduces pendingServerSettings.ts, a module that tracks in-flight server setting patches per environment using a retain/release counter model.
  • When a server settings update is dispatched, the patch is retained as an optimistic overlay and applied immediately in useMergedSettings so the UI reflects the change before the server responds.
  • The overlay persists until all outstanding writes for that environment settle (success or failure), preventing concurrent edits from reverting each other.
  • usePendingServerPatches bridges the patch state into React via useSyncExternalStore so components re-render when pending patches change.
  • Risk: failed writes drop the overlay immediately, so a failed update will revert the UI to the server value even if other in-flight writes for that environment are still pending.

Macroscope summarized b27a313.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 04cbfdb7-3b9c-4b45-a705-4b0c54c43753

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ 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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 25, 2026
Comment thread apps/web/src/hooks/useSettings.ts
@macroscopeapp

macroscopeapp Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

2 blocking correctness issues found. This PR introduces a new optimistic overlay system for settings with significant runtime behavior changes. Multiple unresolved review comments identify high-severity bugs in the echo counting and failure handling logic that could recreate the very issue this PR aims to fix.

You can customize Macroscope's approvability policy. Learn more.

Comment thread apps/web/src/hooks/pendingServerSettings.ts Outdated
Comment thread apps/web/src/hooks/useSettings.ts Outdated

if (Object.keys(serverPatch).length > 0) {
if (environmentId) {
const pendingId = retainPendingServerPatch(

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.

🟡 Medium hooks/useSettings.ts:294

A failed server settings update can be silently committed by a later update that overlaps it in flight. For example, toggle provider A then immediately toggle provider B: B's providerInstances map is computed from the optimistically overlaid settings (which include A's pending edit) and dispatched to the server. If A's RPC fails, dropPendingServerPatch removes A's local overlay, but B's already-sent patch still carries A's edit and persists it on the server. The overlaid base used to compute each outgoing patch is never reconciled when an earlier patch is dropped, so the server keeps a value the user intended to discard.

The root cause is that retainPendingServerPatch snapshots serverSettingsRef.current as the base, and a subsequent patch is computed from settings that include the to-be-dropped overlay. When a patch fails and is dropped, any later patch already computed from that overlaid base carries the failed edit forward. Consider recomputing the base by stripping failed-but-not-yet-dropped overlays when building each outgoing patch, or blocking/rebasing subsequent writes until prior ones settle.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/hooks/useSettings.ts around line 294:

A failed server settings update can be silently committed by a later update that overlaps it in flight. For example, toggle provider A then immediately toggle provider B: B's `providerInstances` map is computed from the optimistically overlaid settings (which include A's pending edit) and dispatched to the server. If A's RPC fails, `dropPendingServerPatch` removes A's local overlay, but B's already-sent patch still carries A's edit and persists it on the server. The overlaid base used to compute each outgoing patch is never reconciled when an earlier patch is dropped, so the server keeps a value the user intended to discard.

The root cause is that `retainPendingServerPatch` snapshots `serverSettingsRef.current` as the base, and a subsequent patch is computed from settings that include the to-be-dropped overlay. When a patch fails and is dropped, any later patch already computed from that overlaid base carries the failed edit forward. Consider recomputing the base by stripping failed-but-not-yet-dropped overlays when building each outgoing patch, or blocking/rebasing subsequent writes until prior ones settle.

Comment thread apps/web/src/hooks/pendingServerSettings.ts Outdated
@colonelpanic8
colonelpanic8 force-pushed the t3code/fix-provider-settings-toggle branch from 0e81a81 to 6574213 Compare July 25, 2026 02:40
Comment thread apps/web/src/hooks/useSettings.ts Outdated

// Every authoritative value that arrives is one `settingsUpdated` broadcast,
// which is what retires the writes the overlay is still covering.
useEffect(() => {

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.

🟡 Medium hooks/useSettings.ts:237

useMergedSettings treats every new serverSettings reference as a write echo. When an update is dispatched while the environment has no config (so the hook is holding DEFAULT_SERVER_SETTINGS), the initial config load is counted as that update's echo. Once the RPC settles, the overlay is removed before the real settingsUpdated broadcast arrives, briefly reverting the UI and allowing another edit to be computed from stale settings. Consider distinguishing a genuine write echo from the first config load so the overlay is not retired early.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/hooks/useSettings.ts around line 237:

`useMergedSettings` treats every new `serverSettings` reference as a write echo. When an update is dispatched while the environment has no config (so the hook is holding `DEFAULT_SERVER_SETTINGS`), the initial config load is counted as that update's echo. Once the RPC settles, the overlay is removed before the real `settingsUpdated` broadcast arrives, briefly reverting the UI and allowing another edit to be computed from stale settings. Consider distinguishing a genuine write echo from the first config load so the overlay is not retired early.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6574213. Configure here.

Comment thread apps/web/src/hooks/pendingServerSettings.ts Outdated
Comment thread apps/web/src/hooks/useSettings.ts Outdated
@colonelpanic8
colonelpanic8 force-pushed the t3code/fix-provider-settings-toggle branch from 6574213 to 4df7b36 Compare July 25, 2026 02:49
Server settings only change locally once the server echoes `settingsUpdated`
back over the websocket, and `useUpdateSettingsTarget` applied nothing
locally in the meantime. Any edit issued inside that round trip was
therefore computed from pre-edit settings, and because keys such as
`providerInstances` are whole-value replacements rather than deep merges,
the second write silently reverted the first: toggle one provider off,
toggle a second off a moment later, and the first provider's switch snaps
back on and never reaches settings.json.

Retain in-flight patches per environment and replay them over the server
value with the same `applyServerSettingsPatch` the server runs, so reads
and follow-up patches build on the edit the user just made. Patches are
dropped once no write is outstanding — including after a failed write, so
the server value stays authoritative.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@colonelpanic8
colonelpanic8 force-pushed the t3code/fix-provider-settings-toggle branch from 4df7b36 to b27a313 Compare July 25, 2026 03:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant