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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-04 10:03:07 -07:00
parent 61c8bdc117
commit e8dc2ae6d1
5 changed files with 182 additions and 1 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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({
<div className="step-progress-empty">{t("taskDetail.progress.noSteps", "(no steps defined)")}</div>
)}
</div>
<div className="detail-section detail-section--original-prompt">
{/**
* 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.
*/}
<h4>{t("taskDetail.originalPrompt.heading", "Original prompt")}</h4>
{hasOriginalTaskPrompt ? (
<div className="detail-original-prompt-text" data-testid="task-detail-original-prompt">
{originalTaskPrompt}
</div>
) : (
<p className="detail-original-prompt-empty">
{t("taskDetail.originalPrompt.empty", "No original prompt recorded.")}
</p>
)}
</div>
<div className="detail-section detail-section--plan-prompt">
{!isEditingSpec && (
<div className="detail-spec-edit-trigger">

View File

@@ -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(
<TaskDetailModal
initialTab="definition"
task={makeTask({
id: "FN-TEST",
title: "Title must not replace description",
description: originalPrompt,
prompt: generatedPrompt,
})}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
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(
<TaskDetailModal
initialTab="definition"
task={makeTask({
id: "FN-EMPTY",
description: " \n\t ",
prompt: "# FN-EMPTY\n\n## Mission\nGenerated plan still visible",
})}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
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(
<TaskDetailContent
embedded
initialTab="definition"
task={makeTask({
id: "FN-EMBED",
description: originalPrompt,
prompt: "# FN-EMBED\n\n## Mission\nEmbedded generated plan",
})}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
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();