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();