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 <noreply@anthropic.com>
This commit is contained in:
parent
ab65f4cadf
commit
04d227c9fe
5 changed files with 62 additions and 22 deletions
3
apps/desktop/src/main/electron-env.d.ts
vendored
3
apps/desktop/src/main/electron-env.d.ts
vendored
|
|
@ -139,6 +139,9 @@ interface ChannelAccountStateInfo {
|
|||
}
|
||||
|
||||
interface ElectronAPI {
|
||||
app: {
|
||||
getFlags: () => Promise<{ forceOnboarding: boolean }>
|
||||
}
|
||||
hub: {
|
||||
init: () => Promise<unknown>
|
||||
getStatus: () => Promise<HubStatus>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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 <Navigate to="/onboarding" replace />
|
||||
const forceOnboarding = useOnboardingStore((s) => s.forceOnboarding)
|
||||
if (!completed || forceOnboarding) return <Navigate to="/onboarding" replace />
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
|
|
@ -52,5 +54,9 @@ const router = createHashRouter([
|
|||
])
|
||||
|
||||
export default function App() {
|
||||
useEffect(() => {
|
||||
useOnboardingStore.getState().initForceFlag()
|
||||
}, [])
|
||||
|
||||
return <RouterProvider router={router} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void>
|
||||
}
|
||||
|
||||
export const useOnboardingStore = create<OnboardingStore>((set, get) => ({
|
||||
completed: false,
|
||||
export const useOnboardingStore = create<OnboardingStore>()(
|
||||
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 }),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue