FN-7305: add prompt file access to task plans
Expose task plans as the user-facing Definition tab label and add a direct PROMPT.md editor entry point. - Rename the task detail Definition tab label to Plan while preserving the existing tab id. - Add an Open PROMPT.md action that uses the file browser provider for project task plan files. - Adjust Plan action row layout for multiple buttons and mobile wrapping. - Cover the Plan label and PROMPT.md action with task detail modal tests. - Add a published package changeset for the dashboard task plan improvement. Files changed: .changeset/tidy-plan-prompt-editor.md | 7 +++ .../dashboard/app/components/TaskDetailModal.css | 14 ++++++ .../dashboard/app/components/TaskDetailModal.tsx | 20 +++++++- .../TaskDetailModal.definition-actions.test.tsx | 55 +++++++++++++++++----- 4 files changed, 83 insertions(+), 13 deletions(-) Fusion-Task-Id: FN-7305 Fusion-Task-Lineage: afe732f9-e9a1-409f-9b09-ae1deccbe3be Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/tidy-plan-prompt-editor.md
Normal file
7
.changeset/tidy-plan-prompt-editor.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Rename the task Definition tab to Plan and add a PROMPT.md editor action.
|
||||||
|
category: feature
|
||||||
|
dev: Dashboard task details now open the current task's PROMPT.md via FileBrowserProvider.
|
||||||
@@ -851,9 +851,23 @@ Expanded chat must also cover the task metadata row — priority, execution mode
|
|||||||
|
|
||||||
|
|
||||||
.detail-spec-edit-trigger {
|
.detail-spec-edit-trigger {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
margin-bottom: var(--space-md);
|
margin-bottom: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.detail-spec-edit-trigger {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-spec-edit-trigger .btn {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.detail-attachments-grid {
|
.detail-attachments-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import { getTaskLogEntryAction, getTaskLogEntryOutcome } from "../utils/taskLogE
|
|||||||
import { getRelativeTimeBucket } from "../utils/relativeTimeAgo";
|
import { getRelativeTimeBucket } from "../utils/relativeTimeAgo";
|
||||||
import { ACTIVE_STATUSES, resolveEffectiveExecutor, resolveEffectivePlanning, resolveEffectiveValidator, type ModelSelection } from "./effective-model-resolution";
|
import { ACTIVE_STATUSES, resolveEffectiveExecutor, resolveEffectivePlanning, resolveEffectiveValidator, type ModelSelection } from "./effective-model-resolution";
|
||||||
import { TaskContextMenu, buildTaskActionMenuModel, getTaskPrAutomationLabel } from "./TaskContextMenu";
|
import { TaskContextMenu, buildTaskActionMenuModel, getTaskPrAutomationLabel } from "./TaskContextMenu";
|
||||||
|
import { useFileBrowser } from "../context/FileBrowserContext";
|
||||||
|
|
||||||
const STALE_PAUSED_REVIEW_LOG_REGEX = /^Stale paused review surfaced \[([^\]]+)\]/;
|
const STALE_PAUSED_REVIEW_LOG_REGEX = /^Stale paused review surfaced \[([^\]]+)\]/;
|
||||||
const EMPTY_MARKDOWN_CHILD_SEPARATOR = "";
|
const EMPTY_MARKDOWN_CHILD_SEPARATOR = "";
|
||||||
@@ -513,6 +514,7 @@ export function TaskDetailContent({
|
|||||||
}: TaskDetailContentProps) {
|
}: TaskDetailContentProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const columnLabel = useColumnLabel();
|
const columnLabel = useColumnLabel();
|
||||||
|
const fileBrowser = useFileBrowser();
|
||||||
const [activeTab, setActiveTab] = useState<TabId>(() => resolveDefaultTab(initialTab, task.column));
|
const [activeTab, setActiveTab] = useState<TabId>(() => resolveDefaultTab(initialTab, task.column));
|
||||||
const [chatExpanded, setChatExpanded] = useState(false);
|
const [chatExpanded, setChatExpanded] = useState(false);
|
||||||
|
|
||||||
@@ -607,6 +609,9 @@ export function TaskDetailContent({
|
|||||||
() => getUnifiedTaskProgress(workingTask),
|
() => getUnifiedTaskProgress(workingTask),
|
||||||
[workingTask.steps, workingTask.enabledWorkflowSteps, workingTask.workflowStepResults],
|
[workingTask.steps, workingTask.enabledWorkflowSteps, workingTask.workflowStepResults],
|
||||||
);
|
);
|
||||||
|
const openPromptFile = useCallback(() => {
|
||||||
|
fileBrowser?.openFile(`.fusion/tasks/${workingTask.id}/PROMPT.md`, { workspace: "project" });
|
||||||
|
}, [fileBrowser, workingTask.id]);
|
||||||
const canRetryTask =
|
const canRetryTask =
|
||||||
task.status === "failed" ||
|
task.status === "failed" ||
|
||||||
task.status === "stuck-killed" ||
|
task.status === "stuck-killed" ||
|
||||||
@@ -3155,7 +3160,7 @@ export function TaskDetailContent({
|
|||||||
className={`detail-tab${activeTab === "definition" ? " detail-tab-active" : ""}`}
|
className={`detail-tab${activeTab === "definition" ? " detail-tab-active" : ""}`}
|
||||||
onClick={() => setActiveTab("definition")}
|
onClick={() => setActiveTab("definition")}
|
||||||
>
|
>
|
||||||
{t("taskDetail.tabs.definition", "Definition")}
|
{t("taskDetail.tabs.definition", "Plan")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`detail-tab${activeTab === "logs" ? " detail-tab-active" : ""}`}
|
className={`detail-tab${activeTab === "logs" ? " detail-tab-active" : ""}`}
|
||||||
@@ -3775,6 +3780,19 @@ export function TaskDetailContent({
|
|||||||
<div className="detail-section">
|
<div className="detail-section">
|
||||||
{!isEditingSpec && (
|
{!isEditingSpec && (
|
||||||
<div className="detail-spec-edit-trigger">
|
<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.
|
||||||
|
*/}
|
||||||
|
{fileBrowser && (
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={openPromptFile}
|
||||||
|
title={t("taskDetail.spec.openPromptTitle", "Open this task's PROMPT.md in the file editor")}
|
||||||
|
>
|
||||||
|
{t("taskDetail.spec.openPromptBtn", "Open PROMPT.md")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button className="btn btn-sm" onClick={enterSpecEditMode}>
|
<button className="btn btn-sm" onClick={enterSpecEditMode}>
|
||||||
{t("taskDetail.spec.editBtn", "Edit")}
|
{t("taskDetail.spec.editBtn", "Edit")}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ import {
|
|||||||
setupTaskDetailModalHooks,
|
setupTaskDetailModalHooks,
|
||||||
} from "./TaskDetailModal.test-helpers";
|
} from "./TaskDetailModal.test-helpers";
|
||||||
import { TaskDetailModal, TaskDetailContent } from "../TaskDetailModal";
|
import { TaskDetailModal, TaskDetailContent } from "../TaskDetailModal";
|
||||||
|
import { FileBrowserProvider } from "../../context/FileBrowserContext";
|
||||||
import { readBoardWorkflowSelection, removeBoardWorkflowSelection, writeBoardWorkflowSelection } from "../../utils/boardWorkflowSelection";
|
import { readBoardWorkflowSelection, removeBoardWorkflowSelection, writeBoardWorkflowSelection } from "../../utils/boardWorkflowSelection";
|
||||||
|
|
||||||
setupTaskDetailModalHooks();
|
setupTaskDetailModalHooks();
|
||||||
|
|
||||||
describe("TaskDetailModal", () => {
|
describe("TaskDetailModal", () => {
|
||||||
describe("Definition tab edit mode", () => {
|
describe("Plan tab edit mode", () => {
|
||||||
it("shows Edit button in Definition tab", () => {
|
it("shows Edit button in Plan tab", () => {
|
||||||
render(
|
render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
task={makeTask({ prompt: "# Test\n\nSpec content." })}
|
task={makeTask({ prompt: "# Test\n\nSpec content." })}
|
||||||
@@ -35,9 +36,39 @@ describe("TaskDetailModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: "Plan" })).toBeTruthy();
|
||||||
|
expect(screen.queryByRole("button", { name: "Definition" })).toBeNull();
|
||||||
expect(screen.getByText("Edit")).toBeTruthy();
|
expect(screen.getByText("Edit")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens the task PROMPT.md file from the near-top Plan action", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const openFile = vi.fn();
|
||||||
|
const { container } = render(
|
||||||
|
<FileBrowserProvider openFile={openFile}>
|
||||||
|
<TaskDetailModal
|
||||||
|
task={makeTask({ id: "FN-099", prompt: "# Test\n\nSpec content." })}
|
||||||
|
initialTab="definition"
|
||||||
|
onClose={noop}
|
||||||
|
onMoveTask={noopMove}
|
||||||
|
onDeleteTask={noopDelete}
|
||||||
|
onMergeTask={noopMerge}
|
||||||
|
onOpenDetail={noopOpenDetail}
|
||||||
|
addToast={noop}
|
||||||
|
/>
|
||||||
|
</FileBrowserProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionRow = container.querySelector(".detail-spec-edit-trigger");
|
||||||
|
expect(actionRow).toBeTruthy();
|
||||||
|
const promptButton = screen.getByRole("button", { name: "Open PROMPT.md" });
|
||||||
|
expect(actionRow?.contains(promptButton)).toBe(true);
|
||||||
|
|
||||||
|
await user.click(promptButton);
|
||||||
|
|
||||||
|
expect(openFile).toHaveBeenCalledWith(".fusion/tasks/FN-099/PROMPT.md", { workspace: "project" });
|
||||||
|
});
|
||||||
|
|
||||||
it("clicking Edit shows textarea with current prompt content", () => {
|
it("clicking Edit shows textarea with current prompt content", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
@@ -191,11 +222,11 @@ describe("TaskDetailModal", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// In-progress tasks show exactly 11 tabs:
|
// In-progress tasks show exactly 11 tabs:
|
||||||
// Chat, Definition, Logs, Changes, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
|
// Chat, Plan, Logs, Changes, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
|
||||||
const tabs = container.querySelectorAll(".detail-tab");
|
const tabs = container.querySelectorAll(".detail-tab");
|
||||||
expect(tabs.length).toBe(11);
|
expect(tabs.length).toBe(11);
|
||||||
expect(tabs[0].textContent).toBe("Chat");
|
expect(tabs[0].textContent).toBe("Chat");
|
||||||
expect(tabs[1].textContent).toBe("Definition");
|
expect(tabs[1].textContent).toBe("Plan");
|
||||||
expect(tabs[2].textContent).toBe("Logs");
|
expect(tabs[2].textContent).toBe("Logs");
|
||||||
expect(tabs[3].textContent).toBe("Changes");
|
expect(tabs[3].textContent).toBe("Changes");
|
||||||
expect(tabs[4].textContent).toBe("Review");
|
expect(tabs[4].textContent).toBe("Review");
|
||||||
@@ -227,7 +258,7 @@ describe("TaskDetailModal", () => {
|
|||||||
const tabs = container.querySelectorAll(".detail-tab");
|
const tabs = container.querySelectorAll(".detail-tab");
|
||||||
expect(tabs.length).toBe(11);
|
expect(tabs.length).toBe(11);
|
||||||
expect(tabs[0].textContent).toBe("Chat");
|
expect(tabs[0].textContent).toBe("Chat");
|
||||||
expect(tabs[1].textContent).toBe("Definition");
|
expect(tabs[1].textContent).toBe("Plan");
|
||||||
expect(tabs[2].textContent).toBe("Logs");
|
expect(tabs[2].textContent).toBe("Logs");
|
||||||
expect(tabs[3].textContent).toBe("Changes");
|
expect(tabs[3].textContent).toBe("Changes");
|
||||||
expect(tabs[4].textContent).toBe("Review");
|
expect(tabs[4].textContent).toBe("Review");
|
||||||
@@ -256,12 +287,12 @@ describe("TaskDetailModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Done task with commit SHA: Summary, Chat, Definition, Logs, Changes, Review, Comments, Artifacts, Model, Workflow, Stats, Routing (12 tabs, no Commits)
|
// Done task with commit SHA: Summary, Chat, Plan, Logs, Changes, Review, Comments, Artifacts, Model, Workflow, Stats, Routing (12 tabs, no Commits)
|
||||||
const tabs = container.querySelectorAll(".detail-tab");
|
const tabs = container.querySelectorAll(".detail-tab");
|
||||||
expect(tabs.length).toBe(12);
|
expect(tabs.length).toBe(12);
|
||||||
expect(tabs[0].textContent).toBe("Summary");
|
expect(tabs[0].textContent).toBe("Summary");
|
||||||
expect(tabs[1].textContent).toBe("Chat");
|
expect(tabs[1].textContent).toBe("Chat");
|
||||||
expect(tabs[2].textContent).toBe("Definition");
|
expect(tabs[2].textContent).toBe("Plan");
|
||||||
expect(tabs[3].textContent).toBe("Logs");
|
expect(tabs[3].textContent).toBe("Logs");
|
||||||
expect(tabs[4].textContent).toBe("Changes");
|
expect(tabs[4].textContent).toBe("Changes");
|
||||||
expect(tabs[5].textContent).toBe("Review");
|
expect(tabs[5].textContent).toBe("Review");
|
||||||
@@ -298,7 +329,7 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(tabs.length).toBe(12);
|
expect(tabs.length).toBe(12);
|
||||||
expect(tabs[0].textContent).toBe("Summary");
|
expect(tabs[0].textContent).toBe("Summary");
|
||||||
expect(tabs[1].textContent).toBe("Chat");
|
expect(tabs[1].textContent).toBe("Chat");
|
||||||
expect(tabs[2].textContent).toBe("Definition");
|
expect(tabs[2].textContent).toBe("Plan");
|
||||||
expect(tabs[3].textContent).toBe("Logs");
|
expect(tabs[3].textContent).toBe("Logs");
|
||||||
expect(tabs[4].textContent).toBe("Changes");
|
expect(tabs[4].textContent).toBe("Changes");
|
||||||
expect(tabs[5].textContent).toBe("Review");
|
expect(tabs[5].textContent).toBe("Review");
|
||||||
@@ -327,9 +358,9 @@ describe("TaskDetailModal", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const triageTabs = triageContainer.querySelectorAll(".detail-tab");
|
const triageTabs = triageContainer.querySelectorAll(".detail-tab");
|
||||||
expect(triageTabs.length).toBe(10); // Chat, Definition, Logs, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
|
expect(triageTabs.length).toBe(10); // Chat, Plan, Logs, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
|
||||||
expect(Array.from(triageTabs).map(t => t.textContent)).toEqual([
|
expect(Array.from(triageTabs).map(t => t.textContent)).toEqual([
|
||||||
"Chat", "Definition", "Logs", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
|
"Chat", "Plan", "Logs", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { container: todoContainer } = render(
|
const { container: todoContainer } = render(
|
||||||
@@ -346,9 +377,9 @@ describe("TaskDetailModal", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const todoTabs = todoContainer.querySelectorAll(".detail-tab");
|
const todoTabs = todoContainer.querySelectorAll(".detail-tab");
|
||||||
expect(todoTabs.length).toBe(10); // Chat, Definition, Logs, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
|
expect(todoTabs.length).toBe(10); // Chat, Plan, Logs, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
|
||||||
expect(Array.from(todoTabs).map(t => t.textContent)).toEqual([
|
expect(Array.from(todoTabs).map(t => t.textContent)).toEqual([
|
||||||
"Chat", "Definition", "Logs", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
|
"Chat", "Plan", "Logs", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user