Files
zclaw_openfang/desktop/src/store/sessionStore.ts
iven 1cf3f585d3 refactor(store): split gatewayStore into specialized domain stores
Major restructuring:
- Split monolithic gatewayStore into 5 focused stores:
  - connectionStore: WebSocket connection and gateway lifecycle
  - configStore: quickConfig, workspaceInfo, MCP services
  - agentStore: clones, usage stats, agent management
  - handStore: hands, approvals, triggers, hand runs
  - workflowStore: workflows, workflow runs, execution

- Update all components to use new stores with selector pattern
- Remove
2026-03-20 22:14:13 +08:00

229 lines
7.1 KiB
TypeScript

/**
* sessionStore.ts - Session Management Store
*
* Extracted from gatewayStore.ts for Store Refactoring.
* Manages Gateway sessions and session messages.
*/
import { create } from 'zustand';
import type { GatewayClient } from '../lib/gateway-client';
// === Types ===
export interface Session {
id: string;
agentId: string;
createdAt: string;
updatedAt?: string;
messageCount?: number;
status?: 'active' | 'archived' | 'expired';
metadata?: Record<string, unknown>;
}
export interface SessionMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
createdAt: string;
tokens?: { input?: number; output?: number };
}
// === Raw API Response Types ===
interface RawSession {
id?: string;
sessionId?: string;
session_id?: string;
agentId?: string;
agent_id?: string;
model?: string;
status?: string;
createdAt?: string;
created_at?: string;
updatedAt?: string;
updated_at?: string;
messageCount?: number;
message_count?: number;
metadata?: Record<string, unknown>;
}
interface RawSessionMessage {
id?: string;
messageId?: string;
message_id?: string;
role?: string;
content?: string;
createdAt?: string;
created_at?: string;
metadata?: Record<string, unknown>;
tokens?: { input?: number; output?: number };
}
// === Client Interface ===
interface SessionClient {
listSessions(opts?: { limit?: number; offset?: number }): Promise<{ sessions?: RawSession[] } | null>;
getSession(sessionId: string): Promise<Record<string, unknown> | null>;
createSession(params: { agent_id: string; metadata?: Record<string, unknown> }): Promise<Record<string, unknown> | null>;
deleteSession(sessionId: string): Promise<void>;
getSessionMessages(sessionId: string, opts?: { limit?: number; offset?: number }): Promise<{ messages?: RawSessionMessage[] } | null>;
}
// === Store Interface ===
export interface SessionStateSlice {
sessions: Session[];
sessionMessages: Record<string, SessionMessage[]>;
isLoading: boolean;
error: string | null;
}
export interface SessionActionsSlice {
loadSessions: (opts?: { limit?: number; offset?: number }) => Promise<void>;
getSession: (sessionId: string) => Promise<Session | undefined>;
createSession: (agentId: string, metadata?: Record<string, unknown>) => Promise<Session | undefined>;
deleteSession: (sessionId: string) => Promise<void>;
loadSessionMessages: (sessionId: string, opts?: { limit?: number; offset?: number }) => Promise<SessionMessage[]>;
}
export type SessionStore = SessionStateSlice & SessionActionsSlice & { client: SessionClient | null };
// === Store Implementation ===
export const useSessionStore = create<SessionStore>((set, get) => ({
// Initial state
sessions: [],
sessionMessages: {},
isLoading: false,
error: null,
client: null,
loadSessions: async (opts?: { limit?: number; offset?: number }) => {
const client = get().client;
if (!client) return;
try {
const result = await client.listSessions(opts);
const sessions: Session[] = (result?.sessions || [])
.filter((s: RawSession) => s.id || s.session_id)
.map((s: RawSession) => ({
id: s.id || s.session_id || '',
agentId: s.agent_id || s.agentId || '',
createdAt: s.created_at || s.createdAt || new Date().toISOString(),
updatedAt: s.updated_at || s.updatedAt,
messageCount: s.message_count || s.messageCount,
status: s.status as Session['status'],
metadata: s.metadata,
}));
set({ sessions });
} catch {
/* ignore if sessions API not available */
}
},
getSession: async (sessionId: string) => {
const client = get().client;
if (!client) return undefined;
try {
const result = await client.getSession(sessionId);
if (!result) return undefined;
const session: Session = {
id: result.id as string,
agentId: result.agent_id as string,
createdAt: result.created_at as string,
updatedAt: result.updated_at as string | undefined,
messageCount: result.message_count as number | undefined,
status: result.status as Session['status'],
metadata: result.metadata as Record<string, unknown> | undefined,
};
set(state => ({
sessions: state.sessions.some(s => s.id === sessionId)
? state.sessions.map(s => s.id === sessionId ? session : s)
: [...state.sessions, session],
}));
return session;
} catch {
return undefined;
}
},
createSession: async (agentId: string, metadata?: Record<string, unknown>) => {
const client = get().client;
if (!client) return undefined;
try {
const result = await client.createSession({ agent_id: agentId, metadata });
if (!result) return undefined;
const session: Session = {
id: result.id as string,
agentId: result.agent_id as string,
createdAt: result.created_at as string,
status: 'active',
metadata,
};
set(state => ({ sessions: [...state.sessions, session] }));
return session;
} catch (err: unknown) {
set({ error: err instanceof Error ? err.message : String(err) });
return undefined;
}
},
deleteSession: async (sessionId: string) => {
const client = get().client;
if (!client) return;
try {
await client.deleteSession(sessionId);
set(state => ({
sessions: state.sessions.filter(s => s.id !== sessionId),
sessionMessages: Object.fromEntries(
Object.entries(state.sessionMessages).filter(([id]) => id !== sessionId)
),
}));
} catch (err: unknown) {
set({ error: err instanceof Error ? err.message : String(err) });
throw err;
}
},
loadSessionMessages: async (sessionId: string, opts?: { limit?: number; offset?: number }) => {
const client = get().client;
if (!client) return [];
try {
const result = await client.getSessionMessages(sessionId, opts);
const messages: SessionMessage[] = (result?.messages || []).map((m: RawSessionMessage) => ({
id: m.id || m.message_id || '',
role: (m.role || 'user') as 'user' | 'assistant' | 'system',
content: m.content || '',
createdAt: m.created_at || m.createdAt || new Date().toISOString(),
tokens: m.tokens,
}));
set(state => ({
sessionMessages: { ...state.sessionMessages, [sessionId]: messages },
}));
return messages;
} catch {
return [];
}
},
}));
// === Client Injection ===
function createSessionClientFromGateway(client: GatewayClient): SessionClient {
return {
listSessions: (opts) => client.listSessions(opts),
getSession: (sessionId) => client.getSession(sessionId),
createSession: (params) => client.createSession(params),
deleteSession: async (sessionId) => { await client.deleteSession(sessionId); },
getSessionMessages: (sessionId, opts) => client.getSessionMessages(sessionId, opts),
};
}
export function setSessionStoreClient(client: unknown): void {
const sessionClient = createSessionClientFromGateway(client as GatewayClient);
useSessionStore.setState({ client: sessionClient });
}