import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Plus, Trash2, Pencil, Check, X, ChevronUp, ChevronDown, Loader2, ListChecks, Bot, PlusCircle, Lightbulb, } from "lucide-react"; import { getErrorMessage, type Task, type TaskCreateInput, type TodoItem, type TodoList } from "@fusion/core"; import { createTask, fetchAgents } from "../api"; import type { Agent } from "../api"; import { useTodoLists } from "../hooks/useTodoLists"; import { useConfirm } from "../hooks/useConfirm"; import "./TodoView.css"; interface TodoViewProps { projectId?: string; addToast: (message: string, type?: "success" | "error" | "info") => void; onPlanningMode?: (initialPlan: string) => void; onTaskCreated?: (task: Task) => void; onClose?: () => void; mobileKeyboardActive?: boolean; } function sortItems(items: TodoItem[]): TodoItem[] { return [...items].sort((a, b) => a.sortOrder - b.sortOrder); } export function TodoView({ projectId, addToast, onPlanningMode, onTaskCreated, mobileKeyboardActive = false, }: TodoViewProps) { const { lists, items, loading, error, selectedListId, setSelectedListId, createList, renameList, deleteList, createItem, updateItem, toggleItem, deleteItem, reorderItems, } = useTodoLists({ projectId, addToast: (message, type) => { if (type === "success" || type === "error" || type === "info" || type === undefined) { addToast(message, type); return; } addToast(message, "info"); }, }); const [editingListId, setEditingListId] = useState(null); const [editingListTitle, setEditingListTitle] = useState(""); const [editingItemId, setEditingItemId] = useState(null); const [editingItemText, setEditingItemText] = useState(""); const [newListTitle, setNewListTitle] = useState(""); const [isAddingList, setIsAddingList] = useState(false); const [newItemText, setNewItemText] = useState(""); const [agents, setAgents] = useState([]); const [agentsLoading, setAgentsLoading] = useState(false); const [showAgentPicker, setShowAgentPicker] = useState(false); const [activeItemForAgent, setActiveItemForAgent] = useState(null); const agentPickerRef = useRef(null); const { confirm } = useConfirm(); const selectedList = useMemo( () => lists.find((list) => list.id === selectedListId) ?? null, [lists, selectedListId], ); const sortedItems = useMemo( () => sortItems(items.filter((item) => item.listId === selectedListId)), [items, selectedListId], ); function resetListDraftState(): void { setEditingListId(null); setEditingListTitle(""); setNewListTitle(""); setIsAddingList(false); } function resetItemDraftState(): void { setEditingItemId(null); setEditingItemText(""); setNewItemText(""); } function handleSelectList(listId: string): void { resetListDraftState(); resetItemDraftState(); setSelectedListId(listId); } const loadAgents = useCallback(async () => { setAgentsLoading(true); try { const loadedAgents = await fetchAgents(undefined, projectId); setAgents(loadedAgents); setShowAgentPicker(true); } catch (err) { addToast(`Failed to load agents: ${getErrorMessage(err)}`, "error"); setShowAgentPicker(false); setActiveItemForAgent(null); } finally { setAgentsLoading(false); } }, [projectId, addToast]); useEffect(() => { setEditingListId(null); setEditingListTitle(""); setNewListTitle(""); setIsAddingList(false); setEditingItemId(null); setEditingItemText(""); setNewItemText(""); setShowAgentPicker(false); setActiveItemForAgent(null); }, [selectedListId]); useEffect(() => { if (!showAgentPicker) { return; } const handleClickOutside = (event: MouseEvent) => { if (agentPickerRef.current && !agentPickerRef.current.contains(event.target as Node)) { setShowAgentPicker(false); setActiveItemForAgent(null); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [showAgentPicker]); function handleStartRenameList(list: TodoList): void { resetItemDraftState(); setEditingListId(list.id); setEditingListTitle(list.title); setIsAddingList(false); } async function handleSaveRenameList(): Promise { if (!editingListId) { return; } const trimmedTitle = editingListTitle.trim(); if (!trimmedTitle) { setEditingListId(null); setEditingListTitle(""); return; } await renameList(editingListId, trimmedTitle); setEditingListId(null); setEditingListTitle(""); } function handleCancelRenameList(): void { setEditingListId(null); setEditingListTitle(""); } function handleStartEditItem(item: TodoItem): void { resetListDraftState(); setEditingItemId(item.id); setEditingItemText(item.text); } async function handleSaveEditItem(): Promise { if (!editingItemId) { return; } const trimmedText = editingItemText.trim(); if (!trimmedText) { setEditingItemId(null); setEditingItemText(""); return; } await updateItem(editingItemId, { text: trimmedText }); setEditingItemId(null); setEditingItemText(""); } function handleCancelEditItem(): void { setEditingItemId(null); setEditingItemText(""); } async function handleAddList(): Promise { const trimmedTitle = newListTitle.trim(); if (!trimmedTitle) { return; } await createList(trimmedTitle); setNewListTitle(""); setIsAddingList(false); } async function handleAddItem(): Promise { if (!selectedListId) { return; } const trimmedText = newItemText.trim(); if (!trimmedText) { return; } await createItem(trimmedText); setNewItemText(""); } async function handleDeleteList(id: string): Promise { const shouldDelete = await confirm({ title: "Delete List", message: "Delete this list and all its items?", danger: true, }); if (!shouldDelete) { return; } await deleteList(id); } async function handleDeleteItem(id: string): Promise { await deleteItem(id); } async function handleMoveItem(itemId: string, direction: "up" | "down"): Promise { const ids = sortedItems.map((item) => item.id); const index = ids.findIndex((id) => id === itemId); if (index < 0) { return; } const targetIndex = direction === "up" ? index - 1 : index + 1; if (targetIndex < 0 || targetIndex >= ids.length) { return; } [ids[index], ids[targetIndex]] = [ids[targetIndex], ids[index]]; await reorderItems(ids); } const handleCreateTaskFromItem = useCallback(async (item: TodoItem) => { try { const input: TaskCreateInput = { description: item.text, column: "triage", source: { sourceType: "dashboard_ui" }, }; const task: Task = await createTask(input, projectId); onTaskCreated?.(task); addToast(`Created ${task.id} from todo`, "success"); } catch (err) { addToast(`Failed to create task: ${getErrorMessage(err)}`, "error"); } }, [projectId, addToast, onTaskCreated]); const handleCreateTaskAndAssign = useCallback(async (item: TodoItem, agentId: string) => { try { const input: TaskCreateInput = { description: item.text, column: "triage", assignedAgentId: agentId, source: { sourceType: "dashboard_ui" }, }; const task: Task = await createTask(input, projectId); onTaskCreated?.(task); const assignedAgent = agents.find((agent) => agent.id === agentId); const agentLabel = assignedAgent?.name ?? agentId; addToast(`Created ${task.id} and assigned to ${agentLabel}`, "success"); setShowAgentPicker(false); setActiveItemForAgent(null); } catch (err) { addToast(`Failed to create and assign task: ${getErrorMessage(err)}`, "error"); } }, [projectId, addToast, agents, onTaskCreated]); if (loading) { return (
); } return (
{error && (
{error}
)} {!selectedList ? (
) : ( <>

{selectedList.title}

setNewItemText(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { void handleAddItem(); } if (event.key === "Escape") { setNewItemText(""); } }} data-testid="new-item-input" />
{sortedItems.length === 0 ? (

No items in this list. Add one above.

) : (
{sortedItems.map((item, index) => { const isEditing = item.id === editingItemId; return (
{ void toggleItem(item.id); }} className="todo-item-checkbox" aria-label={`Toggle ${item.text}`} data-testid={`toggle-item-${item.id}`} /> {isEditing ? ( setEditingItemText(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { void handleSaveEditItem(); } if (event.key === "Escape") { handleCancelEditItem(); } }} autoFocus data-testid={`edit-item-input-${item.id}`} /> ) : ( )}
{isEditing ? ( <> ) : ( <>
{showAgentPicker && activeItemForAgent === item.id && (
{ event.preventDefault(); }} > {agentsLoading ? (
Loading agents...
) : agents.length > 0 ? ( agents .map((agent) => ( )) ) : (
No agents available
)}
)}
)}
); })}
)} )}
); }