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:
7
.changeset/fn-8432-planning-tablet-mobile-plan-layout.md
Normal file
7
.changeset/fn-8432-planning-tablet-mobile-plan-layout.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
<!-- 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.
|
||||
|
||||
|
||||
@@ -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(<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) => {
|
||||
it("keeps only Next question and the localized Other input affordance", async () => {
|
||||
mockViewport(viewport);
|
||||
|
||||
@@ -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 () => {
|
||||
mockViewport("mobile");
|
||||
mockActiveSession();
|
||||
|
||||
Reference in New Issue
Block a user