Files
Peter Steinberger a4c01c1a77 feat(ui): save appearance preferences per user profile (#130340)
* feat(ui): save appearance preferences per user profile

When a Control UI connection is bound to an authenticated user profile
(trusted-proxy, Tailscale, GitHub identity), theme, theme mode, and accent
become per-profile: stored in the existing user_preferences KV store via
users.prefs.set, overriding gateway-wide ui.prefs, with live cross-device
updates over a new additive users.prefs.changed event scoped to the same
merged profile. Restore default deletes only the profile key and falls back
to the gateway-wide value. talk.config projects the caller's profile accent
for native clients. Profile-less connections (token/password/none auth)
keep the existing gateway-config behavior byte-identically.

Release-note context: on multi-person gateways appearance choices are now
personal and follow you across devices; an admin restyling their own UI no
longer reskins the whole team.

* chore(protocol): allowlist users.prefs.changed for mobile clients

iOS and Android resolve the profile accent through talk.config on connect
and config refresh; live per-profile appearance push on natives is a named
follow-up.

* chore(protocol): regenerate Swift/Kotlin models for users.prefs.changed

* fix(ui): honor explicit defaults and identity switches for profile appearance

Addresses the three ClawSweeper review findings:
- resetValue for profile-bound appearance keys is the deletion fallback
  (gateway value), so an explicit product-default selection persists as a
  profile write instead of being misclassified as a reset.
- An identity switch between two known scopes forces a full appearance
  reconcile and clears appearance keys the returning identity never set,
  so a shared browser cannot keep the previous profile's look (boot keeps
  the last-seen shortcut).
- talk.config joins the profile-dependent dispatch gate so a pending
  GitHub identity sync completes before the profile accent is projected;
  token clients pass through untouched.

* chore(ui): satisfy assertion-safety ratchet and docs formatting

* refactor(ui): extract server-prefs storage primitives

server-prefs.ts crossed the 700-line cap after the review fixes; the
stateless localStorage persistence primitives move to
server-prefs-storage.ts (no behavior change). Shrink-prunes the
assertion-safety baseline for the moved casts.

* fix(ui): keep imported custom themes out of profile storage

Custom palettes are browser-local by contract, so a profile must never
carry theme=custom to a browser that cannot render it. The wire contract
drops custom from the storable theme values (readers self-heal any stored
value), a profile-bound custom selection stays retained browser-local
(including the offline-queued path), and the exhaustive theme test now
encodes the exception.
2026-08-26 16:48:26 -07:00
..