import { create } from "zustand"; import { persist } from "zustand/middleware"; export const useOpenClawStore = create( persist( (set) => ({ // Raw data openclawStatus: null, openclawSessions: [], openclawSessionsDefaults: null, openclawSessionDetail: null, openclawSessionHistory: [], openclawCronJobs: [], openclawApprovals: [], openclawResolvedSessionKey: null, openclawChatMessagesBySession: {}, openclawChatDraftBySession: {}, openclawChatSendingBySession: {}, openclawSessionSubscriptions: {}, // Loading states isStatusLoading: false, isSessionsLoading: false, isSessionDetailLoading: false, isCronLoading: false, isApprovalsLoading: false, isChatSending: false, // Error states statusError: null, sessionsError: null, sessionDetailError: null, cronError: null, approvalsError: null, chatError: null, // Agents state agents: [], agentsLoading: false, agentsError: null, agentsPresence: {}, // Skills state skills: [], skillsLoading: false, skillsError: null, // Models state models: [], modelsLoading: false, modelsError: null, // Hooks state hooks: [], hooksLoading: false, hooksError: null, // Plugins state plugins: [], pluginsLoading: false, pluginsError: null, // Secrets audit state secretsAudit: null, secretsAuditLoading: false, secretsAuditError: null, // Security audit state securityAudit: null, securityAuditLoading: false, securityAuditError: null, // Daemon status state daemonStatus: null, daemonStatusLoading: false, daemonStatusError: null, // Pairing state pairing: null, pairingLoading: false, pairingError: null, // QR code state qrCode: null, qrCodeLoading: false, qrCodeError: null, // Update status state updateStatus: null, updateStatusLoading: false, updateStatusError: null, // Models aliases state modelsAliases: null, modelsAliasesLoading: false, modelsAliasesError: null, // Models fallbacks state modelsFallbacks: [], modelsFallbacksLoading: false, modelsFallbacksError: null, // Models image fallbacks state modelsImageFallbacks: [], modelsImageFallbacksLoading: false, modelsImageFallbacksError: null, // Skill update state skillUpdate: null, skillUpdateLoading: false, skillUpdateError: null, // Workspace files state (per agent, keyed by workspace path) workspaceFiles: {}, workspaceFilesLoading: false, workspaceFilesError: null, // Workspace file content (keyed by "agentId:filename") workspaceFileContent: {}, // Selected session key for detail/history drill-down selectedSessionKey: null, // WebSocket client ref (set by App.jsx on connection) clientRef: null, setClientRef: (ref) => set({ clientRef: ref }), // Setters setOpenclawStatus: (data) => set({ openclawStatus: data, statusError: null }), setOpenclawSessions: (data) => set({ openclawSessions: data?.sessions || [], openclawSessionsDefaults: data?.defaults || null, sessionsError: null, }), appendOpenclawSession: (session) => set((state) => { const key = session?.key || session?.sessionKey; if (!key) { return {}; } const existing = state.openclawSessions || []; const deduped = existing.filter((item) => (item?.key || item?.sessionKey) !== key); return { openclawSessions: [session, ...deduped] }; }), removeOpenclawSession: (sessionKey) => set((state) => ({ openclawSessions: (state.openclawSessions || []).filter( (item) => (item?.key || item?.sessionKey) !== sessionKey ), selectedSessionKey: state.selectedSessionKey === sessionKey ? null : state.selectedSessionKey, })), setOpenclawSessionDetail: (data) => set({ openclawSessionDetail: data?.session || null, sessionDetailError: data?.error || null }), setOpenclawSessionHistory: (data) => set({ openclawSessionHistory: data?.history || [], sessionDetailError: data?.error || null }), setOpenclawCronJobs: (data) => set({ openclawCronJobs: data?.cron || [], cronError: null }), setOpenclawApprovals: (data) => set({ openclawApprovals: data?.approvals || [], approvalsError: null }), setOpenclawResolvedSessionKey: (key) => set({ openclawResolvedSessionKey: key || null }), setOpenclawChatDraft: (sessionKey, value) => set((state) => ({ openclawChatDraftBySession: { ...state.openclawChatDraftBySession, [sessionKey]: value }, })), appendOpenclawChatMessage: (sessionKey, message) => set((state) => { const current = state.openclawChatMessagesBySession[sessionKey] || []; const sameMessageIndex = current.findIndex((item) => { const sameId = Boolean(message?.id && item?.id && message.id === item.id); const sameMessageId = Boolean( message?.messageId && item?.messageId && message.messageId === item.messageId ); const sameSeq = Boolean( message?.seq !== undefined && message?.seq !== null && item?.seq !== undefined && item?.seq !== null && message.seq === item.seq && message?.role === item?.role ); const incomingText = String(message?.text || '').trim(); const existingText = String(item?.text || '').trim(); const incomingTs = Date.parse(message?.timestamp || ''); const existingTs = Date.parse(item?.timestamp || ''); const nearInTime = Number.isFinite(incomingTs) && Number.isFinite(existingTs) && Math.abs(incomingTs - existingTs) < 1500; const sameAssistantText = message?.role === 'assistant' && item?.role === 'assistant' && incomingText && existingText && ( incomingText === existingText || incomingText.startsWith(existingText) || existingText.startsWith(incomingText) ) && nearInTime; return sameId || sameMessageId || sameSeq || sameAssistantText; }); if (sameMessageIndex >= 0) { const next = [...current]; next[sameMessageIndex] = { ...next[sameMessageIndex], ...message }; return { openclawChatMessagesBySession: { ...state.openclawChatMessagesBySession, [sessionKey]: next, }, }; } return { openclawChatMessagesBySession: { ...state.openclawChatMessagesBySession, [sessionKey]: [...current, message], }, }; }), replaceOpenclawChatHistory: (sessionKey, messages) => set((state) => { const incoming = Array.isArray(messages) ? messages : []; const existing = state.openclawChatMessagesBySession[sessionKey] || []; const merged = []; const seen = new Set(); const signatureFor = (message) => { if (!message) return ""; if (message.id) return `id:${message.id}`; if (message.messageId) return `mid:${message.messageId}`; if (message.seq !== undefined && message.seq !== null) return `seq:${message.seq}`; return `txt:${message.role || ""}:${String(message.text || "").trim()}`; }; for (const message of [...incoming, ...existing]) { const signature = signatureFor(message); if (!signature || seen.has(signature)) { continue; } seen.add(signature); merged.push(message); } return { openclawChatMessagesBySession: { ...state.openclawChatMessagesBySession, [sessionKey]: merged, }, }; }), setOpenclawChatSendingForSession: (sessionKey, value) => set((state) => ({ openclawChatSendingBySession: { ...state.openclawChatSendingBySession, [sessionKey]: Boolean(value) }, isChatSending: Boolean(value), })), setOpenclawSessionSubscribed: (sessionKey, value) => set((state) => ({ openclawSessionSubscriptions: { ...state.openclawSessionSubscriptions, [sessionKey]: Boolean(value) }, })), setSelectedSessionKey: (key) => set({ selectedSessionKey: key }), setStatusLoading: (v) => set({ isStatusLoading: v }), setSessionsLoading: (v) => set({ isSessionsLoading: v }), setSessionDetailLoading: (v) => set({ isSessionDetailLoading: v }), setCronLoading: (v) => set({ isCronLoading: v }), setApprovalsLoading: (v) => set({ isApprovalsLoading: v }), setStatusError: (e) => set({ statusError: e }), setSessionsError: (e) => set({ sessionsError: e }), setSessionDetailError: (e) => set({ sessionDetailError: e }), setCronError: (e) => set({ cronError: e }), setApprovalsError: (e) => set({ approvalsError: e }), setChatError: (e) => set({ chatError: e }), setAgents: (agents) => set({ agents }), setAgentsLoading: (loading) => set({ agentsLoading: loading }), setAgentsError: (error) => set({ agentsError: error }), setAgentsPresence: (presence) => set({ agentsPresence: presence }), setSkills: (skills) => set({ skills }), setSkillsLoading: (loading) => set({ skillsLoading: loading }), setSkillsError: (error) => set({ skillsError: error }), setModels: (models) => set({ models }), setModelsLoading: (loading) => set({ modelsLoading: loading }), setModelsError: (error) => set({ modelsError: error }), setHooks: (hooks) => set({ hooks }), setHooksLoading: (loading) => set({ hooksLoading: loading }), setHooksError: (error) => set({ hooksError: error }), setPlugins: (plugins) => set({ plugins }), setPluginsLoading: (loading) => set({ pluginsLoading: loading }), setPluginsError: (error) => set({ pluginsError: error }), setSecretsAudit: (data) => set({ secretsAudit: data }), setSecretsAuditLoading: (loading) => set({ secretsAuditLoading: loading }), setSecretsAuditError: (error) => set({ secretsAuditError: error }), setSecurityAudit: (data) => set({ securityAudit: data }), setSecurityAuditLoading: (loading) => set({ securityAuditLoading: loading }), setSecurityAuditError: (error) => set({ securityAuditError: error }), setDaemonStatus: (data) => set({ daemonStatus: data }), setDaemonStatusLoading: (loading) => set({ daemonStatusLoading: loading }), setDaemonStatusError: (error) => set({ daemonStatusError: error }), setPairing: (data) => set({ pairing: data }), setPairingLoading: (loading) => set({ pairingLoading: loading }), setPairingError: (error) => set({ pairingError: error }), setQrCode: (data) => set({ qrCode: data }), setQrCodeLoading: (loading) => set({ qrCodeLoading: loading }), setQrCodeError: (error) => set({ qrCodeError: error }), setUpdateStatus: (data) => set({ updateStatus: data }), setUpdateStatusLoading: (loading) => set({ updateStatusLoading: loading }), setUpdateStatusError: (error) => set({ updateStatusError: error }), setModelsAliases: (data) => set({ modelsAliases: data }), setModelsAliasesLoading: (loading) => set({ modelsAliasesLoading: loading }), setModelsAliasesError: (error) => set({ modelsAliasesError: error }), setModelsFallbacks: (data) => set({ modelsFallbacks: data }), setModelsFallbacksLoading: (loading) => set({ modelsFallbacksLoading: loading }), setModelsFallbacksError: (error) => set({ modelsFallbacksError: error }), setModelsImageFallbacks: (data) => set({ modelsImageFallbacks: data }), setModelsImageFallbacksLoading: (loading) => set({ modelsImageFallbacksLoading: loading }), setModelsImageFallbacksError: (error) => set({ modelsImageFallbacksError: error }), setSkillUpdate: (data) => set({ skillUpdate: data }), setSkillUpdateLoading: (loading) => set({ skillUpdateLoading: loading }), setSkillUpdateError: (error) => set({ skillUpdateError: error }), setWorkspaceFiles: (workspace, data) => set((state) => ({ workspaceFiles: { ...state.workspaceFiles, [workspace]: data }, })), setWorkspaceFilesLoading: (loading) => set({ workspaceFilesLoading: loading }), setWorkspaceFilesError: (error) => set({ workspaceFilesError: error }), setWorkspaceFileContent: (key, content) => set((state) => ({ workspaceFileContent: { ...state.workspaceFileContent, [key]: content }, })), }), { name: "openclaw-store", // Skip persisting ephemeral UI state partialize: (state) => ({ // Persist only data, not loading/error/UI states openclawStatus: state.openclawStatus, openclawSessions: state.openclawSessions, openclawCronJobs: state.openclawCronJobs, openclawApprovals: state.openclawApprovals, agents: state.agents, agentsPresence: state.agentsPresence, skills: state.skills, models: state.models, hooks: state.hooks, plugins: state.plugins, secretsAudit: state.secretsAudit, securityAudit: state.securityAudit, daemonStatus: state.daemonStatus, pairing: state.pairing, qrCode: state.qrCode, updateStatus: state.updateStatus, modelsAliases: state.modelsAliases, modelsFallbacks: state.modelsFallbacks, modelsImageFallbacks: state.modelsImageFallbacks, skillUpdate: state.skillUpdate, }), } ) );