FN-8065: preview plans before refinement questions

Show a read-only plan preview at the Planning Mode deepening checkpoint.

- Persist pending plan details on the checkpoint question for fresh and restored sessions.
- Render formatted plan content and deliverables above refinement choices.
- Cover preview behavior and document the checkpoint flow.

Files changed:
 .../fn-8065-planning-checkpoint-plan-preview.md    |  7 +++
 docs/dashboard-guide.md                            |  3 +-
 packages/core/src/types.ts                         | 11 ++++
 .../dashboard/app/components/PlanningModeModal.css | 60 ++++++++++++++++++++++
 .../dashboard/app/components/PlanningModeModal.tsx | 44 ++++++++++++++++
 .../PlanningModeModal.planning-flow.test.tsx       | 24 ++++++++-
 .../planning-interview-formatters.test.ts          | 19 +++++++
 packages/dashboard/src/planning.ts                 | 10 ++++
 8 files changed, 175 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-8065

Fusion-Task-Lineage: 13cd52c3-af82-4723-a6b1-3366775213e1

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-16 02:54:33 -07:00
parent c2475b012d
commit f3b68c9fff
8 changed files with 175 additions and 3 deletions

View File

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

View File

@@ -468,8 +468,9 @@ Use **Copy prompt** in the error panel or an active interview question to copy t
<!-- FNXC:PlanningModeDeepeningCheckpoint 2026-07-02-12:18: Planning Mode must pause before every final summary at a mandatory "Would you like to go deeper?" checkpoint so users can request inferred follow-up themes, enter a custom topic, or proceed without deepening. -->
<!-- FNXC:PlanningMode 2026-07-05-00:25: The planning AI now proposes plan-specific deepening themes (deepeningThemes on the completion payload) so this checkpoint surfaces topics tailored to the user's actual plan, including angles they had not anticipated, instead of a fixed generic set. The regex-derived generic themes remain the fallback whenever the AI supplies none (FN-7616 / issue #1912). -->
<!-- FNXC:PlanningMode 2026-07-16-00:00: FN-8065 / GitHub #2150 requires the deepening checkpoint to show the generated plan read-only above its refinement choices, so users can review the title, Markdown description, and deliverables before proceeding. -->
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`

View File

@@ -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 */

View File

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

View File

@@ -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<QuestionResponse>({});
const [textValue, setTextValue] = useState("");
const [commentValue, setCommentValue] = useState("");
@@ -2741,6 +2750,41 @@ function QuestionForm({ question: rawQuestion, progress, historyEntries, onSubmi
<span className="planning-progress-text">{t("planning.questionProgress", "Question {{progress}} of ~3", { progress })}</span>
</div>
{/*
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 && (
<section className="planning-checkpoint-plan-preview" aria-labelledby="planning-checkpoint-plan-preview-heading">
<div className="planning-checkpoint-plan-preview-header">
<h4 id="planning-checkpoint-plan-preview-heading">
{t("planning.checkpointPlanPreviewHeading", "Your plan so far")}
</h4>
<p className="text-muted">
{t("planning.checkpointPlanPreviewDescription", "Review the plan below, then choose to refine further or proceed.")}
</p>
</div>
<h5 className="planning-checkpoint-plan-preview-title">{planPreview.title}</h5>
{planPreview.description && (
<div className="planning-checkpoint-plan-preview-description markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{planPreview.description}</ReactMarkdown>
</div>
)}
{planPreview.keyDeliverables.length > 0 && (
<div className="planning-checkpoint-plan-preview-deliverables">
<h5>{t("planning.keyDeliverables", "Key Deliverables")}</h5>
<ul>
{planPreview.keyDeliverables.map((deliverable, index) => (
<li key={`${deliverable}-${index}`}>{deliverable}</li>
))}
</ul>
</div>
)}
</section>
)}
<div className="planning-question-content">
{/*
FNXC:PlanningInterview 2026-07-16-00:00:

View File

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

View File

@@ -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", () => {

View File

@@ -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,
};
}