761 lines
22 KiB
TypeScript
761 lines
22 KiB
TypeScript
'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<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) => ({
|
|
// 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: [
|
|
{ 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 }),
|
|
}))
|
|
)
|