355 lines
13 KiB
JavaScript
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,
|
|
}),
|
|
}
|
|
)
|
|
);
|