# Componentes Principales ## KanbanBoard ```typescript // components/kanban/KanbanBoard.tsx import { useMemo } from 'react' import { DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core' import { useTasks, useUpdateTask } from '@/hooks/useTasks' import KanbanColumn from './KanbanColumn' import { Task, TaskState } from '@/types/task' const COLUMNS: { id: TaskState; title: string; color: string }[] = [ { id: 'backlog', title: 'Backlog', color: 'gray' }, { id: 'in_progress', title: 'En Progreso', color: 'blue' }, { id: 'needs_input', title: 'Necesita Respuestas', color: 'yellow' }, { id: 'ready_to_test', title: 'Listo para Probar', color: 'purple' }, { id: 'approved', title: 'Aprobado', color: 'green' }, { id: 'staging', title: 'Staging', color: 'indigo' }, { id: 'production', title: 'Producción', color: 'emerald' }, ] interface KanbanBoardProps { projectId: string } export function KanbanBoard({ projectId }: KanbanBoardProps) { const { data: tasks = [], isLoading } = useTasks({ projectId }) const updateTask = useUpdateTask() const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }) ) const tasksByState = useMemo(() => { return COLUMNS.reduce((acc, column) => { acc[column.id] = tasks.filter((task) => task.state === column.id) return acc }, {} as Record) }, [tasks]) const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event if (!over || active.id === over.id) return const taskId = active.id as string const newState = over.id as TaskState updateTask.mutate({ taskId, updates: { state: newState }, }) } if (isLoading) { return
Loading...
} return (
{COLUMNS.map((column) => ( ))}
) } ``` ## KanbanColumn ```typescript // components/kanban/KanbanColumn.tsx import { useDroppable } from '@dnd-kit/core' import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' import TaskCard from './TaskCard' import { Task, TaskState } from '@/types/task' interface KanbanColumnProps { id: TaskState title: string color: string tasks: Task[] } export default function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) { const { setNodeRef } = useDroppable({ id }) return (

{title} ({tasks.length})

t.id)} strategy={verticalListSortingStrategy}>
{tasks.map((task) => ( ))}
{tasks.length === 0 && (
Sin tareas
)}
) } ``` ## TaskCard ```typescript // components/kanban/TaskCard.tsx import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { Clock, User, GitBranch, AlertCircle } from 'lucide-react' import { Task } from '@/types/task' import { useNavigate } from 'react-router-dom' interface TaskCardProps { task: Task } const PRIORITY_COLORS = { low: 'bg-gray-100 text-gray-800', medium: 'bg-blue-100 text-blue-800', high: 'bg-orange-100 text-orange-800', urgent: 'bg-red-100 text-red-800', } export default function TaskCard({ task }: TaskCardProps) { const navigate = useNavigate() const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id, }) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, } return (
navigate(`/tasks/${task.id}`)} >

{task.title}

{task.priority}
{task.description && (

{task.description}

)}
{task.assignedAgent && (
Agent {task.assignedAgent.podName.slice(0, 8)}
)} {task.branchName && (
{task.branchName}
)} {task.state === 'needs_input' && (
Pregunta pendiente
)}
{task.actualDurationMinutes && (
{task.actualDurationMinutes}min
)} {task.previewUrl && ( e.stopPropagation()} > Ver Preview → )}
) } ``` ## WebTerminal ```typescript // components/terminal/WebTerminal.tsx import { useEffect, useRef } from 'react' import { Terminal } from 'xterm' import { FitAddon } from 'xterm-addon-fit' import { WebLinksAddon } from 'xterm-addon-web-links' import 'xterm/css/xterm.css' interface WebTerminalProps { agentId: string podName: string } export function WebTerminal({ agentId, podName }: WebTerminalProps) { const terminalRef = useRef(null) const xtermRef = useRef() const fitAddonRef = useRef() useEffect(() => { if (!terminalRef.current) return // Create terminal const term = new Terminal({ cursorBlink: true, fontSize: 14, fontFamily: 'Menlo, Monaco, "Courier New", monospace', theme: { background: '#1e1e1e', foreground: '#d4d4d4', }, }) const fitAddon = new FitAddon() const webLinksAddon = new WebLinksAddon() term.loadAddon(fitAddon) term.loadAddon(webLinksAddon) term.open(terminalRef.current) fitAddon.fit() xtermRef.current = term fitAddonRef.current = fitAddon // Connect to backend WebSocket for terminal const ws = new WebSocket(`ws://localhost:3000/terminal/${agentId}`) ws.onopen = () => { term.writeln(`Connected to ${podName}`) term.writeln('') } ws.onmessage = (event) => { term.write(event.data) } term.onData((data) => { ws.send(data) }) // Handle resize const handleResize = () => { fitAddon.fit() } window.addEventListener('resize', handleResize) return () => { term.dispose() ws.close() window.removeEventListener('resize', handleResize) } }, [agentId, podName]) return (
) } ``` ## TaskForm ```typescript // components/tasks/TaskForm.tsx import { useState } from 'react' import { useCreateTask } from '@/hooks/useTasks' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { Select } from '@/components/ui/Select' import { toast } from 'react-hot-toast' interface TaskFormProps { projectId: string onSuccess?: () => void } export function TaskForm({ projectId, onSuccess }: TaskFormProps) { const [title, setTitle] = useState('') const [description, setDescription] = useState('') const [priority, setPriority] = useState<'low' | 'medium' | 'high' | 'urgent'>('medium') const createTask = useCreateTask() const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!title.trim()) { toast.error('El título es requerido') return } try { await createTask.mutateAsync({ projectId, title, description, priority, }) toast.success('Tarea creada') setTitle('') setDescription('') setPriority('medium') onSuccess?.() } catch (error) { toast.error('Error al crear tarea') } } return (
setTitle(e.target.value)} placeholder="Ej: Implementar autenticación" required />