From 04d227c9fe6b779deb330c0cb094c1cb6b1fa4b2 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Tue, 10 Feb 2026 23:15:10 +0800 Subject: [PATCH] feat(desktop): persist onboarding state with --force-onboarding flag Use Zustand persist middleware with localStorage to remember onboarding completion across app restarts. Only the completed flag is persisted; transient UI state resets each launch. Add --force-onboarding CLI flag to re-show onboarding even when already completed. Co-Authored-By: Claude Opus 4.6 --- apps/desktop/src/main/electron-env.d.ts | 3 + apps/desktop/src/main/index.ts | 8 ++- apps/desktop/src/preload/index.ts | 6 ++ apps/desktop/src/renderer/src/App.tsx | 8 ++- .../src/renderer/src/stores/onboarding.ts | 59 ++++++++++++------- 5 files changed, 62 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src/main/electron-env.d.ts b/apps/desktop/src/main/electron-env.d.ts index a58fcdb2..04eb0643 100644 --- a/apps/desktop/src/main/electron-env.d.ts +++ b/apps/desktop/src/main/electron-env.d.ts @@ -139,6 +139,9 @@ interface ChannelAccountStateInfo { } interface ElectronAPI { + app: { + getFlags: () => Promise<{ forceOnboarding: boolean }> + } hub: { init: () => Promise getStatus: () => Promise diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 6f3ffc67..ad958405 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -44,7 +44,7 @@ process.stderr?.on?.('error', (err: NodeJS.ErrnoException) => { throw err }) -import { app, BrowserWindow, shell } from 'electron' +import { app, BrowserWindow, shell, ipcMain } from 'electron' import { fileURLToPath } from 'node:url' import path from 'node:path' import { registerAllIpcHandlers, initializeApp, cleanupAll, setupDeviceConfirmation } from './ipc/index.js' @@ -63,6 +63,9 @@ export const RENDERER_DIST = path.join(__dirname, '../renderer') process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST +// CLI flags +const forceOnboarding = process.argv.includes('--force-onboarding') + let win: BrowserWindow | null function createWindow() { @@ -110,6 +113,9 @@ app.on('before-quit', () => { }) app.whenReady().then(async () => { + // App-level IPC handlers + ipcMain.handle('app:getFlags', () => ({ forceOnboarding })) + // Register all IPC handlers before creating window registerAllIpcHandlers() diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 70fe35bb..44acd172 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -97,6 +97,12 @@ export interface LocalChatApproval { // ============================================================================ const electronAPI = { + // App-level + app: { + /** Get CLI flags passed to the app */ + getFlags: (): Promise<{ forceOnboarding: boolean }> => ipcRenderer.invoke('app:getFlags'), + }, + // Hub management hub: { init: () => ipcRenderer.invoke('hub:init'), diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index 240fe0e1..7865bbbe 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react' import { createHashRouter, Navigate, RouterProvider } from 'react-router-dom' import Layout from './pages/layout' import HomePage from './pages/home' @@ -15,7 +16,8 @@ import { useOnboardingStore } from './stores/onboarding' function OnboardingGuard({ children }: { children: React.ReactNode }) { const completed = useOnboardingStore((s) => s.completed) - if (!completed) return + const forceOnboarding = useOnboardingStore((s) => s.forceOnboarding) + if (!completed || forceOnboarding) return return <>{children} } @@ -52,5 +54,9 @@ const router = createHashRouter([ ]) export default function App() { + useEffect(() => { + useOnboardingStore.getState().initForceFlag() + }, []) + return } diff --git a/apps/desktop/src/renderer/src/stores/onboarding.ts b/apps/desktop/src/renderer/src/stores/onboarding.ts index b0eccef0..8f5f6a57 100644 --- a/apps/desktop/src/renderer/src/stores/onboarding.ts +++ b/apps/desktop/src/renderer/src/stores/onboarding.ts @@ -1,4 +1,5 @@ import { create } from "zustand" +import { persist } from "zustand/middleware" interface AcknowledgementsState { fileSystem: boolean @@ -9,6 +10,7 @@ interface AcknowledgementsState { interface OnboardingStore { completed: boolean + forceOnboarding: boolean acknowledgements: AcknowledgementsState allAcknowledged: boolean providerConfigured: boolean @@ -17,30 +19,47 @@ interface OnboardingStore { setProviderConfigured: (configured: boolean) => void setClientConnected: (connected: boolean) => void completeOnboarding: () => void + initForceFlag: () => Promise } -export const useOnboardingStore = create((set, get) => ({ - completed: false, +export const useOnboardingStore = create()( + persist( + (set, get) => ({ + completed: false, + forceOnboarding: false, - acknowledgements: { - fileSystem: false, - shellExecution: false, - llmRequests: false, - localStorage: false, - }, - allAcknowledged: false, - providerConfigured: false, - clientConnected: false, + acknowledgements: { + fileSystem: false, + shellExecution: false, + llmRequests: false, + localStorage: false, + }, + allAcknowledged: false, + providerConfigured: false, + clientConnected: false, - setAcknowledgement: (key, value) => { - const acknowledgements = { ...get().acknowledgements, [key]: value } - const allAcknowledged = Object.values(acknowledgements).every(Boolean) - set({ acknowledgements, allAcknowledged }) - }, + setAcknowledgement: (key, value) => { + const acknowledgements = { ...get().acknowledgements, [key]: value } + const allAcknowledged = Object.values(acknowledgements).every(Boolean) + set({ acknowledgements, allAcknowledged }) + }, - setProviderConfigured: (configured) => set({ providerConfigured: configured }), + setProviderConfigured: (configured) => set({ providerConfigured: configured }), - setClientConnected: (connected) => set({ clientConnected: connected }), + setClientConnected: (connected) => set({ clientConnected: connected }), - completeOnboarding: () => set({ completed: true }), -})) + completeOnboarding: () => set({ completed: true }), + + initForceFlag: async () => { + const flags = await window.electronAPI.app.getFlags() + if (flags.forceOnboarding) { + set({ forceOnboarding: true }) + } + }, + }), + { + name: 'multica-onboarding', + partialize: (state) => ({ completed: state.completed }), + } + ) +)