"use client"; import { memo, useCallback, useState } from "react"; import Link from "next/link"; import type { AgentProfileOption } from "@/lib/state/slices"; import type { WorkflowSnapshotData } from "@/lib/state/slices/kanban/types"; import { WorkflowSelectorRow } from "@/components/workflow-selector-row"; import { AgentLogo } from "@/components/agent-logo"; import type { DialogFormState } from "@/components/task-create-dialog-types"; import type { useKeyboardShortcutHandler } from "@/hooks/use-keyboard-shortcut"; import { TaskFormInputs } from "@/components/task-create-dialog-selectors"; import { FreshBranchToggle, computeBranchPlaceholder, } from "@/components/task-create-dialog-fresh-branch"; import type { JiraTicket } from "@/lib/types/jira"; import type { LinearIssue } from "@/lib/types/linear"; type SelectorOption = { value: string; label: string; renderLabel: () => React.ReactNode; }; type CreateEditSelectorsProps = { isTaskStarted: boolean; hasRepositorySelection: boolean; branchOptions: SelectorOption[]; branch: string; onBranchChange: (value: string) => void; branchesLoading: boolean; onRefreshBranches?: () => void; branchesFetchedAt?: string; branchesFetchError?: string; localBranchesLoading: boolean; agentProfiles: AgentProfileOption[]; agentProfilesLoading: boolean; agentProfileOptions: SelectorOption[]; agentProfileId: string; onAgentProfileChange: (value: string) => void; isCreatingSession: boolean; executorProfileOptions: Array<{ value: string; label: string; renderLabel?: () => React.ReactNode; }>; executorProfileId: string; onExecutorProfileChange: (value: string) => void; executorsLoading: boolean; BranchSelectorComponent: React.ComponentType<{ options: SelectorOption[]; value: string; onValueChange: (value: string) => void; disabled: boolean; placeholder: string; searchPlaceholder: string; emptyMessage: string; onRefresh?: () => void; refreshing?: boolean; fetchedAt?: string; fetchError?: string; loading?: boolean; }>; AgentSelectorComponent: React.ComponentType<{ options: SelectorOption[]; value: string; onValueChange: (value: string) => void; disabled: boolean; placeholder: string; triggerClassName?: string; }>; ExecutorProfileSelectorComponent: React.ComponentType<{ options: Array<{ value: string; label: string; renderLabel?: () => React.ReactNode }>; value: string; onValueChange: (value: string) => void; disabled: boolean; placeholder: string; triggerClassName?: string; }>; isLocalExecutor: boolean; useGitHubUrl: boolean; workflowAgentLocked: boolean; freshBranchEnabled: boolean; onToggleFreshBranch: (enabled: boolean) => void; currentLocalBranch: string; branchLocked?: boolean; }; type AgentColumnProps = Pick< CreateEditSelectorsProps, | "agentProfiles" | "agentProfilesLoading" | "agentProfileOptions" | "agentProfileId" | "onAgentProfileChange" | "isCreatingSession" | "AgentSelectorComponent" | "workflowAgentLocked" >; function AgentColumn({ agentProfiles, agentProfilesLoading, agentProfileOptions, agentProfileId, onAgentProfileChange, isCreatingSession, AgentSelectorComponent, workflowAgentLocked, }: AgentColumnProps) { if (agentProfiles.length === 0 && !agentProfilesLoading) { return (
No agents found. Add agent
); } const placeholder = agentProfilesLoading ? "Loading agents..." : "Select agent"; return ( <> {workflowAgentLocked && (

Agent set by workflow

)} ); } function computeBranchDisabled(args: { branchLocked?: boolean; lockedToCurrentBranch: boolean; hasRepositorySelection: boolean; branchesLoading: boolean; localBranchesLoading: boolean; optionCount: number; }): boolean { if (args.branchLocked) return true; if (args.lockedToCurrentBranch) return true; if (!args.hasRepositorySelection) return true; if (args.branchesLoading || args.localBranchesLoading) return true; return args.optionCount === 0; } export const CreateEditSelectors = memo(function CreateEditSelectors( props: CreateEditSelectorsProps, ) { if (props.isTaskStarted) return null; const { hasRepositorySelection, branchOptions, branch, onBranchChange, branchesLoading, onRefreshBranches, branchesFetchedAt, branchesFetchError, localBranchesLoading, executorProfileOptions, executorProfileId, onExecutorProfileChange, executorsLoading, isLocalExecutor, useGitHubUrl, freshBranchEnabled, onToggleFreshBranch, currentLocalBranch, BranchSelectorComponent, ExecutorProfileSelectorComponent, branchLocked, } = props; const isLocalWithoutGitHubUrl = isLocalExecutor && !useGitHubUrl; // When `branchLocked` is true the caller pinned a specific branch (e.g. // Improve Kandev) — suppress the local-executor "current branch" shortcut and // the FreshBranchToggle so users can't drift away from the pinned value. const lockedToCurrentBranch = !branchLocked && isLocalWithoutGitHubUrl && !freshBranchEnabled; const branchPlaceholder = computeBranchPlaceholder({ lockedToCurrentBranch, currentLocalBranch, hasRepositorySelection, loading: branchesLoading || localBranchesLoading, optionCount: branchOptions.length, }); const branchDisabled = computeBranchDisabled({ branchLocked, lockedToCurrentBranch, hasRepositorySelection, branchesLoading, localBranchesLoading, optionCount: branchOptions.length, }); // When the local executor locks to the current branch, ignore any branch // value the user picked under a different executor — the placeholder // should reflect the actual checked-out branch instead. const branchValue = lockedToCurrentBranch ? "" : branch; return (
{isLocalWithoutGitHubUrl && !branchLocked && ( )}
); }); type SessionSelectorsProps = { agentProfileOptions: SelectorOption[]; agentProfileId: string; onAgentProfileChange: (value: string) => void; agentProfilesLoading: boolean; isCreatingSession: boolean; executorProfileOptions: Array<{ value: string; label: string; renderLabel?: () => React.ReactNode; }>; executorProfileId: string; onExecutorProfileChange: (value: string) => void; executorsLoading: boolean; AgentSelectorComponent: React.ComponentType<{ options: SelectorOption[]; value: string; onValueChange: (value: string) => void; disabled: boolean; placeholder: string; triggerClassName?: string; }>; ExecutorProfileSelectorComponent: React.ComponentType<{ options: Array<{ value: string; label: string; renderLabel?: () => React.ReactNode }>; value: string; onValueChange: (value: string) => void; disabled: boolean; placeholder: string; triggerClassName?: string; }>; }; export const SessionSelectors = memo(function SessionSelectors({ agentProfileOptions, agentProfileId, onAgentProfileChange, agentProfilesLoading, isCreatingSession, executorProfileOptions, executorProfileId, onExecutorProfileChange, executorsLoading, AgentSelectorComponent, ExecutorProfileSelectorComponent, }: SessionSelectorsProps) { return (
); }); type WorkflowSectionProps = { isCreateMode: boolean; isTaskStarted: boolean; workflows: Array<{ id: string; name: string; agent_profile_id?: string; hidden?: boolean; [key: string]: unknown; }>; snapshots: Record; effectiveWorkflowId: string | null; onWorkflowChange: (value: string) => void; agentProfiles: AgentProfileOption[]; }; export const WorkflowSection = memo(function WorkflowSection({ isCreateMode, isTaskStarted, workflows: allWorkflows, snapshots, effectiveWorkflowId, onWorkflowChange, agentProfiles, }: WorkflowSectionProps) { const [lastUsedWorkflowId, setLastUsedWorkflowId] = useState(null); // Hidden workflows (e.g. improve-kandev) are excluded from the picker; they // remain reachable via their dedicated entry point. const workflows = allWorkflows.filter((w) => !w.hidden); const handleWorkflowChange = useCallback( (workflowId: string) => { setLastUsedWorkflowId(workflowId); onWorkflowChange(workflowId); }, [onWorkflowChange], ); if (!isCreateMode || isTaskStarted) return null; if (!effectiveWorkflowId || workflows.length > 1) { return ( ); } // Single selected workflow — show agent override info if any overrides exist if (workflows.length === 1) { const singleWorkflow = workflows[0]; if (!singleWorkflow) return null; const snapshot = snapshots[singleWorkflow.id]; const workflowProfile = singleWorkflow.agent_profile_id ? agentProfiles.find((p) => p.id === singleWorkflow.agent_profile_id) : null; const stepsWithOverrides = (snapshot?.steps ?? []) .filter((s) => s.agent_profile_id) .map((s) => ({ name: s.title, profile: agentProfiles.find((p) => p.id === s.agent_profile_id), })) .filter((s) => s.profile); if (!workflowProfile && stepsWithOverrides.length === 0) return null; return (
{workflowProfile && ( {workflowProfile.label} )} {stepsWithOverrides.map((s) => ( {s.name}: {s.profile!.label} ))}
); } return null; }); export type DialogPromptSectionProps = { isSessionMode: boolean; isTaskStarted: boolean; isPassthroughProfile: boolean; initialDescription: string; hasDescription: boolean; fs: DialogFormState; handleKeyDown: ReturnType; enhance?: { onEnhance: () => void; isLoading: boolean; isConfigured: boolean }; workspaceId?: string | null; onJiraImport?: (ticket: JiraTicket) => void; onLinearImport?: (issue: LinearIssue) => void; /** Extension slot rendered below the description textarea (e.g. log-capture toggle). */ extraFormSlot?: React.ReactNode; /** Optional override for the description textarea placeholder. */ descriptionPlaceholder?: string; /** Optional slot rendered above the description textarea (e.g. a tab toggle). */ aboveDescriptionSlot?: React.ReactNode; }; // importBindings collapses the optional Jira/Linear import callbacks into the // shape TaskFormInputs expects, dropping integrations that aren't applicable // (session mode, started tasks, or no callback wired). Keeps DialogPromptSection // below the cyclomatic-complexity bar. function importBindings( enabled: boolean, workspaceId: string | null, isPassthroughProfile: boolean, onImport: ((value: T) => void) | undefined, ) { if (!enabled || !onImport) return undefined; return { workspaceId, disabled: isPassthroughProfile, onImport }; } export function DialogPromptSection({ isSessionMode, isTaskStarted, isPassthroughProfile, initialDescription, hasDescription, fs, handleKeyDown, enhance, workspaceId, onJiraImport, onLinearImport, extraFormSlot, descriptionPlaceholder, aboveDescriptionSlot, }: DialogPromptSectionProps) { const importsEnabled = !isSessionMode && !isTaskStarted; const ws = workspaceId ?? null; const placeholder = isPassthroughProfile ? "Passthrough mode — prompt not supported" : descriptionPlaceholder; return ( <> {aboveDescriptionSlot} {extraFormSlot} {isPassthroughProfile && hasDescription && (

Prompt ignored — passthrough mode active

)} ); }