FN-5801: add markdown preview toggle for planning description

Add a markdown/plain toggle to the planning summary description so users can preview formatted content before creating a task.

- add a Description header toggle that switches between editable plain textarea and rendered markdown preview
- render description preview with react-markdown and remark-gfm while preserving expand/collapse layout behavior
- add preview container styles and spacing adjustments for summary form
- add UI interaction test coverage for markdown toggle behavior and rendered heading/bold content
- document the new markdown/plain description toggle in dashboard guide

Files changed:
 docs/dashboard-guide.md                            |  1 +
 packages/dashboard/app/components/PlanningModeModal.css | 18 +++++++-
 packages/dashboard/app/components/PlanningModeModal.tsx | 32 ++++++++++---
 packages/dashboard/app/components/__tests__/PlanningModeModal.ui-interactions.test.tsx | 54 ++++++++++++++++++++++
 4 files changed, 98 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-5801

Fusion-Task-Lineage: 34972830-7d51-4fcd-bf00-3de8f5aff028
This commit is contained in:
gsxdsm
2026-05-31 21:59:06 -07:00
parent 0a16fc690a
commit 4ea43c0c87
4 changed files with 98 additions and 7 deletions

View File

@@ -109,6 +109,7 @@ Planning Mode now includes branch controls on the summary screen before you crea
- `Create custom new branch` - `Create custom new branch`
- **Branch name** is required when using `existing` or `custom new` strategies. - **Branch name** is required when using `existing` or `custom new` strategies.
- **Merge target / base branch (optional)** uses a dropdown of existing local branches (with common names like `main`/`master`/`trunk`/`develop` listed first) plus a **Custom…** fallback when you need to type a branch that is not local yet. - **Merge target / base branch (optional)** uses a dropdown of existing local branches (with common names like `main`/`master`/`trunk`/`develop` listed first) plus a **Custom…** fallback when you need to type a branch that is not local yet.
- **Description** supports a `Markdown`/`Plain` toggle in the summary header row: `Plain` keeps the editable textarea, while `Markdown` renders formatted preview (`react-markdown` + GFM) in the same footprint for easier review before task creation.
These values are sent with the Planning Mode create-task request as `branchSelection`, so created tasks persist branch/base-branch settings consistently with other branch-aware task creation flows. These values are sent with the Planning Mode create-task request as `branchSelection`, so created tasks persist branch/base-branch settings consistently with other branch-aware task creation flows.

View File

@@ -606,6 +606,22 @@
box-shadow: var(--focus-ring); box-shadow: var(--focus-ring);
} }
.planning-description-preview {
width: 100%;
min-height: calc(var(--space-md) * 7.5);
max-height: calc(var(--space-2xl) * 20);
padding: var(--space-md);
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text);
overflow-y: auto;
}
.planning-description-preview.expanded {
min-height: calc(var(--space-md) * 12.5);
}
.planning-comment-section { .planning-comment-section {
margin-top: var(--space-lg); margin-top: var(--space-lg);
} }
@@ -940,7 +956,7 @@
.planning-summary-form .form-group { .planning-summary-form .form-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: var(--space-sm);
padding: var(--space-lg); padding: var(--space-lg);
background: var(--card); background: var(--card);
border: 1px solid var(--border); border: 1px solid var(--border);

View File

@@ -1,5 +1,7 @@
import "./PlanningModeModal.css"; import "./PlanningModeModal.css";
import { useState, useCallback, useEffect, useRef, useMemo } from "react"; import { useState, useCallback, useEffect, useRef, useMemo } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import type { Task, PlanningQuestion, PlanningSummary, TaskPriority } from "@fusion/core"; import type { Task, PlanningQuestion, PlanningSummary, TaskPriority } from "@fusion/core";
import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, getErrorMessage } from "@fusion/core"; import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, getErrorMessage } from "@fusion/core";
import { import {
@@ -2422,6 +2424,7 @@ function SummaryView({
isLoading, isLoading,
}: SummaryViewProps) { }: SummaryViewProps) {
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const [renderMarkdown, setRenderMarkdown] = useState(false);
const [selectedDependencies, setSelectedDependencies] = useState<string[]>( const [selectedDependencies, setSelectedDependencies] = useState<string[]>(
summary.suggestedDependencies summary.suggestedDependencies
); );
@@ -2463,6 +2466,17 @@ function SummaryView({
<div className="form-group"> <div className="form-group">
<label> <label>
Description Description
<button
type="button"
className="planning-expand-btn"
aria-pressed={renderMarkdown}
aria-label={renderMarkdown ? "Show raw text" : "Show formatted markdown"}
title={renderMarkdown ? "Show raw text" : "Show formatted markdown"}
data-testid="planning-description-markdown-toggle"
onClick={() => setRenderMarkdown(!renderMarkdown)}
>
{renderMarkdown ? "Plain" : "Markdown"}
</button>
<button <button
type="button" type="button"
className="planning-expand-btn" className="planning-expand-btn"
@@ -2471,12 +2485,18 @@ function SummaryView({
{isExpanded ? "Collapse" : "Expand"} {isExpanded ? "Collapse" : "Expand"}
</button> </button>
</label> </label>
<textarea {renderMarkdown ? (
ref={descriptionAutosizeRef} <div className={`planning-description-preview markdown-body ${isExpanded ? "expanded" : ""}`}>
className={`planning-textarea ${isExpanded ? "expanded" : ""}`} <ReactMarkdown remarkPlugins={[remarkGfm]}>{summary.description}</ReactMarkdown>
value={summary.description} </div>
onChange={(e) => onSummaryChange({ ...summary, description: e.target.value })} ) : (
/> <textarea
ref={descriptionAutosizeRef}
className={`planning-textarea ${isExpanded ? "expanded" : ""}`}
value={summary.description}
onChange={(e) => onSummaryChange({ ...summary, description: e.target.value })}
/>
)}
</div> </div>
<div className="task-detail-section"> <div className="task-detail-section">

View File

@@ -728,4 +728,58 @@ describe("PlanningModeModal", () => {
}); });
}); });
describe("Summary markdown preview toggle", () => {
it("toggles description between plain textarea and formatted markdown preview", async () => {
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
setTimeout(() => {
handlers.onSummary?.({
...mockSummary,
description: "## Heading\n\n- item\n\n**bold**",
});
}, 10);
return {
close: vi.fn(),
isConnected: vi.fn().mockReturnValue(true),
};
});
const { container } = render(
<PlanningModeModal
isOpen={true}
onClose={mockOnClose}
onTaskCreated={mockOnTaskCreated}
onTasksCreated={vi.fn()}
tasks={mockTasks}
/>,
);
fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), {
target: { value: "Build auth system" },
});
fireEvent.click(screen.getByText("Start Planning"));
await waitFor(() => {
expect(screen.getByText("Planning Complete!")).toBeDefined();
});
expect(container.querySelector(".planning-textarea")).not.toBeNull();
expect(container.querySelector(".planning-description-preview")).toBeNull();
fireEvent.click(screen.getByTestId("planning-description-markdown-toggle"));
await waitFor(() => {
expect(container.querySelector(".planning-description-preview")).not.toBeNull();
expect(screen.getByRole("heading", { level: 2, name: "Heading" })).toBeDefined();
expect(container.querySelector("strong")?.textContent).toBe("bold");
});
fireEvent.click(screen.getByTestId("planning-description-markdown-toggle"));
await waitFor(() => {
expect(container.querySelector(".planning-textarea")).not.toBeNull();
});
});
});
}); });