import React from 'react'; import { RiAddLine, RiDeleteBinLine, RiSendPlaneLine } from '@remixicon/react'; import { toast } from '@/components/ui'; import { Checkbox } from '@/components/ui/checkbox'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { getProjectNotesAndTodos, OPENCHAMBER_PROJECT_NOTES_MAX_LENGTH, OPENCHAMBER_PROJECT_TODO_TEXT_MAX_LENGTH, saveProjectNotesAndTodos, type OpenChamberProjectTodoItem, type ProjectRef, } from '@/lib/openchamberConfig'; import { useUIStore } from '@/stores/useUIStore'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useInputStore } from '@/sync/input-store'; import { createWorktreeDraft } from '@/lib/worktreeSessionCreator'; import { cn } from '@/lib/utils'; interface ProjectNotesTodoPanelProps { projectRef: ProjectRef | null; projectLabel?: string | null; canCreateWorktree?: boolean; onActionComplete?: () => void; className?: string; } const createTodoId = (): string => { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID(); } return `todo_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; }; export const ProjectNotesTodoPanel: React.FC = ({ projectRef, projectLabel, canCreateWorktree = false, onActionComplete, className, }) => { const [isLoading, setIsLoading] = React.useState(false); const [notes, setNotes] = React.useState(''); const [todos, setTodos] = React.useState([]); const [newTodoText, setNewTodoText] = React.useState(''); const [sendingTodoId, setSendingTodoId] = React.useState(null); const [expandedTodoIds, setExpandedTodoIds] = React.useState>(() => new Set()); const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft); const setPendingInputText = useInputStore((state) => state.setPendingInputText); const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); const persistProjectData = React.useCallback( async (nextNotes: string, nextTodos: OpenChamberProjectTodoItem[]) => { if (!projectRef) { return false; } const saved = await saveProjectNotesAndTodos(projectRef, { notes: nextNotes, todos: nextTodos, }); if (!saved) { toast.error('Failed to save project notes'); } return saved; }, [projectRef] ); React.useEffect(() => { if (!projectRef) { setNotes(''); setTodos([]); setNewTodoText(''); setExpandedTodoIds(new Set()); return; } let cancelled = false; setIsLoading(true); (async () => { try { const data = await getProjectNotesAndTodos(projectRef); if (cancelled) { return; } setNotes(data.notes); setTodos(data.todos); setNewTodoText(''); setExpandedTodoIds(new Set()); } catch { if (!cancelled) { toast.error('Failed to load project notes'); setNotes(''); setTodos([]); } } finally { if (!cancelled) { setIsLoading(false); } } })(); return () => { cancelled = true; }; }, [projectRef]); const handleNotesBlur = React.useCallback(() => { void persistProjectData(notes, todos); }, [notes, persistProjectData, todos]); const handleAddTodo = React.useCallback(() => { const trimmed = newTodoText.trim(); if (!trimmed) { return; } const nextTodos = [ ...todos, { id: createTodoId(), text: trimmed.slice(0, OPENCHAMBER_PROJECT_TODO_TEXT_MAX_LENGTH), completed: false, createdAt: Date.now(), }, ]; setTodos(nextTodos); setNewTodoText(''); void persistProjectData(notes, nextTodos); }, [newTodoText, notes, persistProjectData, todos]); const handleToggleTodoExpanded = React.useCallback((id: string) => { setExpandedTodoIds((previous) => { const next = new Set(previous); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }, []); const handleToggleTodo = React.useCallback( (id: string, completed: boolean) => { const nextTodos = todos.map((todo) => (todo.id === id ? { ...todo, completed } : todo)); setTodos(nextTodos); void persistProjectData(notes, nextTodos); }, [notes, persistProjectData, todos] ); const handleDeleteTodo = React.useCallback( (id: string) => { const nextTodos = todos.filter((todo) => todo.id !== id); setTodos(nextTodos); void persistProjectData(notes, nextTodos); }, [notes, persistProjectData, todos] ); const handleClearCompletedTodos = React.useCallback(() => { const nextTodos = todos.filter((todo) => !todo.completed); if (nextTodos.length === todos.length) { return; } setTodos(nextTodos); void persistProjectData(notes, nextTodos); }, [notes, persistProjectData, todos]); const todoInputValue = newTodoText.slice(0, OPENCHAMBER_PROJECT_TODO_TEXT_MAX_LENGTH); const completedTodoCount = todos.reduce((count, todo) => count + (todo.completed ? 1 : 0), 0); const routeToChat = React.useCallback(() => { setActiveMainTab('chat'); setSessionSwitcherOpen(false); }, [setActiveMainTab, setSessionSwitcherOpen]); const handleSendToNewSession = React.useCallback( (todoText: string) => { if (!projectRef) { return; } routeToChat(); openNewSessionDraft({ directoryOverride: projectRef.path, initialPrompt: todoText, }); toast.success('Todo sent to new session'); onActionComplete?.(); }, [onActionComplete, openNewSessionDraft, projectRef, routeToChat] ); const handleSendToCurrentSession = React.useCallback( (todoText: string) => { if (!currentSessionId) { toast.error('No active session selected'); return; } routeToChat(); const fenced = `\`\`\`md\n${todoText}\n\`\`\``; setPendingInputText(fenced, 'append'); toast.success('Todo sent to current session'); onActionComplete?.(); }, [currentSessionId, onActionComplete, routeToChat, setPendingInputText] ); const handleSendToNewWorktreeSession = React.useCallback( async (todoId: string, todoText: string) => { if (!projectRef) { return; } if (!canCreateWorktree) { toast.error('Worktree actions are only available for Git repositories'); return; } setSendingTodoId(todoId); try { routeToChat(); const newWorktreePath = await createWorktreeDraft({ initialPrompt: todoText }); if (!newWorktreePath) { return; } toast.success('Todo sent to new worktree session'); onActionComplete?.(); } finally { setSendingTodoId(null); } }, [canCreateWorktree, onActionComplete, projectRef, routeToChat] ); if (!projectRef) { return (

Select a project to add notes and todos.

); } return (

Quick notes - {projectLabel?.trim() || projectRef.path.split('/').filter(Boolean).pop() || projectRef.path}

{notes.length}/{OPENCHAMBER_PROJECT_NOTES_MAX_LENGTH}