FN-8432: document responsive Planning Mode navigation

Clarify and verify tablet tab navigation and mobile list-first Planning Mode behavior.

- Add tablet progressive-tab coverage across interview states.
- Assert mobile saved-session list-first navigation and plan visibility.
- Update mobile and dashboard guidance with the intentional session-selection flow.
- Add a patch changeset for the Planning Mode fix.

Files changed:
 .../fn-8432-planning-tablet-mobile-plan-layout.md  |  7 ++++
 MOBILE.md                                          |  2 +-
 docs/dashboard-guide.md                            |  2 +-
 .../PlanningModeModal.planning-flow.test.tsx       | 43 +++++++++++++++++++++-
 .../PlanningModeModal.ui-interactions.test.tsx     | 25 +++++++++++++
 5 files changed, 75 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-8432

Fusion-Task-Lineage: e412069e-87e2-4a61-9556-6908f86b56c5

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-20 10:07:44 -07:00
parent 81abe53957
commit 76822d8c4c
5 changed files with 75 additions and 4 deletions

View File

@@ -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.

View File

@@ -101,7 +101,7 @@ gesture delivery enabled across `cap sync` regenerations.
### Planning Mode ### 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 ### Chat and Quick Chat mobile scroll/readability behavior

View File

@@ -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. Use **New session** to restart planning with a different idea.
<!-- FNXC:PlanningMode 2026-07-18-16:00: Planning Mode is an infinite, user-controlled interview. Each answer updates the running plan and produces another context-aware high-impact question; only Validate plan finalizes it. --> <!-- FNXC:PlanningMode 2026-07-18-16:00: Planning Mode is an infinite, user-controlled interview. Each answer updates the running plan and produces another context-aware high-impact question; only Validate plan finalizes it. -->
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. 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.

View File

@@ -3636,18 +3636,57 @@ describe("PlanningModeModal", () => {
expect(screen.getByRole("button", { name: "Running plan" })).toBeDefined(); expect(screen.getByRole("button", { name: "Running plan" })).toBeDefined();
expect(screen.getByRole("button", { name: "Answered questions" })).toBeDefined(); expect(screen.getByRole("button", { name: "Answered questions" })).toBeDefined();
expect(screen.getByRole("button", { name: "Next question" })).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" })); fireEvent.click(screen.getByRole("button", { name: "Sessions" }));
expect(await screen.findByRole("complementary", { name: "Planning sessions" })).toBeDefined(); expect(await screen.findByRole("complementary", { name: "Planning sessions" })).toBeDefined();
fireEvent.click(screen.getByRole("button", { name: "Sessions" })); 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(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(); 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(); 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(<PlanningModeModal isOpen={true} onClose={mockOnClose} onTaskCreated={mockOnTaskCreated} onTasksCreated={vi.fn()} tasks={mockTasks} resumeSessionId={`session-tablet-${status}`} />);
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) => { describe.each(["desktop", "mobile"] as const)("single interview action on %s", (viewport) => {
it("keeps only Next question and the localized Other input affordance", async () => { it("keeps only Next question and the localized Other input affordance", async () => {
mockViewport(viewport); mockViewport(viewport);

View File

@@ -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(<PlanningModeModal isOpen={true} onClose={mockOnClose} onTaskCreated={mockOnTaskCreated} onTasksCreated={vi.fn()} tasks={mockTasks} initialSessions={sessions} />);
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 () => { it("returns an active mobile interview to usable session rows without a running plan", async () => {
mockViewport("mobile"); mockViewport("mobile");
mockActiveSession(); mockActiveSession();