'use client'; import { useEffect, useRef, useState, FormEvent } from 'react'; import Link from 'next/link'; import { IconSettings } from '@tabler/icons-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose, } from '@kandev/ui/dialog'; import { Label } from '@kandev/ui/label'; import { Input } from '@kandev/ui/input'; import { Textarea } from '@kandev/ui/textarea'; import { Button } from '@kandev/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@kandev/ui/select'; import { Combobox } from './combobox'; import { Badge } from '@kandev/ui/badge'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@kandev/ui/tooltip'; 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'; interface TaskCreateDialogProps { open: boolean; onOpenChange: (open: boolean) => void; mode?: 'task' | 'session'; workspaceId: string | null; boardId: string | null; defaultColumnId: string | null; columns: Array<{ id: string; title: string }>; editingTask?: { id: string; title: string; description?: string; columnId: 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']; }; submitLabel?: string; } 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; }; function RepositorySelector({ options, value, onValueChange, disabled, placeholder, searchPlaceholder, emptyMessage, }: 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; }; 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; }; function AgentSelector({ options, value, onValueChange, disabled, placeholder, triggerClassName, }: AgentSelectorProps) { return ( ); } export function TaskCreateDialog({ open, onOpenChange, mode = 'task', workspaceId, boardId, defaultColumnId, columns, editingTask, onSuccess, onCreateSession, initialValues, submitLabel = 'Create', }: TaskCreateDialogProps) { const isSessionMode = mode === 'session'; const isEditMode = submitLabel !== 'Create'; const [title, setTitle] = useState(initialValues?.title ?? ''); const [description, setDescription] = useState(initialValues?.description ?? ''); const [repositoryId, setRepositoryId] = useState(initialValues?.repositoryId ?? ''); const [branch, setBranch] = useState(initialValues?.branch ?? ''); const [startAgent, setStartAgent] = useState(!isEditMode); const [agentProfileId, setAgentProfileId] = useState(''); const [environmentId, setEnvironmentId] = useState(''); const [executorId, setExecutorId] = useState(''); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); const [discoveredRepositories, setDiscoveredRepositories] = useState([]); const [discoveredRepoPath, setDiscoveredRepoPath] = useState(''); const [selectedLocalRepo, setSelectedLocalRepo] = useState(null); const [localBranches, setLocalBranches] = useState([]); const [localBranchesLoading, setLocalBranchesLoading] = useState(false); const [discoverReposLoading, setDiscoverReposLoading] = useState(false); const [discoverReposLoaded, setDiscoverReposLoaded] = useState(false); const workspaces = useAppStore((state) => state.workspaces.items); const agentProfiles = useAppStore((state) => state.agentProfiles.items); const executors = useAppStore((state) => state.executors.items); const environments = useAppStore((state) => state.environments.items); const settingsData = useAppStore((state) => state.settingsData); const descriptionRef = useRef(null); const { toast } = useToast(); useSettingsData(open); const { repositories, isLoading: repositoriesLoading } = useRepositories(workspaceId, open); const { branches, isLoading: branchesLoading } = useRepositoryBranches( repositoryId || null, Boolean(open && repositoryId) ); const agentProfilesLoading = open && !settingsData.agentsLoaded; const environmentsLoading = open && !settingsData.environmentsLoaded; const executorsLoading = open && !settingsData.executorsLoaded; useEffect(() => { if (!open) return; setTitle(initialValues?.title ?? ''); setDescription(initialValues?.description ?? ''); setRepositoryId(initialValues?.repositoryId ?? ''); setBranch(initialValues?.branch ?? ''); setStartAgent(!isEditMode); setAgentProfileId(''); setEnvironmentId(''); setExecutorId(''); }, [ isEditMode, initialValues?.branch, initialValues?.description, initialValues?.repositoryId, initialValues?.title, open, ]); useEffect(() => { if (!open) return; setDiscoveredRepositories([]); setDiscoveredRepoPath(''); setSelectedLocalRepo(null); setLocalBranches([]); setDiscoverReposLoaded(false); }, [open, workspaceId]); const workspaceDefaults = workspaceId ? workspaces.find((workspace: Workspace) => workspace.id === workspaceId) : null; useEffect(() => { if (!open || !workspaceId) return; if (!repositoryId && !selectedLocalRepo && repositories.length === 1) { setRepositoryId(repositories[0].id); } }, [open, repositories, repositoryId, selectedLocalRepo, workspaceId]); useEffect(() => { if (!open || !workspaceId) return; if (repositoriesLoading) return; if (discoverReposLoaded || discoverReposLoading) return; setDiscoverReposLoading(true); discoverRepositoriesAction(workspaceId) .then((response) => { setDiscoveredRepositories(response.repositories); }) .catch((error) => { toast({ title: 'Failed to discover repositories', description: error instanceof Error ? error.message : 'Request failed', variant: 'error', }); setDiscoveredRepositories([]); }) .finally(() => { setDiscoverReposLoading(false); setDiscoverReposLoaded(true); }); }, [ discoverReposLoaded, discoverReposLoading, open, repositories.length, repositoriesLoading, toast, workspaceId, ]); useEffect(() => { if (!repositoryId) return; if (!branch) { const preferredBranch = selectPreferredBranch(branches); if (preferredBranch) { setBranch(preferredBranch); } } }, [branch, branches, repositoryId]); const handleSelectLocalRepository = (path: string) => { const selected = discoveredRepositories.find((repo) => repo.path === path) ?? null; setDiscoveredRepoPath(path); setSelectedLocalRepo(selected); setRepositoryId(''); setBranch(''); setLocalBranches([]); }; const hasRepositorySelection = Boolean(repositoryId || selectedLocalRepo); const branchOptions = repositoryId ? branches : localBranches; const normalizeRepoPath = (path: string) => path.replace(/\\/g, '/').replace(/\/+$/g, ''); const workspaceRepoPaths = new Set( repositories .map((repo: Repository) => repo.local_path) .filter(Boolean) .map((path: string) => normalizeRepoPath(path)) ); const localRepoOptions = discoveredRepositories.filter( (repo: LocalRepository) => !workspaceRepoPaths.has(normalizeRepoPath(repo.path)) ); const repositoryOptions: RepositoryOption[] = [ ...repositories.map((repo: Repository) => ({ value: repo.id, label: repo.name, renderLabel: () => ( {repo.name} {truncateRepoPath(repo.local_path)} ), })), ...localRepoOptions.map((repo: LocalRepository) => ({ value: repo.path, label: formatUserHomePath(repo.path), renderLabel: () => ( {truncateRepoPath(repo.path)} ), })), ]; useEffect(() => { if (!open || !workspaceId || !selectedLocalRepo) return; setLocalBranchesLoading(true); listLocalRepositoryBranchesAction(workspaceId, selectedLocalRepo.path) .then((response) => { setLocalBranches(response.branches); }) .catch((error) => { toast({ title: 'Failed to load branches', description: error instanceof Error ? error.message : 'Request failed', variant: 'error', }); setLocalBranches([]); }) .finally(() => { setLocalBranchesLoading(false); }); }, [open, selectedLocalRepo, toast, workspaceId]); useEffect(() => { if (repositoryId || localBranches.length === 0) return; if (branch) return; const preferredBranch = selectPreferredBranch(localBranches); if (preferredBranch) { setBranch(preferredBranch); } }, [branch, localBranches, repositoryId]); useEffect(() => { if (!open || agentProfileId || agentProfiles.length === 0) return; if (!isSessionMode && !startAgent) return; if (isEditMode && agentProfiles.length > 1) return; const defaultProfileId = workspaceDefaults?.default_agent_profile_id ?? null; if (defaultProfileId && agentProfiles.some((profile: AgentProfileOption) => profile.id === defaultProfileId)) { setAgentProfileId(defaultProfileId); return; } setAgentProfileId(agentProfiles[0].id); }, [open, isEditMode, agentProfileId, agentProfiles, workspaceDefaults, isSessionMode, startAgent]); useEffect(() => { if (!open || isEditMode || environmentId || environments.length === 0) return; if (!isSessionMode && !startAgent) return; const defaultEnvironmentId = workspaceDefaults?.default_environment_id ?? null; if ( defaultEnvironmentId && environments.some((environment: Environment) => environment.id === defaultEnvironmentId) ) { setEnvironmentId(defaultEnvironmentId); return; } const localEnvironment = environments.find( (environment: Environment) => environment.kind === DEFAULT_LOCAL_ENVIRONMENT_KIND ); setEnvironmentId(localEnvironment?.id ?? environments[0].id); }, [open, isEditMode, environmentId, environments, workspaceDefaults, isSessionMode, startAgent]); useEffect(() => { if (!open || isEditMode || executorId || executors.length === 0) return; if (!isSessionMode && !startAgent) return; const defaultExecutorId = workspaceDefaults?.default_executor_id ?? null; if (defaultExecutorId && executors.some((executor: Executor) => executor.id === defaultExecutorId)) { setExecutorId(defaultExecutorId); return; } const localExecutor = executors.find( (executor: Executor) => executor.type === DEFAULT_LOCAL_EXECUTOR_TYPE ); setExecutorId(localExecutor?.id ?? executors[0].id); }, [open, isEditMode, executorId, executors, workspaceDefaults, isSessionMode, startAgent]); useEffect(() => { const textarea = descriptionRef.current; if (!textarea) return; textarea.style.height = 'auto'; textarea.style.height = `${textarea.scrollHeight}px`; }, [description]); // Use keyboard shortcut hook for Cmd+Enter / Ctrl+Enter const handleKeyDown = useKeyboardShortcutHandler(SHORTCUTS.SUBMIT, (event) => { handleSubmit(event as unknown as FormEvent); }); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); // Session mode - create a new session if (isSessionMode) { const trimmedDescription = description.trim(); if (!trimmedDescription || !agentProfileId) return; onCreateSession?.({ prompt: trimmedDescription, agentProfileId, executorId, environmentId, }); // Reset form setDescription(''); setAgentProfileId(''); setExecutorId(''); setEnvironmentId(''); setShowAdvancedSettings(false); onOpenChange(false); return; } // Task mode - create or edit task const trimmedTitle = title.trim(); if (!trimmedTitle) return; if (isEditMode && editingTask) { try { const updatePayload: Parameters[1] = { title: trimmedTitle, description: description.trim(), }; const needsRepository = startAgent && !editingTask.repositoryId; if (repositoryId) { updatePayload.repositories = [{ repository_id: repositoryId, base_branch: branch || undefined }]; } else if (selectedLocalRepo && needsRepository) { console.warn('[TaskCreateDialog] Local repo selection in edit mode not fully supported'); } const updatedTask = await updateTask(editingTask.id, updatePayload); let taskSessionId: string | null = null; if (startAgent && agentProfileId) { const client = getWebSocketClient(); if (client) { try { interface StartResponse { success: boolean; task_id: string; agent_instance_id: string; session_id?: string; state: string; } const response = await client.request( 'orchestrator.start', { task_id: editingTask.id, agent_profile_id: agentProfileId, prompt: description.trim() || '', }, 15000 ); taskSessionId = response?.session_id ?? null; } catch (error) { console.error('[TaskCreateDialog] failed to start agent:', error); } } } onSuccess?.(updatedTask, 'edit', { taskSessionId }); } finally { setTitle(''); setDescription(''); setRepositoryId(''); setBranch(''); setStartAgent(false); setAgentProfileId(''); onOpenChange(false); } return; } if (!workspaceId || !boardId) return; if (!repositoryId && !selectedLocalRepo) return; const columnId = editingTask?.columnId ?? defaultColumnId; if (!columnId) return; let targetColumnId = columnId; let targetState: Task['state'] = 'CREATED'; if (startAgent && !isEditMode) { const progressColumn = columns.find((column) => column.title.toLowerCase().includes('progress') ); targetColumnId = progressColumn?.id ?? columnId; targetState = 'IN_PROGRESS'; } try { const taskResponse = await createTask({ workspace_id: workspaceId, board_id: boardId, column_id: targetColumnId, title: trimmedTitle, description: description.trim(), repositories: repositoryId ? [ { repository_id: repositoryId, base_branch: branch || undefined, }, ] : selectedLocalRepo ? [ { repository_id: '', base_branch: branch || undefined, local_path: selectedLocalRepo.path, default_branch: selectedLocalRepo.default_branch || undefined, }, ] : [], state: targetState, start_agent: Boolean(startAgent && !isEditMode && agentProfileId), agent_profile_id: startAgent ? agentProfileId || undefined : undefined, }); console.log('[TaskCreateDialog] task created', { taskId: taskResponse.id, startAgent, agentProfileId, }); onSuccess?.(taskResponse, 'create', { taskSessionId: taskResponse.session_id ?? null, }); } finally { setTitle(''); setDescription(''); setRepositoryId(''); setBranch(''); setStartAgent(!isEditMode); setAgentProfileId(''); setEnvironmentId(''); setExecutorId(''); onOpenChange(false); } }; const handleCancel = () => { setTitle(''); setDescription(''); setRepositoryId(''); setBranch(''); setStartAgent(!isEditMode); setAgentProfileId(''); setEnvironmentId(''); setExecutorId(''); onOpenChange(false); }; return ( {isSessionMode ? 'Create New Session' : submitLabel === 'Create' ? 'Create Task' : 'Edit Task'}
{!isSessionMode && (
setTitle(e.target.value)} disabled={isSessionMode} />
)} {isSessionMode && (
)}
{isSessionMode && }