'use client'; import { useEffect, useRef, useState, FormEvent, memo, useMemo, useCallback } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { IconLoader2, IconGitBranch, IconFileInvoice, IconSend, IconChevronDown, IconTerminal2 } from '@tabler/icons-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose, } from '@kandev/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@kandev/ui/dropdown-menu'; import { Textarea } from '@kandev/ui/textarea'; import { Button } from '@kandev/ui/button'; import { Combobox } from './combobox'; import { Badge } from '@kandev/ui/badge'; import { ScrollOnOverflow } from '@kandev/ui/scroll-on-overflow'; import type { LocalRepository, Task, Workspace, Repository, Environment, Executor, Branch } from '@/lib/types/http'; import type { AgentProfileOption } from '@/lib/state/slices'; import { DEFAULT_LOCAL_ENVIRONMENT_KIND, DEFAULT_LOCAL_EXECUTOR_TYPE, formatUserHomePath, selectPreferredBranch, truncateRepoPath, } from '@/lib/utils'; import { createTask, updateTask } from '@/lib/api'; import { useAppStore } from '@/components/state-provider'; import { useRepositories } from '@/hooks/domains/workspace/use-repositories'; import { useRepositoryBranches } from '@/hooks/domains/workspace/use-repository-branches'; import { useSettingsData } from '@/hooks/domains/settings/use-settings-data'; import { getWebSocketClient } from '@/lib/ws/connection'; import { SHORTCUTS } from '@/lib/keyboard/constants'; import { useKeyboardShortcutHandler } from '@/hooks/use-keyboard-shortcut'; import { KeyboardShortcutTooltip } from '@/components/keyboard-shortcut-tooltip'; import { useToast } from '@/components/toast-provider'; import { discoverRepositoriesAction, listLocalRepositoryBranchesAction } from '@/app/actions/workspaces'; import { getLocalStorage, setLocalStorage } from '@/lib/local-storage'; import { STORAGE_KEYS } from '@/lib/settings/constants'; import { linkToSession } from '@/lib/links'; import { getExecutorIcon } from '@/lib/executor-icons'; import { AgentLogo } from '@/components/agent-logo'; import { listWorkflowSteps } from '@/lib/api/domains/workflow-api'; import { WorkflowSelectorRow } from '@/components/workflow-selector-row'; import { useDockviewStore } from '@/lib/state/dockview-store'; import { useContextFilesStore } from '@/lib/state/context-files-store'; interface TaskCreateDialogProps { open: boolean; onOpenChange: (open: boolean) => void; mode?: 'create' | 'edit' | 'session'; workspaceId: string | null; workflowId: string | null; defaultStepId: string | null; steps: Array<{ id: string; title: string; events?: { on_enter?: Array<{ type: string; config?: Record }>; on_turn_complete?: Array<{ type: string; config?: Record }> } }>; editingTask?: { id: string; title: string; description?: string; workflowStepId: string; state?: Task['state']; repositoryId?: string } | null; onSuccess?: (task: Task, mode: 'create' | 'edit', meta?: { taskSessionId?: string | null }) => void; onCreateSession?: (data: { prompt: string; agentProfileId: string; executorId: string; environmentId: string; }) => void; initialValues?: { title: string; description?: string; repositoryId?: string; branch?: string; state?: Task['state']; }; taskId?: string | null; } interface OrchestratorStartResponse { success: boolean; task_id: string; agent_instance_id: string; session_id?: string; state: string; } function autoSelectBranch(branchList: Branch[], setBranch: (value: string) => void): void { const lastUsedBranch = getLocalStorage(STORAGE_KEYS.LAST_BRANCH, null); if (lastUsedBranch && branchList.some((b) => { const displayName = b.type === 'remote' && b.remote ? `${b.remote}/${b.name}` : b.name; return displayName === lastUsedBranch; })) { setBranch(lastUsedBranch); return; } const preferredBranch = selectPreferredBranch(branchList); if (preferredBranch) { setBranch(preferredBranch); } } type RepositoryOption = { value: string; label: string; renderLabel: () => React.ReactNode; }; type RepositorySelectorProps = { options: RepositoryOption[]; value: string; onValueChange: (value: string) => void; disabled: boolean; placeholder: string; searchPlaceholder: string; emptyMessage: string; triggerClassName?: string; }; const RepositorySelector = memo(function RepositorySelector({ options, value, onValueChange, disabled, placeholder, searchPlaceholder, emptyMessage, triggerClassName, }: RepositorySelectorProps) { return ( ); }); type BranchOption = { value: string; label: string; renderLabel: () => React.ReactNode; }; type BranchSelectorProps = { options: BranchOption[]; value: string; onValueChange: (value: string) => void; disabled: boolean; placeholder: string; searchPlaceholder: string; emptyMessage: string; }; const BranchSelector = memo(function BranchSelector({ options, value, onValueChange, disabled, placeholder, searchPlaceholder, emptyMessage, }: BranchSelectorProps) { return ( ); }); type AgentSelectorProps = { options: Array<{ value: string; label: string; renderLabel: () => React.ReactNode }>; value: string; onValueChange: (value: string) => void; disabled: boolean; placeholder: string; triggerClassName?: string; }; const AgentSelector = memo(function AgentSelector({ options, value, onValueChange, disabled, placeholder, triggerClassName, }: AgentSelectorProps) { return ( ); }); type ExecutorSelectorProps = { options: Array<{ value: string; label: string; renderLabel?: () => React.ReactNode }>; value: string; onValueChange: (value: string) => void; disabled: boolean; placeholder: string; triggerClassName?: string; }; const ExecutorSelector = memo(function ExecutorSelector({ options, value, onValueChange, disabled, placeholder, triggerClassName, }: ExecutorSelectorProps) { return ( ); }); type InlineTaskNameProps = { value: string; onChange: (value: string) => void; autoFocus?: boolean; }; const InlineTaskName = memo(function InlineTaskName({ value, onChange, autoFocus, }: InlineTaskNameProps) { const inputRef = useRef(null); useEffect(() => { if (autoFocus && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, [autoFocus]); return ( onChange(e.target.value)} placeholder="task-name" size={Math.max(value.length, 9)} className="bg-transparent border-none outline-none focus:ring-0 text-sm font-medium min-w-0 rounded-md px-1.5 py-0.5 -mx-1.5 hover:bg-muted focus:bg-muted transition-colors" /> ); }); // Memoized description input to prevent re-rendering the entire dialog on every keystroke type TaskFormInputsProps = { isSessionMode: boolean; autoFocus?: boolean; initialDescription: string; onDescriptionChange: (hasContent: boolean) => void; onKeyDown: (e: React.KeyboardEvent) => void; descriptionValueRef: React.RefObject<{ getValue: () => string } | null>; disabled?: boolean; placeholder?: string; }; const TaskFormInputs = memo(function TaskFormInputs({ isSessionMode, autoFocus, initialDescription, onDescriptionChange, onKeyDown, descriptionValueRef, disabled, placeholder, }: TaskFormInputsProps) { const [description, setDescription] = useState(initialDescription); const textareaRef = useRef(null); useEffect(() => { const ref = descriptionValueRef as React.MutableRefObject<{ getValue: () => string } | null>; if (ref) { ref.current = { getValue: () => description }; } }, [description, descriptionValueRef]); // Auto-resize textarea + optional auto-focus with cursor at end useEffect(() => { const textarea = textareaRef.current; if (!textarea) return; textarea.style.height = 'auto'; textarea.style.height = `${textarea.scrollHeight}px`; }, [description]); useEffect(() => { if (!autoFocus) return; const textarea = textareaRef.current; if (!textarea) return; textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); }, [autoFocus]); const handleDescriptionChange = useCallback((e: React.ChangeEvent) => { const newValue = e.target.value; const hadContent = description.trim().length > 0; const hasContent = newValue.trim().length > 0; setDescription(newValue); if (hadContent !== hasContent) { onDescriptionChange(hasContent); } }, [description, onDescriptionChange]); return (