// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { useHost } from '@/host'; import { TaskItem } from './TaskItem'; import { TaskState, TaskStateType } from '@/components/TaskState'; import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; import { ChatTaskStatus, TaskStatus } from '@/types/constants'; import { ChevronDown, Circle, CircleCheckBig, CircleSlash, LoaderCircle, Plus, TriangleAlert, } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; const TASK_CARD_EXPAND_STORAGE_PREFIX = 'eigent:task-card-expanded'; function getTaskCardExpandStorageKey( chatId: string | undefined, activeTaskId: string | undefined ): string | null { if (!activeTaskId) return null; if (chatId) return `${TASK_CARD_EXPAND_STORAGE_PREFIX}:${chatId}:${activeTaskId}`; return `${TASK_CARD_EXPAND_STORAGE_PREFIX}:${activeTaskId}`; } function readStoredTaskCardExpanded(key: string | null): boolean { if (!key || typeof window === 'undefined') return false; try { return sessionStorage.getItem(key) === '1'; } catch { return false; } } function writeStoredTaskCardExpanded(key: string | null, expanded: boolean) { if (!key || typeof window === 'undefined') return; try { sessionStorage.setItem(key, expanded ? '1' : '0'); } catch { /* ignore quota / private mode */ } } interface TaskCardProps { taskInfo: any[]; taskAssigning: Agent[]; taskRunning: TaskInfo[]; taskType: 1 | 2 | 3; progressValue: number; summaryTask: string; onAddTask: () => void; onUpdateTask: (taskIndex: number, content: string) => void; onSaveTask: () => void; onDeleteTask: (taskIndex: number) => void; clickable?: boolean; chatId?: string; } export function TaskCard({ taskInfo, taskType, taskRunning, progressValue, summaryTask, onAddTask, onUpdateTask, onSaveTask, onDeleteTask, clickable = true, chatId, }: TaskCardProps) { const host = useHost(); const electronAPI = host?.electronAPI; const contentRef = useRef(null); const [contentHeight, setContentHeight] = useState('auto'); const [selectedState, setSelectedState] = useState('all'); const [filterTasks, setFilterTasks] = useState([]); //Get Chatstore and ProjectStore for the active project's task const { chatStore, projectStore } = useChatStoreAdapter(); // Extract values for dependency arrays (must be before any conditional returns) const activeTaskId = chatStore?.activeTaskId as string; const activeTask = chatStore?.tasks?.[activeTaskId]; const activeTaskStatus = activeTask?.status; const expandStorageKey = getTaskCardExpandStorageKey(chatId, activeTaskId); const [isExpanded, setIsExpanded] = useState(() => readStoredTaskCardExpanded( getTaskCardExpandStorageKey(chatId, activeTaskId) ) ); useEffect(() => { setIsExpanded(readStoredTaskCardExpanded(expandStorageKey)); }, [expandStorageKey]); useEffect(() => { writeStoredTaskCardExpanded(expandStorageKey, isExpanded); }, [expandStorageKey, isExpanded]); useEffect(() => { const tasks = taskRunning || []; if (selectedState === 'all') { setFilterTasks(tasks); } else { const newFiltered = tasks.filter((task) => { switch (selectedState) { case 'done': return task.status === TaskStatus.COMPLETED && !task.reAssignTo; case 'reassigned': return !!task.reAssignTo; case 'ongoing': return ( task.status !== TaskStatus.FAILED && task.status !== TaskStatus.COMPLETED && task.status !== TaskStatus.SKIPPED && task.status !== TaskStatus.WAITING && task.status !== TaskStatus.EMPTY && !task.reAssignTo ); case 'pending': return ( (task.status === TaskStatus.SKIPPED || task.status === TaskStatus.WAITING || task.status === TaskStatus.EMPTY) && !task.reAssignTo ); case 'failed': return task.status === TaskStatus.FAILED && !task.reAssignTo; default: return false; } }); setFilterTasks(newFiltered); } }, [selectedState, taskInfo, taskRunning]); // Improved height calculation logic useEffect(() => { if (!contentRef.current) return; const updateHeight = () => { if (contentRef.current) { const scrollHeight = contentRef.current.scrollHeight; setContentHeight(scrollHeight); } }; // Update height immediately updateHeight(); // Use ResizeObserver to monitor content changes const resizeObserver = new ResizeObserver(() => { updateHeight(); }); resizeObserver.observe(contentRef.current); // Update height when taskRunning changes const timeoutId = setTimeout(updateHeight, 100); return () => { resizeObserver.disconnect(); clearTimeout(timeoutId); }; }, [taskRunning, isExpanded]); // Handle height updates specifically for expand/collapse state changes useEffect(() => { if (!contentRef.current || !isExpanded) return; const updateHeightOnExpand = () => { if (contentRef.current && isExpanded) { // Small delay to ensure DOM is fully rendered requestAnimationFrame(() => { if (contentRef.current) { setContentHeight(contentRef.current.scrollHeight); } }); } }; // Update height immediately when expanded updateHeightOnExpand(); // Additional delay when expanded to ensure all animations complete if (isExpanded) { const timeoutId = setTimeout(updateHeightOnExpand, 300); return () => clearTimeout(timeoutId); } }, [isExpanded]); // Early return after all hooks if (!chatStore) { return
Loading...
; } return (
{summaryTask && (
{summaryTask.split('|')[0].replace(/"/g, '')}
)} {summaryTask && (
{taskType === 1 && ( task.content !== '').length || 0 } done={ taskInfo.filter( (task) => task.content !== '' && task.status === TaskStatus.COMPLETED ).length || 0 } progress={ taskInfo.filter( (task) => task.content !== '' && task.status !== TaskStatus.COMPLETED && task.status !== TaskStatus.FAILED && task.status !== TaskStatus.SKIPPED && task.status !== TaskStatus.WAITING && task.status !== TaskStatus.EMPTY ).length || 0 } skipped={ taskInfo.filter( (task) => task.content !== '' && (task.status === TaskStatus.SKIPPED || task.status === TaskStatus.WAITING || task.status === TaskStatus.EMPTY) ).length || 0 } failed={ taskInfo.filter( (task) => task.content !== '' && task.status === TaskStatus.FAILED ).length || 0 } forceVisible={true} clickable={clickable} /> )} {taskType !== 1 && ( task.status === TaskStatus.COMPLETED && !task.reAssignTo ).length || 0 } reAssignTo={ taskRunning?.filter((task) => task.reAssignTo)?.length || 0 } progress={ taskRunning?.filter( (task) => task.status !== TaskStatus.COMPLETED && task.status !== TaskStatus.FAILED && task.status !== TaskStatus.SKIPPED && task.status !== TaskStatus.WAITING && task.status !== TaskStatus.EMPTY && !task.reAssignTo ).length || 0 } skipped={ taskRunning?.filter( (task) => (task.status === TaskStatus.SKIPPED || task.status === TaskStatus.WAITING || task.status === TaskStatus.EMPTY) && !task.reAssignTo ).length || 0 } failed={ taskRunning?.filter( (task) => task.status === TaskStatus.FAILED && !task.reAssignTo ).length || 0 } forceVisible={true} selectedState={selectedState} onStateChange={setSelectedState} clickable={clickable} /> )}
{taskType === 1 && ( )} {taskType === 2 && (
{isExpanded && (
{taskRunning?.filter( (task) => task.status === TaskStatus.COMPLETED || task.status === TaskStatus.FAILED ).length || 0} /{taskRunning?.length || 0}
)}
)}
)}
{taskType === 1 && (
{taskInfo.map((task, taskIndex) => (
onUpdateTask(taskIndex, content)} onSave={() => onSaveTask()} onDelete={() => onDeleteTask(taskIndex)} />
))}
)} {taskType === 2 && (
{filterTasks.map((task: TaskInfo) => { return (
{ if (task.agent) { // Switch to the chatStore that owns this task card (for multi-turn conversations) if (chatId && projectStore.activeProjectId) { const activeProjectId = projectStore.activeProjectId; const activeChatStore = projectStore.getActiveChatStore(); const currentChatId = activeChatStore ? Object.keys( projectStore.projects[activeProjectId] .chatStores ).find( (id) => projectStore.projects[activeProjectId] .chatStores[id] === activeChatStore ) : null; // Only switch if this is a different chat if (currentChatId && currentChatId !== chatId) { projectStore.setActiveChatStore( activeProjectId, chatId ); } } // Set the active workspace and agent chatStore.setActiveWorkspace( chatStore.activeTaskId as string, 'workflow' ); chatStore.setActiveAgent( chatStore.activeTaskId as string, task.agent?.agent_id ); electronAPI?.hideAllWebview(); } }} key={`taskList-${task.id}`} className={`gap-2 rounded-lg px-sm py-sm ease-in-out animate-in fade-in-0 slide-in-from-left-2 flex transition-all duration-300 ${ task.status === TaskStatus.COMPLETED ? 'bg-ds-bg-completed-subtle-default' : task.status === TaskStatus.FAILED ? 'bg-ds-bg-error-subtle-default' : task.status === TaskStatus.RUNNING ? 'bg-ds-bg-running-subtle-default' : task.status === TaskStatus.BLOCKED ? 'bg-ds-bg-blocked-subtle-default' : task.status === TaskStatus.SKIPPED || task.status === TaskStatus.WAITING || task.status === TaskStatus.EMPTY ? 'bg-ds-bg-pending-subtle-default' : 'bg-ds-bg-running-subtle-default' } cursor-pointer border border-solid border-transparent ${ task.status === TaskStatus.COMPLETED ? 'hover:border-ds-border-status-completed-default-focus' : task.status === TaskStatus.FAILED ? 'hover:border-ds-border-status-error-default-focus' : task.status === TaskStatus.RUNNING ? 'hover:border-ds-border-status-running-default-focus' : task.status === TaskStatus.BLOCKED ? 'hover:border-ds-border-status-blocked-default-focus' : task.status === TaskStatus.SKIPPED || task.status === TaskStatus.WAITING || task.status === TaskStatus.EMPTY ? 'hover:border-ds-border-status-pending-default-hover' : 'border-transparent' } `} >
{task.status === TaskStatus.RUNNING && ( )} {task.status === TaskStatus.SKIPPED && ( )} {task.status === TaskStatus.COMPLETED && ( )} {task.status === TaskStatus.FAILED && ( )} {task.status === TaskStatus.BLOCKED && ( )} {(task.status === TaskStatus.EMPTY || task.status === TaskStatus.WAITING) && ( )}
{task.content}
); })}
)}
); }