diff --git a/packages/dashboard/app/components/NewTaskModal.tsx b/packages/dashboard/app/components/NewTaskModal.tsx index 36143c7ba9..7d0e2441cc 100644 --- a/packages/dashboard/app/components/NewTaskModal.tsx +++ b/packages/dashboard/app/components/NewTaskModal.tsx @@ -71,6 +71,11 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, const [autoMerge, setAutoMerge] = useState(undefined); const [priority, setPriority] = useState(DEFAULT_TASK_PRIORITY); const [nodeId, setNodeId] = useState(undefined); + /** + * FNXC:NewTaskDialogAffordances 2026-06-21-18:35: + * The New Task dialog must expose the same Fast/standard execution-mode affordance as QuickEntryBox's `quick-entry-fast-toggle`. Reuse TaskForm's `task-form-execution-mode-select` and forward only Fast into `TaskCreateInput.executionMode` so Standard keeps the store default. + */ + const [executionMode, setExecutionMode] = useState<"standard" | "fast">("standard"); const [githubTrackingEnabled, setGithubTrackingEnabled] = useState(false); const [githubRepoOverride, setGithubRepoOverride] = useState(""); @@ -180,13 +185,14 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, autoMerge !== undefined || priority !== DEFAULT_TASK_PRIORITY || nodeId !== undefined || + executionMode === "fast" || branchMode !== "project-default" || branch !== "" || baseBranch !== "" || githubTrackingEnabled || githubRepoOverrideTrimmed !== ""; setHasDirtyState(isDirty); - }, [description, dependencies, pendingImages, selectedWorkflowId, enabledWorkflowSteps, executorModel, validatorModel, planningModel, thinkingLevel, selectedAgentId, reviewLevel, autoMerge, priority, nodeId, branchMode, branch, baseBranch, githubTrackingEnabled, githubRepoOverrideTrimmed]); + }, [description, dependencies, pendingImages, selectedWorkflowId, enabledWorkflowSteps, executorModel, validatorModel, planningModel, thinkingLevel, selectedAgentId, reviewLevel, autoMerge, priority, nodeId, executionMode, branchMode, branch, baseBranch, githubTrackingEnabled, githubRepoOverrideTrimmed]); const resetForm = useCallback(() => { // Clean up object URLs @@ -209,6 +215,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, setAutoMerge(undefined); setPriority(DEFAULT_TASK_PRIORITY); setNodeId(undefined); + setExecutionMode("standard"); setBranchMode("project-default"); setBranch(""); setBaseBranch(""); @@ -281,6 +288,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, ...(autoMerge !== undefined ? { autoMerge } : {}), priority, nodeId, + ...(executionMode === "fast" ? { executionMode: "fast" } : {}), branchSelection: { mode: branchMode, ...(isBranchNameRequired && branch.trim() ? { branchName: branch.trim() } : {}), @@ -335,6 +343,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, setAutoMerge(undefined); setPriority(DEFAULT_TASK_PRIORITY); setNodeId(undefined); + setExecutionMode("standard"); setBranchMode("project-default"); setBranch(""); setBaseBranch(""); @@ -346,7 +355,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, } finally { setIsSubmitting(false); } - }, [description, dependencies, pendingImages, executorModel, validatorModel, planningModel, thinkingLevel, isSubmitting, githubRepoOverrideInvalid, hasInvalidBranchSelection, onCreateTask, addToast, onClose, projectId, presetMode, selectedPresetId, selectedWorkflowId, enabledWorkflowSteps, selectedAgentId, reviewLevel, autoMerge, priority, nodeId, branchMode, isBranchNameRequired, branch, baseBranch, githubTrackingEnabled, githubRepoOverrideTrimmed, t]); + }, [description, dependencies, pendingImages, executorModel, validatorModel, planningModel, thinkingLevel, isSubmitting, githubRepoOverrideInvalid, hasInvalidBranchSelection, onCreateTask, addToast, onClose, projectId, presetMode, selectedPresetId, selectedWorkflowId, enabledWorkflowSteps, selectedAgentId, reviewLevel, autoMerge, priority, nodeId, executionMode, branchMode, isBranchNameRequired, branch, baseBranch, githubTrackingEnabled, githubRepoOverrideTrimmed, t]); // Handle keyboard shortcuts const handleKeyDown = useCallback((e: React.KeyboardEvent) => { @@ -565,6 +574,8 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, nodeId={nodeId} onNodeIdChange={setNodeId} nodeOptions={nodes} + executionMode={executionMode} + onExecutionModeChange={setExecutionMode} githubTrackingEnabled={githubTrackingEnabled} onGithubTrackingEnabledChange={setGithubTrackingEnabled} githubRepoOverride={githubRepoOverride} diff --git a/packages/dashboard/app/components/__tests__/NewTaskModal.test.tsx b/packages/dashboard/app/components/__tests__/NewTaskModal.test.tsx index 64a3294c00..2c2d34501b 100644 --- a/packages/dashboard/app/components/__tests__/NewTaskModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/NewTaskModal.test.tsx @@ -167,12 +167,81 @@ describe("NewTaskModal", () => { fireEvent.click(screen.getByTestId("task-form-more-options-toggle")); + expect(screen.getByTestId("task-form-execution-mode-select")).toBeInTheDocument(); expect(screen.getByTestId("task-form-github-tracking")).toBeInTheDocument(); expect(screen.getByTestId("task-priority-select")).toBeInTheDocument(); expect(screen.getByText(/Attachments/i)).toBeInTheDocument(); expect(screen.getByText(/Node Override/i)).toBeInTheDocument(); }); + it("renders the Fast and standard execution-mode affordance inside More options", () => { + renderNewTaskModal(); + + fireEvent.click(screen.getByTestId("task-form-more-options-toggle")); + + const select = screen.getByTestId("task-form-execution-mode-select") as HTMLSelectElement; + expect(select).toBeInTheDocument(); + expect(select).toHaveValue("standard"); + expect(Array.from(select.options).map((option) => option.value)).toEqual(["standard", "fast"]); + }); + + it("includes executionMode fast in the create payload when Fast is selected", async () => { + const { props } = renderNewTaskModal(); + + fireEvent.click(screen.getByTestId("task-form-more-options-toggle")); + fireEvent.change(screen.getByTestId("task-form-execution-mode-select"), { target: { value: "fast" } }); + fireEvent.change(screen.getByPlaceholderText("What needs to be done?"), { target: { value: "Fast parity task" } }); + fireEvent.click(screen.getByRole("button", { name: "Create Task" })); + + await waitFor(() => { + expect(props.onCreateTask).toHaveBeenCalledWith( + expect.objectContaining({ executionMode: "fast" }), + ); + }); + await waitFor(() => { + expect(screen.getByTestId("task-form-execution-mode-select")).toHaveValue("standard"); + }); + }); + + it("omits executionMode from the create payload when Standard is selected", async () => { + const { props } = renderNewTaskModal(); + + fireEvent.change(screen.getByRole("textbox"), { target: { value: "Standard parity task" } }); + fireEvent.click(screen.getByRole("button", { name: "Create Task" })); + + await waitFor(() => { + expect(props.onCreateTask).toHaveBeenCalledTimes(1); + }); + const payload = vi.mocked(props.onCreateTask).mock.calls[0][0] as Record; + expect(payload).not.toHaveProperty("executionMode"); + }); + + it("resets executionMode to standard after canceling and discarding changes", async () => { + const { props, rerender } = renderNewTaskModal(); + + fireEvent.click(screen.getByTestId("task-form-more-options-toggle")); + fireEvent.change(screen.getByTestId("task-form-execution-mode-select"), { target: { value: "fast" } }); + + await waitFor(() => { + expect(screen.getByTestId("task-form-execution-mode-select")).toHaveValue("fast"); + }); + fireEvent.click(screen.getByRole("button", { name: "Cancel" })); + + await waitFor(() => { + expect(mockConfirm).toHaveBeenCalledWith({ + title: "Discard Changes", + message: "You have unsaved changes. Discard them?", + danger: true, + }); + }); + + rerender(); + rerender(); + fireEvent.click(screen.getByTestId("task-form-more-options-toggle")); + + expect(screen.getByTestId("task-form-execution-mode-select")).toHaveValue("standard"); + }); + it("hands trimmed descriptions to planning and subtask callbacks without discard confirmation", () => { const onPlanningMode = vi.fn(); const onSubtaskBreakdown = vi.fn();