import { useState, useCallback, useEffect, useRef } from "react"; import type { Task, TaskCreateInput } from "@fusion/core"; import type { ToastType } from "../hooks/useToast"; import { uploadAttachment, fetchAgents } from "../api"; import type { Agent } from "../api"; import { Bot } from "lucide-react"; import { TaskForm, type PendingImage } from "./TaskForm"; interface NewTaskModalProps { isOpen: boolean; onClose: () => void; projectId?: string; tasks: Task[]; // for dependency selection onCreateTask: (input: TaskCreateInput) => Promise; addToast: (message: string, type?: ToastType) => void; onPlanningMode?: (initialPlan: string) => void; onSubtaskBreakdown?: (description: string) => void; } export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, addToast, onPlanningMode, onSubtaskBreakdown }: NewTaskModalProps) { const [description, setDescription] = useState(""); const [dependencies, setDependencies] = useState([]); const [pendingImages, setPendingImages] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [executorModel, setExecutorModel] = useState(""); const [validatorModel, setValidatorModel] = useState(""); const [planningModel, setPlanningModel] = useState(""); const [thinkingLevel, setThinkingLevel] = useState(""); const [selectedPresetId, setSelectedPresetId] = useState(""); const [presetMode, setPresetMode] = useState<"default" | "preset" | "custom">("default"); const [hasDirtyState, setHasDirtyState] = useState(false); const [selectedWorkflowSteps, setSelectedWorkflowSteps] = useState([]); const [workflowStepsExplicitlySet, setWorkflowStepsExplicitlySet] = useState(false); // Agent assignment state const [selectedAgentId, setSelectedAgentId] = useState(null); const [agents, setAgents] = useState([]); const [showAgentPicker, setShowAgentPicker] = useState(false); const [agentsLoading, setAgentsLoading] = useState(false); const agentPickerRef = useRef(null); // Handler for workflow step changes that detects explicit user interaction const handleWorkflowStepsChange = useCallback((steps: string[]) => { setWorkflowStepsExplicitlySet(true); setSelectedWorkflowSteps(steps); }, []); // Callback when defaultOn steps are auto-applied by TaskForm const handleDefaultOnApplied = useCallback(() => { // defaultOn auto-selection is not "explicit" user interaction setWorkflowStepsExplicitlySet(false); }, []); // Load agents for agent picker const loadAgents = useCallback(async () => { if (agents.length > 0) { setShowAgentPicker(true); return; } setAgentsLoading(true); try { const result = await fetchAgents(undefined, projectId); setAgents(result); setShowAgentPicker(true); } catch (err: any) { addToast(err?.message ? `Failed to load agents: ${err.message}` : "Failed to load agents", "error"); setShowAgentPicker(false); } finally { setAgentsLoading(false); } }, [agents.length, projectId, addToast]); // Close agent picker when clicking outside useEffect(() => { if (!showAgentPicker) return; const handleClickOutside = (e: MouseEvent) => { if (agentPickerRef.current && !agentPickerRef.current.contains(e.target as Node)) { setShowAgentPicker(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [showAgentPicker]); // Track dirty state useEffect(() => { const isDirty = description.trim() !== "" || dependencies.length > 0 || pendingImages.length > 0 || executorModel !== "" || validatorModel !== "" || planningModel !== "" || thinkingLevel !== "" || selectedWorkflowSteps.length > 0 || selectedAgentId !== null; setHasDirtyState(isDirty); }, [description, dependencies, pendingImages, executorModel, validatorModel, planningModel, thinkingLevel, selectedWorkflowSteps, selectedAgentId]); const handleClose = useCallback(() => { if (hasDirtyState) { if (!confirm("You have unsaved changes. Discard them?")) return; } // Clean up object URLs pendingImages.forEach((img) => URL.revokeObjectURL(img.previewUrl)); // Reset form setPendingImages([]); setDescription(""); setDependencies([]); setExecutorModel(""); setValidatorModel(""); setPlanningModel(""); setThinkingLevel(""); setSelectedPresetId(""); setPresetMode("default"); setSelectedWorkflowSteps([]); setWorkflowStepsExplicitlySet(false); setSelectedAgentId(null); setShowAgentPicker(false); setHasDirtyState(false); onClose(); }, [hasDirtyState, onClose, pendingImages]); const handleSubmit = useCallback(async () => { const trimmedDesc = description.trim(); if (!trimmedDesc || isSubmitting) return; setIsSubmitting(true); try { const executorSlashIdx = executorModel.indexOf("/"); const validatorSlashIdx = validatorModel.indexOf("/"); const planningSlashIdx = planningModel.indexOf("/"); const task = await onCreateTask({ title: undefined, description: trimmedDesc, column: "triage", dependencies: dependencies.length ? dependencies : undefined, // When user explicitly cleared all workflow steps, send empty array to prevent backend re-applying defaults. // When user hasn't interacted with workflow steps (or left auto-selected defaults), send undefined to let backend apply defaults. enabledWorkflowSteps: workflowStepsExplicitlySet ? (selectedWorkflowSteps.length > 0 ? selectedWorkflowSteps : []) : undefined, ...(selectedAgentId ? { assignedAgentId: selectedAgentId } : {}), modelPresetId: presetMode === "preset" ? selectedPresetId || undefined : undefined, modelProvider: executorModel && executorSlashIdx !== -1 ? executorModel.slice(0, executorSlashIdx) : undefined, modelId: executorModel && executorSlashIdx !== -1 ? executorModel.slice(executorSlashIdx + 1) : undefined, validatorModelProvider: validatorModel && validatorSlashIdx !== -1 ? validatorModel.slice(0, validatorSlashIdx) : undefined, validatorModelId: validatorModel && validatorSlashIdx !== -1 ? validatorModel.slice(validatorSlashIdx + 1) : undefined, planningModelProvider: planningModel && planningSlashIdx !== -1 ? planningModel.slice(0, planningSlashIdx) : undefined, planningModelId: planningModel && planningSlashIdx !== -1 ? planningModel.slice(planningSlashIdx + 1) : undefined, thinkingLevel: thinkingLevel !== "" ? thinkingLevel as "minimal" | "low" | "medium" | "high" : undefined, }); // Upload pending images as attachments if (pendingImages.length > 0) { const failures: string[] = []; for (const img of pendingImages) { try { await uploadAttachment(task.id, img.file, projectId); } catch { failures.push(img.file.name); } } if (failures.length > 0) { addToast(`Failed to upload: ${failures.join(", ")}`, "error"); } } // Clean up pendingImages.forEach((img) => URL.revokeObjectURL(img.previewUrl)); setPendingImages([]); setDescription(""); setDependencies([]); setExecutorModel(""); setValidatorModel(""); setPlanningModel(""); setThinkingLevel(""); setSelectedPresetId(""); setPresetMode("default"); setSelectedWorkflowSteps([]); setWorkflowStepsExplicitlySet(false); setSelectedAgentId(null); setShowAgentPicker(false); addToast(`Created ${task.id}`, "success"); onClose(); } catch (err: any) { addToast(err.message || "Failed to create task", "error"); } finally { setIsSubmitting(false); } }, [description, dependencies, pendingImages, executorModel, validatorModel, planningModel, thinkingLevel, isSubmitting, onCreateTask, addToast, onClose, projectId, presetMode, selectedPresetId, selectedWorkflowSteps, workflowStepsExplicitlySet, selectedAgentId]); // Handle keyboard shortcuts const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault(); handleClose(); } }, [handleClose]); // Compute selected agent label for display const selectedAgent = selectedAgentId ? agents.find((agent) => agent.id === selectedAgentId) : undefined; const selectedAgentLabel = selectedAgent?.name ?? selectedAgentId; if (!isOpen) return null; return (
e.stopPropagation()} >

New Task

{/* Agent Assignment */}
{showAgentPicker && (
e.preventDefault()}>
Select agent
{agentsLoading &&
Loading agents...
} {!agentsLoading && agents.filter((a) => a.state !== "terminated").map((a) => (
e.preventDefault()} onClick={() => { setSelectedAgentId(a.id === selectedAgentId ? null : a.id); setShowAgentPicker(false); }} data-testid={`agent-option-${a.id}`} > {a.role} {a.name}
))} {!agentsLoading && agents.filter((a) => a.state !== "terminated").length === 0 && (
No agents available
)} {selectedAgentId && (
e.preventDefault()} onClick={() => { setSelectedAgentId(null); setShowAgentPicker(false); }} > Clear selection
)}
)}
); }