'use client' import { create } from 'zustand' import { subscribeWithSelector } from 'zustand/middleware' // Enhanced types for Mission Control export interface Session { id: string key: string kind: string age: string model: string tokens: string flags: string[] active: boolean startTime?: number lastActivity?: number messageCount?: number cost?: number } export interface LogEntry { id: string timestamp: number level: 'info' | 'warn' | 'error' | 'debug' source: string session?: string message: string data?: any } export interface CronJob { name: string schedule: string command: string enabled: boolean lastRun?: number nextRun?: number lastStatus?: 'success' | 'error' | 'running' lastError?: string } export interface SpawnRequest { id: string task: string model: string label: string timeoutSeconds: number status: 'pending' | 'running' | 'completed' | 'failed' createdAt: number completedAt?: number result?: string error?: string } export interface MemoryFile { path: string name: string type: 'file' | 'directory' size?: number modified?: number children?: MemoryFile[] } export interface TokenUsage { model: string sessionId: string date: string inputTokens: number outputTokens: number totalTokens: number cost: number } export interface ModelConfig { alias: string name: string provider: string description: string costPer1k: number } // Mission Control Phase 2 Types export interface Task { id: number title: string description?: string status: 'inbox' | 'assigned' | 'in_progress' | 'review' | 'quality_review' | 'done' priority: 'low' | 'medium' | 'high' | 'urgent' project_id?: number project_ticket_no?: number project_name?: string project_prefix?: string ticket_ref?: string assigned_to?: string created_by: string created_at: number updated_at: number due_date?: number estimated_hours?: number actual_hours?: number outcome?: 'success' | 'failed' | 'partial' | 'abandoned' error_message?: string resolution?: string feedback_rating?: number feedback_notes?: string retry_count?: number completed_at?: number tags?: string[] metadata?: any } export interface Agent { id: number name: string role: string session_key?: string soul_content?: string status: 'offline' | 'idle' | 'busy' | 'error' last_seen?: number last_activity?: string created_at: number updated_at: number config?: any taskStats?: { total: number assigned: number in_progress: number completed: number } } export interface Activity { id: number type: string entity_type: string entity_id: number actor: string description: string data?: any created_at: number entity?: { type: string id?: number title?: string name?: string status?: string content_preview?: string task_title?: string } } export interface Notification { id: number recipient: string type: string title: string message: string source_type?: string source_id?: number read_at?: number delivered_at?: number created_at: number source?: { type: string id?: number title?: string name?: string status?: string content_preview?: string task_title?: string } } export interface Comment { id: number task_id: number author: string content: string created_at: number parent_id?: number mentions?: string[] replies?: Comment[] } export interface ChatMessage { id: number conversation_id: string from_agent: string to_agent: string | null content: string message_type: 'text' | 'system' | 'handoff' | 'status' | 'command' metadata?: any read_at?: number created_at: number pendingStatus?: 'sending' | 'sent' | 'failed' } export interface Conversation { id: string name?: string participants: string[] lastMessage?: ChatMessage unreadCount: number updatedAt: number } export interface StandupReport { date: string generatedAt: string summary: { totalAgents: number totalCompleted: number totalInProgress: number totalAssigned: number totalReview: number totalBlocked: number totalActivity: number overdue: number } agentReports: Array<{ agent: { name: string role: string status: string last_seen?: number last_activity?: string } completedToday: Task[] inProgress: Task[] assigned: Task[] review: Task[] blocked: Task[] activity: { actionCount: number commentsCount: number } }> teamAccomplishments: Task[] teamBlockers: Task[] overdueTasks: Task[] } export interface CurrentUser { id: number username: string display_name: string role: 'admin' | 'operator' | 'viewer' provider?: 'local' | 'google' email?: string | null avatar_url?: string | null } export interface ConnectionStatus { isConnected: boolean url: string lastConnected?: Date reconnectAttempts: number latency?: number sseConnected?: boolean } interface MissionControlStore { // WebSocket & Connection connection: ConnectionStatus lastMessage: any setConnection: (connection: Partial) => void setLastMessage: (message: any) => void // Mission Control Phase 2 - Tasks tasks: Task[] selectedTask: Task | null setTasks: (tasks: Task[]) => void setSelectedTask: (task: Task | null) => void addTask: (task: Task) => void updateTask: (taskId: number, updates: Partial) => void deleteTask: (taskId: number) => void // Mission Control Phase 2 - Agents agents: Agent[] selectedAgent: Agent | null setAgents: (agents: Agent[]) => void setSelectedAgent: (agent: Agent | null) => void addAgent: (agent: Agent) => void updateAgent: (agentId: number, updates: Partial) => void deleteAgent: (agentId: number) => void // Mission Control Phase 2 - Activities activities: Activity[] setActivities: (activities: Activity[]) => void addActivity: (activity: Activity) => void // Mission Control Phase 2 - Notifications notifications: Notification[] unreadNotificationCount: number setNotifications: (notifications: Notification[]) => void addNotification: (notification: Notification) => void markNotificationRead: (notificationId: number) => void markAllNotificationsRead: () => void // Mission Control Phase 2 - Comments taskComments: Record setTaskComments: (taskId: number, comments: Comment[]) => void addTaskComment: (taskId: number, comment: Comment) => void // Mission Control Phase 2 - Standup standupReports: StandupReport[] currentStandupReport: StandupReport | null setStandupReports: (reports: StandupReport[]) => void setCurrentStandupReport: (report: StandupReport | null) => void // Sessions sessions: Session[] selectedSession: string | null setSessions: (sessions: Session[]) => void setSelectedSession: (sessionId: string | null) => void updateSession: (sessionId: string, updates: Partial) => void // Logs logs: LogEntry[] logFilters: { level?: string source?: string session?: string search?: string } addLog: (log: LogEntry) => void setLogFilters: (filters: Partial<{ level?: string source?: string session?: string search?: string }>) => void clearLogs: () => void // Agent Spawning spawnRequests: SpawnRequest[] addSpawnRequest: (request: SpawnRequest) => void updateSpawnRequest: (id: string, updates: Partial) => void // Cron Management cronJobs: CronJob[] setCronJobs: (jobs: CronJob[]) => void updateCronJob: (name: string, updates: Partial) => void // Memory Browser memoryFiles: MemoryFile[] selectedMemoryFile: string | null memoryContent: string | null setMemoryFiles: (files: MemoryFile[]) => void setSelectedMemoryFile: (path: string | null) => void setMemoryContent: (content: string | null) => void // Token Usage & Cost Tracking tokenUsage: TokenUsage[] addTokenUsage: (usage: TokenUsage) => void getUsageByModel: (timeframe: 'day' | 'week' | 'month') => Record getTotalCost: (timeframe: 'day' | 'week' | 'month') => number // Model Configuration availableModels: ModelConfig[] setAvailableModels: (models: ModelConfig[]) => void // Agent Chat chatMessages: ChatMessage[] conversations: Conversation[] activeConversation: string | null chatInput: string isSendingMessage: boolean chatPanelOpen: boolean setChatMessages: (messages: ChatMessage[]) => void addChatMessage: (message: ChatMessage) => void replacePendingMessage: (tempId: number, message: ChatMessage) => void updatePendingMessage: (tempId: number, updates: Partial) => void removePendingMessage: (tempId: number) => void setConversations: (conversations: Conversation[]) => void setActiveConversation: (conversationId: string | null) => void setChatInput: (input: string) => void setIsSendingMessage: (loading: boolean) => void setChatPanelOpen: (open: boolean) => void markConversationRead: (conversationId: string) => void // Auth currentUser: CurrentUser | null setCurrentUser: (user: CurrentUser | null) => void // UI State activeTab: string sidebarExpanded: boolean collapsedGroups: string[] liveFeedOpen: boolean setActiveTab: (tab: string) => void toggleSidebar: () => void setSidebarExpanded: (expanded: boolean) => void toggleGroup: (groupId: string) => void toggleLiveFeed: () => void } export const useMissionControl = create()( subscribeWithSelector((set, get) => ({ // Connection state connection: { isConnected: false, url: '', reconnectAttempts: 0 }, lastMessage: null, setConnection: (connection) => set((state) => ({ connection: { ...state.connection, ...connection } })), setLastMessage: (message) => set({ lastMessage: message }), // Sessions sessions: [], selectedSession: null, setSessions: (sessions) => set({ sessions }), setSelectedSession: (sessionId) => set({ selectedSession: sessionId }), updateSession: (sessionId, updates) => set((state) => ({ sessions: state.sessions.map((session) => session.id === sessionId ? { ...session, ...updates } : session ), })), // Logs logs: [], logFilters: {}, addLog: (log) => set((state) => { // Check if log already exists to prevent duplicates const existingLogIndex = state.logs.findIndex(existingLog => existingLog.id === log.id) if (existingLogIndex !== -1) { // Update existing log const updatedLogs = [...state.logs] updatedLogs[existingLogIndex] = log return { logs: updatedLogs } } // Add new log at the beginning (newest first) return { logs: [log, ...state.logs].slice(0, 1000), // Keep last 1000 logs } }), setLogFilters: (filters) => set((state) => ({ logFilters: { ...state.logFilters, ...filters }, })), clearLogs: () => set({ logs: [] }), // Agent Spawning spawnRequests: [], addSpawnRequest: (request) => set((state) => ({ spawnRequests: [request, ...state.spawnRequests], })), updateSpawnRequest: (id, updates) => set((state) => ({ spawnRequests: state.spawnRequests.map((req) => req.id === id ? { ...req, ...updates } : req ), })), // Cron Management cronJobs: [], setCronJobs: (jobs) => set({ cronJobs: jobs }), updateCronJob: (name, updates) => set((state) => ({ cronJobs: state.cronJobs.map((job) => job.name === name ? { ...job, ...updates } : job ), })), // Memory Browser memoryFiles: [], selectedMemoryFile: null, memoryContent: null, setMemoryFiles: (files) => set({ memoryFiles: files }), setSelectedMemoryFile: (path) => set({ selectedMemoryFile: path }), setMemoryContent: (content) => set({ memoryContent: content }), // Token Usage tokenUsage: [], addTokenUsage: (usage) => set((state) => ({ tokenUsage: [...state.tokenUsage, usage], })), getUsageByModel: (timeframe) => { const { tokenUsage } = get() const now = new Date() let cutoff: Date switch (timeframe) { case 'day': cutoff = new Date(now.getTime() - 24 * 60 * 60 * 1000) break case 'week': cutoff = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) break case 'month': cutoff = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000) break default: cutoff = new Date(0) } return tokenUsage .filter((usage) => new Date(usage.date) >= cutoff) .reduce((acc, usage) => { acc[usage.model] = (acc[usage.model] || 0) + usage.totalTokens return acc }, {} as Record) }, getTotalCost: (timeframe) => { const { tokenUsage } = get() const now = new Date() let cutoff: Date switch (timeframe) { case 'day': cutoff = new Date(now.getTime() - 24 * 60 * 60 * 1000) break case 'week': cutoff = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) break case 'month': cutoff = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000) break default: cutoff = new Date(0) } return tokenUsage .filter((usage) => new Date(usage.date) >= cutoff) .reduce((acc, usage) => acc + usage.cost, 0) }, // Model Configuration availableModels: [ { alias: 'haiku', name: 'anthropic/claude-3-5-haiku-latest', provider: 'anthropic', description: 'Ultra-cheap, simple tasks', costPer1k: 0.25 }, { alias: 'sonnet', name: 'anthropic/claude-sonnet-4-20250514', provider: 'anthropic', description: 'Standard workhorse', costPer1k: 3.0 }, { alias: 'opus', name: 'anthropic/claude-opus-4-5', provider: 'anthropic', description: 'Premium quality', costPer1k: 15.0 }, { alias: 'deepseek', name: 'ollama/deepseek-r1:14b', provider: 'ollama', description: 'Local reasoning (free)', costPer1k: 0.0 }, { alias: 'groq-fast', name: 'groq/llama-3.1-8b-instant', provider: 'groq', description: '840 tok/s, ultra fast', costPer1k: 0.05 }, { alias: 'groq', name: 'groq/llama-3.3-70b-versatile', provider: 'groq', description: 'Fast + quality balance', costPer1k: 0.59 }, { alias: 'kimi', name: 'moonshot/kimi-k2.5', provider: 'moonshot', description: 'Alternative provider', costPer1k: 1.0 }, { alias: 'minimax', name: 'minimax/minimax-m2.1', provider: 'minimax', description: 'Cost-effective (1/10th price), strong coding', costPer1k: 0.3 }, ], setAvailableModels: (models) => set({ availableModels: models }), // Auth currentUser: null, setCurrentUser: (user) => set({ currentUser: user }), // UI State — sidebar & layout persistence activeTab: 'overview', sidebarExpanded: (() => { if (typeof window === 'undefined') return false try { return localStorage.getItem('mc-sidebar-expanded') === 'true' } catch { return false } })(), collapsedGroups: (() => { if (typeof window === 'undefined') return [] as string[] try { const raw = localStorage.getItem('mc-sidebar-groups') return raw ? JSON.parse(raw) as string[] : [] } catch { return [] as string[] } })(), liveFeedOpen: (() => { if (typeof window === 'undefined') return true try { return localStorage.getItem('mc-livefeed-open') !== 'false' } catch { return true } })(), setActiveTab: (tab) => set({ activeTab: tab }), toggleSidebar: () => set((state) => { const next = !state.sidebarExpanded try { localStorage.setItem('mc-sidebar-expanded', String(next)) } catch {} return { sidebarExpanded: next } }), setSidebarExpanded: (expanded) => { try { localStorage.setItem('mc-sidebar-expanded', String(expanded)) } catch {} set({ sidebarExpanded: expanded }) }, toggleGroup: (groupId) => set((state) => { const next = state.collapsedGroups.includes(groupId) ? state.collapsedGroups.filter(g => g !== groupId) : [...state.collapsedGroups, groupId] try { localStorage.setItem('mc-sidebar-groups', JSON.stringify(next)) } catch {} return { collapsedGroups: next } }), toggleLiveFeed: () => set((state) => { const next = !state.liveFeedOpen try { localStorage.setItem('mc-livefeed-open', String(next)) } catch {} return { liveFeedOpen: next } }), // Mission Control Phase 2 - Tasks tasks: [], selectedTask: null, setTasks: (tasks) => set({ tasks }), setSelectedTask: (task) => set({ selectedTask: task }), addTask: (task) => set((state) => ({ tasks: [task, ...state.tasks] })), updateTask: (taskId, updates) => set((state) => ({ tasks: state.tasks.map((task) => task.id === taskId ? { ...task, ...updates } : task ), selectedTask: state.selectedTask?.id === taskId ? { ...state.selectedTask, ...updates } : state.selectedTask })), deleteTask: (taskId) => set((state) => ({ tasks: state.tasks.filter((task) => task.id !== taskId), selectedTask: state.selectedTask?.id === taskId ? null : state.selectedTask })), // Mission Control Phase 2 - Agents agents: [], selectedAgent: null, setAgents: (agents) => set({ agents }), setSelectedAgent: (agent) => set({ selectedAgent: agent }), addAgent: (agent) => set((state) => ({ agents: [agent, ...state.agents] })), updateAgent: (agentId, updates) => set((state) => ({ agents: state.agents.map((agent) => agent.id === agentId ? { ...agent, ...updates } : agent ), selectedAgent: state.selectedAgent?.id === agentId ? { ...state.selectedAgent, ...updates } : state.selectedAgent })), deleteAgent: (agentId) => set((state) => ({ agents: state.agents.filter((agent) => agent.id !== agentId), selectedAgent: state.selectedAgent?.id === agentId ? null : state.selectedAgent })), // Mission Control Phase 2 - Activities activities: [], setActivities: (activities) => set({ activities }), addActivity: (activity) => set((state) => ({ activities: [activity, ...state.activities].slice(0, 1000) // Keep last 1000 })), // Mission Control Phase 2 - Notifications notifications: [], unreadNotificationCount: 0, setNotifications: (notifications) => set({ notifications, unreadNotificationCount: notifications.filter(n => !n.read_at).length }), addNotification: (notification) => set((state) => ({ notifications: [notification, ...state.notifications], unreadNotificationCount: state.unreadNotificationCount + 1 })), markNotificationRead: (notificationId) => set((state) => ({ notifications: state.notifications.map((notification) => notification.id === notificationId ? { ...notification, read_at: Math.floor(Date.now() / 1000) } : notification ), unreadNotificationCount: Math.max(0, state.unreadNotificationCount - 1) })), markAllNotificationsRead: () => set((state) => ({ notifications: state.notifications.map((notification) => notification.read_at ? notification : { ...notification, read_at: Math.floor(Date.now() / 1000) } ), unreadNotificationCount: 0 })), // Mission Control Phase 2 - Comments taskComments: {}, setTaskComments: (taskId, comments) => set((state) => ({ taskComments: { ...state.taskComments, [taskId]: comments } })), addTaskComment: (taskId, comment) => set((state) => ({ taskComments: { ...state.taskComments, [taskId]: [comment, ...(state.taskComments[taskId] || [])] } })), // Agent Chat chatMessages: [], conversations: [], activeConversation: null, chatInput: '', isSendingMessage: false, chatPanelOpen: false, setChatMessages: (messages) => set({ chatMessages: messages.slice(-500) }), addChatMessage: (message) => set((state) => { // Deduplicate: skip if a message with the same server ID already exists if (message.id > 0 && state.chatMessages.some(m => m.id === message.id)) { return state } const messages = [...state.chatMessages, message].slice(-500) const conversations = state.conversations.map((conv) => conv.id === message.conversation_id ? { ...conv, lastMessage: message, updatedAt: message.created_at } : conv ) return { chatMessages: messages, conversations } }), replacePendingMessage: (tempId, message) => set((state) => ({ chatMessages: state.chatMessages.map(m => m.id === tempId ? { ...message, pendingStatus: 'sent' } : m ), })), updatePendingMessage: (tempId, updates) => set((state) => ({ chatMessages: state.chatMessages.map(m => m.id === tempId ? { ...m, ...updates } : m ), })), removePendingMessage: (tempId) => set((state) => ({ chatMessages: state.chatMessages.filter(m => m.id !== tempId), })), setConversations: (conversations) => set({ conversations }), setActiveConversation: (conversationId) => set({ activeConversation: conversationId }), setChatInput: (input) => set({ chatInput: input }), setIsSendingMessage: (loading) => set({ isSendingMessage: loading }), setChatPanelOpen: (open) => set({ chatPanelOpen: open }), markConversationRead: (conversationId) => set((state) => ({ conversations: state.conversations.map((conv) => conv.id === conversationId ? { ...conv, unreadCount: 0 } : conv ), chatMessages: state.chatMessages.map((msg) => msg.conversation_id === conversationId && !msg.read_at ? { ...msg, read_at: Math.floor(Date.now() / 1000) } : msg ) })), // Mission Control Phase 2 - Standup standupReports: [], currentStandupReport: null, setStandupReports: (reports) => set({ standupReports: reports }), setCurrentStandupReport: (report) => set({ currentStandupReport: report }), })) )