mission-control/src/store/index.ts

796 lines
23 KiB
TypeScript

'use client'
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
import { MODEL_CATALOG } from '@/lib/models'
// 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 {
id?: string
name: string
schedule: string
command: string
model?: string
agentId?: string
timezone?: string
delivery?: 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' | 'critical' | '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 {
// Dashboard Mode (local vs full gateway)
dashboardMode: 'full' | 'local'
gatewayAvailable: boolean
bannerDismissed: boolean
subscription: { type: string; rateLimitTier?: string } | null
setDashboardMode: (mode: 'full' | 'local') => void
setGatewayAvailable: (available: boolean) => void
dismissBanner: () => void
setSubscription: (sub: { type: string; rateLimitTier?: string } | null) => void
// Update availability
updateAvailable: { latestVersion: string; releaseUrl: string; releaseNotes: string } | null
updateDismissedVersion: string | null
setUpdateAvailable: (info: { latestVersion: string; releaseUrl: string; releaseNotes: string } | null) => void
dismissUpdate: (version: string) => void
// WebSocket & Connection
connection: ConnectionStatus
lastMessage: any
setConnection: (connection: Partial<ConnectionStatus>) => 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<Task>) => 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<Agent>) => 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<number, Comment[]>
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<Session>) => 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<SpawnRequest>) => void
// Cron Management
cronJobs: CronJob[]
setCronJobs: (jobs: CronJob[]) => void
updateCronJob: (name: string, updates: Partial<CronJob>) => 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<string, number>
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<ChatMessage>) => 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<MissionControlStore>()(
subscribeWithSelector((set, get) => ({
// Dashboard Mode
dashboardMode: 'full' as const,
gatewayAvailable: true,
bannerDismissed: false,
subscription: null,
setDashboardMode: (mode) => set({ dashboardMode: mode }),
setGatewayAvailable: (available) => set({ gatewayAvailable: available }),
dismissBanner: () => set({ bannerDismissed: true }),
setSubscription: (sub) => set({ subscription: sub }),
// Update availability
updateAvailable: null,
updateDismissedVersion: (() => {
if (typeof window === 'undefined') return null
try { return localStorage.getItem('mc-update-dismissed-version') } catch { return null }
})(),
setUpdateAvailable: (info) => set({ updateAvailable: info }),
dismissUpdate: (version) => {
try { localStorage.setItem('mc-update-dismissed-version', version) } catch {}
set({ updateDismissedVersion: version })
},
// 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<string, number>)
},
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: [...MODEL_CATALOG],
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 }),
}))
)