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} -