diff --git a/.changeset/fn-8065-planning-checkpoint-plan-preview.md b/.changeset/fn-8065-planning-checkpoint-plan-preview.md new file mode 100644 index 0000000000..1fc6eec2bf --- /dev/null +++ b/.changeset/fn-8065-planning-checkpoint-plan-preview.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Planning Mode now previews the generated plan before you choose whether to refine it. +category: feature +dev: The deepening checkpoint PlanningQuestion carries an optional planPreview payload. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 71458385c2..7ebc639153 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -468,8 +468,9 @@ Use **Copy prompt** in the error panel or an active interview question to copy t + -Before Planning Mode shows **Planning Complete!** or the final plan summary, it first asks **Would you like to go deeper?**. The suggested themes are plan-specific: the planning AI proposes topics tailored to your plan's title, description, and deliverables as part of its completion response, surfacing angles you may not have anticipated. When the AI does not supply any themes, Planning Mode falls back to a generic, regex-derived set (scope, edge cases, UX, dependencies, testing, rollout) inferred from the interview text. Either way, select one or more suggested themes to continue the interview, use **Other** to add a custom topic, or choose **No, continue to final summary** to reveal the pending summary and task-creation actions. +Before Planning Mode shows **Planning Complete!** or the final plan summary, it first asks **Would you like to go deeper?**. A read-only preview of the generated plan appears above the refinement options so you can review its title, formatted description, and key deliverables before deciding. The suggested themes are plan-specific: the planning AI proposes topics tailored to your plan's title, description, and deliverables as part of its completion response, surfacing angles you may not have anticipated. When the AI does not supply any themes, Planning Mode falls back to a generic, regex-derived set (scope, edge cases, UX, dependencies, testing, rollout) inferred from the interview text. Either way, select one or more suggested themes to continue the interview, use **Other** to add a custom topic, or choose **Proceed to final plan** to reveal the pending summary and task-creation actions. - **Branch strategy** options mirror Subtask Breakdown semantics: - `Use project/default branch` diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 4e072aa5f1..970a7ea907 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -5621,6 +5621,17 @@ export interface PlanningQuestion { question: string; description?: string; options?: Array<{ id: string; label: string; description?: string }>; + /** + * FNXC:PlanningMode 2026-07-16-00:00: + * FN-8065 / GitHub #2150 requires the deepening checkpoint to carry a read-only preview + * of its withheld pendingSummary. Keeping this optional preserves legacy persisted + * currentQuestion rows and leaves ordinary interview questions unchanged. + */ + planPreview?: { + title: string; + description: string; + keyDeliverables: string[]; + }; } /** The final summary generated after planning conversation completes */ diff --git a/packages/dashboard/app/components/PlanningModeModal.css b/packages/dashboard/app/components/PlanningModeModal.css index 2f846a918c..f8c707184e 100644 --- a/packages/dashboard/app/components/PlanningModeModal.css +++ b/packages/dashboard/app/components/PlanningModeModal.css @@ -997,6 +997,62 @@ FN-8003 keeps Copy prompt in the question progress header so an interview can be gap: var(--space-lg); } +/* +FNXC:PlanningMode 2026-07-16-00:00: +FN-8065 / GitHub #2150 adds a read-only plan preview before the deepening choices. +It remains in the outer planning view's scroll flow so desktop and mobile users can +read the complete plan without introducing a competing nested scrollbar. +*/ +.planning-checkpoint-plan-preview { + display: flex; + flex-direction: column; + gap: var(--space-md); + padding: var(--space-lg); + background: color-mix(in srgb, var(--todo) 8%, var(--surface)); + border: solid color-mix(in srgb, var(--todo) 35%, var(--border)); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); +} + +.planning-checkpoint-plan-preview-header { + display: flex; + flex-direction: column; + gap: var(--space-xs); +} + +.planning-checkpoint-plan-preview-header h4, +.planning-checkpoint-plan-preview-title, +.planning-checkpoint-plan-preview-deliverables h5 { + margin: 0; +} + +.planning-checkpoint-plan-preview-title { + font-size: var(--font-size-lg); +} + +.planning-checkpoint-plan-preview-description > :first-child, +.planning-checkpoint-plan-preview-deliverables ul { + margin-top: 0; +} + +.planning-checkpoint-plan-preview-description > :last-child, +.planning-checkpoint-plan-preview-deliverables ul { + margin-bottom: 0; +} + +.planning-checkpoint-plan-preview-deliverables { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.planning-checkpoint-plan-preview-deliverables ul { + display: flex; + flex-direction: column; + gap: var(--space-xs); + padding-inline-start: var(--space-lg); +} + /* FNXC:PlanningInterview 2026-07-16-00:00: Interview questions now use MailboxMessageContent for sanitized GFM. Keep the @@ -1635,6 +1691,10 @@ Tablet embedded Planning keeps the desktop two-pane shell, so the summary footer gap: var(--space-md); } + .planning-checkpoint-plan-preview { + padding: var(--space-md); + } + .planning-view-footer, .planning-actions { padding: var(--space-md) var(--space-lg) calc(var(--space-lg) + env(safe-area-inset-bottom)); diff --git a/packages/dashboard/app/components/PlanningModeModal.tsx b/packages/dashboard/app/components/PlanningModeModal.tsx index 6e86c7fba7..db8ae624a9 100644 --- a/packages/dashboard/app/components/PlanningModeModal.tsx +++ b/packages/dashboard/app/components/PlanningModeModal.tsx @@ -2601,6 +2601,15 @@ function QuestionForm({ question: rawQuestion, progress, historyEntries, onSubmi const question = normalizeQuestionOptions(rawQuestion); const questionOptions = question.options ?? []; const isDeepeningCheckpoint = question.id === PLANNING_DEEPEN_CHECKPOINT_ID; + const planPreview = isDeepeningCheckpoint && question.planPreview + ? { + title: typeof question.planPreview.title === "string" ? question.planPreview.title : "", + description: typeof question.planPreview.description === "string" ? question.planPreview.description : "", + keyDeliverables: Array.isArray(question.planPreview.keyDeliverables) + ? question.planPreview.keyDeliverables.filter((deliverable): deliverable is string => typeof deliverable === "string") + : [], + } + : undefined; const [response, setResponse] = useState({}); const [textValue, setTextValue] = useState(""); const [commentValue, setCommentValue] = useState(""); @@ -2741,6 +2750,41 @@ function QuestionForm({ question: rawQuestion, progress, historyEntries, onSubmi {t("planning.questionProgress", "Question {{progress}} of ~3", { progress })} + {/* + FNXC:PlanningMode 2026-07-16-00:00: + FN-8065 / GitHub #2150 requires the deepening checkpoint to show its persisted + pendingSummary preview before users choose whether to refine or proceed. The strict + checkpoint-and-payload guard preserves ordinary questions and legacy checkpoint rows. + */} + {planPreview && ( +
+
+

+ {t("planning.checkpointPlanPreviewHeading", "Your plan so far")} +

+

+ {t("planning.checkpointPlanPreviewDescription", "Review the plan below, then choose to refine further or proceed.")} +

+
+
{planPreview.title}
+ {planPreview.description && ( +
+ {planPreview.description} +
+ )} + {planPreview.keyDeliverables.length > 0 && ( +
+
{t("planning.keyDeliverables", "Key Deliverables")}
+
    + {planPreview.keyDeliverables.map((deliverable, index) => ( +
  • {deliverable}
  • + ))} +
+
+ )} +
+ )} +
{/* FNXC:PlanningInterview 2026-07-16-00:00: 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 8594990969..f2e64423b5 100644 --- a/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx @@ -240,6 +240,11 @@ describe("PlanningModeModal", () => { { id: "theme-ux", label: "UX and interaction details" }, { id: "theme-testing", label: "Testing and verification" }, ], + planPreview: { + title: "Checkpoint preview title", + description: "Checkpoint **Markdown** description", + keyDeliverables: ["Preview deliverable one", "Preview deliverable two"], + }, }); }, 0); return { close: vi.fn() }; @@ -260,7 +265,12 @@ describe("PlanningModeModal", () => { }); fireEvent.click(screen.getByText("Start Planning")); - expect(await screen.findByText(PLANNING_DEEPEN_CHECKPOINT_QUESTION)).toBeInTheDocument(); + const checkpointQuestion = await screen.findByText(PLANNING_DEEPEN_CHECKPOINT_QUESTION); + const previewTitle = screen.getByText("Checkpoint preview title"); + expect(previewTitle.compareDocumentPosition(checkpointQuestion) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(screen.getByText("Markdown").tagName).toBe("STRONG"); + expect(screen.getByText("Preview deliverable one")).toBeInTheDocument(); + expect(screen.getByText("Preview deliverable two")).toBeInTheDocument(); expect(screen.queryByText("Planning Complete!")).toBeNull(); expect(screen.queryByRole("button", { name: "Create Single Task" })).toBeNull(); expect(screen.queryByRole("button", { name: "Break into Tasks" })).toBeNull(); @@ -435,6 +445,7 @@ describe("PlanningModeModal", () => { await waitFor(() => { expect(screen.getByText("What is the scope?")).toBeDefined(); }); + expect(screen.queryByText("Your plan so far")).toBeNull(); }); /* @@ -1914,6 +1925,11 @@ describe("PlanningModeModal", () => { { id: PLANNING_DEEPEN_PROCEED_OPTION_ID, label: "Proceed to final plan" }, { id: "theme-testing", label: "Testing and verification" }, ], + planPreview: { + title: "Restored checkpoint plan", + description: "Restored plan description", + keyDeliverables: ["Restored deliverable"], + }, }), result: null, thinkingOutput: "", @@ -1934,7 +1950,11 @@ describe("PlanningModeModal", () => { /> ); - expect(await screen.findByText(PLANNING_DEEPEN_CHECKPOINT_QUESTION)).toBeInTheDocument(); + const checkpointQuestion = await screen.findByText(PLANNING_DEEPEN_CHECKPOINT_QUESTION); + const previewTitle = screen.getByText("Restored checkpoint plan"); + expect(previewTitle.compareDocumentPosition(checkpointQuestion) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(screen.getByText("Restored plan description")).toBeInTheDocument(); + expect(screen.getByText("Restored deliverable")).toBeInTheDocument(); expect(screen.getByText("Proceed to final plan")).toBeInTheDocument(); expect(screen.queryByText("Planning Complete!")).toBeNull(); expect(screen.queryByRole("button", { name: "Create Single Task" })).toBeNull(); diff --git a/packages/dashboard/src/__tests__/planning-interview-formatters.test.ts b/packages/dashboard/src/__tests__/planning-interview-formatters.test.ts index cfa3d5f889..63091062e9 100644 --- a/packages/dashboard/src/__tests__/planning-interview-formatters.test.ts +++ b/packages/dashboard/src/__tests__/planning-interview-formatters.test.ts @@ -65,6 +65,25 @@ describe("planning deepening checkpoint helpers", () => { "UX and interaction details", "Testing and verification", ]); + expect(question.planPreview).toEqual({ + title: summaryWithSurfaces.title, + description: summaryWithSurfaces.description, + keyDeliverables: summaryWithSurfaces.keyDeliverables, + }); + }); + + it("includes an empty deliverables preview instead of an unchecked payload", () => { + const question = buildDeepeningCheckpointQuestion([], { + ...summaryWithSurfaces, + keyDeliverables: [], + }); + + expect(question.planPreview).toEqual({ + title: summaryWithSurfaces.title, + description: summaryWithSurfaces.description, + keyDeliverables: [], + }); + expect(question.options?.[0]?.id).toBe(PLANNING_DEEPEN_PROCEED_OPTION_ID); }); it("falls back to safe default themes when no conversation themes are inferred", () => { diff --git a/packages/dashboard/src/planning.ts b/packages/dashboard/src/planning.ts index 114d81f98f..1cc73fef88 100644 --- a/packages/dashboard/src/planning.ts +++ b/packages/dashboard/src/planning.ts @@ -654,17 +654,27 @@ Planning Mode final summaries are user-gated, not AI-gated. Every AI completion FNXC:PlanningMode 2026-07-05-00:20: buildDeepeningCheckpointOptions prefers the AI's plan-specific deepeningThemes when the completion payload supplied any; it falls back to the generic regex-derived CHECKPOINT_THEME_CANDIDATES only when the AI supplied none (FN-7616 / issue #1912). The reserved proceed option is always first and deterministic in both branches. + +FNXC:PlanningMode 2026-07-16-00:00: +FN-8065 / GitHub #2150 places the withheld pendingSummary preview directly on the persisted checkpoint question. That makes the same read-only plan available through fresh SSE, restored sessions, retry recovery, and the missed-SSE poll watchdog without adding another transport path. */ export function buildDeepeningCheckpointQuestion( history: Array<{ question: PlanningQuestion; response: unknown }>, summary: PlanningSummary, ): PlanningQuestion { + const planPreview = { + title: typeof summary.title === "string" ? summary.title : "", + description: typeof summary.description === "string" ? summary.description : "", + keyDeliverables: normalizeStringArray(summary.keyDeliverables), + }; + return { id: PLANNING_DEEPEN_CHECKPOINT_ID, type: "multi_select", question: PLANNING_DEEPEN_CHECKPOINT_QUESTION, description: "Select any areas you want to explore further, write an unlisted topic, or proceed to the final plan.", options: buildDeepeningCheckpointOptions(history, summary), + planPreview, }; }