3a0c45a0b2
After the main merge, prettier expanded my hand-compacted destructuring in TaskCreateDialog onto multiple lines, putting the function at 115 lines (over the 100 cap). Extract the two prop-assembly blocks into buildDialogFormBodyProps + buildDialogFooterProps helper functions so the orchestrator becomes a thin JSX wrapper. No behaviour change — the helpers project the same shape, 1:1. Also fixes the upstream CI failure: prettier --check was rejecting the hand-compacted destructuring lines.
613 lines
22 KiB
TypeScript
613 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import { FormEvent, useCallback } from "react";
|
|
import type { JiraTicket } from "@/lib/types/jira";
|
|
import type { LinearIssue } from "@/lib/types/linear";
|
|
import { Dialog, DialogContent, DialogHeader, DialogFooter } from "@kandev/ui/dialog";
|
|
import type { Task, Repository } from "@/lib/types/http";
|
|
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 { 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 { useTaskSubmitHandlers } from "@/components/task-create-dialog-submit";
|
|
import { CreateModeSelectors } from "@/components/task-create-dialog-create-mode-selectors";
|
|
import { RepoChipsRow } from "@/components/task-create-dialog-repo-chips";
|
|
import { useToast } from "@/components/toast-provider";
|
|
import {
|
|
useDialogFormState,
|
|
useTaskCreateDialogEffects,
|
|
useDialogHandlers,
|
|
useLockedFieldSync,
|
|
useSessionRepoName,
|
|
useTaskCreateDialogData,
|
|
computeIsTaskStarted,
|
|
type DialogFormState,
|
|
type TaskCreateDialogInitialValues,
|
|
} from "@/components/task-create-dialog-state";
|
|
import type { DialogFormBodyProps } from "@/components/task-create-dialog-types";
|
|
|
|
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<string, unknown> }>;
|
|
on_turn_complete?: Array<{ type: string; config?: Record<string, unknown> }>;
|
|
};
|
|
}>;
|
|
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;
|
|
/**
|
|
* 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> | 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 computeHasAllBranches(fs: DialogFormState): boolean {
|
|
if (fs.noRepository) return true;
|
|
if (fs.useGitHubUrl) return !!fs.githubBranch;
|
|
return fs.repositories.length > 0 && fs.repositories.every((r) => !!r.branch);
|
|
}
|
|
|
|
function CreateModeBody(props: DialogFormBodyProps) {
|
|
const {
|
|
isCreateMode,
|
|
isEditMode,
|
|
isTaskStarted,
|
|
workspaceId,
|
|
onJiraImport,
|
|
onLinearImport,
|
|
agentProfileOptions,
|
|
executorProfileOptions,
|
|
agentProfiles,
|
|
agentProfilesLoading,
|
|
executorsLoading,
|
|
isCreatingSession,
|
|
fs,
|
|
onTaskNameChange,
|
|
onRowRepositoryChange,
|
|
onRowBranchChange,
|
|
onAgentProfileChange,
|
|
onExecutorProfileChange,
|
|
onToggleGitHubUrl,
|
|
onGitHubUrlChange,
|
|
onToggleFreshBranch,
|
|
workflowAgentLocked,
|
|
repositories,
|
|
freshBranchAvailable,
|
|
isLocalExecutor,
|
|
} = props;
|
|
const showTaskName = (isCreateMode || isEditMode) && !isTaskStarted;
|
|
const taskNameAutoFocus = !isEditMode && !fs.useGitHubUrl;
|
|
return (
|
|
<>
|
|
<RepoChipsRow
|
|
fs={fs}
|
|
repositories={repositories}
|
|
isTaskStarted={isTaskStarted}
|
|
workspaceId={workspaceId}
|
|
onRowRepositoryChange={onRowRepositoryChange}
|
|
onRowBranchChange={onRowBranchChange}
|
|
onToggleGitHubUrl={onToggleGitHubUrl}
|
|
onGitHubUrlChange={onGitHubUrlChange}
|
|
freshBranchAvailable={freshBranchAvailable}
|
|
freshBranchEnabled={fs.freshBranchEnabled}
|
|
onToggleFreshBranch={onToggleFreshBranch}
|
|
isLocalExecutor={isLocalExecutor}
|
|
onToggleNoRepository={props.onToggleNoRepository}
|
|
onWorkspacePathChange={props.onWorkspacePathChange}
|
|
/>
|
|
{showTaskName && (
|
|
<InlineTaskName
|
|
value={fs.taskName}
|
|
onChange={onTaskNameChange}
|
|
autoFocus={taskNameAutoFocus}
|
|
/>
|
|
)}
|
|
<DialogPromptSection
|
|
isSessionMode={false}
|
|
isTaskStarted={isTaskStarted}
|
|
isPassthroughProfile={props.isPassthroughProfile}
|
|
initialDescription={props.initialDescription}
|
|
hasDescription={props.hasDescription}
|
|
fs={fs}
|
|
handleKeyDown={props.handleKeyDown}
|
|
enhance={props.enhance}
|
|
workspaceId={workspaceId}
|
|
onJiraImport={onJiraImport}
|
|
onLinearImport={onLinearImport}
|
|
descriptionPlaceholder={props.descriptionPlaceholder}
|
|
aboveDescriptionSlot={props.aboveDescriptionSlot}
|
|
extraFormSlot={props.extraFormSlot}
|
|
autoFocusDescription={!isTaskStarted && !(showTaskName && taskNameAutoFocus)}
|
|
/>
|
|
<CreateModeSelectors
|
|
isTaskStarted={isTaskStarted}
|
|
agentProfileOptions={agentProfileOptions}
|
|
executorProfileOptions={executorProfileOptions}
|
|
agentProfiles={agentProfiles}
|
|
agentProfilesLoading={agentProfilesLoading}
|
|
executorsLoading={executorsLoading}
|
|
isCreatingSession={isCreatingSession}
|
|
fs={fs}
|
|
onAgentProfileChange={onAgentProfileChange}
|
|
onExecutorProfileChange={onExecutorProfileChange}
|
|
workflowAgentLocked={workflowAgentLocked}
|
|
noCompatibleAgent={props.noCompatibleAgent}
|
|
executorProfileName={props.executorProfileName}
|
|
/>
|
|
{props.bottomSlot}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function SessionModeBody(props: DialogFormBodyProps) {
|
|
return (
|
|
<>
|
|
<DialogPromptSection
|
|
isSessionMode
|
|
isTaskStarted={props.isTaskStarted}
|
|
isPassthroughProfile={props.isPassthroughProfile}
|
|
initialDescription={props.initialDescription}
|
|
hasDescription={props.hasDescription}
|
|
fs={props.fs}
|
|
handleKeyDown={props.handleKeyDown}
|
|
enhance={props.enhance}
|
|
workspaceId={props.workspaceId}
|
|
onJiraImport={props.onJiraImport}
|
|
/>
|
|
<SessionSelectors
|
|
agentProfileOptions={props.agentProfileOptions}
|
|
agentProfileId={props.fs.agentProfileId}
|
|
onAgentProfileChange={props.onAgentProfileChange}
|
|
agentProfilesLoading={props.agentProfilesLoading}
|
|
isCreatingSession={props.isCreatingSession}
|
|
executorProfileOptions={props.executorProfileOptions}
|
|
executorProfileId={props.fs.executorProfileId}
|
|
onExecutorProfileChange={props.onExecutorProfileChange}
|
|
executorsLoading={props.executorsLoading}
|
|
AgentSelectorComponent={AgentSelector}
|
|
ExecutorProfileSelectorComponent={ExecutorProfileSelector}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function DialogFormBody(props: DialogFormBodyProps) {
|
|
const { isSessionMode, isCreateMode, isTaskStarted, workflows, snapshots } = props;
|
|
return (
|
|
<div className="flex-1 space-y-4 overflow-y-auto pr-1">
|
|
{isSessionMode ? <SessionModeBody {...props} /> : <CreateModeBody {...props} />}
|
|
<WorkflowSection
|
|
isCreateMode={isCreateMode}
|
|
isTaskStarted={isTaskStarted}
|
|
workflows={workflows as Parameters<typeof WorkflowSection>[0]["workflows"]}
|
|
snapshots={snapshots as Parameters<typeof WorkflowSection>[0]["snapshots"]}
|
|
effectiveWorkflowId={props.effectiveWorkflowId}
|
|
onWorkflowChange={props.onWorkflowChange}
|
|
agentProfiles={props.agentProfiles}
|
|
workflowLocked={props.workflowLocked}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 useJiraImportHandler(fs: DialogFormState) {
|
|
return useCallback(
|
|
(ticket: JiraTicket) => {
|
|
const title = `[${ticket.key}] ${ticket.summary}`;
|
|
fs.setTaskName(title);
|
|
fs.setHasTitle(true);
|
|
const description = ticket.description?.trim()
|
|
? `${ticket.description}\n\n---\nJira: ${ticket.url}`
|
|
: `Jira: ${ticket.url}`;
|
|
fs.descriptionInputRef.current?.setValue(description);
|
|
fs.setHasDescription(true);
|
|
},
|
|
[fs],
|
|
);
|
|
}
|
|
|
|
function useLinearImportHandler(fs: DialogFormState) {
|
|
return useCallback(
|
|
(issue: LinearIssue) => {
|
|
const title = `[${issue.identifier}] ${issue.title}`;
|
|
fs.setTaskName(title);
|
|
fs.setHasTitle(true);
|
|
const description = issue.description?.trim()
|
|
? `${issue.description}\n\n---\nLinear: ${issue.url}`
|
|
: `Linear: ${issue.url}`;
|
|
fs.descriptionInputRef.current?.setValue(description);
|
|
fs.setHasDescription(true);
|
|
},
|
|
[fs],
|
|
);
|
|
}
|
|
|
|
type SubmitWiringArgs = {
|
|
props: TaskCreateDialogProps;
|
|
fs: ReturnType<typeof useDialogFormState>;
|
|
computed: ReturnType<typeof useTaskCreateDialogData>["computed"];
|
|
workspaceRepositories: ReturnType<typeof useTaskCreateDialogData>["repositories"];
|
|
repositoryLocalPath: string;
|
|
isSessionMode: boolean;
|
|
isEditMode: boolean;
|
|
};
|
|
|
|
function useSubmitHandlersWiring({
|
|
props,
|
|
fs,
|
|
computed,
|
|
workspaceRepositories,
|
|
repositoryLocalPath,
|
|
isSessionMode,
|
|
isEditMode,
|
|
}: SubmitWiringArgs) {
|
|
const { workspaceId, workflowId, editingTask, onSuccess, onCreateSession, onOpenChange } = props;
|
|
const { parentTaskId } = props;
|
|
const taskId = props.taskId ?? null;
|
|
return useTaskSubmitHandlers({
|
|
isSessionMode,
|
|
isEditMode,
|
|
isPassthroughProfile: computed.isPassthroughProfile,
|
|
taskName: fs.taskName,
|
|
workspaceId,
|
|
workflowId,
|
|
effectiveWorkflowId: computed.effectiveWorkflowId,
|
|
effectiveDefaultStepId: computed.effectiveDefaultStepId,
|
|
repositories: fs.repositories,
|
|
discoveredRepositories: fs.discoveredRepositories,
|
|
workspaceRepositories,
|
|
useGitHubUrl: fs.useGitHubUrl,
|
|
githubUrl: fs.githubUrl,
|
|
githubPrHeadBranch: fs.githubPrHeadBranch,
|
|
githubBranch: fs.githubBranch,
|
|
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,
|
|
setRepositories: fs.setRepositories,
|
|
setGitHubBranch: fs.setGitHubBranch,
|
|
setAgentProfileId: fs.setAgentProfileId,
|
|
setExecutorId: fs.setExecutorId,
|
|
setSelectedWorkflowId: fs.setSelectedWorkflowId,
|
|
setFetchedSteps: fs.setFetchedSteps,
|
|
clearDraft: fs.clearDraft,
|
|
freshBranchEnabled: fs.freshBranchEnabled,
|
|
isLocalExecutor: computed.isLocalExecutor,
|
|
repositoryLocalPath,
|
|
noRepository: fs.noRepository,
|
|
workspacePath: fs.workspacePath,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Resolves the on-disk path for the (single) selected row, used by the
|
|
* fresh-branch consent flow. Multi-row tasks hide fresh-branch in the UI,
|
|
* so we only need to resolve a path when there is exactly one row.
|
|
*/
|
|
function resolveSingleRowLocalPath(fs: DialogFormState, repositories: Repository[]): string {
|
|
if (fs.repositories.length !== 1) return "";
|
|
const row = fs.repositories[0];
|
|
if (row.localPath) return row.localPath;
|
|
if (row.repositoryId) {
|
|
return repositories.find((r) => r.id === row.repositoryId)?.local_path ?? "";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function useTaskCreateDialogSetup(props: TaskCreateDialogProps) {
|
|
const { open, mode = "create", workspaceId, workflowId, defaultStepId } = props;
|
|
const { editingTask, initialValues } = props;
|
|
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,
|
|
computed,
|
|
} = useTaskCreateDialogData(open, workspaceId, workflowId, defaultStepId, fs);
|
|
const repositoryLocalPath = resolveSingleRowLocalPath(fs, repositories);
|
|
useTaskCreateDialogEffects(fs, {
|
|
open,
|
|
workspaceId,
|
|
workflowId,
|
|
repositories,
|
|
repositoriesLoading,
|
|
agentProfiles,
|
|
executors,
|
|
workspaceDefaults: computed.workspaceDefaults,
|
|
toast,
|
|
workflows,
|
|
isLocalExecutor: computed.isLocalExecutor,
|
|
});
|
|
useLockedFieldSync(open, workflowId, initialValues, fs);
|
|
const handlers = useDialogHandlers(fs, repositories);
|
|
const submitHandlers = useSubmitHandlersWiring({
|
|
props,
|
|
fs,
|
|
computed,
|
|
workspaceRepositories: repositories,
|
|
repositoryLocalPath,
|
|
isSessionMode,
|
|
isEditMode,
|
|
});
|
|
const guardedHandleSubmit = useGuardedSubmit(
|
|
submitHandlers.handleSubmit,
|
|
props.submitBlockedReason,
|
|
);
|
|
const handleKeyDown = useKeyboardShortcutHandler(SHORTCUTS.SUBMIT, (event) => {
|
|
guardedHandleSubmit(event as unknown as FormEvent);
|
|
});
|
|
// Fresh-branch is single-row + local executor + not URL mode. The chip row
|
|
// can hold any number of repos; we hide the toggle whenever the question
|
|
// ("which repo do we discard local changes in?") becomes ambiguous.
|
|
const freshBranchAvailable =
|
|
!fs.useGitHubUrl && computed.isLocalExecutor && fs.repositories.length === 1;
|
|
return {
|
|
fs,
|
|
isSessionMode,
|
|
isEditMode,
|
|
isCreateMode,
|
|
isTaskStarted,
|
|
sessionRepoName,
|
|
workflows,
|
|
agentProfiles,
|
|
snapshots,
|
|
repositories,
|
|
repositoriesLoading,
|
|
computed,
|
|
handlers,
|
|
submitHandlers,
|
|
handleKeyDown,
|
|
freshBranchAvailable,
|
|
guardedHandleSubmit,
|
|
enhance: useEnhanceForDialog(fs),
|
|
handleJiraImport: useJiraImportHandler(fs),
|
|
handleLinearImport: useLinearImportHandler(fs),
|
|
};
|
|
}
|
|
|
|
// Buttons are disabled when submitBlockedReason is set, but the form can still
|
|
// be submitted via Enter; gate the submit path here so a wrapper's async
|
|
// bootstrap step always finishes before any task is created.
|
|
function useGuardedSubmit(
|
|
handleSubmit: (e: FormEvent) => void,
|
|
blockedReason: string | null | undefined,
|
|
) {
|
|
const blocked = Boolean(blockedReason);
|
|
return useCallback(
|
|
(e: FormEvent) => {
|
|
if (blocked) e.preventDefault();
|
|
else handleSubmit(e);
|
|
},
|
|
[blocked, handleSubmit],
|
|
);
|
|
}
|
|
|
|
// Build the props object passed to DialogFormBody. Extracted from
|
|
// TaskCreateDialog so the orchestrator stays under the 100-line cap; the
|
|
// shape just spreads setup + dialog props 1:1 so there's no behaviour change.
|
|
function buildDialogFormBodyProps(
|
|
setup: ReturnType<typeof useTaskCreateDialogSetup>,
|
|
props: TaskCreateDialogProps,
|
|
): DialogFormBodyProps {
|
|
const { fs, computed, handlers } = setup;
|
|
const repoLocked = !!props.lockedFields?.repository;
|
|
return {
|
|
isSessionMode: setup.isSessionMode,
|
|
isCreateMode: setup.isCreateMode,
|
|
isEditMode: setup.isEditMode,
|
|
isTaskStarted: setup.isTaskStarted,
|
|
onTaskNameChange: handlers.handleTaskNameChange,
|
|
onRowRepositoryChange: handlers.handleRowRepositoryChange,
|
|
onRowBranchChange: handlers.handleRowBranchChange,
|
|
isPassthroughProfile: computed.isPassthroughProfile,
|
|
initialDescription: fs.currentDefaults.description,
|
|
hasDescription: fs.hasDescription,
|
|
workspaceId: props.workspaceId,
|
|
onJiraImport: setup.handleJiraImport,
|
|
onLinearImport: setup.handleLinearImport,
|
|
agentProfileOptions: computed.agentProfileOptions,
|
|
executorProfileOptions: computed.executorProfileOptions,
|
|
agentProfiles: setup.agentProfiles,
|
|
agentProfilesLoading: computed.agentProfilesLoading,
|
|
executorsLoading: computed.executorsLoading,
|
|
isCreatingSession: fs.isCreatingSession,
|
|
workflows: setup.workflows,
|
|
snapshots: setup.snapshots,
|
|
effectiveWorkflowId: computed.effectiveWorkflowId ?? null,
|
|
fs,
|
|
handleKeyDown: setup.handleKeyDown,
|
|
onAgentProfileChange: handlers.handleAgentProfileChange,
|
|
onExecutorProfileChange: handlers.handleExecutorProfileChange,
|
|
onWorkflowChange: handlers.handleWorkflowChange,
|
|
onToggleGitHubUrl: repoLocked ? undefined : handlers.handleToggleGitHubUrl,
|
|
onGitHubUrlChange: handlers.handleGitHubUrlChange,
|
|
onToggleFreshBranch: handlers.handleToggleFreshBranch,
|
|
onToggleNoRepository: repoLocked ? undefined : handlers.handleToggleNoRepository,
|
|
onWorkspacePathChange: handlers.handleWorkspacePathChange,
|
|
enhance: setup.enhance,
|
|
workflowAgentLocked: computed.workflowAgentLocked,
|
|
repositories: setup.repositories,
|
|
freshBranchAvailable: setup.freshBranchAvailable,
|
|
isLocalExecutor: computed.isLocalExecutor,
|
|
noCompatibleAgent: computed.noCompatibleAgent,
|
|
executorProfileName: computed.selectedExecutorProfileName,
|
|
extraFormSlot: props.extraFormSlot,
|
|
aboveDescriptionSlot: props.aboveDescriptionSlot,
|
|
bottomSlot: props.bottomSlot,
|
|
descriptionPlaceholder: props.descriptionPlaceholder,
|
|
workflowLocked: props.lockedFields?.workflow,
|
|
};
|
|
}
|
|
|
|
function buildDialogFooterProps(
|
|
setup: ReturnType<typeof useTaskCreateDialogSetup>,
|
|
props: TaskCreateDialogProps,
|
|
) {
|
|
const { fs, computed, submitHandlers } = setup;
|
|
return {
|
|
isSessionMode: setup.isSessionMode,
|
|
isCreateMode: setup.isCreateMode,
|
|
isEditMode: setup.isEditMode,
|
|
isTaskStarted: setup.isTaskStarted,
|
|
isPassthroughProfile: computed.isPassthroughProfile,
|
|
isCreatingSession: fs.isCreatingSession,
|
|
isCreatingTask: fs.isCreatingTask,
|
|
hasTitle: fs.hasTitle,
|
|
hasDescription: fs.hasDescription,
|
|
hasRepositorySelection: computed.hasRepositorySelection,
|
|
hasAllBranches: computeHasAllBranches(fs),
|
|
agentProfileId: computed.effectiveAgentProfileId,
|
|
workspaceId: props.workspaceId,
|
|
effectiveWorkflowId: computed.effectiveWorkflowId ?? null,
|
|
executorHint: computed.executorHint,
|
|
noCompatibleAgent: computed.noCompatibleAgent,
|
|
executorProfileName: computed.selectedExecutorProfileName,
|
|
onCancel: submitHandlers.handleCancel,
|
|
onUpdateWithoutAgent: submitHandlers.handleUpdateWithoutAgent,
|
|
onCreateWithoutAgent: submitHandlers.handleCreateWithoutAgent,
|
|
onCreateWithPlanMode: submitHandlers.handleCreateWithPlanMode,
|
|
submitBlockedReason: props.submitBlockedReason,
|
|
};
|
|
}
|
|
|
|
export function TaskCreateDialog(props: TaskCreateDialogProps) {
|
|
const setup = useTaskCreateDialogSetup(props);
|
|
return (
|
|
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
|
<DialogContent
|
|
data-testid="create-task-dialog"
|
|
className="w-full h-full max-w-full max-h-full rounded-none sm:w-[900px] sm:h-auto sm:max-w-none sm:max-h-[85vh] sm:rounded-lg flex flex-col"
|
|
>
|
|
<DialogHeader>
|
|
<DialogHeaderContent
|
|
isCreateMode={setup.isCreateMode}
|
|
isEditMode={setup.isEditMode}
|
|
sessionRepoName={setup.sessionRepoName}
|
|
initialTitle={props.initialValues?.title}
|
|
/>
|
|
</DialogHeader>
|
|
<form onSubmit={setup.guardedHandleSubmit} className="flex flex-col gap-4 overflow-hidden">
|
|
<DialogFormBody {...buildDialogFormBodyProps(setup, props)} />
|
|
<DialogFooter className="border-t border-border pt-3 flex-col gap-3 sm:flex-row sm:gap-2">
|
|
<TaskCreateDialogFooter {...buildDialogFooterProps(setup, props)} />
|
|
</DialogFooter>
|
|
</form>
|
|
<PendingDiscardModal pending={setup.submitHandlers.pendingDiscard} />
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
function PendingDiscardModal({
|
|
pending,
|
|
}: {
|
|
pending: ReturnType<typeof useTaskSubmitHandlers>["pendingDiscard"];
|
|
}) {
|
|
if (!pending) return null;
|
|
return (
|
|
<DiscardLocalChangesDialog
|
|
open
|
|
onOpenChange={(next) => {
|
|
if (!next) pending.resolve(false);
|
|
}}
|
|
dirtyFiles={pending.dirtyFiles}
|
|
repoPath={pending.repoPath}
|
|
onConfirm={() => pending.resolve(true)}
|
|
onCancel={() => pending.resolve(false)}
|
|
/>
|
|
);
|
|
}
|