'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { useMissionControl } from '@/store' import { useSmartPoll } from '@/lib/use-smart-poll' import { AgentAvatar } from '@/components/ui/agent-avatar' import { MarkdownRenderer } from '@/components/markdown-renderer' interface Task { id: number title: string description?: string status: 'inbox' | 'assigned' | 'in_progress' | 'review' | 'quality_review' | 'done' priority: 'low' | 'medium' | 'high' | 'critical' | 'urgent' assigned_to?: string created_by: string created_at: number updated_at: number due_date?: number estimated_hours?: number actual_hours?: number tags?: string[] metadata?: any aegisApproved?: boolean } interface Agent { id: number name: string role: string status: 'offline' | 'idle' | 'busy' | 'error' taskStats?: { total: number assigned: number in_progress: number completed: number } } interface Comment { id: number task_id: number author: string content: string created_at: number parent_id?: number mentions?: string[] replies?: Comment[] } const statusColumns = [ { key: 'inbox', title: 'Inbox', color: 'bg-secondary text-foreground' }, { key: 'assigned', title: 'Assigned', color: 'bg-blue-500/20 text-blue-400' }, { key: 'in_progress', title: 'In Progress', color: 'bg-yellow-500/20 text-yellow-400' }, { key: 'review', title: 'Review', color: 'bg-purple-500/20 text-purple-400' }, { key: 'quality_review', title: 'Quality Review', color: 'bg-indigo-500/20 text-indigo-400' }, { key: 'done', title: 'Done', color: 'bg-green-500/20 text-green-400' }, ] const priorityColors: Record = { low: 'border-green-500', medium: 'border-yellow-500', high: 'border-orange-500', critical: 'border-red-500', } export function TaskBoardPanel() { const { tasks: storeTasks, setTasks: storeSetTasks, selectedTask, setSelectedTask } = useMissionControl() const [agents, setAgents] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [aegisMap, setAegisMap] = useState>({}) const [draggedTask, setDraggedTask] = useState(null) const [showCreateModal, setShowCreateModal] = useState(false) const [editingTask, setEditingTask] = useState(null) const dragCounter = useRef(0) // Augment store tasks with aegisApproved flag (computed, not stored) const tasks: Task[] = storeTasks.map(t => ({ ...t, aegisApproved: Boolean(aegisMap[t.id]) })) // Fetch tasks and agents const fetchData = useCallback(async () => { try { setLoading(true) setError(null) const [tasksResponse, agentsResponse] = await Promise.all([ fetch('/api/tasks'), fetch('/api/agents') ]) if (!tasksResponse.ok || !agentsResponse.ok) { throw new Error('Failed to fetch data') } const tasksData = await tasksResponse.json() const agentsData = await agentsResponse.json() const tasksList = tasksData.tasks || [] const taskIds = tasksList.map((task: Task) => task.id) let newAegisMap: Record = {} if (taskIds.length > 0) { try { const reviewResponse = await fetch(`/api/quality-review?taskIds=${taskIds.join(',')}`) if (reviewResponse.ok) { const reviewData = await reviewResponse.json() const latest = reviewData.latest || {} newAegisMap = Object.fromEntries( Object.entries(latest).map(([id, row]: [string, any]) => [ Number(id), row?.reviewer === 'aegis' && row?.status === 'approved' ]) ) } } catch { newAegisMap = {} } } storeSetTasks(tasksList) setAegisMap(newAegisMap) setAgents(agentsData.agents || []) } catch (err) { setError(err instanceof Error ? err.message : 'An error occurred') } finally { setLoading(false) } }, [storeSetTasks]) useEffect(() => { fetchData() }, [fetchData]) // Poll as SSE fallback — pauses when SSE is delivering events useSmartPoll(fetchData, 30000, { pauseWhenSseConnected: true }) // Group tasks by status const tasksByStatus = statusColumns.reduce((acc, column) => { acc[column.key] = tasks.filter(task => task.status === column.key) return acc }, {} as Record) // Drag and drop handlers const handleDragStart = (e: React.DragEvent, task: Task) => { setDraggedTask(task) e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData('text/html', e.currentTarget.outerHTML) } const handleDragEnter = (e: React.DragEvent, status: string) => { e.preventDefault() dragCounter.current++ e.currentTarget.classList.add('drag-over') } const handleDragLeave = (e: React.DragEvent) => { dragCounter.current-- if (dragCounter.current === 0) { e.currentTarget.classList.remove('drag-over') } } const handleDragOver = (e: React.DragEvent) => { e.preventDefault() } const { updateTask } = useMissionControl() const handleDrop = async (e: React.DragEvent, newStatus: string) => { e.preventDefault() dragCounter.current = 0 e.currentTarget.classList.remove('drag-over') if (!draggedTask || draggedTask.status === newStatus) { setDraggedTask(null) return } const previousStatus = draggedTask.status try { if (newStatus === 'done') { const reviewResponse = await fetch(`/api/quality-review?taskId=${draggedTask.id}`) if (!reviewResponse.ok) { throw new Error('Unable to verify Aegis approval') } const reviewData = await reviewResponse.json() const latest = reviewData.reviews?.find((review: any) => review.reviewer === 'aegis') if (!latest || latest.status !== 'approved') { throw new Error('Aegis approval is required before moving to done') } } // Optimistically update via Zustand store updateTask(draggedTask.id, { status: newStatus as Task['status'], updated_at: Math.floor(Date.now() / 1000) }) // Update on server const response = await fetch('/api/tasks', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tasks: [{ id: draggedTask.id, status: newStatus }] }) }) if (!response.ok) { const data = await response.json().catch(() => ({})) throw new Error(data.error || 'Failed to update task status') } } catch (err) { // Revert optimistic update via Zustand store updateTask(draggedTask.id, { status: previousStatus }) setError(err instanceof Error ? err.message : 'Failed to update task status') } finally { setDraggedTask(null) } } // Format relative time for tasks const formatTaskTimestamp = (timestamp: number) => { const now = new Date().getTime() const time = new Date(timestamp * 1000).getTime() const diff = now - time const seconds = Math.floor(diff / 1000) const minutes = Math.floor(seconds / 60) const hours = Math.floor(minutes / 60) const days = Math.floor(hours / 24) if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago` if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago` if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago` return 'just now' } const getTagColor = (tag: string) => { const lowerTag = tag.toLowerCase() if (lowerTag.includes('urgent') || lowerTag.includes('critical')) { return 'bg-red-500/20 text-red-400 border-red-500/30' } if (lowerTag.includes('bug') || lowerTag.includes('fix')) { return 'bg-orange-500/20 text-orange-400 border-orange-500/30' } if (lowerTag.includes('feature') || lowerTag.includes('enhancement')) { return 'bg-green-500/20 text-green-400 border-green-500/30' } if (lowerTag.includes('research') || lowerTag.includes('analysis')) { return 'bg-purple-500/20 text-purple-400 border-purple-500/30' } if (lowerTag.includes('deploy') || lowerTag.includes('release')) { return 'bg-blue-500/20 text-blue-400 border-blue-500/30' } return 'bg-muted-foreground/10 text-muted-foreground border-muted-foreground/20' } // Get agent name by session key const getAgentName = (sessionKey?: string) => { const agent = agents.find(a => a.name === sessionKey) return agent?.name || sessionKey || 'Unassigned' } if (loading) { return (
Loading tasks...
) } return (
{/* Header */}

Task Board

{/* Error Display */} {error && (
{error}
)} {/* Kanban Board */}
{statusColumns.map(column => (
handleDragEnter(e, column.key)} onDragLeave={handleDragLeave} onDragOver={handleDragOver} onDrop={(e) => handleDrop(e, column.key)} > {/* Column Header */}

{column.title}

{tasksByStatus[column.key]?.length || 0}
{/* Column Body */}
{tasksByStatus[column.key]?.map(task => (
handleDragStart(e, task)} onClick={() => setSelectedTask(task)} className={`bg-surface-1 rounded-lg p-3 cursor-pointer hover:bg-surface-2 transition-smooth border-l-4 ${priorityColors[task.priority]} ${ draggedTask?.id === task.id ? 'opacity-50' : '' }`} >

{task.title}

{task.aegisApproved && ( Aegis Approved )} {task.priority}
{task.description && (
)}
{task.assigned_to ? ( <> {getAgentName(task.assigned_to)} ) : ( Unassigned )} {formatTaskTimestamp(task.created_at)}
{task.tags && task.tags.length > 0 && (
{task.tags.slice(0, 3).map((tag, index) => ( {tag} ))} {task.tags.length > 3 && ( +{task.tags.length - 3} )}
)} {/* Enhanced timestamp display */} {task.updated_at && task.updated_at !== task.created_at && (
Updated {formatTaskTimestamp(task.updated_at)}
)} {task.due_date && (
Due: {formatTaskTimestamp(task.due_date)}
)}
))} {/* Empty State */} {tasksByStatus[column.key]?.length === 0 && (
No tasks in {column.title.toLowerCase()}
)}
))}
{/* Task Detail Modal */} {selectedTask && !editingTask && ( setSelectedTask(null)} onUpdate={fetchData} onEdit={(taskToEdit) => { setEditingTask(taskToEdit) setSelectedTask(null) }} /> )} {/* Create Task Modal */} {showCreateModal && ( setShowCreateModal(false)} onCreated={fetchData} /> )} {/* Edit Task Modal */} {editingTask && ( setEditingTask(null)} onUpdated={() => { fetchData(); setEditingTask(null) }} /> )}
) } // Task Detail Modal Component (placeholder - would be implemented separately) function TaskDetailModal({ task, agents, onClose, onUpdate, onEdit }: { task: Task agents: Agent[] onClose: () => void onUpdate: () => void onEdit: (task: Task) => void }) { const [comments, setComments] = useState([]) const [loadingComments, setLoadingComments] = useState(false) const [commentText, setCommentText] = useState('') const [commentAuthor, setCommentAuthor] = useState('system') const [commentError, setCommentError] = useState(null) const [broadcastMessage, setBroadcastMessage] = useState('') const [broadcastStatus, setBroadcastStatus] = useState(null) const [reviews, setReviews] = useState([]) const [reviewStatus, setReviewStatus] = useState<'approved' | 'rejected'>('approved') const [reviewNotes, setReviewNotes] = useState('') const [reviewError, setReviewError] = useState(null) const [activeTab, setActiveTab] = useState<'details' | 'comments' | 'quality'>('details') const [reviewer, setReviewer] = useState('aegis') const fetchReviews = useCallback(async () => { try { const response = await fetch(`/api/quality-review?taskId=${task.id}`) if (!response.ok) throw new Error('Failed to fetch reviews') const data = await response.json() setReviews(data.reviews || []) } catch (error) { setReviewError('Failed to load quality reviews') } }, [task.id]) const fetchComments = useCallback(async () => { try { setLoadingComments(true) const response = await fetch(`/api/tasks/${task.id}/comments`) if (!response.ok) throw new Error('Failed to fetch comments') const data = await response.json() setComments(data.comments || []) } catch (error) { setCommentError('Failed to load comments') } finally { setLoadingComments(false) } }, [task.id]) useEffect(() => { fetchComments() }, [fetchComments]) useEffect(() => { fetchReviews() }, [fetchReviews]) useSmartPoll(fetchComments, 15000) const handleAddComment = async (e: React.FormEvent) => { e.preventDefault() if (!commentText.trim()) return try { setCommentError(null) const response = await fetch(`/api/tasks/${task.id}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ author: commentAuthor || 'system', content: commentText }) }) if (!response.ok) throw new Error('Failed to add comment') setCommentText('') await fetchComments() onUpdate() } catch (error) { setCommentError('Failed to add comment') } } const handleBroadcast = async (e: React.FormEvent) => { e.preventDefault() if (!broadcastMessage.trim()) return try { setBroadcastStatus(null) const response = await fetch(`/api/tasks/${task.id}/broadcast`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ author: commentAuthor || 'system', message: broadcastMessage }) }) const data = await response.json() if (!response.ok) throw new Error(data.error || 'Broadcast failed') setBroadcastMessage('') setBroadcastStatus(`Sent to ${data.sent || 0} subscribers`) } catch (error) { setBroadcastStatus('Failed to broadcast') } } const handleSubmitReview = async (e: React.FormEvent) => { e.preventDefault() try { setReviewError(null) const response = await fetch('/api/quality-review', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId: task.id, reviewer, status: reviewStatus, notes: reviewNotes }) }) const data = await response.json() if (!response.ok) throw new Error(data.error || 'Failed to submit review') setReviewNotes('') await fetchReviews() onUpdate() } catch (error) { setReviewError('Failed to submit review') } } const renderComment = (comment: Comment, depth: number = 0) => (
0 ? 'ml-4' : ''}`}>
{comment.author} {new Date(comment.created_at * 1000).toLocaleString()}
{comment.content}
{comment.replies && comment.replies.length > 0 && (
{comment.replies.map(reply => renderComment(reply, depth + 1))}
)}
) return (

{task.title}

{task.description ? (
) : (

No description

)}
{(['details', 'comments', 'quality'] as const).map(tab => ( ))}
{activeTab === 'details' && (
Status: {task.status}
Priority: {task.priority}
Assigned to: {task.assigned_to ? ( <> {task.assigned_to} ) : ( Unassigned )}
Created: {new Date(task.created_at * 1000).toLocaleDateString()}
)} {activeTab === 'comments' && (

Comments

{commentError && (
{commentError}
)} {loadingComments ? (
Loading comments...
) : comments.length === 0 ? (
No comments yet.
) : (
{comments.map(comment => renderComment(comment))}
)}
setCommentAuthor(e.target.value)} className="w-full bg-surface-1 text-foreground border border-border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary/50" />