From 612195fde661f00a70b8205d2b752d3eaf872979 Mon Sep 17 00:00:00 2001 From: Fusion Agent Date: Wed, 19 Aug 2026 00:44:39 +0000 Subject: [PATCH] FN-022: add workflow selection and start action Add workflow-aware task creation controls and a guarded Start action for quick task intake. - Add workflow selection and resolved board metadata loading to New Task modal. - Support starting eligible tasks in their workflow destination with clear success or partial-failure feedback. - Wire move-task handling through modal callers and cover workflow/start behavior with tests and documentation. Files changed: .changeset/fn-022-new-task-workflow-start.md | 7 + docs/dashboard-guide.md | 4 +- packages/dashboard/app/components/AppModals.tsx | 1 + packages/dashboard/app/components/NewTaskModal.tsx | 158 +++++++++++++++-- .../dashboard/app/components/QuickEntryBox.tsx | 5 +- packages/dashboard/app/components/TaskForm.tsx | 28 ++- .../app/components/__tests__/AppModals.test.tsx | 33 +++- .../app/components/__tests__/NewTaskModal.test.tsx | 192 ++++++++++++++++++++- .../app/utils/__tests__/quickAddStart.test.ts | 16 +- packages/dashboard/app/utils/quickAddStart.ts | 15 ++ 10 files changed, 441 insertions(+), 18 deletions(-) Fusion-Task-Id: FN-022 Fusion-Task-Lineage: ce794b44-3c31-4bfc-a259-ecfd78531be1 Co-authored-by: Fusion --- .changeset/fn-022-new-task-workflow-start.md | 7 + docs/dashboard-guide.md | 4 +- .../dashboard/app/components/AppModals.tsx | 1 + .../dashboard/app/components/NewTaskModal.tsx | 158 +++++++++++++- .../app/components/QuickEntryBox.tsx | 5 +- .../dashboard/app/components/TaskForm.tsx | 28 ++- .../components/__tests__/AppModals.test.tsx | 33 ++- .../__tests__/NewTaskModal.test.tsx | 192 +++++++++++++++++- .../app/utils/__tests__/quickAddStart.test.ts | 16 +- packages/dashboard/app/utils/quickAddStart.ts | 15 ++ 10 files changed, 441 insertions(+), 18 deletions(-) create mode 100644 .changeset/fn-022-new-task-workflow-start.md diff --git a/.changeset/fn-022-new-task-workflow-start.md b/.changeset/fn-022-new-task-workflow-start.md new file mode 100644 index 0000000000..dfa023524d --- /dev/null +++ b/.changeset/fn-022-new-task-workflow-start.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Preserve New Task workflow choices and add a guarded Start action for manual-intake workflows. +category: fix +dev: Start uses server-derived manual-intake metadata and validated workflow move targets. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 69723dc6b4..95eec4e756 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -492,7 +492,9 @@ Behavior: Workflows define how a task moves through planning, execution, review, workflow steps, merge, and any custom graph policy. Most coding tasks can stay on the default Coding workflow, but task and board workflow controls can select a different built-in or custom workflow per task. For the built-in catalog and runtime semantics, see [Workflow Steps → Workflow overview](./workflow-steps.md#workflow-overview). -When creating a task from the full **New Task** dialog, the **Workflow** advanced control opens a styled dropdown instead of a native select. Built-in workflows show the Fusion mark, custom workflows show their configured compact icon when present, **No workflow** remains the explicit opt-out, and leaving the picker untouched still inherits the project/default workflow. Opening the dialog while viewing a specific Board or List workflow preselects that workflow across lane, sidebar, keyboard-shortcut, and description-seeded entry points; opening from **All workflows** instead leaves the picker unset so the project default applies. +When creating a task from the full **New Task** dialog, the **Workflow** advanced control opens a styled dropdown instead of a native select. Built-in workflows show the Fusion mark, custom workflows show their configured compact icon when present, **No workflow** remains the explicit opt-out, and leaving the picker untouched still inherits the project/default workflow. Opening the dialog while viewing a specific Board or List workflow preselects that workflow across lane, sidebar, keyboard-shortcut, and description-seeded entry points; opening from **All workflows** instead leaves the picker unset so the project default applies. The selected workflow is preserved when you choose **Create** or acknowledge a duplicate warning. + +Manual-intake workflows also expose **Start** beside **Create** only when the server-provided workflow metadata proves a safe working destination. Coding (Ideas) creates directly in its validated working lane; other manual workflows create in intake and then perform one validated move. Start reports tasks as queued for planning, not as already planning, and a failed follow-up move reports the created-but-not-started partial outcome without deleting the task. Optional workflow steps can be toggled from the task **Edit** form's **More options → Workflow Steps** control or from the task's **Workflow** tab. The edit form uses the task's resolved workflow and preserves the task's current stored selection when it opens; workflow-authored `defaultOn` values remain a create-time/runtime default, not an edit-form re-seed. diff --git a/packages/dashboard/app/components/AppModals.tsx b/packages/dashboard/app/components/AppModals.tsx index 0aee967c56..5a78defb8e 100644 --- a/packages/dashboard/app/components/AppModals.tsx +++ b/packages/dashboard/app/components/AppModals.tsx @@ -483,6 +483,7 @@ export function AppModals({ onClose={closeNewTaskWithNav} tasks={tasks} onCreateTask={handleModalCreateWithOnboardingTracking} + onMoveTask={(taskId, column) => taskOperations.moveTask(taskId, column as Column)} addToast={addToast} projectId={projectId} initialDescription={modalManager.newTaskInitialDescription ?? ""} diff --git a/packages/dashboard/app/components/NewTaskModal.tsx b/packages/dashboard/app/components/NewTaskModal.tsx index c3db025ef9..4bc9511b49 100644 --- a/packages/dashboard/app/components/NewTaskModal.tsx +++ b/packages/dashboard/app/components/NewTaskModal.tsx @@ -2,7 +2,7 @@ import "./NewTaskModal.css"; import { useState, useCallback, useEffect, useRef, type ChangeEvent } from "react"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; -import { DEFAULT_TASK_PRIORITY, type Task, type TaskPriority, type ThinkingLevel } from "@fusion/core"; +import { DEFAULT_TASK_PRIORITY, type ColumnId, type Task, type TaskPriority, type ThinkingLevel } from "@fusion/core"; import { getErrorMessage } from "@fusion/core"; import type { ToastType } from "../hooks/useToast"; import { @@ -11,6 +11,8 @@ import { checkDuplicateTasks, fetchGitRemotes, uploadAttachment, + fetchBoardWorkflows, + type BoardWorkflowsPayload, type CreateTaskInput, type DuplicateMatch, type GitHubIssue, @@ -31,6 +33,7 @@ import { useNodes } from "../hooks/useNodes"; import { useViewportMode } from "../hooks/useViewportMode"; import { useAgentsMapCache } from "../hooks/useAgentsMapCache"; import { FloatingWindow } from "./FloatingWindow"; +import { resolveQuickAddStartInitialColumn, resolveQuickAddStartTargetColumn, resolveQuickAddStartWorkflowTarget, validateQuickAddStartWorkflow, workflowSupportsQuickAddStart, type ValidatedQuickAddWorkflow } from "../utils/quickAddStart"; type NewTaskCreateInput = Omit & { branchSelection?: { @@ -46,6 +49,7 @@ interface NewTaskModalProps { projectId?: string; tasks: Task[]; // for dependency selection onCreateTask: (input: NewTaskCreateInput) => Promise; + onMoveTask?: (taskId: string, column: ColumnId) => Promise; addToast: (message: string, type?: ToastType) => void; initialDescription?: string; initialWorkflowId?: string | null; @@ -87,6 +91,14 @@ function buildGitHubPullPrompt(pull: GitHubReferenceOption): string { return `Fetch and read this GitHub pull request, inspect the conversation, review comments, check failures, and changed files as needed, then resolve or address all actionable PR review comments.\n\nPR: ${pull.url}\n\nKeep the PR intent intact while making the requested fixes, and verify the result with targeted tests.`; } +function isUsableBoardWorkflowsPayload(value: unknown): value is BoardWorkflowsPayload { + if (!value || typeof value !== "object" || Array.isArray(value)) return false; + const payload = value as Partial; + return payload.flagEnabled === true + && typeof payload.defaultWorkflowId === "string" + && Array.isArray(payload.workflows); +} + function defaultGitHubRemote(remotes: GitRemote[]): GitRemote | undefined { if (remotes.length === 1) return remotes[0]; return remotes.find((remote) => remote.name === "origin"); @@ -323,7 +335,7 @@ function NewTaskGitHubReferencePicker({ isOpen, projectId, disabled = false, onS ); } -export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, addToast, initialDescription = "", initialWorkflowId, onPlanningMode, onSubtaskBreakdown }: NewTaskModalProps) { +export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, onMoveTask, addToast, initialDescription = "", initialWorkflowId, onPlanningMode, onSubtaskBreakdown }: NewTaskModalProps) { const { t } = useTranslation("app"); const { confirm } = useConfirm(); const viewportMode = useViewportMode(); @@ -373,11 +385,20 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, // `null` = explicit "No workflow", `string` = a specific workflow. Materialized // atomically at create time via the `workflowId` create parameter. const [selectedWorkflowId, setSelectedWorkflowId] = useState(undefined); + /* + FNXC:NewTaskWorkflowStart 2026-08-19-00:16: + Preserve the tri-state workflow choice across duplicate acknowledgement. The modal locks the + controls while checking duplicates, but this snapshot also prevents a late rerender from + replacing the operator's explicit string, null opt-out, or omitted default intent. + */ + const pendingWorkflowSelectionRef = useRef(undefined); + const pendingStartWorkflowRef = useRef(null); // Optional workflow steps the user opted into; TaskForm fetches + seeds these // from the selected workflow's defaultOn and lifts the enabled set up here. const [enabledWorkflowSteps, setEnabledWorkflowSteps] = useState([]); const [shouldSubmitEnabledWorkflowSteps, setShouldSubmitEnabledWorkflowSteps] = useState(false); const [hasUserSelectedEnabledWorkflowSteps, setHasUserSelectedEnabledWorkflowSteps] = useState(false); + const [boardWorkflows, setBoardWorkflows] = useState(null); const [reviewLevel, setReviewLevel] = useState(undefined); const [autoMerge, setAutoMerge] = useState(undefined); const [priority, setPriority] = useState(DEFAULT_TASK_PRIORITY); @@ -459,6 +480,31 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, wasOpenRef.current = isOpen; }, [initialDescription, initialWorkflowId, isOpen]); + /* + FNXC:NewTaskWorkflowStart 2026-08-19-00:17: + Start eligibility comes from the board endpoint's resolved manualIntake metadata, not the + editable workflow picker catalog. Clear it on every modal open so stale project/workflow data + cannot expose a lifecycle action after a project switch or metadata failure. + */ + useEffect(() => { + if (!isOpen) { + setBoardWorkflows(null); + return; + } + let cancelled = false; + setBoardWorkflows(null); + fetchBoardWorkflows(projectId) + .then((payload) => { + if (!cancelled && isUsableBoardWorkflowsPayload(payload)) setBoardWorkflows(payload); + }) + .catch(() => { + if (!cancelled) setBoardWorkflows(null); + }); + return () => { + cancelled = true; + }; + }, [isOpen, projectId]); + // Load agents for agent picker const loadAgents = useCallback(() => { setShowAgentPicker(true); @@ -520,6 +566,22 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, const isBranchNameRequired = branchMode === "existing" || branchMode === "custom-new" || branchMode === "shared-group"; const hasInvalidBranchSelection = isBranchNameRequired && !branch.trim(); + const resolvedStartWorkflowId = selectedWorkflowId === null + ? null + : selectedWorkflowId ?? boardWorkflows?.defaultWorkflowId; + const startWorkflowCandidate = resolvedStartWorkflowId && boardWorkflows + ? boardWorkflows.workflows.find((workflow) => workflow.id === resolvedStartWorkflowId) + : undefined; + const validatedStartWorkflow = validateQuickAddStartWorkflow(startWorkflowCandidate); + const startWorkflowTarget = resolveQuickAddStartWorkflowTarget(validatedStartWorkflow); + const canStartTask = Boolean( + validatedStartWorkflow + && workflowSupportsQuickAddStart(validatedStartWorkflow) + && startWorkflowTarget + && onMoveTask, + ); + const canStartTaskNow = canStartTask && Boolean(description.trim()) && !isSubmitting; + const handleExecutorModelChange = useCallback((value: string, meta?: TaskFormValueChangeMeta) => { setExecutorModel(value); if (meta?.source === "initialization") { @@ -631,7 +693,12 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, onClose(); }, [onClose, resetForm]); - const performCreate = useCallback(async (trimmedDesc: string, acknowledgedDuplicates?: string[]) => { + const performCreate = useCallback(async ( + trimmedDesc: string, + acknowledgedDuplicates: string[] | undefined, + workflowSelection: string | null | undefined, + startWorkflow: ValidatedQuickAddWorkflow | null, + ) => { const executorSlashIdx = executorModel.indexOf("/"); const validatorSlashIdx = validatorModel.indexOf("/"); const planningSlashIdx = planningModel.indexOf("/"); @@ -648,7 +715,17 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, // - undefined → omit (store inherits the project default, today's behavior) // - null → explicit "No workflow" (store skips default materialization) // - string → that workflow, materialized atomically at create time. - ...(selectedWorkflowId !== undefined ? { workflowId: selectedWorkflowId } : {}), + ...(startWorkflow + ? { workflowId: startWorkflow.id } + : workflowSelection !== undefined + ? { workflowId: workflowSelection } + : {}), + ...(startWorkflow + ? (() => { + const initialColumn = resolveQuickAddStartInitialColumn(startWorkflow); + return initialColumn ? { column: initialColumn as ColumnId } : {}; + })() + : {}), // Optional steps are omitted only when no controls were available. Fast always submits explicit []/ids so async metadata races cannot fall back to store defaultOn gates. ...(shouldSubmitEnabledWorkflowSteps || executionMode === "fast" ? { enabledWorkflowSteps } : {}), ...(selectedAgentId ? { assignedAgentId: selectedAgentId } : {}), @@ -691,6 +768,33 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, // the executor can never observe the task with the wrong step set. const task = await onCreateTask(createInput); + let startSucceeded = false; + if (startWorkflow) { + const initialColumn = resolveQuickAddStartInitialColumn(startWorkflow); + if (initialColumn) { + // Coding (Ideas) has a proven direct destination in the atomic create request. + startSucceeded = true; + } else if ( + onMoveTask + && typeof task.id === "string" + && task.id.trim() + && typeof task.column === "string" + && task.column.trim() + && typeof (task as Task & { workflowId?: unknown }).workflowId === "string" + && (task as Task & { workflowId?: string }).workflowId === startWorkflow.id + ) { + const target = resolveQuickAddStartTargetColumn(startWorkflow, task.column); + if (target) { + try { + await onMoveTask(task.id, target as ColumnId); + startSucceeded = true; + } catch { + // The created task remains visible; the final toast reports this partial outcome. + } + } + } + } + // Upload pending images as attachments if (pendingImages.length > 0) { const failures: string[] = []; @@ -707,11 +811,23 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, } resetForm(); - addToast(t("newTaskModal.taskCreated", "Created {{taskId}}", { taskId: task.id }), "success"); + if (startWorkflow) { + addToast( + startSucceeded + ? t("newTaskModal.taskQueued", "Queued {{taskId}} for planning", { taskId: task.id }) + : t("newTaskModal.taskCreatedNotStarted", "Created {{taskId}}, but could not start it", { taskId: task.id }), + startSucceeded ? "success" : "error", + ); + } else { + addToast(t("newTaskModal.taskCreated", "Created {{taskId}}", { taskId: task.id }), "success"); + } onClose(); - }, [executorModel, credentialInstanceId, validatorModel, validatorCredentialInstanceId, planningModel, planningCredentialInstanceId, thinkingLevel, plannerOversightLevel, dependencies, selectedWorkflowId, shouldSubmitEnabledWorkflowSteps, enabledWorkflowSteps, selectedAgentId, presetMode, selectedPresetId, reviewLevel, autoMerge, priority, nodeId, executionMode, branchMode, isBranchNameRequired, branch, baseBranch, githubTrackingEnabled, githubRepoOverrideTrimmed, onCreateTask, pendingImages, resetForm, addToast, t, onClose, projectId]); + }, [executorModel, credentialInstanceId, validatorModel, validatorCredentialInstanceId, planningModel, planningCredentialInstanceId, thinkingLevel, plannerOversightLevel, dependencies, shouldSubmitEnabledWorkflowSteps, enabledWorkflowSteps, selectedAgentId, presetMode, selectedPresetId, reviewLevel, autoMerge, priority, nodeId, executionMode, branchMode, isBranchNameRequired, branch, baseBranch, githubTrackingEnabled, githubRepoOverrideTrimmed, onCreateTask, onMoveTask, pendingImages, resetForm, addToast, t, onClose, projectId]); - const handleSubmit = useCallback(async () => { + const handleSubmit = useCallback(async (startWorkflow: ValidatedQuickAddWorkflow | null = null) => { + const workflowSelection = selectedWorkflowId; + pendingWorkflowSelectionRef.current = workflowSelection; + pendingStartWorkflowRef.current = startWorkflow; const trimmedDesc = description.trim(); if (!trimmedDesc || isSubmitting || githubRepoOverrideInvalid || hasInvalidBranchSelection) return; @@ -729,15 +845,22 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, } try { - await performCreate(trimmedDesc); + await performCreate(trimmedDesc, undefined, workflowSelection, startWorkflow); } catch (err) { addToast(getErrorMessage(err) || t("newTaskModal.failedToCreate", "Failed to create task"), "error"); } finally { if (!keepSubmittingForDuplicateChoice) { + pendingWorkflowSelectionRef.current = undefined; + pendingStartWorkflowRef.current = null; setIsSubmitting(false); } } - }, [description, isSubmitting, githubRepoOverrideInvalid, hasInvalidBranchSelection, projectId, addToast, t, performCreate]); + }, [description, isSubmitting, githubRepoOverrideInvalid, hasInvalidBranchSelection, projectId, addToast, t, performCreate, selectedWorkflowId]); + + const handleStartSubmit = useCallback(() => { + if (!canStartTaskNow || !validatedStartWorkflow) return; + void handleSubmit(validatedStartWorkflow); + }, [canStartTaskNow, handleSubmit, validatedStartWorkflow]); const handleDuplicateOpen = useCallback((taskId: string) => { setDuplicateMatches(null); @@ -752,6 +875,8 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, const trimmedDesc = description.trim(); const matches = duplicateMatches; if (!trimmedDesc || !matches || matches.length === 0) { + pendingWorkflowSelectionRef.current = undefined; + pendingStartWorkflowRef.current = null; setDuplicateMatches(null); setIsSubmitting(false); return; @@ -760,15 +885,23 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, setDuplicateMatches(null); setIsSubmitting(true); try { - await performCreate(trimmedDesc, matches.map((match) => match.id)); + await performCreate( + trimmedDesc, + matches.map((match) => match.id), + pendingWorkflowSelectionRef.current, + pendingStartWorkflowRef.current, + ); } catch (err) { addToast(getErrorMessage(err) || t("newTaskModal.failedToCreate", "Failed to create task"), "error"); } finally { + pendingWorkflowSelectionRef.current = undefined; setIsSubmitting(false); } }, [description, duplicateMatches, performCreate, addToast, t]); const handleDuplicateCancel = useCallback(() => { + pendingWorkflowSelectionRef.current = undefined; + pendingStartWorkflowRef.current = null; setDuplicateMatches(null); setIsSubmitting(false); }, []); @@ -1096,9 +1229,12 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, onGithubTrackingEnabledChange={handleGithubTrackingEnabledChange} githubRepoOverride={githubRepoOverride} onGithubRepoOverrideChange={setGithubRepoOverride} - onCreateSubmit={handleSubmit} + onCreateSubmit={() => { void handleSubmit(); }} createSubmitLabel={isSubmitting ? t("newTaskModal.creating", "Creating...") : t("newTaskModal.createTask", "Create Task")} createSubmitDisabled={!description.trim() || isSubmitting || githubRepoOverrideInvalid || hasInvalidBranchSelection} + onStartSubmit={canStartTask && (Boolean(description.trim()) || isSubmitting) ? handleStartSubmit : undefined} + startSubmitLabel={isSubmitting ? t("newTaskModal.starting", "Starting...") : t("newTaskModal.startTask", "Start")} + startSubmitDisabled={!canStartTaskNow} renderBelowPrimary={quickFields} hideDependencies={true} autoExpandMoreOptionsOnSelection={false} diff --git a/packages/dashboard/app/components/QuickEntryBox.tsx b/packages/dashboard/app/components/QuickEntryBox.tsx index c131ee6c8b..e6f8dc56e5 100644 --- a/packages/dashboard/app/components/QuickEntryBox.tsx +++ b/packages/dashboard/app/components/QuickEntryBox.tsx @@ -21,7 +21,7 @@ import { WorkflowOptionalStepsDropdown } from "./WorkflowOptionalStepsDropdown"; import { WorkflowIcon } from "./WorkflowIcon"; import { PendingAttachmentPreviews } from "./PendingAttachmentPreviews"; import { getPriorityColorVar, getPriorityIcon, getPriorityLabel } from "../utils/priorityIndicator"; -import { validateQuickAddStartWorkflow, workflowSupportsQuickAddStart, resolveQuickAddStartInitialColumn, resolveQuickAddStartTargetColumn, type ValidatedQuickAddWorkflow } from "../utils/quickAddStart"; +import { validateQuickAddStartWorkflow, workflowSupportsQuickAddStart, resolveQuickAddStartInitialColumn, resolveQuickAddStartWorkflowTarget, resolveQuickAddStartTargetColumn, type ValidatedQuickAddWorkflow } from "../utils/quickAddStart"; import { computeFixedMenuPosition, getLayoutViewportSize } from "../utils/fixedMenuPosition"; import { isInsidePortaledModelMenu } from "../utils/portalSurfaces"; import { useQuickAddSubmitOnEnter } from "../hooks/useQuickAddSubmitOnEnter"; @@ -388,6 +388,7 @@ export function QuickEntryBox({ onCreate, onMoveTask, addToast, tasks = [], avai const selectedWorkflowForCreate = workflowId === undefined ? undefined : quickEntryWorkflowId; const validatedStartWorkflow = useMemo(() => validateQuickAddStartWorkflow(selectedQuickEntryWorkflow), [selectedQuickEntryWorkflow]); const startInitialColumn = validatedStartWorkflow ? resolveQuickAddStartInitialColumn(validatedStartWorkflow) : null; + const startWorkflowTarget = validatedStartWorkflow ? resolveQuickAddStartWorkflowTarget(validatedStartWorkflow) : null; /* FNXC:QuickAddStart 2026-07-31-23:51: Start is a VISIBLE button in the quick-add action row for eligible workflows only, replacing the hidden @@ -397,7 +398,7 @@ export function QuickEntryBox({ onCreate, onMoveTask, addToast, tasks = [], avai for the follow-up move). Workflows without a waiting lane render no Start button at all — Save stays the single create affordance there. */ - const canQuickAddStart = Boolean(validatedStartWorkflow && workflowSupportsQuickAddStart(validatedStartWorkflow) && (startInitialColumn || onMoveTask)); + const canQuickAddStart = Boolean(validatedStartWorkflow && workflowSupportsQuickAddStart(validatedStartWorkflow) && startWorkflowTarget && (startInitialColumn || onMoveTask)); const canQuickAddStartNow = canQuickAddStart && Boolean(description.trim()) && !isSubmitting; useEffect(() => { diff --git a/packages/dashboard/app/components/TaskForm.tsx b/packages/dashboard/app/components/TaskForm.tsx index a4c1eeae0a..29d816ca23 100644 --- a/packages/dashboard/app/components/TaskForm.tsx +++ b/packages/dashboard/app/components/TaskForm.tsx @@ -176,10 +176,19 @@ export interface TaskFormProps { onClose?: () => void; // Create-mode primary submission. NewTaskModal owns duplicate checks and payload shaping; - // TaskForm only places the visible Create affordance in the quick-action row. + // TaskForm only places the visible Create/Start affordances in the quick-action row. onCreateSubmit?: () => void; createSubmitLabel?: string; createSubmitDisabled?: boolean; + /* + * FNXC:NewTaskWorkflowStart 2026-08-19-00:17: + * Start is supplied only by a host that has validated server-derived manual-intake metadata and + * a safe destination. Keeping this optional prevents an ineligible workflow from leaving an + * empty button shell in either the desktop modal or mobile sheet. + */ + onStartSubmit?: () => void; + startSubmitLabel?: string; + startSubmitDisabled?: boolean; /** Optional content to render between the primary section and the "More options" toggle. */ renderBelowPrimary?: React.ReactNode; @@ -259,6 +268,9 @@ export function TaskForm({ onCreateSubmit, createSubmitLabel, createSubmitDisabled, + onStartSubmit, + startSubmitLabel, + startSubmitDisabled, renderBelowPrimary, renderBelowModelConfiguration, hideDependencies, @@ -1032,6 +1044,20 @@ export function TaskForm({ {createSubmitLabel ?? t("taskForm.createTask", "Create")} )} + {onStartSubmit && ( + + )} {onPlanningMode && (