Files
evotraders/frontend/src/store/openclawStore.js

355 lines
13 KiB
JavaScript

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,
}),
}
)
);