From e8dc2ae6d166f538b080d98c1d71ec2fc5da3eba Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 4 Jul 2026 10:03:07 -0700 Subject: [PATCH] FN-7498: show original task prompts in Plan tab Display the initial task prompt separately from the generated plan in task details. - Add a read-only Original prompt section above generated PROMPT.md content in task detail Plan views. - Preserve plain-text formatting and responsive wrapping for original prompts while leaving plan edit/revision controls scoped to generated prompts. - Cover modal, embedded, empty-state, CSS, docs, and release-note behavior. Files changed: .changeset/fn-7498-original-task-prompt.md | 7 ++ docs/dashboard-guide.md | 1 + .../dashboard/app/components/TaskDetailModal.css | 30 +++++ .../dashboard/app/components/TaskDetailModal.tsx | 18 +++ ...lModal.inline-editing-and-integrations.test.tsx | 127 ++++++++++++++++++++- 5 files changed, 182 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-7498 Fusion-Task-Lineage: df83c73d-d731-4ba4-942b-bd93ec1b9712 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7498-original-task-prompt.md | 7 + docs/dashboard-guide.md | 1 + .../app/components/TaskDetailModal.css | 30 +++++ .../app/components/TaskDetailModal.tsx | 18 +++ ...l.inline-editing-and-integrations.test.tsx | 127 +++++++++++++++++- 5 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 .changeset/fn-7498-original-task-prompt.md diff --git a/.changeset/fn-7498-original-task-prompt.md b/.changeset/fn-7498-original-task-prompt.md new file mode 100644 index 0000000000..e4a1a1b17d --- /dev/null +++ b/.changeset/fn-7498-original-task-prompt.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Show each task's original prompt in the Plan tab alongside the generated plan. +category: fix +dev: Adds a read-only Task Detail original-prompt section backed by task.description. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index b7423eae1f..c78808f8d3 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -1120,6 +1120,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. - 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 3c14705710..cdd40bd4f0 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -2015,7 +2015,11 @@ The desktop table needs a token-scale min-width and non-anywhere model-name wrap /* 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. */ +.detail-section--original-prompt, .detail-section--plan-prompt { display: flex; flex-direction: column; @@ -2024,6 +2028,26 @@ The Plan prompt editor is a task-detail body surface, not a narrow prose inset. max-width: 100%; } +.detail-original-prompt-text { + 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; +} + +.detail-original-prompt-empty { + margin: 0; + color: var(--text-muted); +} + .detail-section--plan-prompt .markdown-body, .detail-section--plan-prompt .detail-prompt, .detail-section--plan-prompt .spec-loading, @@ -2238,6 +2262,12 @@ The Plan prompt editor is a task-detail body surface, not a narrow prose inset. min-height: 0; } + .detail-section--original-prompt, + .detail-section--original-prompt .detail-original-prompt-text { + min-width: 0; + max-width: 100%; + } + .detail-section--plan-prompt .detail-spec-edit-trigger, .detail-section--plan-prompt .spec-editor-actions-row, .detail-section--plan-prompt .spec-editor-revision-actions { diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index bfafc53ba8..9aa3f55928 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -667,6 +667,8 @@ export function TaskDetailContent({ : task.overlapBlockedBy === undefined ? fullDetail.overlapBlockedBy : task.overlapBlockedBy, } as TaskDetail) : ({ ...task, prompt: "" } as TaskDetail); + const originalTaskPrompt = workingTask.description ?? ""; + const hasOriginalTaskPrompt = originalTaskPrompt.trim().length > 0; /* FNXC:WorkflowStepResults 2026-06-26-18:00: The detail Progress bar must show a segment for each ENABLED workflow step, not only @@ -4271,6 +4273,22 @@ export function TaskDetailContent({
{t("taskDetail.progress.noSteps", "(no steps defined)")}
)} +
+ {/** + * 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. + */} +

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

+ {hasOriginalTaskPrompt ? ( +
+ {originalTaskPrompt} +
+ ) : ( +

+ {t("taskDetail.originalPrompt.empty", "No original prompt recorded.")} +

+ )} +
{!isEditingSpec && (
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 aec68bf6fc..13add5729a 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 @@ -70,6 +70,19 @@ function getRuleBlock(css: string, selector: string): string { return ruleMatch?.[1] ?? ""; } +function getRuleBlockFromSelectorList(css: string, selector: string): string { + const selectorIndex = css.indexOf(selector); + if (selectorIndex === -1) { + return ""; + } + const ruleStart = css.indexOf("{", selectorIndex); + if (ruleStart === -1) { + return ""; + } + const ruleEnd = css.indexOf("}", ruleStart); + return ruleEnd === -1 ? "" : css.slice(ruleStart + 1, ruleEnd); +} + describe("TaskDetailModal", () => { describe("source issue metadata", () => { beforeEach(() => { @@ -389,6 +402,118 @@ 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"; + 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(); + expect(screen.getByRole("heading", { name: "Mission" })).toBeTruthy(); + expect(screen.getByText("Generated plan")).toBeTruthy(); + + 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); + expect(planSection?.contains(screen.getByRole("button", { name: "Edit" }))).toBe(true); + expect(originalSection?.contains(screen.getByRole("button", { name: "Edit" }))).toBe(false); + expect(originalPromptNode.textContent).not.toBe("Title must not replace description"); + }); + + it("renders a non-boxed empty fallback 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(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", () => { + const originalPrompt = "Embedded prompt\nwith a second line"; + const { container } = render( + , + ); + + expect(container.querySelector(".task-detail-content--embedded")).toBeTruthy(); + 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", () => { + const css = readDashboardStylesSource(); + const promptTextBlock = getRuleBlock(css, ".detail-original-prompt-text"); + 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(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("max-width: 100%;"); + }); + }); + describe("inline editing", () => { beforeEach(() => { vi.clearAllMocks(); @@ -2810,7 +2935,7 @@ describe("TaskDetailModal", () => { const mobileCss = getMediaBlocks(css, "@media (max-width: 768px)").join("\n"); const enableRule = getRuleBlock(mobileCss, ".detail-github-tracking-enable"); const headerRule = getRuleBlock(mobileCss, ".detail-source-header"); - const githubSummaryRule = getRuleBlock(mobileCss, ".detail-github-tracking-section .detail-source-summary"); + const githubSummaryRule = getRuleBlockFromSelectorList(mobileCss, ".detail-github-tracking-section .detail-source-summary"); const sourceSummaryRule = getRuleBlock(mobileCss, ".detail-source-section .detail-source-summary"); expect(mobileCss).toBeTruthy();