Summary
Allow disabling cloud sync for individual settings (VSCode-style), so a device can keep certain preferences local — e.g. a per-device theme while everything else still syncs.
Per-object sync control already exists for entity types (Hosts, Identities, SSH Keys, Folders, Port Forwarding, Snippets) via src/stores/syncPrefsStore.ts — per-type toggles in Settings → Sync plus per-object exclusions with a cloud-off badge. Settings are not part of that system. They sync through a separate path (src/services/user-data/registry.ts): 4 handler domains — Themes, UI Preferences, Shortcuts, App Settings — bundled into settings.json and merged last-write-wins per domain. Today it's all-or-nothing; there's no way to hold back a single setting.
Approach — VSCode-style hybrid
VSCode Settings Sync has two layers, and we mirror them:
- Per-domain toggles — the everyday control (like VSCode's category on/off).
- Ignored-settings list — an opt-out list of individual setting keys that never sync, even within a synced domain (like
settingsSync.ignoredSettings).
Data model
Extend syncPrefsStore (or a sibling slice):
syncSettingDomains: Record<string, boolean> — per-domain toggles for the 4 handlers (default true).
ignoredSettingKeys: string[] — dotted paths of individual settings to never sync (e.g. themes.active, uiPreferences.uiScale, appSettings.locale).
Settings registry
Each UserDataHandler exposes its addressable leaf keys as { id (dotted path), label }. Flat scalars (UI Preferences, most App Settings — including the toggles map) map 1:1. Collections (custom-themes array, shortcuts array) surface as one or two coarse keys. This registry powers the ignore-list UI.
Filtering — a real "stays on this device" guarantee
Unlike the existing entity toggles (which only gate the push trigger — see note below), settings filtering runs in TS and is authoritative:
buildUserDataBundle — drop any domain whose toggle is off; within included domains, delete ignored leaf keys (generic dotted-path delete on the plain exported JSON) before bundling.
applyUserDataBundle / mergeUserDataBundle — skip off domains; skip ignored keys when applying remote, so the local value is preserved. Per-domain LWW stays as-is for everything else.
- Trigger-gate (nice-to-have): settings setters already call
scheduleSync (uiStore, appSettingsTimestampStore, …); optionally skip when the changed key is ignored/off. The real guarantee is the bundle filter, not this.
UI (Settings → Sync)
- A new "Settings" toggle group (4 domains) reusing the existing sync-preferences toggle-list pattern in
SyncSection.tsx.
- An "Ignored settings" manager: a searchable list of all registered settings with per-key sync / don't-sync checkboxes, mirroring VSCode's configure panel.
Phasing
- Phase 1 — domain toggles + domain-level filtering (small, self-contained).
- Phase 2 — settings registry + ignored-keys filtering + ignore-list UI.
Out of scope
The existing entity per-object toggle only gates the push trigger (scheduleSync); it does not withhold the object from the uploaded blob — backup_export (Rust, src-tauri/src/commands/sync.rs) reads every .json in the config dir wholesale, so an "excluded" host still reaches the server on the next sync that fires for any reason. That's a separate pre-existing quirk tracked in #43; the settings work here is done properly (filtered in TS) and is not affected by it.
Summary
Allow disabling cloud sync for individual settings (VSCode-style), so a device can keep certain preferences local — e.g. a per-device theme while everything else still syncs.
Per-object sync control already exists for entity types (Hosts, Identities, SSH Keys, Folders, Port Forwarding, Snippets) via
src/stores/syncPrefsStore.ts— per-type toggles in Settings → Sync plus per-object exclusions with acloud-offbadge. Settings are not part of that system. They sync through a separate path (src/services/user-data/registry.ts): 4 handler domains — Themes, UI Preferences, Shortcuts, App Settings — bundled intosettings.jsonand merged last-write-wins per domain. Today it's all-or-nothing; there's no way to hold back a single setting.Approach — VSCode-style hybrid
VSCode Settings Sync has two layers, and we mirror them:
settingsSync.ignoredSettings).Data model
Extend
syncPrefsStore(or a sibling slice):syncSettingDomains: Record<string, boolean>— per-domain toggles for the 4 handlers (defaulttrue).ignoredSettingKeys: string[]— dotted paths of individual settings to never sync (e.g.themes.active,uiPreferences.uiScale,appSettings.locale).Settings registry
Each
UserDataHandlerexposes its addressable leaf keys as{ id (dotted path), label }. Flat scalars (UI Preferences, most App Settings — including thetogglesmap) map 1:1. Collections (custom-themes array, shortcuts array) surface as one or two coarse keys. This registry powers the ignore-list UI.Filtering — a real "stays on this device" guarantee
Unlike the existing entity toggles (which only gate the push trigger — see note below), settings filtering runs in TS and is authoritative:
buildUserDataBundle— drop any domain whose toggle is off; within included domains, delete ignored leaf keys (generic dotted-path delete on the plain exported JSON) before bundling.applyUserDataBundle/mergeUserDataBundle— skip off domains; skip ignored keys when applying remote, so the local value is preserved. Per-domain LWW stays as-is for everything else.scheduleSync(uiStore,appSettingsTimestampStore, …); optionally skip when the changed key is ignored/off. The real guarantee is the bundle filter, not this.UI (Settings → Sync)
SyncSection.tsx.Phasing
Out of scope
The existing entity per-object toggle only gates the push trigger (
scheduleSync); it does not withhold the object from the uploaded blob —backup_export(Rust,src-tauri/src/commands/sync.rs) reads every.jsonin the config dir wholesale, so an "excluded" host still reaches the server on the next sync that fires for any reason. That's a separate pre-existing quirk tracked in #43; the settings work here is done properly (filtered in TS) and is not affected by it.