import * as React from 'react'
import { invoke as tauriInvoke } from '@tauri-apps/api/core'
import { isTauriHost } from '@/lib/api/desktop'
export type ConfigState = {
configPath: string
cwd: string
vaultRoot: string
daemonEnabled: boolean
daemonBin: string
defaultActor: string
currentProjectId: string | null
ready: boolean
setConfigPath: (value: string) => void
setVaultRoot: (value: string) => void
setDefaultActor: (value: string) => void
setCurrentProjectId: (value: string | null) => void
}
const KEY = 'spool.desktop.config.v2'
const DEFAULT_ACTOR_KEY = 'spool.defaultActor'
const PROJECT_ID_KEY = 'spool.currentProjectId'
type Persisted = {
configPath: string
cwd: string
vaultRoot: string
daemonEnabled: boolean
daemonBin: string
defaultActor: string
currentProjectId: string | null
}
const initial: Persisted = {
configPath: '',
cwd: '',
vaultRoot: '',
daemonEnabled: false,
daemonBin: '',
defaultActor: '',
currentProjectId: null,
}
function load(): Persisted {
if (typeof window === 'undefined') return initial
try {
const raw = window.localStorage.getItem(KEY)
const fallbackActor = window.localStorage.getItem(DEFAULT_ACTOR_KEY) ?? ''
const fallbackProjectRaw = window.localStorage.getItem(PROJECT_ID_KEY)
const fallbackProject =
fallbackProjectRaw === null || fallbackProjectRaw === ''
? null
: fallbackProjectRaw
if (!raw) {
return {
...initial,
defaultActor: fallbackActor,
currentProjectId: fallbackProject,
}
}
const parsed = { ...initial, ...(JSON.parse(raw) as Partial<Persisted>) }
// Discard stale relative config paths from older versions
if (parsed.configPath && !parsed.configPath.startsWith('/') && !parsed.configPath.startsWith('~')) {
parsed.configPath = initial.configPath
}
if (!parsed.defaultActor && fallbackActor) {
parsed.defaultActor = fallbackActor
}
if (parsed.currentProjectId === undefined) {
parsed.currentProjectId = fallbackProject
}
return parsed
} catch {
return initial
}
}
const ConfigContext = React.createContext<ConfigState | null>(null)
export function ConfigProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = React.useState<Persisted>(load)
const [ready, setReady] = React.useState(false)
React.useEffect(() => {
if (isTauriHost()) {
tauriInvoke<{ config_path: string; home: string }>('desktop_get_defaults')
.then((defaults) => {
setState((prev) => ({
...prev,
configPath: defaults.config_path,
cwd: prev.cwd || defaults.home,
}))
setReady(true)
})
.catch(() => setReady(true))
} else {
setReady(true)
}
}, [])
React.useEffect(() => {
if (!ready) return
try {
window.localStorage.setItem(KEY, JSON.stringify(state))
// Mirror to standalone keys as a fallback for older surfaces
window.localStorage.setItem(DEFAULT_ACTOR_KEY, state.defaultActor ?? '')
if (state.currentProjectId === null || state.currentProjectId === undefined) {
window.localStorage.removeItem(PROJECT_ID_KEY)
} else {
window.localStorage.setItem(PROJECT_ID_KEY, state.currentProjectId)
}
} catch {
// ignore persistence failure
}
}, [state, ready])
const value = React.useMemo<ConfigState>(
() => ({
...state,
ready,
setConfigPath: (configPath) =>
setState((prev) => ({ ...prev, configPath })),
setVaultRoot: (vaultRoot) =>
setState((prev) => ({ ...prev, vaultRoot })),
setDefaultActor: (defaultActor) =>
setState((prev) => ({ ...prev, defaultActor })),
setCurrentProjectId: (currentProjectId) =>
setState((prev) => ({ ...prev, currentProjectId })),
}),
[state, ready]
)
return <ConfigContext.Provider value={value}>{children}</ConfigContext.Provider>
}
export function useConfig(): ConfigState {
const ctx = React.useContext(ConfigContext)
if (!ctx) throw new Error('useConfig must be used inside <ConfigProvider>')
return ctx
}
export function buildDaemonRequest(cfg: ConfigState) {
if (!cfg.daemonEnabled) return null
return {
enabled: true,
daemon_bin: cfg.daemonBin.trim() ? cfg.daemonBin.trim() : null,
}
}