import { useEffect, useMemo, useRef, useState } from 'react'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { Checkbox } from '@renderer/components/ui/checkbox'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@renderer/components/ui/dialog'; import { Input } from '@renderer/components/ui/input'; import { Label } from '@renderer/components/ui/label'; import { MemberSelect } from '@renderer/components/ui/MemberSelect'; import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea'; import { TiptapEditor } from '@renderer/components/ui/tiptap'; import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence'; import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence'; import { useTaskSuggestions } from '@renderer/hooks/useTaskSuggestions'; import { useStore } from '@renderer/store'; import { selectTeamDataForName } from '@renderer/store/slices/teamSlice'; import { chipToken, serializeChipsWithText } from '@renderer/types/inlineChip'; import { formatAgentRole } from '@renderer/utils/formatAgentRole'; import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { extractTaskRefsFromText, stripEncodedTaskReferenceMetadata, } from '@renderer/utils/taskReferenceUtils'; import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState'; import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { AlertTriangle, ChevronDown, ChevronRight, Search } from 'lucide-react'; import type { InlineChip } from '@renderer/types/inlineChip'; import type { MentionSuggestion } from '@renderer/types/mention'; import type { ResolvedTeamMember, TaskRef, TeamTaskWithKanban } from '@shared/types'; interface CreateTaskDialogProps { open: boolean; teamName: string; members: ResolvedTeamMember[]; tasks: TeamTaskWithKanban[]; isTeamAlive?: boolean; defaultSubject?: string; defaultDescription?: string; defaultOwner?: string; defaultStartImmediately?: boolean; defaultChip?: InlineChip; onClose: () => void; onSubmit: ( subject: string, description: string, owner?: string, blockedBy?: string[], related?: string[], prompt?: string, startImmediately?: boolean, descriptionTaskRefs?: TaskRef[], promptTaskRefs?: TaskRef[] ) => void; submitting?: boolean; } export const CreateTaskDialog = ({ open, teamName, members, tasks, isTeamAlive = false, defaultSubject = '', defaultDescription = '', defaultOwner = '', defaultStartImmediately, defaultChip, onClose, onSubmit, submitting = false, }: CreateTaskDialogProps): React.JSX.Element => { const colorMap = useMemo(() => buildMemberColorMap(members), [members]); const projectPath = useStore( (s) => selectTeamDataForName(s, teamName)?.config.projectPath ?? null ); const { suggestions: taskSuggestions } = useTaskSuggestions(teamName); const [subject, setSubject] = useState(defaultSubject); const descriptionDraft = useDraftPersistence({ key: `createTask:${teamName}:description`, initialValue: defaultDescription || undefined, }); const descChipDraft = useChipDraftPersistence(`createTask:${teamName}:descChips`); const [owner, setOwner] = useState(defaultOwner); const [blockedBy, setBlockedBy] = useState([]); const [related, setRelated] = useState([]); const [startImmediately, setStartImmediately] = useState(true); const promptDraft = useDraftPersistence({ key: `createTask:${teamName}:prompt` }); const [blockedBySearch, setBlockedBySearch] = useState(''); const [relatedSearch, setRelatedSearch] = useState(''); const [showOptionalFields, setShowOptionalFields] = useState(false); const prevOpenRef = useRef(false); // Reset form when dialog opens (avoid setState during render) useEffect(() => { if (open && !prevOpenRef.current) { // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional sync on prop change setSubject(defaultSubject); if (defaultChip) { const token = chipToken(defaultChip); descriptionDraft.setValue(token + '\n'); descChipDraft.setChips([defaultChip]); } else if (defaultDescription) { descriptionDraft.setValue(defaultDescription); descChipDraft.clearChipDraft(); } else { descriptionDraft.clearDraft(); descChipDraft.clearChipDraft(); } setOwner(defaultOwner); setBlockedBy([]); setRelated([]); setStartImmediately(defaultStartImmediately ?? isTeamAlive); promptDraft.clearDraft(); setBlockedBySearch(''); setRelatedSearch(''); setShowOptionalFields(false); } prevOpenRef.current = open; }, [ open, defaultSubject, defaultDescription, defaultOwner, defaultStartImmediately, defaultChip, isTeamAlive, descriptionDraft, descChipDraft, promptDraft, ]); const mentionSuggestions = useMemo( () => members.map((m) => ({ id: m.name, name: m.name, subtitle: formatAgentRole(m.role) ?? formatAgentRole(m.agentType) ?? undefined, color: colorMap.get(m.name), })), [members, colorMap] ); const requiresOwner = defaultStartImmediately === true; const canSubmit = subject.trim().length > 0 && !submitting && (!requiresOwner || !!owner); // Only show non-internal, non-deleted tasks as candidates for blocking const availableTasks = tasks.filter( (t) => t.status !== 'deleted' && getTeamTaskWorkflowColumn(t) !== 'approved' ); const toggleBlockedBy = (taskId: string): void => { setBlockedBy((prev) => prev.includes(taskId) ? prev.filter((id) => id !== taskId) : [...prev, taskId] ); }; const toggleRelated = (taskId: string): void => { setRelated((prev) => prev.includes(taskId) ? prev.filter((id) => id !== taskId) : [...prev, taskId] ); }; const handleSubmit = (): void => { if (!canSubmit) return; const trimmedDescription = stripEncodedTaskReferenceMetadata(descriptionDraft.value.trim()); const trimmedPrompt = stripEncodedTaskReferenceMetadata(promptDraft.value.trim()); const serializedDesc = serializeChipsWithText(trimmedDescription, descChipDraft.chips); const descriptionTaskRefs = extractTaskRefsFromText(descriptionDraft.value, taskSuggestions); const promptTaskRefs = trimmedPrompt ? extractTaskRefsFromText(promptDraft.value, taskSuggestions) : []; onSubmit( subject.trim(), serializedDesc, owner || undefined, blockedBy.length > 0 ? blockedBy : undefined, related.length > 0 ? related : undefined, trimmedPrompt || undefined, startImmediately, descriptionTaskRefs, promptTaskRefs ); descriptionDraft.clearDraft(); descChipDraft.clearChipDraft(); promptDraft.clearDraft(); }; const handleOpenChange = (nextOpen: boolean): void => { if (!nextOpen) { onClose(); } }; const assigneeField = (
setOwner(v ?? '')} placeholder={requiresOwner ? 'Select a member' : 'Select member...'} allowUnassigned={!requiresOwner} />
); return ( Create Task The task will be created in the team's tasks/ directory and appear on the Kanban board. {!isTeamAlive ? (

Team is offline. The task will be added to TODO — launch the team to start execution.

) : null}
setSubject(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && canSubmit) handleSubmit(); }} />
{assigneeField} {/* Toggle button for optional fields */} {/* Collapsible optional fields */}
Saved ) : null } />
{availableTasks.length > 0 ? (
{availableTasks.length > 3 ? (
setBlockedBySearch(e.target.value)} className="w-full bg-transparent py-0.5 pl-5 text-xs text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:outline-none" />
) : null}
{availableTasks .filter( (t) => !blockedBySearch || t.subject.toLowerCase().includes(blockedBySearch.toLowerCase()) || t.id.includes(blockedBySearch) || t.displayId?.includes(blockedBySearch) ) .map((t) => { const isSelected = blockedBy.includes(t.id); return ( ); })}
{blockedBy.length > 0 ? (

Task will be blocked by:{' '} {blockedBy.map((id) => `#${deriveTaskDisplayId(id)}`).join(', ')}

) : null}
) : null} {availableTasks.length > 0 ? (
{availableTasks.length > 3 ? (
setRelatedSearch(e.target.value)} className="w-full bg-transparent py-0.5 pl-5 text-xs text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:outline-none" />
) : null}
{availableTasks .filter( (t) => !relatedSearch || t.subject.toLowerCase().includes(relatedSearch.toLowerCase()) || t.id.includes(relatedSearch) || t.displayId?.includes(relatedSearch) ) .map((t) => { const isSelected = related.includes(t.id); return ( ); })}
{related.length > 0 ? (

Related: {related.map((id) => `#${deriveTaskDisplayId(id)}`).join(', ')}

) : null}
) : null}
{owner ? (
setStartImmediately(v === true)} disabled={!isTeamAlive} />
{!isTeamAlive ? (

Team is offline. Launch the team first to start tasks immediately.

) : null}
) : null}
); };