// ======================================== // Workflow Store // ======================================== // Manages workflow sessions, tasks, and related data import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; import type { WorkflowStore, WorkflowState, SessionMetadata, TaskData, LiteTaskSession, WorkflowFilters, WorkflowSorting, } from '../types/store'; import { switchWorkspace as apiSwitchWorkspace, fetchRecentPaths, removeRecentPath as apiRemoveRecentPath } from '../lib/api'; // Helper to generate session key from ID const sessionKey = (sessionId: string): string => { return `session-${sessionId}`.replace(/[^a-zA-Z0-9-]/g, '-'); }; // Default filters const defaultFilters: WorkflowFilters = { status: null, search: '', dateRange: { start: null, end: null }, }; // Default sorting const defaultSorting: WorkflowSorting = { field: 'created_at', direction: 'desc', }; // Initial state const initialState: WorkflowState = { // Core data workflowData: { activeSessions: [], archivedSessions: [], }, projectPath: '', recentPaths: [], serverPlatform: 'win32', // Data stores sessionDataStore: {}, liteTaskDataStore: {}, taskJsonStore: {}, // Active session activeSessionId: null, // Filters and sorting filters: defaultFilters, sorting: defaultSorting, }; export const useWorkflowStore = create()( devtools( persist( (set, get) => ({ ...initialState, // ========== Session Actions ========== setSessions: (active: SessionMetadata[], archived: SessionMetadata[]) => { const sessionDataStore: Record = {}; // Build sessionDataStore from both arrays [...active, ...archived].forEach((session) => { const key = sessionKey(session.session_id); sessionDataStore[key] = session; }); set( { workflowData: { activeSessions: active, archivedSessions: archived, }, sessionDataStore, }, false, 'setSessions' ); }, addSession: (session: SessionMetadata) => { const key = sessionKey(session.session_id); set( (state) => ({ workflowData: { ...state.workflowData, activeSessions: [...state.workflowData.activeSessions, session], }, sessionDataStore: { ...state.sessionDataStore, [key]: session, }, }), false, 'addSession' ); }, updateSession: (sessionId: string, updates: Partial) => { const key = sessionKey(sessionId); set( (state) => { const session = state.sessionDataStore[key]; if (!session) return state; const updatedSession = { ...session, ...updates, updated_at: new Date().toISOString() }; // Update in the appropriate array const isActive = session.location === 'active'; const targetArray = isActive ? 'activeSessions' : 'archivedSessions'; return { sessionDataStore: { ...state.sessionDataStore, [key]: updatedSession, }, workflowData: { ...state.workflowData, [targetArray]: state.workflowData[targetArray].map((s) => s.session_id === sessionId ? updatedSession : s ), }, }; }, false, 'updateSession' ); }, removeSession: (sessionId: string) => { const key = sessionKey(sessionId); set( (state) => { const { [key]: removed, ...remainingStore } = state.sessionDataStore; return { sessionDataStore: remainingStore, workflowData: { activeSessions: state.workflowData.activeSessions.filter( (s) => s.session_id !== sessionId ), archivedSessions: state.workflowData.archivedSessions.filter( (s) => s.session_id !== sessionId ), }, }; }, false, 'removeSession' ); }, archiveSession: (sessionId: string) => { const key = sessionKey(sessionId); set( (state) => { const session = state.sessionDataStore[key]; if (!session || session.location === 'archived') return state; const archivedSession: SessionMetadata = { ...session, location: 'archived', status: 'archived', updated_at: new Date().toISOString(), }; return { sessionDataStore: { ...state.sessionDataStore, [key]: archivedSession, }, workflowData: { activeSessions: state.workflowData.activeSessions.filter( (s) => s.session_id !== sessionId ), archivedSessions: [...state.workflowData.archivedSessions, archivedSession], }, }; }, false, 'archiveSession' ); }, // ========== Task Actions ========== addTask: (sessionId: string, task: TaskData) => { const key = sessionKey(sessionId); set( (state) => { const session = state.sessionDataStore[key]; if (!session) return state; // Check for duplicate const existingTask = session.tasks?.find((t) => t.task_id === task.task_id); if (existingTask) return state; const updatedSession: SessionMetadata = { ...session, tasks: [...(session.tasks || []), task], updated_at: new Date().toISOString(), }; return { sessionDataStore: { ...state.sessionDataStore, [key]: updatedSession, }, }; }, false, 'addTask' ); }, updateTask: (sessionId: string, taskId: string, updates: Partial) => { const key = sessionKey(sessionId); set( (state) => { const session = state.sessionDataStore[key]; if (!session?.tasks) return state; const updatedTasks = session.tasks.map((task) => task.task_id === taskId ? { ...task, ...updates, updated_at: new Date().toISOString() } : task ); const updatedSession: SessionMetadata = { ...session, tasks: updatedTasks, updated_at: new Date().toISOString(), }; return { sessionDataStore: { ...state.sessionDataStore, [key]: updatedSession, }, }; }, false, 'updateTask' ); }, removeTask: (sessionId: string, taskId: string) => { const key = sessionKey(sessionId); set( (state) => { const session = state.sessionDataStore[key]; if (!session?.tasks) return state; const updatedSession: SessionMetadata = { ...session, tasks: session.tasks.filter((t) => t.task_id !== taskId), updated_at: new Date().toISOString(), }; return { sessionDataStore: { ...state.sessionDataStore, [key]: updatedSession, }, }; }, false, 'removeTask' ); }, // ========== Lite Task Actions ========== setLiteTaskSession: (key: string, session: LiteTaskSession) => { set( (state) => ({ liteTaskDataStore: { ...state.liteTaskDataStore, [key]: session, }, }), false, 'setLiteTaskSession' ); }, removeLiteTaskSession: (key: string) => { set( (state) => { const { [key]: removed, ...remaining } = state.liteTaskDataStore; return { liteTaskDataStore: remaining }; }, false, 'removeLiteTaskSession' ); }, // ========== Task JSON Store ========== setTaskJson: (key: string, data: unknown) => { set( (state) => ({ taskJsonStore: { ...state.taskJsonStore, [key]: data, }, }), false, 'setTaskJson' ); }, removeTaskJson: (key: string) => { set( (state) => { const { [key]: removed, ...remaining } = state.taskJsonStore; return { taskJsonStore: remaining }; }, false, 'removeTaskJson' ); }, // ========== Active Session ========== setActiveSessionId: (sessionId: string | null) => { set({ activeSessionId: sessionId }, false, 'setActiveSessionId'); }, // ========== Project Path ========== setProjectPath: (path: string) => { set({ projectPath: path }, false, 'setProjectPath'); }, addRecentPath: (path: string) => { set( (state) => { // Remove if exists, add to front const filtered = state.recentPaths.filter((p) => p !== path); const updated = [path, ...filtered].slice(0, 10); // Keep max 10 return { recentPaths: updated }; }, false, 'addRecentPath' ); }, setServerPlatform: (platform: 'win32' | 'darwin' | 'linux') => { set({ serverPlatform: platform }, false, 'setServerPlatform'); }, // ========== Workspace Actions ========== switchWorkspace: async (path: string) => { const response = await apiSwitchWorkspace(path); const sessionDataStore: Record = {}; // Build sessionDataStore from both arrays [...response.activeSessions, ...response.archivedSessions].forEach((session) => { const key = sessionKey(session.session_id); sessionDataStore[key] = session; }); set( { projectPath: response.projectPath, recentPaths: response.recentPaths, workflowData: { activeSessions: response.activeSessions, archivedSessions: response.archivedSessions, }, sessionDataStore, }, false, 'switchWorkspace' ); // Trigger query invalidation callback const callback = get()._invalidateQueriesCallback; if (callback) { callback(); } }, removeRecentPath: async (path: string) => { const updatedPaths = await apiRemoveRecentPath(path); set({ recentPaths: updatedPaths }, false, 'removeRecentPath'); }, refreshRecentPaths: async () => { const paths = await fetchRecentPaths(); set({ recentPaths: paths }, false, 'refreshRecentPaths'); }, registerQueryInvalidator: (callback: () => void) => { set({ _invalidateQueriesCallback: callback }, false, 'registerQueryInvalidator'); }, // ========== Filters and Sorting ========== setFilters: (filters: Partial) => { set( (state) => ({ filters: { ...state.filters, ...filters }, }), false, 'setFilters' ); }, setSorting: (sorting: Partial) => { set( (state) => ({ sorting: { ...state.sorting, ...sorting }, }), false, 'setSorting' ); }, resetFilters: () => { set({ filters: defaultFilters, sorting: defaultSorting }, false, 'resetFilters'); }, // ========== Computed Selectors ========== getActiveSession: () => { const { activeSessionId, sessionDataStore } = get(); if (!activeSessionId) return null; const key = sessionKey(activeSessionId); return sessionDataStore[key] || null; }, getFilteredSessions: () => { const { workflowData, filters, sorting } = get(); // Combine active and archived based on filter let sessions = [...workflowData.activeSessions, ...workflowData.archivedSessions]; // Apply status filter if (filters.status && filters.status.length > 0) { sessions = sessions.filter((s) => filters.status!.includes(s.status)); } // Apply search filter if (filters.search) { const searchLower = filters.search.toLowerCase(); sessions = sessions.filter( (s) => s.session_id.toLowerCase().includes(searchLower) || s.title?.toLowerCase().includes(searchLower) || s.description?.toLowerCase().includes(searchLower) ); } // Apply date range filter if (filters.dateRange.start || filters.dateRange.end) { sessions = sessions.filter((s) => { const createdAt = new Date(s.created_at); if (filters.dateRange.start && createdAt < filters.dateRange.start) return false; if (filters.dateRange.end && createdAt > filters.dateRange.end) return false; return true; }); } // Apply sorting sessions.sort((a, b) => { let comparison = 0; switch (sorting.field) { case 'created_at': comparison = new Date(a.created_at).getTime() - new Date(b.created_at).getTime(); break; case 'updated_at': comparison = new Date(a.updated_at || a.created_at).getTime() - new Date(b.updated_at || b.created_at).getTime(); break; case 'title': comparison = (a.title || a.session_id).localeCompare(b.title || b.session_id); break; case 'status': comparison = a.status.localeCompare(b.status); break; } return sorting.direction === 'desc' ? -comparison : comparison; }); return sessions; }, getSessionByKey: (key: string) => { return get().sessionDataStore[key]; }, }), { name: 'ccw-workflow-store', version: 1, // State version for migration support partialize: (state) => ({ projectPath: state.projectPath, }), migrate: (persistedState, version) => { // Migration logic for future state shape changes if (version < 1) { // No migrations needed for initial version // Example: if (version === 0) { persistedState.newField = defaultValue; } } return persistedState as typeof persistedState; }, onRehydrateStorage: () => { // Only log in development to avoid noise in production if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line no-console console.log('[WorkflowStore] Hydrating from localStorage...'); } return (state, error) => { if (error) { // eslint-disable-next-line no-console console.error('[WorkflowStore] Rehydration error:', error); return; } if (state?.projectPath) { if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line no-console console.log('[WorkflowStore] Found persisted projectPath, re-initializing workspace:', state.projectPath); } // Use setTimeout to ensure the store is fully initialized before calling switchWorkspace setTimeout(() => { if (state.switchWorkspace) { state.switchWorkspace(state.projectPath); } }, 0); } }; }, } ), { name: 'WorkflowStore' } ) ); // Selectors for common access patterns export const selectWorkflowData = (state: WorkflowStore) => state.workflowData; export const selectActiveSessions = (state: WorkflowStore) => state.workflowData.activeSessions; export const selectArchivedSessions = (state: WorkflowStore) => state.workflowData.archivedSessions; export const selectActiveSessionId = (state: WorkflowStore) => state.activeSessionId; export const selectProjectPath = (state: WorkflowStore) => state.projectPath; export const selectFilters = (state: WorkflowStore) => state.filters; export const selectSorting = (state: WorkflowStore) => state.sorting;