Skip to content

fix(settings): actually persist theme/wallpaper/dock across sessions - #1617

Merged
jaylfc merged 1 commit into
devfrom
fix/settings-persist-across-login
Jul 4, 2026
Merged

fix(settings): actually persist theme/wallpaper/dock across sessions#1617
jaylfc merged 1 commit into
devfrom
fix/settings-persist-across-login

Conversation

@jaylfc

@jaylfc jaylfc commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #1601 and #1603. Both were reported after #1611 landed (which wired up the apply path + debounced auto-save/restore machinery) — theme, wallpaper, dock position and dock icon size all still apply correctly in-session but revert to defaults after logout + login.

Root cause: the restore effects for desktop settings and the active theme live in SystemShortcuts (App.tsx), which mounts as a sibling of LoginGate — before LoginGate's own /auth/status check resolves. On a fresh login right after a logout, that mount-time restore fires while the session is still unauthenticated, gets a 401, and — since it's a one-shot effect gated by a ref — is never retried once the user actually logs in. The in-memory stores stay at their defaults for the rest of that session, which is exactly the reported symptom. (The backend side, tinyagentos/desktop_settings.py, was already correctly disk-backed via SQLite, keyed by user — that part was fine.)

There was also a secondary race: the dock/wallpaper auto-save effects were gated on the same flag the restore effect flipped the instant it started running, not once its fetch actually resolved. A slow restore GET could lose to the debounced auto-save PUT (500ms wallpaper / 1s dock) and get overwritten with the pre-restore default value.

