"use client"; import { FormEvent, useCallback } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@kandev/ui/dialog"; import type { Task } from "@/lib/types/http"; import type { AgentProfileOption } from "@/lib/state/slices"; import { SHORTCUTS } from "@/lib/keyboard/constants"; import { useIsUtilityConfigured } from "@/hooks/use-is-utility-configured"; import { useKeyboardShortcutHandler } from "@/hooks/use-keyboard-shortcut"; import { useUtilityAgentGenerator } from "@/hooks/use-utility-agent-generator"; import { TaskCreateDialogFooter } from "@/components/task-create-dialog-footer"; import { CreateEditSelectors, SessionSelectors, WorkflowSection, DialogPromptSection, } from "@/components/task-create-dialog-form-body"; import { useRepositoryOptions, useBranchOptions, useAgentProfileOptions, } from "@/components/task-create-dialog-options"; import { RepositorySelector, BranchSelector, AgentSelector, ExecutorProfileSelector, InlineTaskName, } from "@/components/task-create-dialog-selectors"; import { useTaskSubmitHandlers } from "@/components/task-create-dialog-submit"; import { useToast } from "@/components/toast-provider"; import { useDialogFormState, useTaskCreateDialogEffects, useDialogHandlers, useSessionRepoName, useTaskCreateDialogData, computeIsTaskStarted, type DialogFormState, type TaskCreateDialogInitialValues, } from "@/components/task-create-dialog-state"; 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 }) => void; initialValues?: TaskCreateDialogInitialValues; taskId?: string | null; parentTaskId?: string; } function getRepositoryPlaceholder( workspaceId: string | null, repositoriesLoading: boolean, discoverReposLoading: boolean, ) { if (!workspaceId) return "Select workspace first"; if (repositoriesLoading || discoverReposLoading) return "Loading..."; return "Select repository"; } type DialogHeaderContentProps = { isCreateMode: boolean; isEditMode: boolean; isTaskStarted: boolean; sessionRepoName?: string; initialTitle?: string; taskName: string; repositoryId: string; discoveredRepoPath: string; workspaceId: string | null; repositoriesLoading: boolean; discoverReposLoading: boolean; headerRepositoryOptions: ReturnType["headerRepositoryOptions"]; onRepositoryChange: (v: string) => void; onTaskNameChange: (v: string) => void; useGitHubUrl: boolean; githubUrl: string; githubUrlError: string | null; onToggleGitHubUrl: () => void; onGitHubUrlChange: (v: string) => void; }; function RepoSourceInput({ useGitHubUrl, githubUrl, githubUrlError, onGitHubUrlChange, isTaskStarted, headerRepositoryOptions, repositoryId, discoveredRepoPath, onRepositoryChange, workspaceId, repositoriesLoading, discoverReposLoading, }: Pick< DialogHeaderContentProps, | "useGitHubUrl" | "githubUrl" | "githubUrlError" | "onGitHubUrlChange" | "isTaskStarted" | "headerRepositoryOptions" | "repositoryId" | "discoveredRepoPath" | "onRepositoryChange" | "workspaceId" | "repositoriesLoading" | "discoverReposLoading" >): React.ReactNode { if (useGitHubUrl) { return (
onGitHubUrlChange(e.target.value)} placeholder="github.com/owner/repo" data-testid="github-url-input" aria-label="GitHub repository URL" aria-invalid={!!githubUrlError} aria-describedby={githubUrlError ? "github-url-error" : undefined} size={Math.max((githubUrl || "").length + 1, "github.com/owner/repo".length)} className={`bg-transparent border-none outline-none focus:ring-0 text-sm font-medium min-w-0 h-7 rounded-md px-2 hover:bg-muted focus:bg-muted transition-colors placeholder:text-muted-foreground ${githubUrlError ? "text-destructive" : ""}`} disabled={isTaskStarted} autoFocus /> {githubUrlError && ( )}
); } return ( ); } function DialogHeaderContent(props: DialogHeaderContentProps) { const { isCreateMode, isEditMode, isTaskStarted, sessionRepoName, initialTitle } = props; const { taskName, onTaskNameChange, useGitHubUrl, onToggleGitHubUrl } = props; if (isCreateMode || isEditMode) { return ( <>
/
{!isTaskStarted && (
)} ); } return (
{sessionRepoName && ( <> {sessionRepoName} / )} {initialTitle || "Task"} / new session
); } type DialogFormBodyProps = { isSessionMode: boolean; isCreateMode: boolean; isTaskStarted: boolean; isPassthroughProfile: boolean; initialDescription: string; hasDescription: boolean; branchOptions: ReturnType; branchesLoading: boolean; agentProfileOptions: ReturnType; executorProfileOptions: Array<{ value: string; label: string; renderLabel?: () => React.ReactNode; }>; agentProfiles: AgentProfileOption[]; agentProfilesLoading: boolean; executorsLoading: boolean; isCreatingSession: boolean; workflows: unknown[]; snapshots: unknown; effectiveWorkflowId: string | null; fs: DialogFormState; handleKeyDown: ReturnType; onBranchChange: (v: string) => void; onAgentProfileChange: (v: string) => void; onExecutorProfileChange: (v: string) => void; onWorkflowChange: (v: string) => void; hasRepositorySelection: boolean; isLocalExecutor: boolean; enhance?: { onEnhance: () => void; isLoading: boolean; isConfigured: boolean }; workflowAgentLocked: boolean; }; function DialogFormBody({ isSessionMode, isCreateMode, isTaskStarted, isPassthroughProfile, initialDescription, hasDescription, branchOptions, branchesLoading, agentProfileOptions, executorProfileOptions, agentProfiles, agentProfilesLoading, executorsLoading, isCreatingSession, workflows, snapshots, effectiveWorkflowId, fs, handleKeyDown, onBranchChange, onAgentProfileChange, onExecutorProfileChange, onWorkflowChange, hasRepositorySelection, isLocalExecutor, enhance, workflowAgentLocked, }: DialogFormBodyProps) { return (
{!isSessionMode && ( )} [0]["workflows"]} snapshots={snapshots as Parameters[0]["snapshots"]} effectiveWorkflowId={effectiveWorkflowId} onWorkflowChange={onWorkflowChange} agentProfiles={agentProfiles} /> {isSessionMode && ( )}
); } function useEnhanceForDialog(fs: DialogFormState) { const isConfigured = useIsUtilityConfigured(); const { enhancePrompt, isEnhancingPrompt } = useUtilityAgentGenerator({ sessionId: null, taskTitle: fs.taskName, }); const onEnhance = useCallback(() => { const current = fs.descriptionInputRef.current?.getValue()?.trim(); if (!current) return; enhancePrompt(current, (enhanced) => { fs.descriptionInputRef.current?.setValue(enhanced); fs.setHasDescription(true); }); }, [enhancePrompt, fs]); return { onEnhance, isLoading: isEnhancingPrompt, isConfigured }; } function useTaskCreateDialogSetup(props: TaskCreateDialogProps) { const { open, onOpenChange, mode = "create", workspaceId, workflowId, defaultStepId } = props; const { editingTask, onSuccess, onCreateSession, initialValues, parentTaskId } = props; const taskId = props.taskId ?? null; const isSessionMode = mode === "session"; const isEditMode = mode === "edit"; const isCreateMode = mode === "create"; const isTaskStarted = computeIsTaskStarted(isEditMode, editingTask); const fs = useDialogFormState(open, workspaceId, workflowId, initialValues); const { toast } = useToast(); const sessionRepoName = useSessionRepoName(isSessionMode); const { workflows, agentProfiles, executors, snapshots, repositories, repositoriesLoading, branches, branchesLoading, computed, } = useTaskCreateDialogData(open, workspaceId, workflowId, defaultStepId, fs); useTaskCreateDialogEffects(fs, { open, workspaceId, workflowId, repositories, repositoriesLoading, branches, agentProfiles, executors, workspaceDefaults: computed.workspaceDefaults, toast, workflows, }); const handlers = useDialogHandlers(fs, repositories); const submitHandlers = useTaskSubmitHandlers({ isSessionMode, isEditMode, isPassthroughProfile: computed.isPassthroughProfile, taskName: fs.taskName, workspaceId, workflowId, effectiveWorkflowId: computed.effectiveWorkflowId, effectiveDefaultStepId: computed.effectiveDefaultStepId, repositoryId: fs.repositoryId, selectedLocalRepo: fs.selectedLocalRepo, useGitHubUrl: fs.useGitHubUrl, githubUrl: fs.githubUrl, githubPrHeadBranch: fs.githubPrHeadBranch, branch: fs.branch, agentProfileId: computed.effectiveAgentProfileId, executorId: fs.executorId, executorProfileId: fs.executorProfileId, editingTask, onSuccess, onCreateSession, onOpenChange, taskId, parentTaskId, descriptionInputRef: fs.descriptionInputRef, setIsCreatingSession: fs.setIsCreatingSession, setIsCreatingTask: fs.setIsCreatingTask, setHasTitle: fs.setHasTitle, setHasDescription: fs.setHasDescription, setTaskName: fs.setTaskName, setRepositoryId: fs.setRepositoryId, setBranch: fs.setBranch, setAgentProfileId: fs.setAgentProfileId, setExecutorId: fs.setExecutorId, setSelectedWorkflowId: fs.setSelectedWorkflowId, setFetchedSteps: fs.setFetchedSteps, clearDraft: fs.clearDraft, }); const handleKeyDown = useKeyboardShortcutHandler(SHORTCUTS.SUBMIT, (event) => { submitHandlers.handleSubmit(event as unknown as FormEvent); }); return { fs, isSessionMode, isEditMode, isCreateMode, isTaskStarted, sessionRepoName, workflows, agentProfiles, snapshots, repositoriesLoading, branchesLoading, computed, handlers, submitHandlers, handleKeyDown, enhance: useEnhanceForDialog(fs), }; } export function TaskCreateDialog(props: TaskCreateDialogProps) { const { open, onOpenChange, initialValues, workspaceId } = props; const setup = useTaskCreateDialogSetup(props); const { fs, isSessionMode, isEditMode, isCreateMode, isTaskStarted } = setup; const { sessionRepoName, workflows, agentProfiles, snapshots } = setup; const { repositoriesLoading, branchesLoading, computed, handlers, handleKeyDown } = setup; const { handleSubmit, handleUpdateWithoutAgent, handleCreateWithoutAgent } = setup.submitHandlers; const { handleCreateWithPlanMode, handleCancel } = setup.submitHandlers; return (
); }