From 3a52c7a7cd2537abd7e0bd28bef77853e75e16a1 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 28 Jun 2026 01:11:03 -0700 Subject: [PATCH] FN-7176: add PR body markdown preview toggle Add a markdown preview mode to the Create PR body editor while preserving raw submission text. - Add a persisted Preview/Edit toggle for the Create PR body field. - Render body markdown through the shared sanitized markdown pipeline in preview mode. - Style the preview region and document the modal behavior. - Cover edit, preview, regenerate, revert, empty/fallback, and submit payload flows. Files changed: docs/dashboard-guide.md | 1 + .../dashboard/app/components/PrCreateModal.css | 24 ++++- .../dashboard/app/components/PrCreateModal.tsx | 51 +++++++++- .../components/__tests__/PrCreateModal.test.tsx | 107 +++++++++++++++++++++ 4 files changed, 181 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-7176 Fusion-Task-Lineage: 82275fb1-f89a-4fa2-9c47-7a7e7982c6cd Co-authored-by: Fusion (runfusion.ai) --- docs/dashboard-guide.md | 1 + .../app/components/PrCreateModal.css | 24 +++- .../app/components/PrCreateModal.tsx | 51 ++++++++- .../__tests__/PrCreateModal.test.tsx | 107 ++++++++++++++++++ 4 files changed, 181 insertions(+), 2 deletions(-) diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 82c7ac8805..ac1e5b3117 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -1003,6 +1003,7 @@ Inspect task definition, logs, review feedback, comments, artifacts, workflow ou - The **Create Pull Request** modal now offers in-app remediation for every blocking preflight check. If `branchOnRemote` is false, use **Push branch to remote** and Fusion will publish `fusion/` to `origin` and refresh preflight. If `conflictsWithBase` is true, use **Resolve conflicts with AI** and Fusion will use an AI coding agent to resolve merge markers on the task branch, commit and push real merge changes, or report success without an empty commit when the selected base is already merged; preflight then refreshes so normal PR creation can continue once all checks pass. - The **Create Pull Request** modal is a floating pop-out like Plan Mission, New Task, and Automations: drag its header or resize from desktop edges/corners, while mobile keeps the full-screen dialog layout. Close it with **X**, **Cancel**, or **Escape**; stray clicks inside or outside the floating shell do not dismiss it. - The modal shell renders immediately: preflight checks and PR options load independently of AI-generated title/body metadata, so slow AI suggestions no longer block base-branch selection, diagnostics, or manual PR authoring. The **Diff & commit preview** section starts collapsed and can be expanded on demand. +- The **Body** section includes a **Preview/Edit** toggle so authors can review the rendered markdown description before creating the PR without changing the submitted raw body text. - AI title/body generation in the dialog is bounded to 15 seconds and is canceled if the request disconnects; on timeout/cancel, Fusion falls back to deterministic task-based PR title/body content instead of leaving the spinner stuck forever. - Project Settings → Project Models includes optional **PR title prompt guidance** and **PR description prompt guidance** fields. Blank fields preserve the default Create PR metadata prompt; populated fields append guidance for the generated title or body sections. - The **Artifacts** tab combines task documents written by agents or users with task-scoped registered media artifacts. The gallery uses thumbnail-first image/video cards, image and video previews can expand into a dismissible full-size lightbox, video and audio use native controls, document artifacts show text previews, and generic artifacts open through their media URL. diff --git a/packages/dashboard/app/components/PrCreateModal.css b/packages/dashboard/app/components/PrCreateModal.css index dd362bfb72..4531afbd41 100644 --- a/packages/dashboard/app/components/PrCreateModal.css +++ b/packages/dashboard/app/components/PrCreateModal.css @@ -149,12 +149,34 @@ FN-7170 hosts Create PR in FloatingWindow: desktop geometry belongs to the share .pr-create-modal__inline-actions { display: flex; + flex-wrap: wrap; gap: var(--space-sm); } +.pr-create-modal__body-input, +.pr-create-modal__body-preview { + min-height: calc(var(--space-2xl) * 4); +} + .pr-create-modal__body-input { font-family: var(--font-mono); - min-height: calc(var(--space-2xl) * 4); +} + +.pr-create-modal__body-preview { + overflow: auto; + padding: var(--space-sm) var(--space-md); + border: var(--btn-border-width) solid var(--border); + border-radius: var(--radius-sm); + background: var(--surface); + color: var(--text); +} + +.pr-create-modal__body-preview > :first-child { + margin-top: 0; +} + +.pr-create-modal__body-preview > :last-child { + margin-bottom: 0; } .pr-create-template-hint { diff --git a/packages/dashboard/app/components/PrCreateModal.tsx b/packages/dashboard/app/components/PrCreateModal.tsx index 5f1b7bce35..57642ebfb8 100644 --- a/packages/dashboard/app/components/PrCreateModal.tsx +++ b/packages/dashboard/app/components/PrCreateModal.tsx @@ -1,6 +1,8 @@ import { useCallback, useEffect, useId, useMemo, useRef, useState, type CSSProperties } from "react"; +import ReactMarkdown from "react-markdown"; import { useTranslation } from "react-i18next"; import { AlertTriangle, CheckCircle2, RefreshCw, Sparkles, X, XCircle } from "lucide-react"; +import remarkGfm from "remark-gfm"; import { getErrorMessage, type PrInfo, type StructuredGhError } from "@fusion/core"; import { createPr, @@ -16,6 +18,7 @@ import { } from "../api"; import type { ToastType } from "../hooks/useToast"; import { FloatingWindow } from "./FloatingWindow"; +import { sharedRehypePlugins } from "./markdownPipeline"; import "./PrCreateModal.css"; interface PrCreateModalProps { @@ -39,6 +42,27 @@ type PreflightCheck = { }; const PR_METADATA_TIMEOUT_MS = 15000; +const PR_CREATE_BODY_PREVIEW_STORAGE_KEY = "fn-pr-create-body-preview"; + +function readBooleanPref(key: string, defaultValue: boolean): boolean { + if (typeof window === "undefined") return defaultValue; + try { + const raw = window.localStorage.getItem(key); + if (raw === null) return defaultValue; + return raw === "true"; + } catch { + return defaultValue; + } +} + +function writeBooleanPref(key: string, value: boolean): void { + if (typeof window === "undefined") return; + try { + window.localStorage.setItem(key, value ? "true" : "false"); + } catch { + // ignore storage failures (quota, private mode, etc.) + } +} /* FNXC:PrCreateModal 2026-06-27-23:48: @@ -199,6 +223,7 @@ export function PrCreateModal({ const [aiBody, setAiBody] = useState(""); const [title, setTitle] = useState(""); const [body, setBody] = useState(""); + const [showBodyPreview, setShowBodyPreview] = useState(() => readBooleanPref(PR_CREATE_BODY_PREVIEW_STORAGE_KEY, false)); const [userEditedTitle, setUserEditedTitle] = useState(false); const [userEditedBody, setUserEditedBody] = useState(false); const [templateUsed, setTemplateUsed] = useState(false); @@ -342,6 +367,10 @@ export function PrCreateModal({ }; }, [loadData, open]); + useEffect(() => { + writeBooleanPref(PR_CREATE_BODY_PREVIEW_STORAGE_KEY, showBodyPreview); + }, [showBodyPreview]); + useEffect(() => { if (!open) return; @@ -630,10 +659,30 @@ export function PrCreateModal({
{userEditedBody && } +
{metadataLoading ?
: null} -