Changes

  • desktop/src/stores/auth-ready-store.ts (new) — tiny store LoginGate publishes to once it reaches its "ready" phase.
  • desktop/src/components/LoginGate.tsx — publish auth-readiness on phase change.
  • desktop/src/hooks/use-on-auth-ready.ts (new) — small hook that runs a callback once per authenticated session and re-arms after a logout/login cycle; used for the active-theme restore.
  • desktop/src/App.tsx — gate the active-theme restore on auth-ready instead of firing on bare mount.
  • desktop/src/hooks/use-session-persistence.ts — gate the whole restore effect on auth-ready (resetting on logout so a later login re-fetches), and split the dock/wallpaper auto-save guards onto their own "restore settled" flags instead of the shared one.
  • Tests: use-session-persistence.test.ts and new use-on-auth-ready.test.ts cover the full set-then-reload cycle (pre-auth: no fetch; login: restore fires; logout+login again: re-restores a different saved value, not stuck on the first session's); tests/test_desktop_settings.py adds a save_preference/get_preference round-trip test and a fresh-store-instance-same-db test proving the backend is durable across restarts, not just in-memory.

Test plan

  • cd desktop && npm run build
  • npx vitest run (298 files, 2434 tests passed)
  • python3 -m pytest tests/test_desktop_settings.py tests/test_routes_desktop.py -q (20 passed)

Summary by CodeRabbit

  • Bug Fixes
    • Desktop themes now restore only after sign-in is confirmed, reducing flashes of the wrong theme during login.
    • Session restores for themes, wallpaper, and dock settings now run reliably after logout/login cycles.
    • Prevented settings autosave from overwriting restored values while data is still loading.
  • Tests
    • Expanded coverage for login readiness, session persistence, and preference storage behavior.

…1601, #1603)

The prior fix (#1611) wired up debounced auto-save and a mount-time restore
for desktop settings, but the restore fetches (theme, wallpaper, dock,
windows, widgets) live in SystemShortcuts, which mounts as a sibling of
LoginGate before its /auth/status check resolves. On a fresh login (right
after a logout) that mount-time restore fires while still unauthenticated,
gets 401'd, and — being a one-shot effect — is never retried once the user
actually logs in. The in-memory stores are left at their defaults for the
rest of that session, which is exactly the reported symptom.

Add an auth-ready-store that LoginGate publishes to once it reaches its
"ready" phase, and gate the restore effects on it (resetting when it drops
back to false so a later re-login re-fetches). Also split the dock/wallpaper
auto-save guards off the outer "restore started" flag onto per-field
"restore settled" flags, since the old shared flag flipped true the instant
the restore effect began rather than once its fetch actually landed — a slow
GET could otherwise lose a race to the debounced auto-save PUT and get
overwritten with the pre-restore default value.
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds an auth-readiness store and a useOnAuthReady hook to defer theme restoration, session-persistence restore, and dock/wallpaper auto-save until authentication is confirmed, re-running restore logic on logout/login cycles, plus new tests including a desktop-settings persistence regression test.

Changes

Auth-ready gated persistence restore

Layer / File(s) Summary
Auth-ready store
desktop/src/stores/auth-ready-store.ts
New Zustand store exposing ready boolean state and setReady action to signal confirmed authentication.
useOnAuthReady hook
desktop/src/hooks/use-on-auth-ready.ts, desktop/src/hooks/__tests__/use-on-auth-ready.test.ts
New hook runs a callback once per authenticated session, resets on logout, re-runs on next login; covered by tests for no-run while logged out, single run, no duplicate reruns, and rerun after logout/login.
LoginGate publishes readiness
desktop/src/components/LoginGate.tsx
LoginGate sets useAuthReadyStore's ready flag to true when its internal phase becomes "ready".
Deferred theme restore
desktop/src/App.tsx
Splits WebKit repaint guard installation into its own mount effect; gates restoreActiveTheme() behind useOnAuthReady.
Session persistence gating
desktop/src/hooks/use-session-persistence.ts, desktop/src/hooks/__tests__/use-session-persistence.test.ts
Restore effect resets and returns early when authReady is false; dock/wallpaper restore flags set in finally blocks; auto-save effects gated on per-domain restored flags instead of a shared flag; tests cover logout/login persistence and a restore-vs-autosave race.
Desktop settings store tests
tests/test_desktop_settings.py
New tests for preference save/get roundtrip, default empty dict, and cross-instance persistence against the same database file.

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

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant LoginGate
    participant AuthReadyStore
    participant App
    participant SessionPersistence

    User->>LoginGate: Authenticates
    LoginGate->>LoginGate: status.phase becomes "ready"
    LoginGate->>AuthReadyStore: setReady(true)

    App->>AuthReadyStore: subscribe via useOnAuthReady
    AuthReadyStore-->>App: authReady = true
    App->>App: restoreActiveTheme()

    SessionPersistence->>AuthReadyStore: read authReady
    AuthReadyStore-->>SessionPersistence: authReady = true
    SessionPersistence->>SessionPersistence: fetch dock/wallpaper/settings
    SessionPersistence->>SessionPersistence: set dockRestored/wallpaperRestored (finally)
    SessionPersistence->>SessionPersistence: enable auto-save effects

    User->>LoginGate: Logs out
    LoginGate->>AuthReadyStore: setReady(false)
    AuthReadyStore-->>SessionPersistence: authReady = false
    SessionPersistence->>SessionPersistence: reset restored flags, skip fetch
Loading

Possibly related PRs

  • jaylfc/taOS#891: Both PRs modify desktop/src/App.tsx around the installWebkitRepaintGuards() setup and theme restore wiring.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: persisting appearance settings across sessions.
Linked Issues check ✅ Passed The changes address #1601 by persisting theme and wallpaper preferences and restoring them after logout/login.
Out of Scope Changes check ✅ Passed The added auth-ready flow, persistence guards, and tests all support the session persistence fix and appear in scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/settings-persist-across-login

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.

@gitar-bot

gitar-bot Bot commented Jul 4, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
desktop/src/hooks/use-session-persistence.ts (1)

189-214: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Windows/widgets/browser still need per-domain restore gates. restored.current flips true before those restore requests finish, so their debounced auto-saves can still fire against default state and overwrite persisted session data. Mirror the dock/wallpaper pattern with separate post-restore flags (or otherwise wait for each fetch to complete).

🤖 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 `@desktop/src/hooks/use-session-persistence.ts` around lines 189 - 214, The
browser/widgets session auto-save logic still relies on the shared restored
flag, which can become true before the per-domain restore fetches complete and
cause default-state overwrites. Update the relevant effects in
use-session-persistence to gate auto-saves on separate post-restore refs/flags
for each domain, following the dockRestored and wallpaperRestored pattern.
Ensure the browser/widgets restore flows only enable their debounced save
callbacks after their own fetch promises resolve, so the initial mount cannot
persist stale defaults.
🧹 Nitpick comments (1)
desktop/src/hooks/use-on-auth-ready.ts (1)

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

Consider useEffectEvent instead of the exhaustive-deps suppression.

Since the project is on React 19.2.7, useEffectEvent is available and is the purpose-built replacement for this exact "exclude callback from deps + eslint-disable" pattern — it keeps callback reading the latest closure without needing the ref juggling or lint suppression, and the effect can safely omit it from deps by design rather than via override.

React's own docs on this pattern: "most users just disable the lint rule and exclude the dependency. But that can lead to bugs since the linter can no longer help you keep the dependencies up to date."

This is optional since the current single call site (restoreActiveTheme(), no closured state) has no actual staleness risk, but the file's own comment signals intent to reuse this hook for other session-scoped restores, where closured values are more likely.

♻️ Optional refactor using `useEffectEvent`
-import { useEffect, useRef } from "react";
+import { useEffect, useEffectEvent, useRef } from "react";
 import { useAuthReadyStore } from "`@/stores/auth-ready-store`";

 export function useOnAuthReady(callback: () => void) {
   const ranForThisSession = useRef(false);
   const authReady = useAuthReadyStore((s) => s.ready);
+  const onReady = useEffectEvent(callback);

   useEffect(() => {
     if (!authReady) {
       ranForThisSession.current = false;
       return;
     }
     if (ranForThisSession.current) return;
     ranForThisSession.current = true;
-    callback();
-    // eslint-disable-next-line react-hooks/exhaustive-deps
+    onReady();
   }, [authReady]);
 }

Please confirm the repo's eslint-plugin-react-hooks version supports useEffectEvent semantics (v6.1.1+ per React docs) before adopting, since an outdated plugin may still flag it incorrectly.

🤖 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 `@desktop/src/hooks/use-on-auth-ready.ts` around lines 15 - 29, The hook
useOnAuthReady currently suppresses react-hooks/exhaustive-deps to exclude
callback from the effect deps, which is brittle and can hide stale closure bugs.
Refactor useOnAuthReady to use React 19’s useEffectEvent for the callback so the
effect can depend only on authReady while still reading the latest callback, and
remove the eslint-disable suppression and ref juggling; confirm the project’s
eslint-plugin-react-hooks version supports useEffectEvent semantics before
making the change.
🤖 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.

Outside diff comments:
In `@desktop/src/hooks/use-session-persistence.ts`:
- Around line 189-214: The browser/widgets session auto-save logic still relies
on the shared restored flag, which can become true before the per-domain restore
fetches complete and cause default-state overwrites. Update the relevant effects
in use-session-persistence to gate auto-saves on separate post-restore
refs/flags for each domain, following the dockRestored and wallpaperRestored
pattern. Ensure the browser/widgets restore flows only enable their debounced
save callbacks after their own fetch promises resolve, so the initial mount
cannot persist stale defaults.

---

Nitpick comments:
In `@desktop/src/hooks/use-on-auth-ready.ts`:
- Around line 15-29: The hook useOnAuthReady currently suppresses
react-hooks/exhaustive-deps to exclude callback from the effect deps, which is
brittle and can hide stale closure bugs. Refactor useOnAuthReady to use React
19’s useEffectEvent for the callback so the effect can depend only on authReady
while still reading the latest callback, and remove the eslint-disable
suppression and ref juggling; confirm the project’s eslint-plugin-react-hooks
version supports useEffectEvent semantics before making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d853ebd-3a6f-44d6-815c-f976097d58e5

📥 Commits

Reviewing files that changed from the base of the PR and between 7db32e4 and 8c19a46.

📒 Files selected for processing (8)
  • desktop/src/App.tsx
  • desktop/src/components/LoginGate.tsx
  • desktop/src/hooks/__tests__/use-on-auth-ready.test.ts
  • desktop/src/hooks/__tests__/use-session-persistence.test.ts
  • desktop/src/hooks/use-on-auth-ready.ts
  • desktop/src/hooks/use-session-persistence.ts
  • desktop/src/stores/auth-ready-store.ts
  • tests/test_desktop_settings.py

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

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant