From 62ddb19cb3ddfc3313534f305d807db6a31c5f0c Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 4 Jul 2026 19:23:25 -0700 Subject: [PATCH] FN-7543: render original task prompt as collapsible markdown in Plan tab Original prompt in the Task Detail Plan tab now renders as Markdown and is collapsed by default behind a chevron toggle, matching the shared PROMPT.md renderer instead of showing raw preserved plain text. - Add a collapse/expand chevron toggle (`.detail-source-header`/`.detail-source-toggle`) for the original-prompt section, collapsed by default; toggle only renders when content exists. - Render the expanded original prompt through the shared ReactMarkdown pipeline (remarkGfm, sharedRehypePlugins, markdownLinkifyComponents) instead of plain preserved text. - Update CSS: replace `.detail-original-prompt-text` box/border/pre-wrap styling with `.detail-section--original-prompt .markdown-body` sizing rules (including mobile breakpoint). - Update dashboard-guide.md docs to describe the collapsed-by-default markdown rendering behavior. - Update TaskDetailModal tests to cover collapsed-by-default state, expand/collapse toggle, markdown rendering, and no-toggle empty state. - Add changeset (@runfusion/fusion patch) describing the user-facing behavior change. Files changed: .changeset/fn-7543-original-prompt-markdown-collapsed.md | 7 +++ docs/dashboard-guide.md | 2 +- packages/dashboard/app/components/TaskDetailModal.css | 13 ++---- packages/dashboard/app/components/TaskDetailModal.tsx | 30 +++++++++++-- packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx | 52 +++++++++++++--------- 5 files changed, 69 insertions(+), 35 deletions(-) Fusion-Task-Id: FN-7543 Fusion-Task-Lineage: b00964c0-404e-41a1-b9e1-336c000d4acc Co-authored-by: Fusion (runfusion.ai) --- ...7543-original-prompt-markdown-collapsed.md | 7 +++ docs/dashboard-guide.md | 2 +- .../app/components/TaskDetailModal.css | 13 ++--- .../app/components/TaskDetailModal.tsx | 30 +++++++++-- ...l.inline-editing-and-integrations.test.tsx | 52 ++++++++++++------- 5 files changed, 69 insertions(+), 35 deletions(-) create mode 100644 .changeset/fn-7543-original-prompt-markdown-collapsed.md diff --git a/.changeset/fn-7543-original-prompt-markdown-collapsed.md b/.changeset/fn-7543-original-prompt-markdown-collapsed.md new file mode 100644 index 0000000000..79284d7736 --- /dev/null +++ b/.changeset/fn-7543-original-prompt-markdown-collapsed.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Original task prompt now renders as Markdown and is collapsed by default in the task Plan tab. +category: feature +dev: Task Detail Plan/Definition tab original-prompt section reuses the existing `.detail-source-toggle`/`.detail-source-chevron--expanded` collapse pattern and the shared `ReactMarkdown` pipeline (`remarkGfm`, `sharedRehypePlugins`, `markdownLinkifyComponents`); backed by read-only `task.description`, no change to the generated `PROMPT.md` editor/revision flow. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 4897a8bf2d..e3c4c39f84 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -1164,7 +1164,7 @@ Inspect task definition, logs, review feedback, comments, artifacts, workflow ou - Backstop reconciliation runs every 15 minutes to close tracked GitHub issues for soft-deleted and archived tasks even after restart; the sweep is paginated so large archive backlogs are eventually drained. - In shared task edit/create forms, GitHub Tracking appears at the bottom of **More options**, after **Workflow Steps**. - From this section you can explicitly enable/disable tracking and manage a per-task repo override (`owner/repo`). Clearing the override saves `null` and falls back to project/global defaults. -- The **Plan** tab shows the stored **Original prompt** as read-only plain text before the generated `PROMPT.md` content, so the exact task prompt remains visible after planning. Editing or requesting AI revision still applies only to the generated plan. +- The **Plan** tab shows the stored **Original prompt** above the generated `PROMPT.md` content, so the exact task prompt remains visible after planning. It is collapsed by default behind a chevron toggle; expanding it renders the prompt as Markdown (the same renderer used for the generated plan body). It stays read-only — editing or requesting AI revision still applies only to the generated plan. - In `in-review`, pull-request controls/status (including stall badges) are in a dedicated **Pull Request** tab instead of the Definition tab. - In the task detail **Pull Request** tab, PR numbers open the linked pull request on GitHub when a PR URL is available. - Task Detail and list split-pane PR affordances follow the live project auto-merge setting: when auto-merge is off, manual **Create PR** / merge actions are shown; when it is on, the tab shows the automatic auto-merge hint unless a per-task override changes the effective behavior. diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index a201f5b303..f8dd477700 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -2296,7 +2296,7 @@ FNXC:TaskDetailPlan 2026-06-30-00:00: The Plan prompt editor is a task-detail body surface, not a narrow prose inset. Scope full-width guards to the Plan prompt section so modal and embedded cards align markdown, empty fallback, inline edit, and AI revision controls without changing unrelated detail sections. FNXC:TaskDetailPlan 2026-07-04-00:00: -The Plan tab must display task.description as the immutable original operator prompt even after PROMPT.md exists. Style the original prompt as plain preserved text, not markdown, and keep the empty state unboxed so absent descriptions do not create a misleading prompt shell. +The original operator prompt is now rendered as Markdown (shared PROMPT.md renderer) and collapsed by default behind a chevron toggle, superseding the earlier plain-preserved-text rule. It reuses the .detail-source-header/.detail-source-toggle collapsible pattern already used for GitLab tracking/retries, and shares the .detail-section--plan-prompt markdown width/wrapping contract so expanded content never overflows. The empty state stays unboxed and toggle-free so absent descriptions do not create a misleading prompt shell. */ .detail-section--original-prompt, .detail-section--plan-prompt { @@ -2307,18 +2307,11 @@ The Plan tab must display task.description as the immutable original operator pr max-width: 100%; } -.detail-original-prompt-text { +.detail-section--original-prompt .markdown-body { box-sizing: border-box; width: 100%; min-width: 0; max-width: 100%; - padding: var(--space-md); - border: var(--btn-border-width) solid var(--border); - border-radius: var(--radius-md); - background: var(--surface); - color: var(--text); - line-height: 1.6; - white-space: pre-wrap; overflow-wrap: anywhere; } @@ -2542,7 +2535,7 @@ The Plan tab must display task.description as the immutable original operator pr } .detail-section--original-prompt, - .detail-section--original-prompt .detail-original-prompt-text { + .detail-section--original-prompt .markdown-body { min-width: 0; max-width: 100%; } diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index a5bb42f110..c9ef750673 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -1059,6 +1059,8 @@ export function TaskDetailContent({ const [sourceIssueExpanded, setSourceIssueExpanded] = useState(false); const [retriesExpanded, setRetriesExpanded] = useState(initialTab === "retries"); const [gitlabTrackingExpanded, setGitlabTrackingExpanded] = useState(false); + // FNXC:TaskDetailPlan 2026-07-04-00:00: Original prompt is collapsed by default (see render site below); operator must click the chevron toggle to reveal the markdown-rendered text. + const [originalPromptExpanded, setOriginalPromptExpanded] = useState(false); const [githubTrackingExpanded, setGithubTrackingExpanded] = useState(false); const [githubRepoOverrideDraft, setGithubRepoOverrideDraft] = useState(task.githubTracking?.repoOverride ?? ""); const [githubTrackingEnabledDraft, setGithubTrackingEnabledDraft] = useState(null); @@ -4686,12 +4688,32 @@ export function TaskDetailContent({ {/** * FNXC:TaskDetailPlan 2026-07-04-00:00: * Operators need the exact prompt they entered to stay visible after planning generates PROMPT.md. Keep this section read-only and backed by task.description so PROMPT.md editing/revision controls cannot imply they mutate the original request. + * + * FNXC:TaskDetailPlan 2026-07-04-00:00: + * The original operator prompt is now rendered as Markdown (shared PROMPT.md renderer) and collapsed by default behind a chevron toggle, superseding the earlier plain-preserved-text rule. It remains read-only and backed by task.description; the generated PROMPT.md editor/revision flow is unaffected. The toggle only renders when there is content — the empty fallback never shows a chevron. */} -

{t("taskDetail.originalPrompt.heading", "Original prompt")}

+
+

{t("taskDetail.originalPrompt.heading", "Original prompt")}

+ {hasOriginalTaskPrompt && ( + + )} +
{hasOriginalTaskPrompt ? ( -
- {originalTaskPrompt} -
+ originalPromptExpanded && ( +
+ + {originalTaskPrompt} + +
+ ) ) : (

{t("taskDetail.originalPrompt.empty", "No original prompt recorded.")} diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx index 4ede1e7e02..55f637adf9 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx @@ -403,8 +403,8 @@ describe("TaskDetailModal", () => { }); describe("Plan tab original prompt", () => { - it("shows the exact original prompt beside the generated plan in the modal", () => { - const originalPrompt = "Exact user prompt\nwith **markdown** and `code`\n```ts\nconst answer = 42;\n```\nhttps://example.com/task"; + it("is collapsed by default and expands to render the original prompt as markdown", () => { + const originalPrompt = "# Heading\n\n- item\n\n`code`"; const generatedPrompt = "# FN-TEST\n\n## Mission\nGenerated plan"; const { container } = render( { ); expect(screen.getByRole("heading", { name: "Original prompt" })).toBeTruthy(); - const originalPromptNode = screen.getByTestId("task-detail-original-prompt"); - expect(originalPromptNode.textContent).toBe(originalPrompt); - expect(originalPromptNode.innerHTML).toContain("**markdown**"); - expect(originalPromptNode.querySelector("strong, code, pre, a")).toBeNull(); + const toggle = screen.getByRole("button", { name: "Expand original prompt" }); + expect(toggle.getAttribute("aria-expanded")).toBe("false"); + // Collapsed by default: the prompt body is not in the DOM until expanded. + expect(screen.queryByTestId("task-detail-original-prompt")).toBeNull(); expect(screen.getByRole("heading", { name: "Mission" })).toBeTruthy(); expect(screen.getByText("Generated plan")).toBeTruthy(); + fireEvent.click(toggle); + + const expandedToggle = screen.getByRole("button", { name: "Collapse original prompt" }); + expect(expandedToggle.getAttribute("aria-expanded")).toBe("true"); + const originalPromptNode = screen.getByTestId("task-detail-original-prompt"); + expect(originalPromptNode.className).toContain("markdown-body"); + // Rendered as markdown, not raw source: a real heading/list/code element exists, + // and the raw markdown characters are gone. + expect(originalPromptNode.querySelector("h1, li, code")).toBeTruthy(); + expect(originalPromptNode.textContent).not.toContain("# Heading"); + expect(originalPromptNode.textContent).not.toContain("`code`"); + const originalSection = container.querySelector(".detail-section--original-prompt"); const planSection = container.querySelector(".detail-section--plan-prompt"); expect(originalSection?.contains(screen.getByText("Original prompt"))).toBe(true); @@ -440,7 +452,7 @@ describe("TaskDetailModal", () => { expect(originalPromptNode.textContent).not.toBe("Title must not replace description"); }); - it("renders a non-boxed empty fallback without hiding the generated plan", () => { + it("renders a non-boxed empty fallback with no toggle, without hiding the generated plan", () => { const { container } = render( { expect(screen.getByText("No original prompt recorded.")).toBeTruthy(); expect(screen.queryByTestId("task-detail-original-prompt")).toBeNull(); + expect(screen.queryByRole("button", { name: "Expand original prompt" })).toBeNull(); expect(container.querySelector(".detail-section--original-prompt .detail-original-prompt-text")).toBeNull(); expect(screen.getByText("Generated plan still visible")).toBeTruthy(); }); - it("shows the same original prompt section in embedded task detail content", () => { + it("shows the same collapsible original prompt section in embedded task detail content", () => { const originalPrompt = "Embedded prompt\nwith a second line"; const { container } = render( { ); expect(container.querySelector(".task-detail-content--embedded")).toBeTruthy(); + expect(screen.queryByTestId("task-detail-original-prompt")).toBeNull(); + fireEvent.click(screen.getByRole("button", { name: "Expand original prompt" })); expect(screen.getByTestId("task-detail-original-prompt").textContent).toBe(originalPrompt); expect(screen.getByText("Embedded generated plan")).toBeTruthy(); }); - it("applies wrapping and mobile CSS contracts to the original prompt section", () => { + it("applies wrapping and mobile CSS contracts to the expanded original prompt section", () => { const css = readDashboardStylesSource(); - const promptTextBlock = getRuleBlock(css, ".detail-original-prompt-text"); + const markdownBodyBlock = getRuleBlock(css, ".detail-section--original-prompt .markdown-body"); const emptyBlock = getRuleBlock(css, ".detail-original-prompt-empty"); const mobileBlock = getMediaBlocks(css, "@media (max-width: 768px)").find((block) => block.includes(".detail-section--original-prompt"), ) ?? ""; expect(css).toContain(".detail-section--original-prompt,\n.detail-section--plan-prompt"); - expect(promptTextBlock).toContain("box-sizing: border-box;"); - expect(promptTextBlock).toContain("width: 100%;"); - expect(promptTextBlock).toContain("min-width: 0;"); - expect(promptTextBlock).toContain("max-width: 100%;"); - expect(promptTextBlock).toContain("padding: var(--space-md);"); - expect(promptTextBlock).toContain("border: var(--btn-border-width) solid var(--border);"); - expect(promptTextBlock).toContain("white-space: pre-wrap;"); - expect(promptTextBlock).toContain("overflow-wrap: anywhere;"); - expect(promptTextBlock).not.toMatch(/#[0-9a-fA-F]{3,8}|rgb\(/); + expect(markdownBodyBlock).toContain("box-sizing: border-box;"); + expect(markdownBodyBlock).toContain("width: 100%;"); + expect(markdownBodyBlock).toContain("min-width: 0;"); + expect(markdownBodyBlock).toContain("max-width: 100%;"); + expect(markdownBodyBlock).toContain("overflow-wrap: anywhere;"); + expect(markdownBodyBlock).not.toMatch(/#[0-9a-fA-F]{3,8}|rgb\(/); expect(emptyBlock).toContain("margin: 0;"); expect(emptyBlock).toContain("color: var(--text-muted);"); expect(mobileBlock).toContain(".detail-section--original-prompt,"); - expect(mobileBlock).toContain(".detail-section--original-prompt .detail-original-prompt-text"); + expect(mobileBlock).toContain(".detail-section--original-prompt .markdown-body"); expect(mobileBlock).toContain("max-width: 100%;"); }); });