FN-7349: expand Plan prompt editor width

Make Plan prompt editing surfaces span the task-detail layout consistently.

- Scope a Plan prompt wrapper around markdown, empty, inline edit, and revision controls.
- Let SpecEditor action rows and text inputs shrink and wrap within modal, embedded, and mobile layouts.
- Cover full-width Plan prompt behavior with task-detail CSS and rendering tests.

Files changed:
 .changeset/plan-prompt-full-width.md               |   7 ++
 packages/dashboard/app/components/SpecEditor.css   |  27 ++++-
 .../dashboard/app/components/TaskDetailModal.css   |  58 ++++++++-
 .../dashboard/app/components/TaskDetailModal.tsx   |  13 +-
 .../TaskDetailModal.definition-actions.test.tsx    | 134 ++++++++++++---------
 ...etailModal.responsive-and-dependencies.test.tsx |  44 +++++++
 6 files changed, 219 insertions(+), 64 deletions(-)

Fusion-Task-Id: FN-7349

Fusion-Task-Lineage: 65defcea-ce70-49fb-bd00-2127239e09d0

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-01 00:28:25 -07:00
parent 058a041714
commit d96eb3c644
6 changed files with 219 additions and 64 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Make the task Plan prompt editor span the full task-detail card width.
category: fix
dev: Adds scoped TaskDetailModal Plan prompt width guards for modal, embedded, and mobile surfaces.

View File

@@ -39,15 +39,25 @@
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: var(--space-sm);
width: 100%;
min-width: 0;
max-width: 100%;
margin-top: var(--space-sm);
}
/* Edit mode container in Definition tab */
/*
FNXC:TaskDetailPlan 2026-06-30-00:00:
Inline Plan editing and AI revision controls share SpecEditor primitives, so these rows must shrink within task-detail modal and embedded cards instead of imposing a narrow or overflowing editor column.
*/
.spec-editor-edit-mode {
display: flex;
flex-direction: column;
gap: var(--space-sm);
width: 100%;
min-width: 0;
max-width: 100%;
}
/* Content area - flexible and scrollable */
@@ -61,7 +71,10 @@
/* Textarea - fills available space in edit mode */
.spec-editor-textarea {
box-sizing: border-box;
width: 100%;
min-width: 0;
max-width: 100%;
min-height: 200px;
height: 100%;
flex: 1;
@@ -121,6 +134,10 @@
/* AI Revision section - fixed at bottom */
.spec-editor-revision {
box-sizing: border-box;
width: 100%;
min-width: 0;
max-width: 100%;
padding: 16px 0;
border-top: 1px solid var(--border);
flex-shrink: 0;
@@ -140,7 +157,10 @@
}
.spec-editor-feedback {
box-sizing: border-box;
width: 100%;
min-width: 0;
max-width: 100%;
padding: 10px 12px;
background: var(--bg);
border: 1px solid var(--border);
@@ -168,6 +188,11 @@
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-sm);
width: 100%;
min-width: 0;
max-width: 100%;
margin-top: var(--space-sm);
}

View File

