FN-6895: add execution mode to new task modal

Expose execution-mode selection when creating tasks from the New Task dialog.

- Add Fast/standard execution-mode state to the New Task modal and pass Fast through create payloads.
- Treat Fast as dirty state and reset the selector after create or discard flows.
- Cover the More options affordance, Fast payload inclusion, Standard payload omission, and reset behavior.

Files changed:
 packages/dashboard/app/components/NewTaskModal.tsx | 15 ++++-
 .../app/components/__tests__/NewTaskModal.test.tsx | 69 ++++++++++++++++++++++
 2 files changed, 82 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-6895

Fusion-Task-Lineage: f52dc78f-7ea0-4539-b256-4354f7b35c25
This commit is contained in:
gsxdsm
2026-06-21 18:50:32 -07:00
parent e2a57f0a80
commit 1270dc192d
2 changed files with 82 additions and 2 deletions

View File

@@ -71,6 +71,11 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
const [autoMerge, setAutoMerge] = useState<boolean | undefined>(undefined);
const [priority, setPriority] = useState<TaskPriority>(DEFAULT_TASK_PRIORITY);
const [nodeId, setNodeId] = useState<string | undefined>(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}

View File

@@ -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<string, unknown>;
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(<NewTaskModal {...props} isOpen={false} />);
rerender(<NewTaskModal {...props} isOpen={true} />);
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();