"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 { 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 { CreateModeSelectors } from "@/components/task-create-dialog-create-mode-selectors";
import {
AgentSelector,
ExecutorProfileSelector,
InlineTaskName,
} from "@/components/task-create-dialog-selectors";
import { RepoChipsRow } from "@/components/task-create-dialog-repo-chips";
import { TaskCreateAdvancedSettings } from "@/components/task-create-dialog-advanced-settings";
import type {
DialogFormBodyProps,
TaskCreateDialogProps,
} 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 type { TaskCreateDialogProps } from "@/components/task-create-dialog-types";
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 a plugin composer action's submit. 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 PROGRAMMATIC_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 handleComposerSubmit = useCallback(() => {
if (nativeSubmitDisabled) return false;
guardedHandleSubmit(PROGRAMMATIC_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)}
/>
);
}