diff --git a/apps/desktop/src-tauri/src/commands.rs b/apps/desktop/src-tauri/src/commands.rs index c41670f7df..681f8b1660 100644 --- a/apps/desktop/src-tauri/src/commands.rs +++ b/apps/desktop/src-tauri/src/commands.rs @@ -17,23 +17,6 @@ pub async fn set_onboarding_needed( app.set_onboarding_needed(v).map_err(|e| e.to_string()) } -#[tauri::command] -#[specta::specta] -pub async fn get_dismissed_toasts( - app: tauri::AppHandle, -) -> Result, String> { - app.get_dismissed_toasts() -} - -#[tauri::command] -#[specta::specta] -pub async fn set_dismissed_toasts( - app: tauri::AppHandle, - v: Vec, -) -> Result<(), String> { - app.set_dismissed_toasts(v) -} - #[tauri::command] #[specta::specta] pub async fn get_onboarding_local( @@ -129,37 +112,3 @@ pub async fn set_tinybase_values( ) -> Result<(), String> { app.set_tinybase_values(v) } - -#[tauri::command] -#[specta::specta] -pub async fn get_pinned_tabs( - app: tauri::AppHandle, -) -> Result, String> { - app.get_pinned_tabs() -} - -#[tauri::command] -#[specta::specta] -pub async fn set_pinned_tabs( - app: tauri::AppHandle, - v: String, -) -> Result<(), String> { - app.set_pinned_tabs(v) -} - -#[tauri::command] -#[specta::specta] -pub async fn get_recently_opened_sessions( - app: tauri::AppHandle, -) -> Result, String> { - app.get_recently_opened_sessions() -} - -#[tauri::command] -#[specta::specta] -pub async fn set_recently_opened_sessions( - app: tauri::AppHandle, - v: String, -) -> Result<(), String> { - app.set_recently_opened_sessions(v) -} diff --git a/apps/desktop/src-tauri/src/ext.rs b/apps/desktop/src-tauri/src/ext.rs index c02012dd11..1897dd3b2b 100644 --- a/apps/desktop/src-tauri/src/ext.rs +++ b/apps/desktop/src-tauri/src/ext.rs @@ -6,20 +6,11 @@ pub trait AppExt { fn get_onboarding_needed(&self) -> Result; fn set_onboarding_needed(&self, v: bool) -> Result<(), String>; - fn get_dismissed_toasts(&self) -> Result, String>; - fn set_dismissed_toasts(&self, v: Vec) -> Result<(), String>; - fn get_onboarding_local(&self) -> Result; fn set_onboarding_local(&self, v: bool) -> Result<(), String>; fn get_tinybase_values(&self) -> Result, String>; fn set_tinybase_values(&self, v: String) -> Result<(), String>; - - fn get_pinned_tabs(&self) -> Result, String>; - fn set_pinned_tabs(&self, v: String) -> Result<(), String>; - - fn get_recently_opened_sessions(&self) -> Result, String>; - fn set_recently_opened_sessions(&self, v: String) -> Result<(), String>; } impl> AppExt for T { @@ -48,24 +39,6 @@ impl> AppExt for T { store.save().map_err(|e| e.to_string()) } - #[tracing::instrument(skip_all)] - fn get_dismissed_toasts(&self) -> Result, String> { - let store = self.desktop_store()?; - store - .get(StoreKey::DismissedToasts) - .map(|opt| opt.unwrap_or_default()) - .map_err(|e| e.to_string()) - } - - #[tracing::instrument(skip_all)] - fn set_dismissed_toasts(&self, v: Vec) -> Result<(), String> { - let store = self.desktop_store()?; - store - .set(StoreKey::DismissedToasts, v) - .map_err(|e| e.to_string())?; - store.save().map_err(|e| e.to_string()) - } - #[tracing::instrument(skip_all)] fn get_onboarding_local(&self) -> Result { let store = self.desktop_store()?; @@ -100,36 +73,4 @@ impl> AppExt for T { .map_err(|e| e.to_string())?; store.save().map_err(|e| e.to_string()) } - - #[tracing::instrument(skip_all)] - fn get_pinned_tabs(&self) -> Result, String> { - let store = self.desktop_store()?; - store.get(StoreKey::PinnedTabs).map_err(|e| e.to_string()) - } - - #[tracing::instrument(skip_all)] - fn set_pinned_tabs(&self, v: String) -> Result<(), String> { - let store = self.desktop_store()?; - store - .set(StoreKey::PinnedTabs, v) - .map_err(|e| e.to_string())?; - store.save().map_err(|e| e.to_string()) - } - - #[tracing::instrument(skip_all)] - fn get_recently_opened_sessions(&self) -> Result, String> { - let store = self.desktop_store()?; - store - .get(StoreKey::RecentlyOpenedSessions) - .map_err(|e| e.to_string()) - } - - #[tracing::instrument(skip_all)] - fn set_recently_opened_sessions(&self, v: String) -> Result<(), String> { - let store = self.desktop_store()?; - store - .set(StoreKey::RecentlyOpenedSessions, v) - .map_err(|e| e.to_string())?; - store.save().map_err(|e| e.to_string()) - } } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 39b218157c..778ac12548 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -328,8 +328,6 @@ fn make_specta_builder() -> tauri_specta::Builder { .commands(tauri_specta::collect_commands![ commands::get_onboarding_needed::, commands::set_onboarding_needed::, - commands::get_dismissed_toasts::, - commands::set_dismissed_toasts::, commands::get_onboarding_local::, commands::set_onboarding_local::, commands::get_env::, @@ -338,10 +336,6 @@ fn make_specta_builder() -> tauri_specta::Builder { commands::resize_window_for_sidebar::, commands::get_tinybase_values::, commands::set_tinybase_values::, - commands::get_pinned_tabs::, - commands::set_pinned_tabs::, - commands::get_recently_opened_sessions::, - commands::set_recently_opened_sessions::, ]) .error_handling(tauri_specta::ErrorHandlingMode::Result) } diff --git a/apps/desktop/src-tauri/src/store.rs b/apps/desktop/src-tauri/src/store.rs index 74cb455d8b..f379779ea3 100644 --- a/apps/desktop/src-tauri/src/store.rs +++ b/apps/desktop/src-tauri/src/store.rs @@ -2,12 +2,11 @@ use tauri_plugin_store2::ScopedStoreKey; #[derive(serde::Deserialize, specta::Type, PartialEq, Eq, Hash, strum::Display)] pub enum StoreKey { + // Also accessed from Rust OnboardingNeeded2, - DismissedToasts, OnboardingLocal, + // For frontend-only values, use TinybaseValues instead TinybaseValues, - PinnedTabs, - RecentlyOpenedSessions, } impl ScopedStoreKey for StoreKey {} diff --git a/apps/desktop/src/components/main/sidebar/toast/useDismissedToasts.ts b/apps/desktop/src/components/main/sidebar/toast/useDismissedToasts.ts index 04c4df9dbb..a65c60ae0a 100644 --- a/apps/desktop/src/components/main/sidebar/toast/useDismissedToasts.ts +++ b/apps/desktop/src/components/main/sidebar/toast/useDismissedToasts.ts @@ -1,25 +1,28 @@ -import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useMemo } from "react"; -import { commands } from "../../../../types/tauri.gen"; +import * as main from "../../../../store/tinybase/store/main"; export function useDismissedToasts(): { dismissedToasts: string[]; dismissToast: (id: string) => void; isDismissed: (id: string) => boolean; } { - const queryClient = useQueryClient(); - - const { data: dismissedToasts = [] } = useQuery({ - queryKey: ["dismissed_toasts"], - queryFn: async () => { - const result = await commands.getDismissedToasts(); - if (result.status === "ok") { - return result.data; + const store = main.UI.useStore(main.STORE_ID); + const dismissedToastsValue = main.UI.useValue("dismissed_toasts", store); + + const dismissedToasts = useMemo(() => { + if (typeof dismissedToastsValue === "string") { + try { + const parsed = JSON.parse(dismissedToastsValue); + if (Array.isArray(parsed)) { + return parsed.filter((id) => typeof id === "string") as string[]; + } + } catch { + return []; } - return []; - }, - }); + } + return []; + }, [dismissedToastsValue]); const dismissedSet = useMemo( () => new Set(dismissedToasts), @@ -28,16 +31,14 @@ export function useDismissedToasts(): { const dismissToast = useCallback( (id: string) => { - if (dismissedSet.has(id)) { + if (!store || dismissedSet.has(id)) { return; } const updated = [...dismissedToasts, id]; - commands.setDismissedToasts(updated).then(() => { - queryClient.invalidateQueries({ queryKey: ["dismissed_toasts"] }); - }); + store.setValue("dismissed_toasts", JSON.stringify(updated)); }, - [dismissedToasts, dismissedSet, queryClient], + [store, dismissedToasts, dismissedSet], ); const isDismissed = useCallback( diff --git a/apps/desktop/src/routes/app/main/_layout.tsx b/apps/desktop/src/routes/app/main/_layout.tsx index 1230a1d341..5fd3d1604b 100644 --- a/apps/desktop/src/routes/app/main/_layout.tsx +++ b/apps/desktop/src/routes/app/main/_layout.tsx @@ -17,9 +17,10 @@ import { ShellProvider } from "../../../contexts/shell"; import { useRegisterTools } from "../../../contexts/tool"; import { ToolRegistryProvider } from "../../../contexts/tool"; import { useDeeplinkHandler } from "../../../hooks/useDeeplinkHandler"; +import * as main from "../../../store/tinybase/store/main"; import { - restorePinnedTabsToStore, - restoreRecentlyOpenedToStore, + usePinnedTabsSync, + useRecentlyOpenedSync, useTabs, } from "../../../store/zustand/tabs"; @@ -37,34 +38,34 @@ function Component() { const liveStatus = useListener((state) => state.live.status); const prevLiveStatus = usePrevious(liveStatus); + const store = main.UI.useStore(main.STORE_ID) as main.Store | undefined; + const pinnedTabsValue = main.UI.useValue("pinned_tabs", store); + useDeeplinkHandler(); + usePinnedTabsSync(); + useRecentlyOpenedSync(); const openDefaultEmptyTab = useCallback(() => { openNew({ type: "empty" }); }, [openNew]); useEffect(() => { - const initializeTabs = async () => { - if (!hasOpenedInitialTab.current) { - hasOpenedInitialTab.current = true; - await restorePinnedTabsToStore( - openNew, - pin, - () => useTabs.getState().tabs, - ); - await restoreRecentlyOpenedToStore((ids) => { - useTabs.setState({ recentlyOpenedSessionIds: ids }); - }); - const currentTabs = useTabs.getState().tabs; - if (currentTabs.length === 0) { - openDefaultEmptyTab(); - } + if (!hasOpenedInitialTab.current) { + // Wait for TinyBase pinned_tabs value to be loaded before checking if we need to open + // an empty tab. The sync hooks (usePinnedTabsSync, useRecentlyOpenedSync) restore tabs + // asynchronously via useEffect. Without this check, we would open an empty tab before + // the pinned tabs are restored, causing a flash of empty content on startup. + if (pinnedTabsValue === undefined) { + return; } - }; - - initializeTabs(); + hasOpenedInitialTab.current = true; + const currentTabs = useTabs.getState().tabs; + if (currentTabs.length === 0) { + openDefaultEmptyTab(); + } + } registerOnEmpty(openDefaultEmptyTab); - }, [openNew, pin, openDefaultEmptyTab, registerOnEmpty]); + }, [pinnedTabsValue, openDefaultEmptyTab, registerOnEmpty]); useEffect(() => { const justStartedListening = diff --git a/apps/desktop/src/store/zustand/tabs/index.ts b/apps/desktop/src/store/zustand/tabs/index.ts index 9980ab598a..badc388eb4 100644 --- a/apps/desktop/src/store/zustand/tabs/index.ts +++ b/apps/desktop/src/store/zustand/tabs/index.ts @@ -14,16 +14,10 @@ import { navigationMiddleware, type NavigationState, } from "./navigation"; -import { - pinnedPersistenceMiddleware, - restorePinnedTabsToStore, -} from "./pinned-persistence"; import { createRecentlyOpenedSlice, type RecentlyOpenedActions, - recentlyOpenedMiddleware, type RecentlyOpenedState, - restoreRecentlyOpenedToStore, } from "./recently-opened"; import { createRestoreSlice, @@ -35,7 +29,8 @@ import { createStateUpdaterSlice, type StateBasicActions } from "./state"; export type { SettingsState, SettingsTab, Tab, TabInput } from "./schema"; export { isSameTab, rowIdfromTab, uniqueIdfromTab } from "./schema"; -export { restorePinnedTabsToStore, restoreRecentlyOpenedToStore }; +export { usePinnedTabsSync } from "./usePinnedTabsSync"; +export { useRecentlyOpenedSync } from "./useRecentlyOpenedSync"; type State = BasicState & NavigationState & @@ -51,29 +46,19 @@ type Actions = BasicActions & type Store = State & Actions; export const useTabs = create()( - recentlyOpenedMiddleware( - pinnedPersistenceMiddleware( - restoreMiddleware( - lifecycleMiddleware( - navigationMiddleware((set, get) => ({ - ...wrapSliceWithLogging("basic", createBasicSlice(set, get)), - ...wrapSliceWithLogging("state", createStateUpdaterSlice(set, get)), - ...wrapSliceWithLogging( - "navigation", - createNavigationSlice(set, get), - ), - ...wrapSliceWithLogging( - "lifecycle", - createLifecycleSlice(set, get), - ), - ...wrapSliceWithLogging("restore", createRestoreSlice(set, get)), - ...wrapSliceWithLogging( - "recentlyOpened", - createRecentlyOpenedSlice(set, get), - ), - })), + restoreMiddleware( + lifecycleMiddleware( + navigationMiddleware((set, get) => ({ + ...wrapSliceWithLogging("basic", createBasicSlice(set, get)), + ...wrapSliceWithLogging("state", createStateUpdaterSlice(set, get)), + ...wrapSliceWithLogging("navigation", createNavigationSlice(set, get)), + ...wrapSliceWithLogging("lifecycle", createLifecycleSlice(set, get)), + ...wrapSliceWithLogging("restore", createRestoreSlice(set, get)), + ...wrapSliceWithLogging( + "recentlyOpened", + createRecentlyOpenedSlice(set, get), ), - ), + })), ), ), ); diff --git a/apps/desktop/src/store/zustand/tabs/pinned-persistence.ts b/apps/desktop/src/store/zustand/tabs/pinned-persistence.ts index 4fb5d074e0..7f4545cfc0 100644 --- a/apps/desktop/src/store/zustand/tabs/pinned-persistence.ts +++ b/apps/desktop/src/store/zustand/tabs/pinned-persistence.ts @@ -1,14 +1,5 @@ -import type { StateCreator, StoreMutatorIdentifier } from "zustand"; - -import { getCurrentWebviewWindowLabel } from "@hypr/plugin-windows"; - -import { commands } from "../../../types/tauri.gen"; -import { - getDefaultState, - type Tab, - type TabInput, - uniqueIdfromTab, -} from "./schema"; +import type { Store } from "../../tinybase/store/main"; +import { type Tab, type TabInput } from "./schema"; export type PinnedTab = TabInput & { pinned: true }; @@ -34,93 +25,15 @@ const deserializePinnedTabs = (data: string): PinnedTab[] => { } }; -export const savePinnedTabs = async (tabs: Tab[]): Promise => { +export const savePinnedTabs = (store: Store, tabs: Tab[]): void => { const serialized = serializePinnedTabs(tabs); - await commands.setPinnedTabs(serialized); + store.setValue("pinned_tabs", serialized); }; -export const loadPinnedTabs = async (): Promise => { - const result = await commands.getPinnedTabs(); - if (result.status === "ok" && result.data) { - return deserializePinnedTabs(result.data); +export const loadPinnedTabs = (store: Store): PinnedTab[] => { + const data = store.getValue("pinned_tabs"); + if (typeof data === "string") { + return deserializePinnedTabs(data); } return []; }; - -type PinnedPersistenceMiddleware = < - T extends { - tabs: Tab[]; - }, - Mps extends [StoreMutatorIdentifier, unknown][] = [], - Mcs extends [StoreMutatorIdentifier, unknown][] = [], ->( - f: StateCreator, -) => StateCreator; - -type PinnedPersistenceMiddlewareImpl = < - T extends { - tabs: Tab[]; - }, ->( - f: StateCreator, -) => StateCreator; - -const getPinnedTabIds = (tabs: Tab[]): string[] => { - return tabs - .filter((t) => t.pinned) - .map(uniqueIdfromTab) - .sort(); -}; - -const pinnedPersistenceMiddlewareImpl: PinnedPersistenceMiddlewareImpl = - (config) => (set, get, api) => { - return config( - (args) => { - const prevState = get(); - const prevPinnedIds = getPinnedTabIds(prevState.tabs); - - set(args); - - const nextState = get(); - const nextPinnedIds = getPinnedTabIds(nextState.tabs); - - const pinnedChanged = - prevPinnedIds.length !== nextPinnedIds.length || - prevPinnedIds.some((id, i) => id !== nextPinnedIds[i]); - - if (pinnedChanged && getCurrentWebviewWindowLabel() === "main") { - savePinnedTabs(nextState.tabs).catch((e) => { - console.error("Failed to save pinned tabs:", e); - }); - } - }, - get, - api, - ); - }; - -export const pinnedPersistenceMiddleware = - pinnedPersistenceMiddlewareImpl as PinnedPersistenceMiddleware; - -export const restorePinnedTabsToStore = async ( - openNew: (tab: TabInput) => void, - pin: (tab: Tab) => void, - getTabs: () => Tab[], -): Promise => { - const pinnedTabs = await loadPinnedTabs(); - - for (const pinnedTab of pinnedTabs) { - const { pinned, ...tabInput } = pinnedTab; - openNew(tabInput); - - const tabs = getTabs(); - const newTab = tabs.find((t) => { - const tabWithDefaults = getDefaultState(tabInput); - return uniqueIdfromTab(t) === uniqueIdfromTab(tabWithDefaults as Tab); - }); - - if (newTab && !newTab.pinned) { - pin(newTab); - } - } -}; diff --git a/apps/desktop/src/store/zustand/tabs/recently-opened.ts b/apps/desktop/src/store/zustand/tabs/recently-opened.ts index d6ff2fa4e3..cd1e5c03f8 100644 --- a/apps/desktop/src/store/zustand/tabs/recently-opened.ts +++ b/apps/desktop/src/store/zustand/tabs/recently-opened.ts @@ -1,8 +1,6 @@ -import type { StateCreator, StoreApi, StoreMutatorIdentifier } from "zustand"; +import type { StoreApi } from "zustand"; -import { getCurrentWebviewWindowLabel } from "@hypr/plugin-windows"; - -import { commands } from "../../../types/tauri.gen"; +import type { Store } from "../../tinybase/store/main"; const MAX_RECENT_SESSIONS = 10; @@ -27,18 +25,19 @@ export const createRecentlyOpenedSlice = ( }, }); -export const saveRecentlyOpenedSessions = async ( +export const saveRecentlyOpenedSessions = ( + store: Store, sessionIds: string[], -): Promise => { +): void => { const serialized = JSON.stringify(sessionIds); - await commands.setRecentlyOpenedSessions(serialized); + store.setValue("recently_opened_sessions", serialized); }; -export const loadRecentlyOpenedSessions = async (): Promise => { - const result = await commands.getRecentlyOpenedSessions(); - if (result.status === "ok" && result.data) { +export const loadRecentlyOpenedSessions = (store: Store): string[] => { + const data = store.getValue("recently_opened_sessions"); + if (typeof data === "string") { try { - const parsed = JSON.parse(result.data); + const parsed = JSON.parse(data); if ( Array.isArray(parsed) && parsed.every((id) => typeof id === "string") @@ -52,58 +51,3 @@ export const loadRecentlyOpenedSessions = async (): Promise => { } return []; }; - -type RecentlyOpenedMiddleware = < - T extends { - recentlyOpenedSessionIds: string[]; - }, - Mps extends [StoreMutatorIdentifier, unknown][] = [], - Mcs extends [StoreMutatorIdentifier, unknown][] = [], ->( - f: StateCreator, -) => StateCreator; - -type RecentlyOpenedMiddlewareImpl = < - T extends { - recentlyOpenedSessionIds: string[]; - }, ->( - f: StateCreator, -) => StateCreator; - -const recentlyOpenedMiddlewareImpl: RecentlyOpenedMiddlewareImpl = - (config) => (set, get, api) => { - return config( - (args) => { - const prevState = get(); - const prevIds = prevState.recentlyOpenedSessionIds; - - set(args); - - const nextState = get(); - const nextIds = nextState.recentlyOpenedSessionIds; - - const idsChanged = - prevIds.length !== nextIds.length || - prevIds.some((id, i) => id !== nextIds[i]); - - if (idsChanged && getCurrentWebviewWindowLabel() === "main") { - saveRecentlyOpenedSessions(nextIds).catch((e) => { - console.error("Failed to save recently opened sessions:", e); - }); - } - }, - get, - api, - ); - }; - -export const recentlyOpenedMiddleware = - recentlyOpenedMiddlewareImpl as RecentlyOpenedMiddleware; - -export const restoreRecentlyOpenedToStore = async ( - set: (ids: string[]) => void, -): Promise => { - const sessionIds = await loadRecentlyOpenedSessions(); - set(sessionIds); -}; diff --git a/apps/desktop/src/store/zustand/tabs/usePinnedTabsSync.ts b/apps/desktop/src/store/zustand/tabs/usePinnedTabsSync.ts new file mode 100644 index 0000000000..24f09ca3c0 --- /dev/null +++ b/apps/desktop/src/store/zustand/tabs/usePinnedTabsSync.ts @@ -0,0 +1,88 @@ +import { useEffect, useRef } from "react"; + +import { getCurrentWebviewWindowLabel } from "@hypr/plugin-windows"; + +import * as main from "../../tinybase/store/main"; +import { useTabs } from "./index"; +import { loadPinnedTabs, savePinnedTabs } from "./pinned-persistence"; +import { getDefaultState, type Tab, uniqueIdfromTab } from "./schema"; + +const getPinnedTabIds = (tabs: Tab[]): string[] => { + return tabs + .filter((t) => t.pinned) + .map(uniqueIdfromTab) + .sort(); +}; + +export const usePinnedTabsSync = () => { + const store = main.UI.useStore(main.STORE_ID) as main.Store | undefined; + const pinnedTabsValue = main.UI.useValue("pinned_tabs", store); + const prevPinnedIdsRef = useRef([]); + const hasInitializedRef = useRef(false); + const { openNew, pin } = useTabs(); + + // Initialize FROM TinyBase when value first appears + useEffect(() => { + if (!store || hasInitializedRef.current) return; + + // Wait for the persisted value to be loaded (non-empty string) + if (typeof pinnedTabsValue !== "string" || pinnedTabsValue === "[]") { + return; + } + + hasInitializedRef.current = true; + const pinnedTabs = loadPinnedTabs(store); + + for (const pinnedTab of pinnedTabs) { + const { pinned, ...tabInput } = pinnedTab; + openNew(tabInput); + + const tabs = useTabs.getState().tabs; + const newTab = tabs.find((t) => { + const tabWithDefaults = getDefaultState(tabInput); + return uniqueIdfromTab(t) === uniqueIdfromTab(tabWithDefaults as Tab); + }); + + if (newTab && !newTab.pinned) { + pin(newTab); + } + } + + prevPinnedIdsRef.current = getPinnedTabIds(useTabs.getState().tabs); + }, [store, pinnedTabsValue, openNew, pin]); + + // Mark as initialized if no pinned tabs to restore (empty or default value) + useEffect(() => { + if (!store || hasInitializedRef.current) return; + + // If we have a store but the value is empty/default, mark as initialized + // This handles the case where there are no pinned tabs to restore + if (pinnedTabsValue === "[]" || pinnedTabsValue === "") { + hasInitializedRef.current = true; + } + }, [store, pinnedTabsValue]); + + // Sync TO TinyBase when Zustand changes + useEffect(() => { + if (!store) return; + + const unsubscribe = useTabs.subscribe((state) => { + const tabs = state.tabs; + const pinnedIds = getPinnedTabIds(tabs); + const prevPinnedIds = prevPinnedIdsRef.current; + + const pinnedChanged = + prevPinnedIds.length !== pinnedIds.length || + prevPinnedIds.some((id, i) => id !== pinnedIds[i]); + + if (pinnedChanged) { + if (getCurrentWebviewWindowLabel() === "main") { + savePinnedTabs(store, tabs); + } + prevPinnedIdsRef.current = pinnedIds; + } + }); + + return unsubscribe; + }, [store]); +}; diff --git a/apps/desktop/src/store/zustand/tabs/useRecentlyOpenedSync.ts b/apps/desktop/src/store/zustand/tabs/useRecentlyOpenedSync.ts new file mode 100644 index 0000000000..f3fc7d33b2 --- /dev/null +++ b/apps/desktop/src/store/zustand/tabs/useRecentlyOpenedSync.ts @@ -0,0 +1,70 @@ +import { useEffect, useRef } from "react"; + +import { getCurrentWebviewWindowLabel } from "@hypr/plugin-windows"; + +import * as main from "../../tinybase/store/main"; +import { useTabs } from "./index"; +import { + loadRecentlyOpenedSessions, + saveRecentlyOpenedSessions, +} from "./recently-opened"; + +export const useRecentlyOpenedSync = () => { + const store = main.UI.useStore(main.STORE_ID) as main.Store | undefined; + const recentlyOpenedValue = main.UI.useValue( + "recently_opened_sessions", + store, + ); + const prevIdsRef = useRef([]); + const hasInitializedRef = useRef(false); + + // Initialize FROM TinyBase when value first appears + useEffect(() => { + if (!store || hasInitializedRef.current) return; + + // Wait for the persisted value to be loaded (non-empty string) + if ( + typeof recentlyOpenedValue !== "string" || + recentlyOpenedValue === "[]" + ) { + return; + } + + hasInitializedRef.current = true; + const ids = loadRecentlyOpenedSessions(store); + + useTabs.setState({ recentlyOpenedSessionIds: ids }); + prevIdsRef.current = ids; + }, [store, recentlyOpenedValue]); + + // Mark as initialized if no recently opened sessions to restore + useEffect(() => { + if (!store || hasInitializedRef.current) return; + + if (recentlyOpenedValue === "[]" || recentlyOpenedValue === "") { + hasInitializedRef.current = true; + } + }, [store, recentlyOpenedValue]); + + // Sync TO TinyBase when Zustand changes + useEffect(() => { + if (!store) return; + + const unsubscribe = useTabs.subscribe((state) => { + const ids = state.recentlyOpenedSessionIds; + const prevIds = prevIdsRef.current; + + const idsChanged = + prevIds.length !== ids.length || prevIds.some((id, i) => id !== ids[i]); + + if (idsChanged) { + if (getCurrentWebviewWindowLabel() === "main") { + saveRecentlyOpenedSessions(store, ids); + } + prevIdsRef.current = ids; + } + }); + + return unsubscribe; + }, [store]); +}; diff --git a/apps/desktop/src/types/tauri.gen.ts b/apps/desktop/src/types/tauri.gen.ts index 28ae7cfdbc..6c3ad9a131 100644 --- a/apps/desktop/src/types/tauri.gen.ts +++ b/apps/desktop/src/types/tauri.gen.ts @@ -22,22 +22,6 @@ async setOnboardingNeeded(v: boolean) : Promise> { else return { status: "error", error: e as any }; } }, -async getDismissedToasts() : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("get_dismissed_toasts") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async setDismissedToasts(v: string[]) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("set_dismissed_toasts", { v }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, async getOnboardingLocal() : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("get_onboarding_local") }; @@ -91,38 +75,6 @@ async setTinybaseValues(v: string) : Promise> { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; } -}, -async getPinnedTabs() : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("get_pinned_tabs") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async setPinnedTabs(v: string) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("set_pinned_tabs", { v }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async getRecentlyOpenedSessions() : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("get_recently_opened_sessions") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async setRecentlyOpenedSessions(v: string) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("set_recently_opened_sessions", { v }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} } } diff --git a/packages/store/src/tinybase.ts b/packages/store/src/tinybase.ts index 255894f2f9..c45226c861 100644 --- a/packages/store/src/tinybase.ts +++ b/packages/store/src/tinybase.ts @@ -173,4 +173,8 @@ export const valueSchemaForTinybase = { current_llm_model: { type: "string" }, current_stt_provider: { type: "string" }, current_stt_model: { type: "string" }, + timezone: { type: "string" }, + pinned_tabs: { type: "string" }, + recently_opened_sessions: { type: "string" }, + dismissed_toasts: { type: "string" }, } as const satisfies InferTinyBaseSchema; diff --git a/packages/store/src/zod.ts b/packages/store/src/zod.ts index 578f336957..74c438ea73 100644 --- a/packages/store/src/zod.ts +++ b/packages/store/src/zod.ts @@ -200,6 +200,9 @@ export const generalSchema = z.object({ current_stt_provider: z.string().optional(), current_stt_model: z.string().optional(), timezone: z.string().optional(), + pinned_tabs: z.string().default("[]"), + recently_opened_sessions: z.string().default("[]"), + dismissed_toasts: z.string().default("[]"), }); export const aiProviderSchema = z