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
229 lines
7.1 KiB
TypeScript
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 });
|
|
}
|