diff --git a/.changeset/planning-session-loading-state.md b/.changeset/planning-session-loading-state.md new file mode 100644 index 0000000000..681d5923a1 --- /dev/null +++ b/.changeset/planning-session-loading-state.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Planning mode now shows a neutral session loader while restoring a saved session instead of "Generating…". +category: fix +dev: New `session_loading` view state in PlanningModeModal; generating copy, Stop button, elapsed timer, and the missed-SSE watchdog are reserved for sessions the server reports as generating. Unrecognized persisted session shapes land in the retryable error view instead of spinning forever. diff --git a/packages/dashboard/app/components/PlanningModeModal.tsx b/packages/dashboard/app/components/PlanningModeModal.tsx index 6a1f153eaf..93b985f247 100644 --- a/packages/dashboard/app/components/PlanningModeModal.tsx +++ b/packages/dashboard/app/components/PlanningModeModal.tsx @@ -166,7 +166,16 @@ type ViewState = | { type: "task_created"; taskId: string; task?: Task } | { type: "error"; session: PlanningSession; errorMessage: string } | { type: "breakdown"; sessionId: string; originalSubtasks: SubtaskItem[]; subtasks: SubtaskItem[]; dirty: boolean } - | { type: "loading" }; + | { type: "loading" } + /* + FNXC:PlanningMode 2026-07-23-00:00: + Fetching a persisted session from the database is not generation. `session_loading` renders a + neutral "Loading session…" spinner during that fetch; the `loading` state (with its + "Generating…" copy, Stop button, elapsed timer, and 8s missed-SSE watchdog) is reserved for + turns the server is actually generating. Before this split, every reload/reopen flashed + "Generating initial plan…" while merely hydrating from the DB. + */ + | { type: "session_loading" }; type PlanningGenerationActivity = "initial_plan" | "plan_update" | "question"; @@ -1621,8 +1630,10 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat setIsRefiningSummary(false); refineSummaryInFlightRef.current = false; setGenerationStartTime(null); - viewRef.current = { type: "loading" }; - setView({ type: "loading" }); + // FNXC:PlanningMode 2026-07-23-00:00: hydrate-from-DB shows the neutral session loader, + // not the generation pane — only a fetched status of "generating" enters `loading` below. + viewRef.current = { type: "session_loading" }; + setView({ type: "session_loading" }); try { const session = await fetchAiSession(sessionId); @@ -1817,6 +1828,16 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat // thinkingOutput — the stream replay reconstructs the loading view exactly once; // seeding here and then replaying doubled the visible output on every reload. connectToPlanningStream(sessionId); + } else { + // FNXC:PlanningMode 2026-07-23-00:00: a persisted row none of the branches above + // recognize (e.g. awaiting_input with neither question nor summary, or complete + // without a result) used to strand the modal on the generation spinner forever. + // Surface it as a retryable error instead of an indefinite loader. + setView({ + type: "error", + session: { sessionId, currentQuestion: null, summary: persistedRunningSummary }, + errorMessage: t("planning.sessionUnrecoverableState", "This session could not be restored. Retry to continue the interview."), + }); } } catch (err) { if (planningSessionLoadEpochRef.current !== loadEpoch || currentSessionIdRef.current !== sessionId) return; @@ -3082,7 +3103,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat Header icon mirrors MissionManager's : same size (20) and same var(--todo) tint + flex-shrink:0, applied via the scoped .planning-modal--embedded .modal-header--embedded .detail-title-row > svg rule (it overrides the shared icon-triage brown so the two headers read as siblings). */} - {selectedSessionId && (view.type === "question" || view.type === "loading" || view.type === "error") && activeSessionTitle && isRenamingSession ? ( + {selectedSessionId && (view.type === "question" || view.type === "loading" || view.type === "session_loading" || view.type === "error") && activeSessionTitle && isRenamingSession ? ( ) : ( - <>

{selectedSessionId && (view.type === "question" || view.type === "loading" || view.type === "error") && activeSessionTitle ? activeSessionTitle : t("planning.title", "Planning Mode")}

- {selectedSessionId && (view.type === "question" || view.type === "loading" || view.type === "error") && activeSessionTitle && } + <>

{selectedSessionId && (view.type === "question" || view.type === "loading" || view.type === "session_loading" || view.type === "error") && activeSessionTitle ? activeSessionTitle : t("planning.title", "Planning Mode")}

+ {selectedSessionId && (view.type === "question" || view.type === "loading" || view.type === "session_loading" || view.type === "error") && activeSessionTitle && } )} {/* @@ -3103,7 +3124,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat title-row Back control on every viewport, avoiding a duplicate Sessions toggle and keeping compact list/detail state synchronized through one handler. */} - {selectedSessionId && (view.type === "question" || view.type === "loading" || view.type === "error" || view.type === "plan_review" || view.type === "create_retry") && ( + {selectedSessionId && (view.type === "question" || view.type === "loading" || view.type === "session_loading" || view.type === "error" || view.type === "plan_review" || view.type === "create_retry") && (