import React from "react"; import { RiArrowUpSLine, RiArrowDownSLine, RiCloseCircleLine } from "@remixicon/react"; import { cn } from "@/lib/utils"; import { useTodoStore, type TodoItem, type TodoStatus } from "@/stores/useTodoStore"; import { useSessionStore } from "@/stores/useSessionStore"; import { useUIStore } from "@/stores/useUIStore"; import { WorkingPlaceholder } from "./message/parts/WorkingPlaceholder"; import { isVSCodeRuntime } from "@/lib/desktop"; const statusConfig: Record = { in_progress: { textClassName: "text-foreground", }, pending: { textClassName: "text-foreground", }, completed: { textClassName: "text-muted-foreground line-through", }, cancelled: { textClassName: "text-muted-foreground line-through", }, }; interface TodoItemRowProps { todo: TodoItem; } const TodoItemRow: React.FC = ({ todo }) => { const config = statusConfig[todo.status] || statusConfig.pending; return (
{todo.content}
); }; const EMPTY_TODOS: TodoItem[] = []; interface StatusRowProps { // Working state isWorking: boolean; statusText: string | null; isGenericStatus?: boolean; isWaitingForPermission?: boolean; wasAborted?: boolean; abortActive?: boolean; completionId?: string | null; isComplete?: boolean; // Abort state (for mobile/vscode) showAbort?: boolean; onAbort?: () => void; // Abort status display showAbortStatus?: boolean; } export const StatusRow: React.FC = ({ isWorking, statusText, isGenericStatus, isWaitingForPermission, wasAborted, abortActive, completionId, isComplete, showAbort, onAbort, showAbortStatus, }) => { const [isExpanded, setIsExpanded] = React.useState(false); const currentSessionId = useSessionStore((state) => state.currentSessionId); const todos = useTodoStore((state) => currentSessionId ? state.sessionTodos.get(currentSessionId) ?? EMPTY_TODOS : EMPTY_TODOS ); const loadTodos = useTodoStore((state) => state.loadTodos); const { isMobile } = useUIStore(); const isCompact = isMobile || isVSCodeRuntime(); // Load todos when session changes React.useEffect(() => { if (currentSessionId) { void loadTodos(currentSessionId); } }, [currentSessionId, loadTodos]); // Filter out cancelled todos for display, sort by status priority const visibleTodos = React.useMemo(() => { const statusOrder: Record = { in_progress: 0, pending: 1, completed: 2, cancelled: 3, }; return [...todos] .filter((todo) => todo.status !== "cancelled") .sort((a, b) => statusOrder[a.status] - statusOrder[b.status]); }, [todos]); // Find the current active todo (first in_progress, or first pending) const activeTodo = React.useMemo(() => { return ( visibleTodos.find((t) => t.status === "in_progress") || visibleTodos.find((t) => t.status === "pending") || null ); }, [visibleTodos]); // Calculate progress const progress = React.useMemo(() => { const total = todos.filter((t) => t.status !== "cancelled").length; const completed = todos.filter((t) => t.status === "completed").length; return { completed, total }; }, [todos]); const hasActiveTodos = visibleTodos.some((t) => t.status === "in_progress" || t.status === "pending"); // Original logic from ChatInput const shouldRenderPlaceholder = !showAbortStatus && (wasAborted || !abortActive); // Track if placeholder is showing result (done/aborted) to keep StatusRow mounted const [placeholderShowingResult, setPlaceholderShowingResult] = React.useState(false); // Keep StatusRow rendered while: // - isWorking (active session) // - isComplete (showing "Done" result) // - wasAborted (showing "Aborted" result) // - placeholderShowingResult (placeholder still displaying result) // - hasActiveTodos or showAbortStatus const hasContent = isWorking || isComplete || wasAborted || placeholderShowingResult || hasActiveTodos || showAbortStatus; // Close popover when clicking outside const popoverRef = React.useRef(null); React.useEffect(() => { if (!isExpanded) return; const handleClickOutside = (event: MouseEvent) => { if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) { setIsExpanded(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isExpanded]); // Don't render if nothing to show if (!hasContent) { return null; } const toggleExpanded = () => setIsExpanded((prev) => !prev); // Abort button for mobile/vscode const abortButton = showAbort && onAbort ? ( ) : null; // Todo trigger button const todoTrigger = hasActiveTodos ? ( ) : null; return (
{/* Main status row */}
{/* Left: Abort status or Working placeholder */}
{showAbortStatus ? (
) : shouldRenderPlaceholder ? ( ) : null}
{/* Right: Abort (mobile only) + Todo */}
{abortButton} {todoTrigger} {/* Popover dropdown */} {isExpanded && hasActiveTodos && (
{/* Header */}
Tasks {progress.completed}/{progress.total}
{/* Todo list */}
{visibleTodos.map((todo) => ( ))}
)}
); };