diff --git a/.changeset/fn-8432-planning-tablet-mobile-plan-layout.md b/.changeset/fn-8432-planning-tablet-mobile-plan-layout.md new file mode 100644 index 0000000000..a671840fe1 --- /dev/null +++ b/.changeset/fn-8432-planning-tablet-mobile-plan-layout.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Planning Mode tablet tabs match mobile; mobile main shows sessions before running plan. +category: fix +dev: Assert compact interview tab parity and gate Running plan to intentional session detail navigation. diff --git a/MOBILE.md b/MOBILE.md index ffbd16f1dc..64a4daad93 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; 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. +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. On mobile, opening Planning with saved sessions lands on the full-pane, scrollable saved-session list with **New session** as its footer; **Running plan** appears only after you intentionally open a session and select its tab. **Sessions** and the mobile back control always return to that list. 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 7e476db2e8..e63d61cec0 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 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. +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. On mobile, Planning opens to the full-pane, scrollable saved-session list when sessions exist; **Running plan** appears only after you intentionally open a session and choose its tab. **Sessions** (and mobile Back) return to that list with **New session** pinned as its footer. 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/__tests__/PlanningModeModal.planning-flow.test.tsx b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx index 5ab9890a76..2f01e7f428 100644 --- a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx @@ -3636,18 +3636,57 @@ describe("PlanningModeModal", () => { expect(screen.getByRole("button", { name: "Running plan" })).toBeDefined(); expect(screen.getByRole("button", { name: "Answered questions" })).toBeDefined(); expect(screen.getByRole("button", { name: "Next question" })).toBeDefined(); + expect(rendered.container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--compact-question"); + + fireEvent.click(screen.getByRole("button", { name: "Running plan" })); + expect(rendered.container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--compact-plan"); + expect(screen.getByRole("button", { name: "Validate plan" })).toBeVisible(); + + fireEvent.click(screen.getByRole("button", { name: "Answered questions" })); + expect(rendered.container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--compact-history"); + expect(screen.getByRole("complementary", { name: "Answered questions" })).toBeVisible(); + + fireEvent.click(screen.getByRole("button", { name: "Question" })); + expect(rendered.container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--compact-question"); fireEvent.click(screen.getByRole("button", { name: "Sessions" })); expect(await screen.findByRole("complementary", { name: "Planning sessions" })).toBeDefined(); fireEvent.click(screen.getByRole("button", { name: "Sessions" })); expect(rendered.container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--show-detail", "planning-modal-body--compact-question"); expect(screen.getByRole("button", { name: "Next question" })).toBeVisible(); - fireEvent.click(screen.getByRole("button", { name: "Running plan" })); - expect(screen.getByRole("button", { name: "Validate plan" })).toBeDefined(); rendered.unmount(); } }); + it.each([ + ["generating", {}], + ["error", { error: "Generation failed" }], + ])("keeps tablet progressive tabs for %s interview state", async (status, fields) => { + mockViewport("tablet"); + mockFetchAiSession.mockResolvedValueOnce({ + id: `session-tablet-${status}`, + type: "planning", + status, + title: "Tablet planning session", + inputPayload: JSON.stringify({ initialPlan: "Tablet plan prompt" }), + conversationHistory: "[]", + result: JSON.stringify(mockSummary), + thinkingOutput: "", + projectId: null, + ...fields, + }); + + const { container } = render(); + + await screen.findByRole("button", { name: "Question" }); + expect(container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--compact-question"); + fireEvent.click(screen.getByRole("button", { name: "Running plan" })); + expect(container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--compact-plan"); + expect(screen.getByRole("button", { name: "Validate plan" })).toBeVisible(); + fireEvent.click(screen.getByRole("button", { name: "Answered questions" })); + expect(container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--compact-history"); + }); + describe.each(["desktop", "mobile"] as const)("single interview action on %s", (viewport) => { it("keeps only Next question and the localized Other input affordance", async () => { mockViewport(viewport); 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 3692a88112..044d35cf3d 100644 --- a/packages/dashboard/app/components/__tests__/PlanningModeModal.ui-interactions.test.tsx +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.ui-interactions.test.tsx @@ -968,6 +968,31 @@ describe("PlanningModeModal", () => { }); } + /* + FNXC:PlanningModeMobileTablet 2026-07-20-11:00: + Opening Planning on a phone with saved sessions is list-first, not an implicit resume. + The plan is permitted only after an intentional session selection and is unmounted again on return. + */ + it("keeps mobile main list-first until an operator opens a session", async () => { + mockViewport("mobile"); + mockActiveSession(); + const { container } = render(); + + expect(container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--show-list"); + expect(screen.getByRole("button", { name: /Active planning session/ })).toBeVisible(); + expect(screen.queryByRole("complementary", { name: "Running plan" })).toBeNull(); + + fireEvent.click(screen.getByRole("button", { name: /Active planning session/ })); + await screen.findByText("What is the scope?"); + expect(container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--show-detail"); + fireEvent.click(screen.getByRole("button", { name: "Running plan" })); + expect(screen.getByRole("complementary", { name: "Running plan" })).toBeVisible(); + + fireEvent.click(screen.getByRole("button", { name: "Sessions" })); + expect(container.querySelector(".planning-modal-body")).toHaveClass("planning-modal-body--show-list"); + expect(screen.queryByRole("complementary", { name: "Running plan" })).toBeNull(); + }); + it("returns an active mobile interview to usable session rows without a running plan", async () => { mockViewport("mobile"); mockActiveSession();