diff --git a/.changeset/fix-mobile-planning-start.md b/.changeset/fix-mobile-planning-start.md new file mode 100644 index 0000000000..1c85216750 --- /dev/null +++ b/.changeset/fix-mobile-planning-start.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Start planning on the first mobile tap and create tasks without a separate validation step. +category: fix +dev: Handles touch activation before keyboard viewport resize and removes the planning create validation gate. diff --git a/packages/core/src/async-ai-session-store.ts b/packages/core/src/async-ai-session-store.ts index eaf0acfadd..3a4a83dd44 100644 --- a/packages/core/src/async-ai-session-store.ts +++ b/packages/core/src/async-ai-session-store.ts @@ -306,7 +306,6 @@ export async function claimPlanningSessionTaskCreation( const existing = await getAiSession(handle, sessionId); if (!existing || existing.type !== "planning") return null; const input = safeJsonParse(existing.inputPayload, {}) as Record; - if (input.validated !== true) return null; const inputPayload = { ...input, createClaimStatus: "creating", claimOwnerToken, claimStartedAt, createdTaskId: undefined }; const rows = await handle.update(schema.project.aiSessions) .set({ inputPayload, updatedAt: claimStartedAt }) diff --git a/packages/dashboard/app/components/PlanningModeModal.tsx b/packages/dashboard/app/components/PlanningModeModal.tsx index 17f9a02050..dc441e0a1f 100644 --- a/packages/dashboard/app/components/PlanningModeModal.tsx +++ b/packages/dashboard/app/components/PlanningModeModal.tsx @@ -1,7 +1,7 @@ import "./PlanningModeModal.css"; import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; -import { useState, useCallback, useEffect, useRef, useMemo, type MouseEvent } from "react"; +import { useState, useCallback, useEffect, useRef, useMemo, type MouseEvent, type PointerEvent as ReactPointerEvent } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import type { Task, PlanningQuestion, PlanningSummary, TaskPriority, ThinkingLevel } from "@fusion/core"; @@ -443,6 +443,14 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat const currentSessionIdRef = useRef(null); const viewRef = useRef({ type: "initial" }); /* + FNXC:PlanningMode 2026-07-20-23:52: + Mobile touch starts on pointer-down because closing the software keyboard resizes the + visual viewport before click, which can move the button and cancel that first click. + Keep a synchronous single-flight guard so touch compatibility events and rapid input + cannot start two planning sessions. + */ + const startPlanningInFlightRef = useRef(false); + /* FNXC:PlanningRetry 2026-07-15-00:00: FN-8332 permits automatic retry only for a generation this mounted Planning Mode instance started. A reloaded session may reconnect to observe a server @@ -968,6 +976,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat setPlanningModelId(undefined); setPlanningThinkingLevel(""); currentSessionIdRef.current = null; + startPlanningInFlightRef.current = false; }, [resetPlanningAutoRetryBudget]); const planningSelectionValue = getModelSelectionValue(planningModelProvider, planningModelId); @@ -1343,7 +1352,8 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat const handleStartPlanning = useCallback(async (planOverride?: string) => { const plan = planOverride ?? initialPlan; const startedPlan = plan.trim(); - if (!startedPlan) return; + if (!startedPlan || startPlanningInFlightRef.current) return; + startPlanningInFlightRef.current = true; setActivePlanPrompt(startedPlan); setError(null); @@ -1409,6 +1419,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat connectToPlanningStream(sessionId); setResponseHistory([]); } catch (err) { + startPlanningInFlightRef.current = false; draftCreatePromiseRef.current = null; draftCreateInFlightRef.current = false; setError(getErrorMessage(err) || t("planning.failedStartSession", "Failed to start planning session")); @@ -2402,7 +2413,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat } }, [projectId, refinementInstructions, t, workspaceQuestion]); - const handleValidatePlan = useCallback(async () => { + const handleProceedWithPlan = useCallback(async () => { const sessionId = currentSessionIdRef.current; const summary = runningSummaryRef.current; if (!sessionId || !summary || validateCreateInFlightRef.current) return; @@ -2410,29 +2421,26 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat validateCreateInFlightRef.current = true; setError(null); setView({ type: "creating_task", session, summary }); - let validationCompleted = false; try { - const validated = await validatePlanningSession(sessionId, projectId); - validationCompleted = true; - const validatedSummary = normalizePlanningSummary(validated.summary); - const task = await createTaskAfterActiveClaim(() => createTaskFromPlanning(sessionId, validatedSummary, projectId, { + const task = await createTaskAfterActiveClaim(() => createTaskFromPlanning(sessionId, summary, projectId, { ...(workflowId !== undefined ? { workflowId } : {}), })); clearPlanningActiveSession(projectId); setView({ type: "task_created", taskId: task.id, task }); } catch (err) { const errorMessage = getErrorMessage(err) || t("planning.failedCreateTask", "Failed to create task"); - if (validationCompleted) { - setView({ type: "create_retry", session, summary, errorMessage }); - } else { - setError(errorMessage); - setView({ type: "plan_review", session, summary }); - } + setView({ type: "create_retry", session, summary, errorMessage }); } finally { validateCreateInFlightRef.current = false; } }, [projectId, t, workflowId, workspaceQuestion]); + const handleStartPlanningPointerDown = useCallback((event: ReactPointerEvent) => { + if (viewportMode !== "mobile" || event.pointerType === "mouse") return; + event.preventDefault(); + void handleStartPlanning(); + }, [handleStartPlanning, viewportMode]); + const handleRetryCreateTask = useCallback(async () => { if (view.type !== "create_retry" || validateCreateInFlightRef.current) return; validateCreateInFlightRef.current = true; @@ -2690,7 +2698,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat > {t("planning.refine", "Refine")} - + ); @@ -3063,7 +3071,8 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat