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:
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user