@@ -2007,6 +2007,42 @@ The desktop table needs a token-scale min-width and non-anywhere model-name wrap
margin-top: 0;
}
/*
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.
*/
.detail-section--plan-prompt {
display: flex;
flex-direction: column;
width: 100%;
min-width: 0;
max-width: 100%;
}
.detail-section--plan-prompt .markdown-body,
.detail-section--plan-prompt .detail-prompt,
.detail-section--plan-prompt .spec-loading,
.detail-section--plan-prompt .spec-editor-edit-mode,
.detail-section--plan-prompt .spec-editor-revision,
.detail-section--plan-prompt .spec-editor-textarea,
.detail-section--plan-prompt .spec-editor-feedback {
box-sizing: border-box;
width: 100%;
min-width: 0;
max-width: 100%;
}
.detail-section--plan-prompt .markdown-body,
.detail-section--plan-prompt .detail-prompt {
overflow-wrap: anywhere;
}
.task-detail-content--embedded .detail-section--plan-prompt {
width: 100%;
min-width: 0;
max-width: 100%;
}
/* Back button for changed-files modal - hidden on desktop */
.changed-files-back-button {
display: none;
@@ -2196,6 +2232,26 @@ The desktop table needs a token-scale min-width and non-anywhere model-name wrap
flex: 1;
min-height: 0;
}
.detail-section--plan-prompt .detail-spec-edit-trigger,
.detail-section--plan-prompt .spec-editor-actions-row,
.detail-section--plan-prompt .spec-editor-revision-actions {
align-items: stretch;
}
.detail-section--plan-prompt .spec-editor-actions-row,
.detail-section--plan-prompt .spec-editor-revision-actions {
flex-wrap: wrap;
}
.detail-section--plan-prompt .spec-editor-actions-row .btn,
.detail-section--plan-prompt .spec-editor-revision-actions .btn {
flex: 1 1 auto;
}
.detail-section--plan-prompt .spec-editor-char-count {
flex: 1 1 auto;
}
}
.detail-section h4 {
@@ -2222,7 +2278,7 @@ The desktop table needs a token-scale min-width and non-anywhere model-name wrap
color: var(--text-muted);
display: flex;
align-items: center;
gap: 8px;
gap: var(--space-sm);
}
.spec-loading::before {

View File

@@ -3923,13 +3923,16 @@ export function TaskDetailContent({
<div className="step-progress-empty">{t("taskDetail.progress.noSteps", "(no steps defined)")}</div>
)}
</div>
<div className="detail-section">
<div className="detail-section detail-section--plan-prompt">
{!isEditingSpec && (
<div className="detail-spec-edit-trigger">
{/*
FNXC:TaskDetailPlan 2026-06-30-00:00:
The Plan tab keeps the internal definition route for stable links, while exposing a direct PROMPT.md editor action so operators can comment on the executable task plan file without replacing the inline AI revision flow.
*/}
{/**
* FNXC:TaskDetailPlan 2026-06-30-00:00:
* The Plan tab keeps the internal definition route for stable links, while exposing a direct PROMPT.md editor action so operators can comment on the executable task plan file without replacing the inline AI revision flow.
*
* FNXC:TaskDetailPlan 2026-06-30-00:00:
* The Plan prompt surfaces must span the task-detail card body in modal and embedded renderings. Keep the scoped wrapper around markdown, no-prompt fallback, inline edit, and AI revision controls so width fixes do not alter unrelated detail sections.
*/}
{fileBrowser && (
<button
className="btn btn-sm"

View File

@@ -83,18 +83,78 @@ describe("TaskDetailModal", () => {
/>,
);
const planSection = container.querySelector(".detail-section--plan-prompt");
expect(planSection).toBeTruthy();
// Initially showing markdown view
expect(container.querySelector(".markdown-body")).toBeTruthy();
const markdown = container.querySelector(".markdown-body");
expect(markdown).toBeTruthy();
expect(planSection?.contains(markdown)).toBe(true);
// Click Edit button
fireEvent.click(screen.getByText("Edit"));
// Should show spec edit textarea (query by class for specificity)
const editMode = container.querySelector(".spec-editor-edit-mode");
const textarea = container.querySelector(".spec-editor-textarea") as HTMLTextAreaElement;
const feedback = container.querySelector(".spec-editor-feedback");
expect(editMode).toBeTruthy();
expect(textarea).toBeTruthy();
expect(feedback).toBeTruthy();
expect(planSection?.contains(editMode)).toBe(true);
expect(planSection?.contains(textarea)).toBe(true);
expect(planSection?.contains(feedback)).toBe(true);
expect(textarea.value).toBe("# Test\n\nSpec content.");
});
it("keeps the no-prompt fallback inside the scoped full-width Plan wrapper", () => {
const { container } = render(
<TaskDetailModal
task={makeTask({ prompt: "" })}
initialTab="definition"
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
const planSection = container.querySelector(".detail-section--plan-prompt");
const fallback = container.querySelector(".detail-prompt");
expect(planSection).toBeTruthy();
expect(fallback).toBeTruthy();
expect(planSection?.contains(fallback)).toBe(true);
});
it("keeps embedded Plan edit controls inside the full-width wrapper", () => {
const { container } = render(
<TaskDetailContent
task={makeTask({ prompt: "# Embedded\n\nSpec content." })}
initialTab="definition"
embedded
onRequestClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
expect(container.querySelector(".task-detail-content--embedded")).toBeTruthy();
const planSection = container.querySelector(".detail-section--plan-prompt");
fireEvent.click(screen.getByText("Edit"));
const editMode = container.querySelector(".spec-editor-edit-mode");
const textarea = container.querySelector(".spec-editor-textarea");
const feedback = container.querySelector(".spec-editor-feedback");
expect(planSection).toBeTruthy();
expect(planSection?.contains(editMode)).toBe(true);
expect(planSection?.contains(textarea)).toBe(true);
expect(planSection?.contains(feedback)).toBe(true);
});
it("clicking Cancel returns to view mode without saving", () => {
const { container } = render(
<TaskDetailModal
@@ -222,20 +282,11 @@ describe("TaskDetailModal", () => {
);
// In-progress tasks show exactly 11 tabs:
// Activity, Plan, Logs, Changes, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
// Activity, Chat, Plan, Changes, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(11);
expect(tabs[0].textContent).toBe("Activity");
expect(tabs[1].textContent).toBe("Plan");
expect(tabs[2].textContent).toBe("Logs");
expect(tabs[3].textContent).toBe("Changes");
expect(tabs[4].textContent).toBe("Review");
expect(tabs[5].textContent).toBe("Comments");
expect(tabs[6].textContent).toBe("Artifacts");
expect(tabs[7].textContent).toBe("Model");
expect(tabs[8].textContent).toBe("Workflow");
expect(tabs[9].textContent).toBe("Stats");
expect(tabs[10].textContent).toBe("Routing");
expect(Array.from(tabs).map(t => t.textContent)).toEqual([
"Activity", "Chat", "Plan", "Changes", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
]);
// Commits tab should NOT be present for non-done tasks
expect(screen.queryByText("Commits")).toBeNull();
});
@@ -256,18 +307,9 @@ describe("TaskDetailModal", () => {
// In-progress task with workflow steps: 11 tabs (Review after Changes, Workflow after Model)
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(11);
expect(tabs[0].textContent).toBe("Activity");
expect(tabs[1].textContent).toBe("Plan");
expect(tabs[2].textContent).toBe("Logs");
expect(tabs[3].textContent).toBe("Changes");
expect(tabs[4].textContent).toBe("Review");
expect(tabs[5].textContent).toBe("Comments");
expect(tabs[6].textContent).toBe("Artifacts");
expect(tabs[7].textContent).toBe("Model");
expect(tabs[8].textContent).toBe("Workflow");
expect(tabs[9].textContent).toBe("Stats");
expect(tabs[10].textContent).toBe("Routing");
expect(Array.from(tabs).map(t => t.textContent)).toEqual([
"Activity", "Chat", "Plan", "Changes", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
]);
});
it("does NOT show Commits tab for done task with mergeDetails.commitSha (changes merged into Changes tab)", () => {
@@ -287,21 +329,11 @@ describe("TaskDetailModal", () => {
/>,
);
// Done task with commit SHA: Activity, Summary, Plan, Logs, Changes, Review, Comments, Artifacts, Model, Workflow, Stats, Routing (12 tabs, no Commits)
// Done task with commit SHA: Activity, Chat, Summary, Plan, Changes, Review, Comments, Artifacts, Model, Workflow, Stats, Routing (12 tabs, no Commits)
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(12);
expect(tabs[0].textContent).toBe("Activity");
expect(tabs[1].textContent).toBe("Summary");
expect(tabs[2].textContent).toBe("Plan");
expect(tabs[3].textContent).toBe("Logs");
expect(tabs[4].textContent).toBe("Changes");
expect(tabs[5].textContent).toBe("Review");
expect(tabs[6].textContent).toBe("Comments");
expect(tabs[7].textContent).toBe("Artifacts");
expect(tabs[8].textContent).toBe("Model");
expect(tabs[9].textContent).toBe("Workflow");
expect(tabs[10].textContent).toBe("Stats");
expect(tabs[11].textContent).toBe("Routing");
expect(Array.from(tabs).map(t => t.textContent)).toEqual([
"Activity", "Chat", "Summary", "Plan", "Changes", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
]);
// Commits tab should NOT be present
expect(screen.queryByText("Commits")).toBeNull();
});
@@ -326,19 +358,9 @@ describe("TaskDetailModal", () => {
// Done task with workflow steps and commit SHA: 12 tabs including Summary and Review (no Commits)
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(12);
expect(tabs[0].textContent).toBe("Activity");
expect(tabs[1].textContent).toBe("Summary");
expect(tabs[2].textContent).toBe("Plan");
expect(tabs[3].textContent).toBe("Logs");
expect(tabs[4].textContent).toBe("Changes");
expect(tabs[5].textContent).toBe("Review");
expect(tabs[6].textContent).toBe("Comments");
expect(tabs[7].textContent).toBe("Artifacts");
expect(tabs[8].textContent).toBe("Model");
expect(tabs[9].textContent).toBe("Workflow");
expect(tabs[10].textContent).toBe("Stats");
expect(tabs[11].textContent).toBe("Routing");
expect(Array.from(tabs).map(t => t.textContent)).toEqual([
"Activity", "Chat", "Summary", "Plan", "Changes", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
]);
// Commits tab should NOT be present
expect(screen.queryByText("Commits")).toBeNull();
});
@@ -358,9 +380,8 @@ describe("TaskDetailModal", () => {
);
const triageTabs = triageContainer.querySelectorAll(".detail-tab");
expect(triageTabs.length).toBe(10); // Activity, Plan, Logs, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
expect(Array.from(triageTabs).map(t => t.textContent)).toEqual([
"Activity", "Plan", "Logs", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
"Activity", "Chat", "Plan", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
]);
const { container: todoContainer } = render(
@@ -377,9 +398,8 @@ describe("TaskDetailModal", () => {
);
const todoTabs = todoContainer.querySelectorAll(".detail-tab");
expect(todoTabs.length).toBe(10); // Activity, Plan, Logs, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
expect(Array.from(todoTabs).map(t => t.textContent)).toEqual([
"Activity", "Plan", "Logs", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
"Activity", "Chat", "Plan", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
]);
});

View File

@@ -283,6 +283,50 @@ describe("TaskDetailModal", () => {
expect(tabletModalBlock).not.toContain("16px");
});
it("keeps Plan prompt surfaces full-width across modal, embedded, and mobile task-detail layouts", () => {
const css = readDashboardStylesSource();
const planBlock = getExactCssRuleBlock(css, ".detail-section--plan-prompt");
const planSurfaceBlock = getExactCssRuleBlock(css, ".detail-section--plan-prompt .markdown-body,\n.detail-section--plan-prompt .detail-prompt,\n.detail-section--plan-prompt .spec-loading,\n.detail-section--plan-prompt .spec-editor-edit-mode,\n.detail-section--plan-prompt .spec-editor-revision,\n.detail-section--plan-prompt .spec-editor-textarea,\n.detail-section--plan-prompt .spec-editor-feedback");
const embeddedPlanBlock = getExactCssRuleBlock(css, ".task-detail-content--embedded .detail-section--plan-prompt");
const editModeBlock = getExactCssRuleBlock(css, ".spec-editor-edit-mode");
const textareaBlock = getExactCssRuleBlock(css, ".spec-editor-textarea");
const feedbackBlock = getExactCssRuleBlock(css, ".spec-editor-feedback");
const actionsBlock = getExactCssRuleBlock(css, ".spec-editor-actions-row");
const revisionActionsBlock = getExactCssRuleBlock(css, ".spec-editor-revision-actions");
const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".detail-section--plan-prompt .spec-editor-actions-row");
for (const [surface, block] of [
["Plan wrapper", planBlock],
["Plan prompt descendants", planSurfaceBlock],
["embedded Plan wrapper", embeddedPlanBlock],
["edit mode", editModeBlock],
["textarea", textareaBlock],
["feedback", feedbackBlock],
["save/cancel actions", actionsBlock],
["AI revision actions", revisionActionsBlock],
] as const) {
expect(block, `${surface} width`).toContain("width: 100%;");
expect(block, `${surface} min-width`).toContain("min-width: 0;");
expect(block, `${surface} max-width`).toContain("max-width: 100%;");
}
expect(planBlock).toContain("display: flex;");
expect(planBlock).toContain("flex-direction: column;");
expect(planSurfaceBlock).toContain("box-sizing: border-box;");
expect(textareaBlock).toContain("box-sizing: border-box;");
expect(feedbackBlock).toContain("box-sizing: border-box;");
expect(actionsBlock).toContain("flex-wrap: wrap;");
expect(revisionActionsBlock).toContain("flex-wrap: wrap;");
expect(mobileBlock).toContain(".detail-section--plan-prompt .spec-editor-actions-row,");
expect(mobileBlock).toContain(".detail-section--plan-prompt .spec-editor-revision-actions");
expect(mobileBlock).toContain("align-items: stretch;");
expect(mobileBlock).toContain("flex-wrap: wrap;");
expect(mobileBlock).toContain(".detail-section--plan-prompt .spec-editor-actions-row .btn,");
expect(mobileBlock).toContain(".detail-section--plan-prompt .spec-editor-revision-actions .btn");
expect(mobileBlock).toContain("flex: 1 1 auto;");
expect(css).not.toMatch(/\.detail-section\s*\{[^}]*width:\s*100%;/);
});
it("keeps task-detail tabs as horizontal scrollers across modal, embedded, mobile, and tablet surfaces", () => {
const css = readDashboardStylesSource();
const baseTabsBlock = getExactCssRuleBlock(css, ".detail-tabs");