From 189dcdc146dc816450025911c804174c7b20a779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=AD=20Adriano?= Date: Mon, 11 May 2026 18:08:54 -0300 Subject: [PATCH 01/12] feat: add local settings Zustand store --- src/stores/local-settings-store.ts | 52 ++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/stores/local-settings-store.ts diff --git a/src/stores/local-settings-store.ts b/src/stores/local-settings-store.ts new file mode 100644 index 00000000..8e1feb1b --- /dev/null +++ b/src/stores/local-settings-store.ts @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +type LocalSettingsState = { + cloudUrl: string + debugPosthog: boolean + isNativeFetchEnabled: boolean + hapticsEnabled: boolean + syncEnabled: boolean + theme: 'light' | 'dark' | 'system' + lastActiveWorkspaceId: string | null +} + +type LocalSettingsActions = { + setLocalSetting: (key: K, value: LocalSettingsState[K]) => void +} + +type LocalSettingsStore = LocalSettingsState & LocalSettingsActions + +export const initialLocalSettings: LocalSettingsState = { + cloudUrl: import.meta.env.VITE_THUNDERBOLT_CLOUD_URL || 'http://localhost:8000/v1', + debugPosthog: false, + isNativeFetchEnabled: false, + hapticsEnabled: true, + syncEnabled: false, + theme: 'system', + lastActiveWorkspaceId: null, +} + +export const useLocalSettingsStore = create()( + persist( + (set) => ({ + ...initialLocalSettings, + setLocalSetting: (key, value) => set({ [key]: value }), + }), + { + name: 'thunderbolt-local-settings', + partialize: ({ setLocalSetting: _, ...settings }) => settings, + }, + ), +) + +/** + * Type-safe synchronous read for non-React consumers. + * Return type narrows per key (e.g. `getLocalSetting('cloudUrl')` → `string`). + */ +export const getLocalSetting = (key: K): LocalSettingsState[K] => + useLocalSettingsStore.getState()[key] From 7e78e8798e6bd4833b36043188f00d911fc54d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=AD=20Adriano?= Date: Mon, 11 May 2026 18:17:14 -0300 Subject: [PATCH 02/12] refactor: migrate async consumers to local settings store --- src/ai/eval/debug-single.ts | 6 ++---- src/ai/eval/runner.ts | 4 ++-- src/ai/fetch.ts | 4 ++-- src/db/powersync/database.ts | 5 ++--- src/hooks/use-app-initialization.ts | 8 ++++---- src/lib/fetch.ts | 8 ++------ src/lib/posthog.tsx | 7 ++++--- 7 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/ai/eval/debug-single.ts b/src/ai/eval/debug-single.ts index 270ceabc..d114f126 100644 --- a/src/ai/eval/debug-single.ts +++ b/src/ai/eval/debug-single.ts @@ -7,9 +7,8 @@ * Usage: bun run src/ai/eval/debug-single.ts */ import { aiFetchStreamingResponse } from '@/ai/fetch' -import { getSettings } from '@/dal' import { setupTestDatabase, teardownTestDatabase } from '@/dal/test-utils' -import { getDb } from '@/db/database' +import { getLocalSetting } from '@/stores/local-settings-store' import { defaultModelGptOss120b } from '@/defaults/models' import { defaultModeChat } from '@/defaults/modes' import { isSsoMode } from '@/lib/auth-mode' @@ -40,8 +39,7 @@ const run = async () => { console.log(`[2/5] Mode: ${defaultModeChat.name}`) console.log(`[2/5] Prompt: "${prompt}"\n`) - const db = getDb() - const { cloudUrl } = await getSettings(db, { cloud_url: 'http://localhost:8000/v1' }) + const cloudUrl = getLocalSetting('cloudUrl') const httpClient = createAuthenticatedClient(cloudUrl, getAuthToken, { credentials: isSsoMode() ? 'include' : undefined, }) diff --git a/src/ai/eval/runner.ts b/src/ai/eval/runner.ts index 29d52ae4..d9fc2f62 100644 --- a/src/ai/eval/runner.ts +++ b/src/ai/eval/runner.ts @@ -8,6 +8,7 @@ import { getSettings } from '@/dal' import { getModel } from '@/dal/models' import { getModelProfile } from '@/dal/model-profiles' import { getDb } from '@/db/database' +import { getLocalSetting } from '@/stores/local-settings-store' import { isSsoMode } from '@/lib/auth-mode' import { getAuthToken } from '@/lib/auth-token' import { createAuthenticatedClient } from '@/lib/http' @@ -24,8 +25,7 @@ let _evalHttpClientPromise: Promise | null = nu const getEvalHttpClient = () => { if (!_evalHttpClientPromise) { _evalHttpClientPromise = (async () => { - const db = getDb() - const { cloudUrl } = await getSettings(db, { cloud_url: 'http://localhost:8000/v1' }) + const cloudUrl = getLocalSetting('cloudUrl') return createAuthenticatedClient(cloudUrl, getAuthToken, { credentials: isSsoMode() ? 'include' : undefined, }) diff --git a/src/ai/fetch.ts b/src/ai/fetch.ts index 12f008a6..d263446b 100644 --- a/src/ai/fetch.ts +++ b/src/ai/fetch.ts @@ -14,6 +14,7 @@ import { } from '@/ai/step-logic' import { getModel, getModelProfile, getSettings } from '@/dal' import { getDb } from '@/db/database' +import { getLocalSetting } from '@/stores/local-settings-store' import { isSsoMode } from '@/lib/auth-mode' import { getAuthToken } from '@/lib/auth-token' import { fetch as baseFetch } from '@/lib/fetch' @@ -72,8 +73,7 @@ type AiFetchStreamingResponseOptions = { export const createModel = async (modelConfig: Model) => { switch (modelConfig.provider) { case 'thunderbolt': { - const db = getDb() - const { cloudUrl } = await getSettings(db, { cloud_url: 'http://localhost:8000/v1' }) + const cloudUrl = getLocalSetting('cloudUrl') const token = getAuthToken() || 'thunderbolt' // SSO web flow authenticates via session cookies — the SSO callback is a // browser redirect, not an XHR, so `set-auth-token` never reaches the diff --git a/src/db/powersync/database.ts b/src/db/powersync/database.ts index 303ed297..267fccba 100644 --- a/src/db/powersync/database.ts +++ b/src/db/powersync/database.ts @@ -3,8 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { fetchConfig } from '@/api/config' -import { getSettings } from '@/dal' -import { defaultSettingCloudUrl } from '@/defaults/settings' +import { getLocalSetting } from '@/stores/local-settings-store' import { withTimeout } from '@/lib/timeout' import type { AbstractPowerSyncDatabase } from '@powersync/common' import { SyncStreamConnectionMethod, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web' @@ -245,7 +244,7 @@ export class PowerSyncDatabaseImpl implements DatabaseInterface { } try { - const { cloudUrl } = await getSettings(this._db, { cloud_url: defaultSettingCloudUrl.value }) + const cloudUrl = getLocalSetting('cloudUrl') // Re-fetch config before connecting so the upload encoder has the correct E2EE flag. // If /config failed at app init (e.g. offline), this retries before sync starts. diff --git a/src/hooks/use-app-initialization.ts b/src/hooks/use-app-initialization.ts index 1e9640f0..30c2ffe3 100644 --- a/src/hooks/use-app-initialization.ts +++ b/src/hooks/use-app-initialization.ts @@ -8,7 +8,7 @@ import { getSettings } from '@/dal' import { getAuthToken } from '@/lib/auth-token' import { Database, getCurrentDatabase, setDatabase } from '@/db/database' import type { AnyDrizzleDatabase } from '@/db/database-interface' -import { defaultSettingCloudUrl } from '@/defaults/settings' +import { getLocalSetting } from '@/stores/local-settings-store' import { createHandleError } from '@/lib/error-utils' import { createAppDir, resetAppDir } from '@/lib/fs' import { isSsoMode } from '@/lib/auth-mode' @@ -59,7 +59,7 @@ const initializePostHog = async (httpClient?: HttpClient): Promise> => { // Step 0: Fetch backend config and hydrate store (only on success). // When fetch fails (offline/error), the store retains its persisted localStorage value. - await fetchConfig(defaultSettingCloudUrl.value!, httpClient) + await fetchConfig(getLocalSetting('cloudUrl'), httpClient) // Step 1: App directory creation let appDirPath: string @@ -115,8 +115,8 @@ const executeInitializationSteps = async (httpClient?: HttpClient): Promise { */ export const initPosthog = async (httpClient?: HttpClient): Promise> => { try { + const cloudUrl = getLocalSetting('cloudUrl') + const debugPosthog = getLocalSetting('debugPosthog') const db = getDb() - const { cloudUrl, dataCollection, debugPosthog } = await getSettings(db, { - cloud_url: 'http://localhost:8000/v1', + const { dataCollection } = await getSettings(db, { data_collection: true, - debug_posthog: false, }) const client = httpClient ?? createClient({ prefixUrl: cloudUrl }) From 5c7a0260eebb2b7aec8f0ea3fbc4c4a4d9198544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=AD=20Adriano?= Date: Mon, 11 May 2026 18:20:54 -0300 Subject: [PATCH 03/12] refactor: migrate syncEnabled to local settings store --- src/db/powersync/database.ts | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/db/powersync/database.ts b/src/db/powersync/database.ts index 267fccba..c279a0ea 100644 --- a/src/db/powersync/database.ts +++ b/src/db/powersync/database.ts @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { fetchConfig } from '@/api/config' -import { getLocalSetting } from '@/stores/local-settings-store' +import { getLocalSetting, useLocalSettingsStore } from '@/stores/local-settings-store' import { withTimeout } from '@/lib/timeout' import type { AbstractPowerSyncDatabase } from '@powersync/common' import { SyncStreamConnectionMethod, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web' @@ -44,9 +44,6 @@ export const getPowerSyncDatabaseConfig = ( return 'safari-tauri' } -/** LocalStorage key for sync enabled flag */ -const syncEnabledKey = 'powersync_sync_enabled' - /** Max time to wait for initial sync before continuing (e.g. when network is down) */ const initialSyncTimeoutMs = 10_000 @@ -89,23 +86,14 @@ export const reconnectSync = async (): Promise => { /** * Check if sync is enabled by user preference */ -export const isSyncEnabled = (): boolean => { - if (typeof localStorage === 'undefined') { - return false - } - return localStorage.getItem(syncEnabledKey) === 'true' -} +export const isSyncEnabled = (): boolean => getLocalSetting('syncEnabled') /** * Set sync enabled preference, connect/disconnect from PowerSync, and dispatch change event */ export const setSyncEnabled = async (enabled: boolean): Promise => { - if (typeof localStorage === 'undefined') { - return - } - - // Update localStorage and dispatch event - localStorage.setItem(syncEnabledKey, String(enabled)) + // Update store and dispatch event + useLocalSettingsStore.getState().setLocalSetting('syncEnabled', enabled) window.dispatchEvent(new CustomEvent(syncEnabledChangeEvent, { detail: enabled })) // Connect or disconnect from PowerSync Cloud From 99ff54fcccae9f1abdb0dbaa38411d73442470dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=AD=20Adriano?= Date: Mon, 11 May 2026 18:36:43 -0300 Subject: [PATCH 04/12] refactor: migrate React consumers to local settings store --- src/components/chat/citation-badge.tsx | 8 +++--- src/components/chat/citation-popover.tsx | 8 +++--- src/components/chat/tool-icon.tsx | 8 +++--- src/components/sign-in/sign-in-form.tsx | 6 ++-- src/components/sso-redirect.tsx | 12 +++----- src/hooks/use-haptics.tsx | 17 ++++++----- src/settings/dev-settings.tsx | 36 +++++++++++++----------- src/settings/preferences.tsx | 12 ++++---- src/widgets/link-preview/widget.tsx | 20 ++++++------- 9 files changed, 63 insertions(+), 64 deletions(-) diff --git a/src/components/chat/citation-badge.tsx b/src/components/chat/citation-badge.tsx index b6d7f142..faa3c7c1 100644 --- a/src/components/chat/citation-badge.tsx +++ b/src/components/chat/citation-badge.tsx @@ -5,7 +5,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet' import { useIsMobile } from '@/hooks/use-mobile' -import { useSettings } from '@/hooks/use-settings' +import { useLocalSettingsStore } from '@/stores/local-settings-store' import type { CitationSource } from '@/types/citation' import { memo, useState } from 'react' import { useCitationPopover } from './citation-popover' @@ -91,7 +91,7 @@ ManagedBadge.displayName = 'ManagedBadge' const StandaloneBadge = memo(({ sources }: { sources: CitationSource[] }) => { const [isOpen, setIsOpen] = useState(false) const { isMobile } = useIsMobile() - const { cloudUrl } = useSettings({ cloud_url: 'http://localhost:8000/v1' }) + const { cloudUrl } = useLocalSettingsStore() const { displayName, additionalCount, ariaLabel } = getBadgeLabel(sources) const badge = ( @@ -118,7 +118,7 @@ const StandaloneBadge = memo(({ sources }: { sources: CitationSource[] }) => { {badge} - + ) @@ -137,7 +137,7 @@ const StandaloneBadge = memo(({ sources }: { sources: CitationSource[] }) => { {sources.length === 1 ? 'Source' : 'Sources'} - + diff --git a/src/components/chat/citation-popover.tsx b/src/components/chat/citation-popover.tsx index 259c3980..ec3fde52 100644 --- a/src/components/chat/citation-popover.tsx +++ b/src/components/chat/citation-popover.tsx @@ -5,7 +5,7 @@ import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover' import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet' import { useIsMobile } from '@/hooks/use-mobile' -import { useSettings } from '@/hooks/use-settings' +import { useLocalSettingsStore } from '@/stores/local-settings-store' import type { CitationSource } from '@/types/citation' import { createContext, @@ -64,7 +64,7 @@ export const CitationPopoverProvider = ({ children }: { children: ReactNode }) = const CitationOverlay = memo(({ popover, close }: { popover: PopoverData | null; close: () => void }) => { const { isMobile } = useIsMobile() - const { cloudUrl } = useSettings({ cloud_url: 'http://localhost:8000/v1' }) + const { cloudUrl } = useLocalSettingsStore() const anchorRef = useRef(null) useEffect(() => { @@ -105,7 +105,7 @@ const CitationOverlay = memo(({ popover, close }: { popover: PopoverData | null; {sources.length === 1 ? 'Source' : 'Sources'} - + ) @@ -124,7 +124,7 @@ const CitationOverlay = memo(({ popover, close }: { popover: PopoverData | null; /> - + ) diff --git a/src/components/chat/tool-icon.tsx b/src/components/chat/tool-icon.tsx index b14e6c99..7ffb2af3 100644 --- a/src/components/chat/tool-icon.tsx +++ b/src/components/chat/tool-icon.tsx @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { useSettings } from '@/hooks/use-settings' +import { useLocalSettingsStore } from '@/stores/local-settings-store' import { getProxiedFaviconUrl } from '@/lib/url-utils' import { cn } from '@/lib/utils' import { motion } from 'framer-motion' @@ -44,13 +44,13 @@ export const extractFaviconUrl = (toolName: string, output: unknown): string | n */ const useToolFavicon = (toolName: string, toolOutput: unknown, isLoading: boolean, isError: boolean) => { const [failedFavicons, setFailedFavicons] = useState>(new Set()) - const { cloudUrl } = useSettings({ cloud_url: 'http://localhost:8000/v1' }) + const { cloudUrl } = useLocalSettingsStore() const handleFaviconError = (url: string) => { setFailedFavicons((prev) => new Set(prev).add(url)) } - if (!toolOutput || isLoading || isError || !cloudUrl.value) { + if (!toolOutput || isLoading || isError) { return { favicon: null, originalFaviconUrl: null, handleFaviconError } } @@ -60,7 +60,7 @@ const useToolFavicon = (toolName: string, toolOutput: unknown, isLoading: boolea return { favicon: null, originalFaviconUrl, handleFaviconError } } - const favicon = getProxiedFaviconUrl(originalFaviconUrl, cloudUrl.value) + const favicon = getProxiedFaviconUrl(originalFaviconUrl, cloudUrl) return { favicon, originalFaviconUrl, handleFaviconError } } catch { return { favicon: null, originalFaviconUrl: null, handleFaviconError } diff --git a/src/components/sign-in/sign-in-form.tsx b/src/components/sign-in/sign-in-form.tsx index 58f5b1de..9c6b3733 100644 --- a/src/components/sign-in/sign-in-form.tsx +++ b/src/components/sign-in/sign-in-form.tsx @@ -4,6 +4,7 @@ import { useAuth, useHttpClient } from '@/contexts' import { useSettings } from '@/hooks/use-settings' +import { useLocalSettingsStore } from '@/stores/local-settings-store' import { isLocalhostUrl } from '@/lib/utils' import { type ReactNode, type RefObject, useCallback, useEffect } from 'react' import { SignInEmailStep } from './sign-in-email-step' @@ -78,8 +79,9 @@ export const SignInForm = ({ }: SignInFormProps) => { const authClient = useAuth() const httpClient = useHttpClient() - const { cloudUrl, preferredName } = useSettings({ cloud_url: 'http://localhost:8000/v1', preferred_name: '' }) - const isLocalhost = isLocalhostUrl(cloudUrl.value) + const { cloudUrl } = useLocalSettingsStore() + const { preferredName } = useSettings({ preferred_name: '' }) + const isLocalhost = isLocalhostUrl(cloudUrl) const displayName = preferredName.value as string const { state, isValidEmail, actions } = useSignInFormState({ diff --git a/src/components/sso-redirect.tsx b/src/components/sso-redirect.tsx index 7ab9277f..56b1f3d5 100644 --- a/src/components/sso-redirect.tsx +++ b/src/components/sso-redirect.tsx @@ -8,7 +8,7 @@ import { setAuthToken } from '@/lib/auth-token' import { isSafeUrl } from '@/lib/url-utils' import { isTauri } from '@/lib/platform' import { startSsoFlowLoopback } from '@/lib/sso-loopback' -import { useSettings } from '@/hooks/use-settings' +import { useLocalSettingsStore } from '@/stores/local-settings-store' import Loading from '@/loading' /** @@ -19,18 +19,14 @@ import Loading from '@/loading' * of navigating the webview (WKWebView drops cookies during cross-origin redirects). */ const SsoRedirect = () => { - const { cloudUrl } = useSettings({ cloud_url: String }) + const { cloudUrl } = useLocalSettingsStore() const [error, setError] = useState(false) const [retryKey, setRetryKey] = useState(0) useEffect(() => { - if (cloudUrl.isLoading || !cloudUrl.value) { - return - } - setError(false) const abortController = new AbortController() - const baseUrl = cloudUrl.value.replace(/\/v1$/, '') + const baseUrl = cloudUrl.replace(/\/v1$/, '') const redirectToSso = async () => { try { @@ -74,7 +70,7 @@ const SsoRedirect = () => { redirectToSso() return () => abortController.abort() - }, [cloudUrl.isLoading, cloudUrl.value, retryKey]) + }, [cloudUrl, retryKey]) if (error) { return ( diff --git a/src/hooks/use-haptics.tsx b/src/hooks/use-haptics.tsx index 5f72e6e2..3e03e345 100644 --- a/src/hooks/use-haptics.tsx +++ b/src/hooks/use-haptics.tsx @@ -4,7 +4,7 @@ import { createContext, type ReactNode, useCallback, useContext } from 'react' import { useWebHaptics } from 'web-haptics/react' -import { useSettings } from './use-settings' +import { useLocalSettingsStore } from '@/stores/local-settings-store' import { triggerImpact, triggerNotification, @@ -34,12 +34,11 @@ const HapticsContext = createContext({ * a provider get silent no-ops, keeping them usable as dumb components. */ export const HapticsProvider = ({ children }: { children: ReactNode }) => { - const { hapticsEnabled } = useSettings({ haptics_enabled: true }) + const { hapticsEnabled } = useLocalSettingsStore() const { trigger } = useWebHaptics({ debug: import.meta.env.DEV }) - const enabled = hapticsEnabled.value === true const triggerSelectionHaptic = useCallback(() => { - if (!enabled) { + if (!hapticsEnabled) { return } if (isTauri() && isMobile()) { @@ -47,11 +46,11 @@ export const HapticsProvider = ({ children }: { children: ReactNode }) => { } else { void trigger('selection') } - }, [enabled, trigger]) + }, [hapticsEnabled, trigger]) const triggerImpactHaptic = useCallback( (style: ImpactFeedbackStyle = 'light') => { - if (!enabled) { + if (!hapticsEnabled) { return } if (isTauri() && isMobile()) { @@ -60,12 +59,12 @@ export const HapticsProvider = ({ children }: { children: ReactNode }) => { void trigger(style) } }, - [enabled, trigger], + [hapticsEnabled, trigger], ) const triggerNotificationHaptic = useCallback( (type: NotificationFeedbackType) => { - if (!enabled) { + if (!hapticsEnabled) { return } if (isTauri() && isMobile()) { @@ -74,7 +73,7 @@ export const HapticsProvider = ({ children }: { children: ReactNode }) => { void trigger(type) } }, - [enabled, trigger], + [hapticsEnabled, trigger], ) return ( diff --git a/src/settings/dev-settings.tsx b/src/settings/dev-settings.tsx index 0876b06d..1e2de698 100644 --- a/src/settings/dev-settings.tsx +++ b/src/settings/dev-settings.tsx @@ -8,16 +8,18 @@ import { PageHeader } from '@/components/ui/page-header' import { SectionCard } from '@/components/ui/section-card' import { Switch } from '@/components/ui/switch' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import { useSettings } from '@/hooks/use-settings' +import { initialLocalSettings, useLocalSettingsStore } from '@/stores/local-settings-store' import { getCapabilities, isTauri } from '@/lib/platform' import { useQuery } from '@tanstack/react-query' export default function DevSettingsPage() { - const { cloudUrl, isNativeFetchEnabled, debugPosthog } = useSettings({ - cloud_url: '', - is_native_fetch_enabled: false, - debug_posthog: false, - }) + const { cloudUrl, isNativeFetchEnabled, debugPosthog, setLocalSetting } = useLocalSettingsStore() + + const isModified = (key: K) => + useLocalSettingsStore.getState()[key] !== initialLocalSettings[key] + + const resetSetting = (key: K) => + setLocalSetting(key, initialLocalSettings[key]) const { data: capabilities } = useQuery({ queryKey: ['capabilities'], @@ -36,15 +38,15 @@ export default function DevSettingsPage() { resetSetting('cloudUrl')} > Cloud URL cloudUrl.setValue(e.target.value || null)} + value={cloudUrl} + onChange={(e) => setLocalSetting('cloudUrl', e.target.value || initialLocalSettings.cloudUrl)} placeholder="http://localhost:8000" />

The URL of the Thunderbolt backend

@@ -58,8 +60,8 @@ export default function DevSettingsPage() { resetSetting('isNativeFetchEnabled')} > Use Native Fetch @@ -69,8 +71,8 @@ export default function DevSettingsPage() { setLocalSetting('isNativeFetchEnabled', value)} disabled={!capabilities?.native_fetch} /> @@ -92,14 +94,14 @@ export default function DevSettingsPage() { resetSetting('debugPosthog')} > Debug PostHog

Enable verbose analytics logging in the console

- + setLocalSetting('debugPosthog', value)} /> diff --git a/src/settings/preferences.tsx b/src/settings/preferences.tsx index 0feed252..cec31461 100644 --- a/src/settings/preferences.tsx +++ b/src/settings/preferences.tsx @@ -7,6 +7,7 @@ import { useSignInModal } from '@/contexts/sign-in-modal-context' import { useCountryUnits } from '@/hooks/use-country-units' import type { LocationData } from '@/hooks/use-location-search' import { useSettings } from '@/hooks/use-settings' +import { initialLocalSettings, useLocalSettingsStore } from '@/stores/local-settings-store' import { useUnitsOptions } from '@/hooks/use-units-options' import { privacyPolicyUrl } from '@/lib/constants' import { extractCountryFromLocation } from '@/lib/country-utils' @@ -112,7 +113,6 @@ export default function PreferencesSettingsPage() { locationLng, dataCollection, experimentalFeatureTasks, - hapticsEnabled, distanceUnit, temperatureUnit, dateFormat, @@ -125,15 +125,15 @@ export default function PreferencesSettingsPage() { location_lng: '', data_collection: true, experimental_feature_tasks: false, - haptics_enabled: true, distance_unit: 'imperial', temperature_unit: 'f', date_format: 'MM/DD/YYYY', time_format: '12h', currency: 'USD', - cloud_url: 'http://localhost:8000/v1', }) + const { hapticsEnabled, setLocalSetting } = useLocalSettingsStore() + // Local state for name input (only save on blur to avoid DB writes on every keystroke) const [nameInput, setNameInput] = useState('') const prevPreferredNameRef = useRef(preferredName.value) @@ -362,14 +362,14 @@ export default function PreferencesSettingsPage() { setLocalSetting('hapticsEnabled', initialLocalSettings.hapticsEnabled)} > Haptic Feedback

Vibrate on tap

- hapticsEnabled.setValue(value)} /> + setLocalSetting('hapticsEnabled', value)} /> diff --git a/src/widgets/link-preview/widget.tsx b/src/widgets/link-preview/widget.tsx index 870076e3..239bb400 100644 --- a/src/widgets/link-preview/widget.tsx +++ b/src/widgets/link-preview/widget.tsx @@ -6,7 +6,7 @@ import { Card, CardHeader } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { useHttpClient } from '@/contexts' import { useMessageCache } from '@/hooks/use-message-cache' -import { useSettings } from '@/hooks/use-settings' +import { useLocalSettingsStore } from '@/stores/local-settings-store' import { fetchLinkPreview } from '@/integrations/thunderbolt-pro/api' import type { SourceMetadata } from '@/types/source' import { LinkPreview } from './display' @@ -47,23 +47,23 @@ export const LinkPreviewSkeleton = () => { } /** Builds a proxied image URL via /proxy-image (when direct image URL is known) */ -const buildProxyImageUrl = (imageUrl: string | null | undefined, cloudUrl: string | null): string | null => { - if (!imageUrl || !cloudUrl?.trim()) { +const buildProxyImageUrl = (imageUrl: string | null | undefined, cloudUrl: string): string | null => { + if (!imageUrl) { return null } return `${cloudUrl}/pro/link-preview/proxy-image/${encodeURIComponent(imageUrl)}` } /** Builds an image URL via /image (extracts og:image from page and proxies it in one request) */ -const buildPageImageUrl = (pageUrl: string, cloudUrl: string | null): string | null => { - if (!pageUrl || !cloudUrl?.trim()) { +const buildPageImageUrl = (pageUrl: string, cloudUrl: string): string | null => { + if (!pageUrl) { return null } return `${cloudUrl}/pro/link-preview/image/${encodeURIComponent(pageUrl)}` } /** Renders a link preview instantly from source registry metadata */ -const InstantLinkPreview = ({ sourceData, cloudUrl }: { sourceData: SourceMetadata; cloudUrl: string | null }) => { +const InstantLinkPreview = ({ sourceData, cloudUrl }: { sourceData: SourceMetadata; cloudUrl: string }) => { return ( { - const { cloudUrl } = useSettings({ cloud_url: 'http://localhost:8000/v1' }) + const { cloudUrl } = useLocalSettingsStore() // Instant render path: resolve from source registry (O(1) index lookup) if (source && sources) { const sourceIndex = parseInt(source, 10) const sourceData = sources[sourceIndex - 1] if (sourceData && sourceData.title) { - return + return } } // Fallback: existing fetch-based path - return + return } /** Fallback component that fetches link preview data via the message cache */ @@ -99,7 +99,7 @@ const FetchLinkPreview = ({ }: { url: string messageId: string - cloudUrl: string | null + cloudUrl: string fetchPreviewFn?: (params: { url: string }) => Promise<{ title: string | null description: string | null From 4cfc1d8a6409d9b61b760b47daa29335e096b2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=AD=20Adriano?= Date: Mon, 11 May 2026 18:40:36 -0300 Subject: [PATCH 05/12] refactor: migrate theme to local settings store --- src/app.tsx | 2 +- src/lib/theme-provider.tsx | 32 +++++++------------------------- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index fb48daad..150a6102 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -252,7 +252,7 @@ export const App = () => { } return ( - + {renderAppContent()} diff --git a/src/lib/theme-provider.tsx b/src/lib/theme-provider.tsx index 6707b918..8c06e1dc 100644 --- a/src/lib/theme-provider.tsx +++ b/src/lib/theme-provider.tsx @@ -2,8 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { useLocalSettingsStore } from '@/stores/local-settings-store' import { invoke } from '@tauri-apps/api/core' -import { createContext, useContext, useEffect, useState, type ReactNode } from 'react' +import { createContext, useCallback, useContext, useEffect, type ReactNode } from 'react' import { isTauri } from './platform' import { setAndroidBarColor } from './set-android-bar-color' @@ -33,12 +34,6 @@ const persistThemeToNativeStore = async (theme: string) => { type Theme = 'dark' | 'light' | 'system' -type ThemeProviderProps = { - children: ReactNode - defaultTheme?: Theme - storageKey?: string -} - type ThemeProviderState = { theme: Theme setTheme: (theme: Theme) => void @@ -51,23 +46,14 @@ const initialState: ThemeProviderState = { const ThemeProviderContext = createContext(initialState) -const isValidTheme = (value: string | null): value is Theme => - value === 'dark' || value === 'light' || value === 'system' +export const ThemeProvider = ({ children }: { children: ReactNode }) => { + const { theme, setLocalSetting } = useLocalSettingsStore() -export const ThemeProvider = ({ - children, - defaultTheme = 'system', - storageKey = 'ui_theme', - ...props -}: ThemeProviderProps) => { - const savedTheme = window.localStorage.getItem(storageKey) - - const [theme, setTheme] = useState(isValidTheme(savedTheme) ? savedTheme : defaultTheme) + const setTheme = useCallback((newTheme: Theme) => setLocalSetting('theme', newTheme), [setLocalSetting]) useEffect(() => { - window.localStorage.setItem(storageKey, theme) persistThemeToNativeStore(theme).catch(() => {}) - }, [storageKey, theme]) + }, [theme]) useEffect(() => { const root = window.document.documentElement @@ -130,11 +116,7 @@ export const ThemeProvider = ({ setTheme, } - return ( - - {children} - - ) + return {children} } export const useTheme = () => { From bb5c26412c54e1ea79185f0fca35858cb4e15af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=AD=20Adriano?= Date: Mon, 11 May 2026 18:42:13 -0300 Subject: [PATCH 06/12] refactor: remove migrated settings from DB defaults --- src/defaults/settings.ts | 45 ---------------------------------------- 1 file changed, 45 deletions(-) diff --git a/src/defaults/settings.ts b/src/defaults/settings.ts index ef850266..f462bc08 100644 --- a/src/defaults/settings.ts +++ b/src/defaults/settings.ts @@ -50,22 +50,6 @@ export const defaultSettingExperimentalFeatureTasks: Setting = { userId: null, } -export const defaultSettingNativeFetchEnabled: Setting = { - key: 'is_native_fetch_enabled', - value: 'false', - updatedAt: null, - defaultHash: null, - userId: null, -} - -export const defaultSettingDebugPosthog: Setting = { - key: 'debug_posthog', - value: 'false', - updatedAt: null, - defaultHash: null, - userId: null, -} - export const defaultSettingPreferredName: Setting = { key: 'preferred_name', value: null, @@ -98,22 +82,6 @@ export const defaultSettingLocationLng: Setting = { userId: null, } -export const defaultSettingCloudUrl: Setting = { - key: 'cloud_url', - value: import.meta.env.VITE_THUNDERBOLT_CLOUD_URL || 'http://localhost:8000/v1', - updatedAt: null, - defaultHash: null, - userId: null, -} - -export const defaultSettingTheme: Setting = { - key: 'ui-theme', - value: 'system', - updatedAt: null, - defaultHash: null, - userId: null, -} - export const defaultSettingDistanceUnit: Setting = { key: 'distance_unit', value: null, @@ -186,14 +154,6 @@ export const defaultSettingIntegrationsDoNotAskAgain: Setting = { userId: null, } -export const defaultSettingHapticsEnabled: Setting = { - key: 'haptics_enabled', - value: 'true', - updatedAt: null, - defaultHash: null, - userId: null, -} - /** * Array of all default settings for iteration */ @@ -201,14 +161,10 @@ export const defaultSettings: ReadonlyArray = [ defaultSettingDataCollection, defaultSettingTriggersEnabled, defaultSettingExperimentalFeatureTasks, - defaultSettingNativeFetchEnabled, - defaultSettingDebugPosthog, defaultSettingPreferredName, defaultSettingLocationName, defaultSettingLocationLat, defaultSettingLocationLng, - defaultSettingCloudUrl, - defaultSettingTheme, defaultSettingDistanceUnit, defaultSettingTemperatureUnit, defaultSettingDateFormat, @@ -218,5 +174,4 @@ export const defaultSettings: ReadonlyArray = [ defaultSettingUserHasCompletedOnboarding, defaultSettingContentViewWidth, defaultSettingIntegrationsDoNotAskAgain, - defaultSettingHapticsEnabled, ] as const From 7983516fb3cc16ed3859c3a42ec6a44e42496705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=AD=20Adriano?= Date: Mon, 11 May 2026 18:47:27 -0300 Subject: [PATCH 07/12] fix: reset local settings on data deletion --- src/lib/cleanup.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/cleanup.ts b/src/lib/cleanup.ts index b9085e73..c9621971 100644 --- a/src/lib/cleanup.ts +++ b/src/lib/cleanup.ts @@ -6,6 +6,7 @@ import { setSyncEnabled } from '@/db/powersync' import { clearAuthToken, clearDeviceId } from '@/lib/auth-token' import { resetAppDir } from '@/lib/fs' import { handleFullWipe } from '@/services/encryption' +import { initialLocalSettings, useLocalSettingsStore } from '@/stores/local-settings-store' type ClearLocalDataOptions = { /** Disable PowerSync sync connection (default: true) */ @@ -49,6 +50,9 @@ export const clearLocalData = async (options?: ClearLocalDataOptions): Promise Date: Wed, 13 May 2026 17:07:59 -0300 Subject: [PATCH 08/12] fix: sync pre-paint theme script with local settings store --- index.html | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index a178cd60..ea0874f0 100644 --- a/index.html +++ b/index.html @@ -21,9 +21,15 @@