{selectedSessionId && !showSessionList && (view.type === "question" || view.type === "loading" || view.type === "error") ? (
@@ -2283,7 +2312,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
FNXC:Planning 2026-06-23-02:00:
Sidebar resize handle — parity with MissionManager's mission-manager__sidebar-resize-handle. Rendered only on desktop (sidebar stacks on mobile). Pointer-drag and arrow-key resize both clamp + persist width.
*/}
- {!isMobile && (
+ {viewportMode === "desktop" && !isShortViewport() && (
)}
+ {selectedSessionId && !showSessionList && (view.type === "question" || view.type === "loading" || view.type === "error") && isCompactInterview && (
+
{error &&
{error}
}
{/*
diff --git a/packages/dashboard/app/components/__tests__/PlanningModeModal.css.test.ts b/packages/dashboard/app/components/__tests__/PlanningModeModal.css.test.ts
index 8d8f0cd2c5..aa593fa348 100644
--- a/packages/dashboard/app/components/__tests__/PlanningModeModal.css.test.ts
+++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.css.test.ts
@@ -69,6 +69,23 @@ describe("PlanningModeModal CSS responsive action contract", () => {
expect(embeddedRule).toMatch(/max-height\s*:\s*100%\s*;/);
});
+ it("uses the compact interview switcher for mobile, short landscape, and tablet instead of crushing three panes", () => {
+ const css = loadPlanningCss();
+ const shortShellCss = getMediaBlocks(css, MOBILE_PLANNING_SHELL_QUERY).join("\n");
+ const tabletCss = getMediaBlocks(css, TABLET_SUMMARY_ACTIONS_QUERY).join("\n");
+
+ expect(findRule(shortShellCss, ".planning-modal-body--compact-interview")).toMatch(/flex-direction\s*:\s*column\s*;/);
+ expect(findRule(shortShellCss, ".planning-compact-pane-switcher")).toMatch(/display\s*:\s*flex\s*;/);
+ expect(findRule(shortShellCss, ".planning-compact-pane-switcher .btn")).toMatch(/min-height\s*:\s*calc\(var\(--space-md\) \* 2\.25\)\s*;/);
+ expect(shortShellCss).toContain(".planning-modal-body--compact-question .planning-running-plan");
+ expect(shortShellCss).toContain(".planning-modal-body--compact-plan .planning-detail");
+ expect(shortShellCss).toContain(".planning-modal-body--compact-history .planning-detail");
+ expect(findRule(tabletCss, ".planning-modal-body--compact-interview")).toMatch(/flex-direction\s*:\s*column\s*;/);
+ expect(tabletCss).toContain(".planning-modal-body--compact-question .planning-running-plan");
+
+ expectSomeRule(css, ".planning-running-plan", /flex\s*:\s*0 1 24rem\s*;/);
+ });
+
it("keeps the mobile sessions list scrolling above the bottom-pinned New session footer", () => {
const css = loadPlanningCss();
const mobileShellCss = getMediaBlocks(css, MOBILE_PLANNING_SHELL_QUERY).join("\n");
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 cb3e7d06a1..5ab9890a76 100644
--- a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx
+++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx
@@ -61,6 +61,7 @@ import {
mockTaskDetail,
MockEventSource,
getMediaBlocks,
+ mockShortLandscapePhone,
mockViewport,
} from "./PlanningModeModal.test-helpers";
@@ -3612,6 +3613,41 @@ describe("PlanningModeModal", () => {
});
});
+ it("uses progressive interview controls on tablet and short-landscape phone sessions", async () => {
+ for (const viewport of ["tablet", "short-landscape"] as const) {
+ if (viewport === "tablet") mockViewport("tablet");
+ else mockShortLandscapePhone();
+ mockFetchAiSession.mockResolvedValueOnce({
+ id: `session-${viewport}`,
+ type: "planning",
+ status: "awaiting_input",
+ title: "Responsive planning session",
+ inputPayload: JSON.stringify({ initialPlan: "Responsive plan prompt" }),
+ conversationHistory: "[]",
+ result: JSON.stringify(mockSummary),
+ thinkingOutput: "",
+ projectId: null,
+ currentQuestion: JSON.stringify(mockQuestion),
+ });
+
+ const rendered = render(
);
+ await screen.findByText("What is the scope?");
+ expect(screen.getByRole("button", { name: "Question" })).toHaveAttribute("aria-pressed", "true");
+ expect(screen.getByRole("button", { name: "Running plan" })).toBeDefined();
+ expect(screen.getByRole("button", { name: "Answered questions" })).toBeDefined();
+ expect(screen.getByRole("button", { name: "Next question" })).toBeDefined();
+
+ 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();
+ }
+ });
+
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.test-helpers.ts b/packages/dashboard/app/components/__tests__/PlanningModeModal.test-helpers.ts
index ed9ff1518b..d2ebcb10df 100644
--- a/packages/dashboard/app/components/__tests__/PlanningModeModal.test-helpers.ts
+++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.test-helpers.ts
@@ -200,4 +200,27 @@ export function mockViewport(mode: "mobile" | "desktop" | "tablet") {
});
}
+/*
+FNXC:PlanningModeMobileTablet 2026-07-20-09:30:
+Short-landscape Planning tests must exercise the wide CSS viewport plus phone-class physical screen
+combination. Returning mobile mode mirrors getViewportMode() after isMobileViewport() accepts that
+phone-class short-height exception; width-only mocks cannot cover the historical desync.
+*/
+export function mockShortLandscapePhone() {
+ mockUseViewportMode.mockReturnValue("mobile");
+ Object.defineProperty(window, "screen", {
+ configurable: true,
+ value: { width: 844, height: 390 },
+ });
+ Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: query === "(max-height: 480px)" || query === "(max-width: 768px), (max-height: 480px)",
+ media: query,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ })),
+ });
+}
+
export type { Task, TaskDetail, PlanningQuestion, PlanningSummary, MergeResult };