"use client"; import { FormEvent, useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Dialog, DialogContent, DialogHeader, DialogFooter } from "@kandev/ui/dialog"; import type { Task } from "@/lib/types/http"; import type { TaskCreateLastUsedState } from "@/lib/state/slices/settings/types"; import { isNativeSubmitDisabled, TaskCreateDialogFooter, } from "@/components/task-create-dialog-footer"; import { DiscardLocalChangesDialog } from "@/components/discard-local-changes-dialog"; import { DialogHeaderContent } from "@/components/task-create-dialog-header"; import { SessionSelectors, WorkflowSection, DialogPromptSection, } from "@/components/task-create-dialog-form-body"; import { AgentSelector, ExecutorProfileSelector, InlineTaskName, } from "@/components/task-create-dialog-selectors"; import { CreateModeSelectors } from "@/components/task-create-dialog-create-mode-selectors"; import { RepoChipsRow } from "@/components/task-create-dialog-repo-chips"; import type { TaskCreateDialogInitialValues } from "@/components/task-create-dialog-state"; import type { DialogFormBodyProps } from "@/components/task-create-dialog-types"; import { buildDialogFooterProps, buildDialogFormBodyProps, } from "@/components/task-create-dialog-prop-builders"; import { resetTaskCreateLastUsedSync } from "@/components/task-create-dialog-handlers"; import { useAppStore } from "@/components/state-provider"; import { TaskCreateDialogPopoverContainerProvider } from "@/hooks/use-task-create-dialog-popover-container"; import { shouldShowTaskTitleField } from "@/components/task-create-dialog-helpers"; import { useTaskCreateDialogSetup } from "@/components/task-create-dialog-setup"; export 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; willNavigate?: boolean }, ) => void; onCreateSession?: (data: { prompt: string; agentProfileId: string; executorId: string; attachments?: ReturnType< typeof import("@/components/task-create-dialog-helpers").toMessageAttachments >; }) => void; initialValues?: TaskCreateDialogInitialValues; taskId?: string | null; parentTaskId?: string; /** * Pin specific form fields to their initial values (used by feature wrappers * like Improve Kandev that fix the repo + branch + workflow). The current * implementation just passes the locks through; the chip row's first repo * is overwritten on each open. The flags are kept for forward compat with * locking the editor UI itself in a future pass. */ lockedFields?: { repository?: boolean; branch?: boolean; workflow?: boolean }; /** Optional submit hook used by Improve Kandev to wrap the description. */ transformDescriptionBeforeSubmit?: (description: string) => Promise | string; /** Optional override for the description placeholder. */ descriptionPlaceholder?: string; /** Optional render slot above the description editor. */ aboveDescriptionSlot?: React.ReactNode; /** Optional render slot inside the dialog (between body and footer). */ extraFormSlot?: React.ReactNode; /** Optional render slot at the bottom of the dialog footer area. */ bottomSlot?: React.ReactNode; /** * When set, every submit button is disabled and the tooltip surfaces this * exact reason (e.g. an async bootstrap step from a feature wrapper hasn't * completed yet). Takes precedence over the usual missing-field reasons. */ submitBlockedReason?: string | null; } function CreateModeBody(props: DialogFormBodyProps) { const { isCreateMode, autoTitle, isEditMode, isTaskStarted, workspaceId, onJiraImport, onLinearImport, fs, onTaskNameChange, onRowRepositoryChange, onRowBranchChange, onToggleRemote, onToggleFreshBranch, repositories, onRefreshRepositories, repositoriesRefreshing, freshBranchAvailable, isLocalExecutor, localRepositoryCreation, } = props; const showTaskName = shouldShowTaskTitleField(isCreateMode, isEditMode, isTaskStarted) && !autoTitle; const taskNameAutoFocus = !autoTitle && !isEditMode && !fs.useRemote; return ( <> {showTaskName && ( )} {props.bottomSlot} ); } function CreateModeAgentSelectors(props: DialogFormBodyProps) { return ( ); } function SessionModeBody(props: DialogFormBodyProps) { return ( <> ); } function DialogFormBody(props: DialogFormBodyProps) { const { isSessionMode, isCreateMode, isTaskStarted, workflows, snapshots } = props; return (
{isSessionMode ? : } [0]["workflows"]} snapshots={snapshots as Parameters[0]["snapshots"]} effectiveWorkflowId={props.effectiveWorkflowId} onWorkflowChange={props.onWorkflowChange} agentProfiles={props.agentProfiles} workflowLocked={props.workflowLocked} />
); } // Synthetic submit event used by the voice auto-send path. Calling the form // handler directly (instead of `form.requestSubmit()`) matches the chat // composer's pattern and avoids the Safari < 16 gap where `requestSubmit` is // missing on `HTMLFormElement`. `guardedHandleSubmit` only reads // `preventDefault` off the event, so a stubbed shape is sufficient. const VOICE_SUBMIT_EVENT = { preventDefault: () => {} } as unknown as FormEvent; export function TaskCreateDialog(props: TaskCreateDialogProps) { const { t } = useTranslation("chat"); const syncedTaskCreateLastUsed = useAppStore((state) => state.userSettings.taskCreateLastUsed); const preserveQueuedLastUsedOnCloseRef = useRef<{ syncedSettings: TaskCreateLastUsedState | null | undefined; } | null>(null); const queuedLastUsedResetHandledRef = useRef(false); const preserveQueuedLastUsedOnClose = useCallback(() => { preserveQueuedLastUsedOnCloseRef.current = { syncedSettings: syncedTaskCreateLastUsed }; }, [syncedTaskCreateLastUsed]); const resetQueuedLastUsedOnClose = useCallback(() => { const preserveQueued = preserveQueuedLastUsedOnCloseRef.current; resetTaskCreateLastUsedSync({ clearQueued: !preserveQueued, syncedSettings: preserveQueued?.syncedSettings, }); preserveQueuedLastUsedOnCloseRef.current = null; queuedLastUsedResetHandledRef.current = true; }, []); const setup = useTaskCreateDialogSetup(props, { preserveQueuedLastUsedOnClose }); const { guardedHandleSubmit } = setup; const [popoverContainer, setPopoverContainer] = useState(null); useEffect(() => { if (props.open) { preserveQueuedLastUsedOnCloseRef.current = null; queuedLastUsedResetHandledRef.current = false; return resetQueuedLastUsedOnClose; } if (queuedLastUsedResetHandledRef.current) { queuedLastUsedResetHandledRef.current = false; return; } resetQueuedLastUsedOnClose(); }, [props.open, resetQueuedLastUsedOnClose]); // Programmatic submissions use the native control's preflight before // entering the same guarded submit handler as the form button. const pendingAttachmentReason = setup.fs.hasPendingAttachmentUploads ? t("chat:attachmentUploadPendingSubmit") : null; const nativeSubmitDisabled = isNativeSubmitDisabled( buildDialogFooterProps(setup, props, pendingAttachmentReason), ); const handleVoiceAutoSend = useCallback(() => { if (nativeSubmitDisabled) return false; guardedHandleSubmit(VOICE_SUBMIT_EVENT); return true; }, [guardedHandleSubmit, nativeSubmitDisabled]); return (
); } function PendingDiscardModal({ pending, }: { pending: ReturnType["submitHandlers"]["pendingDiscard"]; }) { if (!pending) return null; return ( { if (!next) pending.resolve(false); }} dirtyFiles={pending.dirtyFiles} repoPath={pending.repoPath} onConfirm={() => pending.resolve(true)} onCancel={() => pending.resolve(false)} /> ); }