From bd558462ed6ae6d7d9b5ed24794d7b6a5f35e3eb Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 20 Jul 2026 09:51:26 -0700 Subject: [PATCH] FN-8427: restore mobile planning session navigation Restore a full-pane saved-session destination for compact Planning Mode. - Unmount the running-plan pane while mobile and tablet session lists are open. - Keep Back and Sessions available from active, completed, and new planning surfaces. - Cover compact navigation behavior and document the mobile interaction. Files changed: .../fn-8427-planning-mobile-session-list-nav.md | 7 ++ MOBILE.md | 2 +- docs/dashboard-guide.md | 2 +- .../dashboard/app/components/PlanningModeModal.css | 56 +++++++++++++++- .../dashboard/app/components/PlanningModeModal.tsx | 33 ++++++--- .../__tests__/PlanningModeModal.css.test.ts | 5 ++ .../PlanningModeModal.ui-interactions.test.tsx | 78 ++++++++++++++++++++++ 7 files changed, 167 insertions(+), 16 deletions(-) Fusion-Task-Id: FN-8427 Fusion-Task-Lineage: 6664d075-0614-4b5a-ba36-34a0ec6a20a3 Co-authored-by: Fusion (runfusion.ai) --- ...n-8427-planning-mobile-session-list-nav.md | 7 ++ MOBILE.md | 2 +- docs/dashboard-guide.md | 2 +- .../app/components/PlanningModeModal.css | 56 ++++++++++++- .../app/components/PlanningModeModal.tsx | 33 +++++--- .../__tests__/PlanningModeModal.css.test.ts | 5 ++ ...PlanningModeModal.ui-interactions.test.tsx | 78 +++++++++++++++++++ 7 files changed, 167 insertions(+), 16 deletions(-) create mode 100644 .changeset/fn-8427-planning-mobile-session-list-nav.md diff --git a/.changeset/fn-8427-planning-mobile-session-list-nav.md b/.changeset/fn-8427-planning-mobile-session-list-nav.md new file mode 100644 index 0000000000..ef8ae951b6 --- /dev/null +++ b/.changeset/fn-8427-planning-mobile-session-list-nav.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Restore mobile navigation back to the Planning session list without a stuck Running plan screen. +category: fix +dev: Hide RunningPlanPane in list mode; ensure Back/Sessions on non-list mobile Planning surfaces. diff --git a/MOBILE.md b/MOBILE.md index 09c7db75c6..ffbd16f1dc 100644 --- a/MOBILE.md +++ b/MOBILE.md @@ -101,7 +101,7 @@ gesture delivery enabled across `cap sync` regenerations. ### Planning Mode -Planning Mode opens directly into the composer pane on mobile when no planning sessions exist, avoiding an empty-sidebar dead end. Desktop keeps its three-pane interview (answered history, current question, and running plan) when there is room. Mobile, phone-class short landscape, and tablet use a progressive interview: **Question** is the default full-width surface, while labeled **Running plan** and **Answered questions** controls switch to those surfaces without stacking three panes. **Sessions** and the mobile back control always return to the saved-session list; **Validate plan** remains on the Running plan surface. Once sessions are saved, mobile shows the session list as usual and the user can navigate between list and detail panes. +Planning Mode opens directly into the composer pane on mobile when no planning sessions exist, avoiding an empty-sidebar dead end. Desktop keeps its three-pane interview (answered history, current question, and running plan) when there is room. Mobile, phone-class short landscape, and tablet use a progressive interview: **Question** is the default full-width surface, while labeled **Running plan** and **Answered questions** controls switch to those surfaces without stacking three panes. **Sessions** and the mobile back control always return to the saved-session list; this is a full-pane, scrollable list with **New session** as its footer and no Running plan stacked beneath it. The same escape is available from interview, summary, breakdown, and a new-session composer when saved sessions exist; **Validate plan** remains on the Running plan surface. ### Chat and Quick Chat mobile scroll/readability behavior diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 1b5a9eacac..ff44b17954 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -529,7 +529,7 @@ When an active Planning AI generation appears stuck, Planning Mode automatically Use **New session** to restart planning with a different idea. -Planning Mode keeps the running plan visible beside answered-question history and the current question on desktop; you can rename a session and keep asking high-impact, context-aware questions until you choose **Validate plan**. On tablet, mobile, and phone-class short landscape, the interview switches between labeled **Question**, **Running plan**, and **Answered questions** surfaces so the current question stays usable instead of competing with three columns. **Sessions** (and mobile Back) return to the saved-session list, while **Validate plan** remains available on the Running plan surface. The running title, description, and deliverables are available throughout the interview—including while the next question is generating or a recoverable error is shown. The AI never ends an interview on its own. Selection questions provide alternatives with pros and cons plus an **Other** free-text choice, whose wording follows your input language and whose answer steers the next question. You may edit an earlier answer by question ID without losing later answers; Planning re-derives the running plan and appends a fresh next question. +Planning Mode keeps the running plan visible beside answered-question history and the current question on desktop; you can rename a session and keep asking high-impact, context-aware questions until you choose **Validate plan**. On tablet, mobile, and phone-class short landscape, the interview switches between labeled **Question**, **Running plan**, and **Answered questions** surfaces so the current question stays usable instead of competing with three columns. **Sessions** (and mobile Back) return to a full-pane, scrollable saved-session list with **New session** pinned as its footer; the Running plan is not stacked under that list. This escape remains available from interview, summary, breakdown, and a new-session composer whenever saved sessions exist, while **Validate plan** remains available on the Running plan surface. The running title, description, and deliverables are available throughout the interview—including while the next question is generating or a recoverable error is shown. The AI never ends an interview on its own. Selection questions provide alternatives with pros and cons plus an **Other** free-text choice, whose wording follows your input language and whose answer steers the next question. You may edit an earlier answer by question ID without losing later answers; Planning re-derives the running plan and appends a fresh next question. Choose **Validate plan** when the running plan is ready for task creation. Validation is durable and is required before **Create task**, **Create tasks**, or **Start breakdown**; those actions reject unvalidated sessions. diff --git a/packages/dashboard/app/components/PlanningModeModal.css b/packages/dashboard/app/components/PlanningModeModal.css index c92198d6ce..f4451ea142 100644 --- a/packages/dashboard/app/components/PlanningModeModal.css +++ b/packages/dashboard/app/components/PlanningModeModal.css @@ -469,6 +469,7 @@ The New session button must look EXACTLY like Missions' primary sidebar create b cursor: pointer; padding: var(--space-xs); border-radius: var(--radius-sm); + flex-shrink: 0; transition: background var(--transition-fast), box-shadow var(--transition-fast); } @@ -620,13 +621,24 @@ The New session button must look EXACTLY like Missions' primary sidebar create b .planning-modal-body--show-detail .planning-sidebar { display: none; } - .planning-modal-body--show-list .planning-detail { + /* + FNXC:PlanningModeMobile 2026-07-20-10:30: + FN-8427 gives session-list mode exclusive ownership of the compact body. JS unmounts + RunningPlanPane; this selector is a layout backstop for future render paths so session + rows stay scrollable above the New session footer instead of sharing height with a plan. + */ + .planning-modal-body--show-list .planning-detail, + .planning-modal-body--show-list .planning-running-plan, + .planning-modal-body--show-list .planning-compact-pane-switcher { display: none; } - /* Show back button on mobile whenever it's rendered (the React component - only renders it when `mobileShowDetail` is true). */ + /* The rendered Back control needs a full touch target and must not be squeezed out of the title row. */ .planning-mobile-back { display: inline-flex; + min-width: calc(var(--space-md) * 2.25); + min-height: calc(var(--space-md) * 2.25); + align-items: center; + justify-content: center; } /* Always keep action buttons visible on mobile (no hover) */ .planning-sidebar-item-delete, @@ -1535,6 +1547,44 @@ Tablet embedded Planning keeps the desktop two-pane shell, so the summary footer display: none; } + /* FNXC:PlanningModeMobile 2026-07-20-10:30: tablet uses the same compact list destination as phone and short shells, so summary/compose detail cannot remain beside saved session rows. */ + .planning-modal-body--show-list { + flex: 1; + min-height: 0; + overflow: hidden; + } + + .planning-modal-body--show-list .planning-sidebar { + flex: 1 1 auto; + height: 100%; + min-height: 0; + max-height: 100%; + } + + .planning-modal-body--show-list .planning-sidebar-list { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + } + + .planning-modal-body--show-list .planning-sidebar-footer { + flex-shrink: 0; + } + + .planning-modal-body--show-list .planning-detail, + .planning-modal-body--show-list .planning-running-plan, + .planning-modal-body--show-list .planning-compact-pane-switcher { + display: none; + } + + .planning-mobile-back { + display: inline-flex; + min-width: calc(var(--space-md) * 2.25); + min-height: calc(var(--space-md) * 2.25); + align-items: center; + justify-content: center; + } + .planning-summary-actions { flex-wrap: wrap; align-items: flex-start; diff --git a/packages/dashboard/app/components/PlanningModeModal.tsx b/packages/dashboard/app/components/PlanningModeModal.tsx index cf3d1769b9..38de00e88e 100644 --- a/packages/dashboard/app/components/PlanningModeModal.tsx +++ b/packages/dashboard/app/components/PlanningModeModal.tsx @@ -521,6 +521,16 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat temporary while a keyboard is open, rather than a global viewport-mode change. */ const isCompactInterview = viewportMode !== "desktop" || isShortViewport(); + /* + FNXC:PlanningModeMobile 2026-07-20-10:30: + FN-8427 makes the saved-session list a real compact destination. Both Back and Sessions + enter this one mode, which must unmount the active interview plan so it cannot consume + flex height beneath session rows. Desktop preserves its three-pane interview until its + explicit Sessions toggle requests the same list destination. + */ + const isSessionListMode = showSessionList || (isCompactInterview && !mobileShowDetail); + // FNXC:PlanningModeMobile 2026-07-20-10:30: Empty mobile state opens the composer because no saved destination exists; once sessions exist, every compact detail surface gets this single Back-to-list escape. + const canReturnToSessionList = isCompactInterview && mobileShowDetail && planningSessions.length > 0; const [compactInterviewPane, setCompactInterviewPane] = useState<"question" | "plan" | "history">("question"); const { addToast } = useToast(); const { pushNav } = useNavigationHistoryContext(); @@ -1192,6 +1202,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat liveGenerationSessionIdRef.current = sessionId; setSelectedSessionId(sessionId); setShowSessionList(false); + setMobileShowDetail(true); connectToPlanningStream(sessionId); setResponseHistory([]); @@ -2215,7 +2226,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat */}
- {mobileShowDetail && ( + {canReturnToSessionList && (
{/* - FNXC:PlanningMode 2026-07-19-15:45: - Keep the running-plan pane mounted for an active session while a question is loading or an - error is recoverable. A session selection is the stable identity across those view states; - tying this pane to only the question view recreated the old dead-end interface. + FNXC:PlanningModeMobile 2026-07-20-10:30: + FN-8427 keeps the running plan through question, loading, and recoverable-error detail + states, but unmounts it in session-list mode. The list must own the entire compact body; + leaving this pane mounted reduced rows to the New session footer on mobile. */} - {selectedSessionId && (view.type === "question" || view.type === "loading" || view.type === "error") && ( + {selectedSessionId && !isSessionListMode && (view.type === "question" || view.type === "loading" || view.type === "error") && ( { const footerRule = findRule(mobileShellCss, ".planning-modal-body--show-list .planning-sidebar-footer"); expect(footerRule).toBeTruthy(); expect(footerRule).toMatch(/flex-shrink\s*:\s*0\s*;/); + + expect(mobileShellCss).toMatch(/\.planning-modal-body--show-list \.planning-running-plan[\s\S]*?display\s*:\s*none\s*;/); + const mobileBackRule = findRule(mobileShellCss, ".planning-mobile-back"); + expect(mobileBackRule).toMatch(/display\s*:\s*inline-flex\s*;/); + expect(mobileBackRule).toMatch(/min-height\s*:\s*calc\(var\(--space-md\) \* 2\.25\)\s*;/); }); }); diff --git a/packages/dashboard/app/components/__tests__/PlanningModeModal.ui-interactions.test.tsx b/packages/dashboard/app/components/__tests__/PlanningModeModal.ui-interactions.test.tsx index 1ad8e9b160..3692a88112 100644 --- a/packages/dashboard/app/components/__tests__/PlanningModeModal.ui-interactions.test.tsx +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.ui-interactions.test.tsx @@ -946,6 +946,84 @@ describe("PlanningModeModal", () => { }); }); + describe("mobile session-list navigation (FN-8427)", () => { + const sessions = [ + { id: "session-active", type: "planning", status: "awaiting_input", title: "Active planning session", projectId: null, updatedAt: "2026-07-20T00:00:00.000Z" }, + { id: "session-other", type: "planning", status: "complete", title: "Other planning session", projectId: null, updatedAt: "2026-07-20T00:00:00.000Z" }, + ]; + + function mockActiveSession() { + mockFetchAiSessions.mockResolvedValue(sessions); + mockFetchAiSession.mockResolvedValue({ + id: "session-active", + type: "planning", + status: "awaiting_input", + title: "Active planning session", + inputPayload: JSON.stringify({ initialPlan: "Restore navigation" }), + conversationHistory: "[]", + currentQuestion: JSON.stringify(mockQuestion), + result: JSON.stringify(mockSummary), + thinkingOutput: "", + projectId: null, + }); + } + + it("returns an active mobile interview to usable session rows without a running plan", async () => { + mockViewport("mobile"); + mockActiveSession(); + const { container } = render(); + + await screen.findByText("What is the scope?"); + expect(screen.getByLabelText("Running plan")).toBeInTheDocument(); + const back = await screen.findByLabelText("Back to sessions"); + expect(back).toBeEnabled(); + fireEvent.click(back); + + expect(container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--show-list"); + expect(screen.getByRole("button", { name: /Other planning session/ })).toBeVisible(); + expect(screen.queryByLabelText("Running plan")).toBeNull(); + expect(screen.getByRole("button", { name: "New session" })).toBeVisible(); + }); + + it("keeps desktop running-plan interview chrome without mobile back navigation", async () => { + mockViewport("desktop"); + mockActiveSession(); + const { container } = render(); + + await screen.findByText("What is the scope?"); + expect(screen.getByLabelText("Running plan")).toBeInTheDocument(); + expect(container.querySelector(".planning-mobile-back")).toBeNull(); + }); + + it("keeps a mobile list escape from summary and new-session compose when sessions exist", async () => { + mockViewport("mobile"); + mockFetchAiSessions.mockResolvedValue(sessions); + mockFetchAiSession.mockResolvedValue({ + id: "session-active", + type: "planning", + status: "complete", + title: "Active planning session", + inputPayload: JSON.stringify({ initialPlan: "Restore navigation", validated: true }), + conversationHistory: "[]", + currentQuestion: null, + result: JSON.stringify(mockSummary), + thinkingOutput: "", + projectId: null, + }); + const { container } = render(); + + await screen.findByText("Planning Complete!"); + fireEvent.click(screen.getByLabelText("Back to sessions")); + expect(screen.getByRole("button", { name: /Other planning session/ })).toBeVisible(); + + fireEvent.click(screen.getByRole("button", { name: "New session" })); + expect(screen.getByPlaceholderText(/e.g., Build a user authentication/)).toBeVisible(); + fireEvent.click(screen.getByLabelText("Back to sessions")); + expect(container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--show-list"); + expect(screen.getByRole("button", { name: /Other planning session/ })).toBeVisible(); + }); + }); + /* FNXC:Planning 2026-06-23-02:00: The embedded Planning sidebar is resizable like Missions: a desktop-only drag handle (role=separator) drives an inline width on .planning-sidebar that persists to localStorage and is clamped to the PLANNING_SIDEBAR_MIN/MAX range. These tests assert the handle exists on desktop, persists a clamped width on arrow-key resize, restores from localStorage, and is absent on mobile (where the sidebar stacks full-width).