FN-8197: move merge details to summary tab

Keep completed task merge metadata with its completion summary.

- Relocate the Merge Details card from Definition to the done-only Summary tab.
- Cover merge-detail states and Definition/mobile containment with dashboard tests.
- Document the Summary tab behavior and add a patch changeset.

Files changed:
 .changeset/fn-8197-merge-details-summary-tab.md    |  7 +++
 docs/dashboard-guide.md                            |  2 +-
 packages/dashboard/app/components/TaskDetailModal.tsx   |  4 +-
 packages/dashboard/app/components/TaskSummaryTab.tsx    |  9 +++
 packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx | 49 ++++++++++------
 packages/dashboard/app/components/__tests__/TaskDetailModal.summary-tab.test.tsx | 65 ++++++++++++++++++++++
 6 files changed, 115 insertions(+), 21 deletions(-)

Fusion-Task-Id: FN-8197

Fusion-Task-Lineage: f4aac014-1e8d-4b8f-91f1-ba40ae879508

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-17 01:17:22 -07:00
parent 6183621ca3
commit a51cba0d69
6 changed files with 117 additions and 23 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Move task Merge Details from Plan to the done-only Summary tab.
category: fix
dev: Keeps completion and merge metadata together without adding a new task-detail tab.

View File

@@ -1310,7 +1310,7 @@ Inspect task definition, logs, review feedback, comments, artifacts, workflow ou
- Editable tasks with descriptions show **Summarize as title** beside the read-mode title; it asks AI to generate a concise title from the description and saves it without opening the edit form.
- The top-level **Chat** tab appears first for active task details and is the default landing tab for non-`done` tasks. It uses the task's effective planning model, but opening the tab is lookup-only: Fusion creates the task-scoped planner chat only after you send a composer message, starter prompt, or planner-question answer. Once a user message exists, the resumable planner chat can appear in the global Chat list; interacted chats are kept when the task reaches `done` and removed when the task is archived. Each send includes bounded server-built task context so the planner can answer current status, progress, recent activity, dependency, and task definition questions. It shows starter prompts for common planning questions, can render structured planner questions, and converts only explicit operator steering intent through the scoped steering tool. The composer stays pinned while the transcript, loading, error, starter, history, and streaming states scroll internally; on mobile/narrow task detail, the default focused Chat layout hides nonessential title/metadata/tab/action rows until you collapse it from the in-view expand control.
- The **Activity → Live**, **Feed**, and **Raw Logs** segments remain immediately after **Chat** and share an expand/collapse control that lets the active Activity segment fill the task-detail modal, then restores the normal header, tabs, and action footer when collapsed.
- The **Summary** tab appears for `done` tasks and remains their default landing tab. It shows the recorded completion summary, changed-file/merge stats when available, completed steps, workflow results, retry counts, and a token usage & cost section broken down by model from the already-loaded task detail; unpriced models show cost as unavailable rather than `$0`.
- The **Summary** tab appears for `done` tasks and remains their default landing tab. It shows the recorded completion summary, the **Merge Details** card (merge status, commit, PR, timestamp, and message), changed-file/merge stats when available, completed steps, workflow results, retry counts, and a token usage & cost section broken down by model from the already-loaded task detail; unpriced models show cost as unavailable rather than `$0`.
- The **Cost** tab is available for tasks in every column and sits immediately after **Comments → Terminal** in the tab strip. It shows the read-time derived per-model cost breakdown (input, output, cached, cache-write, total tokens, derived USD) and a task total; no token usage shows an explicit empty state, while unpriced or zero-usage rows use `—` instead of a guessed `$0`.
<!-- FNXC:Settings-ThinkingLevel 2026-07-13-00:27: The task-detail Models tab now persists validatorThinkingLevel and planningThinkingLevel separately so Reviewer and Planning lanes can choose reasoning effort without changing the Executor lane's task.thinkingLevel. -->
- The **Models** tab exposes inline **Thinking Level** selectors for **Executor Model**, **Reviewer Model**, and **Planning Model**. Executor saves the shared task thinking level, while Reviewer and Planning save independent per-lane overrides; leaving either lane on **Default** inherits the shared task thinking level and then the configured workflow/project defaults.

View File

@@ -41,7 +41,6 @@ import { TaskComments } from "./TaskComments";
import { TaskChatTab } from "./TaskChatTab";
import { TaskPlannerChatTab } from "./TaskPlannerChatTab";
import { TaskReviewTab } from "./TaskReviewTab";
import { MergeDetails } from "./MergeDetails";
import { TaskChangesTab } from "./TaskChangesTab";
import { TaskSummaryTab } from "./TaskSummaryTab";
import { TaskCostTab } from "./TaskCostTab";
@@ -5246,8 +5245,7 @@ export function TaskDetailContent({
</div>
) : (
<>
{/* FNXC:TaskDetailSummaryTab 2026-06-27-00:00: The former inline Definition-tab completion summary is intentionally removed to avoid duplicating the new done-only Summary tab; Definition keeps merge/retry/source metadata below. */}
<MergeDetails task={task} />
{/* FNXC:TaskDetailSummaryTab 2026-07-29-00:00: FN-8197 keeps Definition focused on plan, retry, and source metadata; completed merge metadata renders exclusively in the done-only Summary tab. */}
{(retrySummary?.total ?? 0) > 0 && (
<div className="detail-section detail-retries-section">
<div className="detail-source-header">

View File

@@ -7,6 +7,7 @@ import type { TaskDetail, TaskStep, WorkflowStepResult } from "@fusion/core";
import type { ModelPricingOverrides } from "../../../core/src/model-pricing";
import { createMermaidCodeComponent, sharedRehypePlugins } from "./markdownPipeline";
import { ProviderIcon } from "./ProviderIcon";
import { MergeDetails } from "./MergeDetails";
import { linkifyFilePaths, linkifyReactChildren } from "../utils/filePathLinkify";
import { inferProviderIconKey } from "../utils/providerIconKey";
import { buildTokenCostRows, formatCost, formatCount, totalCostForRows } from "../utils/taskTokenCost";
@@ -170,6 +171,14 @@ export function TaskSummaryTab({ task, pricingOverrides }: TaskSummaryTabProps)
</section>
) : null}
{/*
FNXC:TaskDetailSummaryTab 2026-07-29-00:00:
FN-8197 / issue #2248 moves Merge Details from Definition into the done-only Summary tab. Both
surfaces apply only to completed tasks, and merge status, PR, and commit-message metadata belong
with completion data rather than the task plan.
*/}
<MergeDetails task={task} />
{hasAgentWork ? (
<section className="task-summary-section task-summary-section--agent-work">
<h4>{t("taskDetail.summaryTab.agentWorkHeading", "Work done by agents")}</h4>

View File

@@ -1557,23 +1557,24 @@ describe("TaskDetailModal", () => {
expect(screen.getByRole("link", { name: "#42" })).toHaveAttribute("href", "https://github.com/owner/repo/pull/42");
});
it("shows linked PR number in merge details for done tasks", () => {
render(
it("shows linked PR number in Summary merge details, not Definition, for done tasks", () => {
const task = makeTask({
column: "done" as Column,
prInfo: {
url: "https://github.com/owner/repo/pull/42",
number: 42,
status: "merged",
title: "Task",
headBranch: "fusion/fn-099",
baseBranch: "main",
commentCount: 0,
},
mergeDetails: { prNumber: 42 },
});
const summary = render(
<TaskDetailModal
initialTab="definition"
task={makeTask({
column: "done" as Column,
prInfo: {
url: "https://github.com/owner/repo/pull/42",
number: 42,
status: "merged",
title: "Task",
headBranch: "fusion/fn-099",
baseBranch: "main",
commentCount: 0,
},
mergeDetails: { prNumber: 42 },
})}
initialTab="summary"
task={task}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
@@ -1583,9 +1584,23 @@ describe("TaskDetailModal", () => {
/>,
);
const links = screen.getAllByRole("link", { name: "#42" });
expect(links.length).toBeGreaterThan(0);
expect(links[0]).toHaveAttribute("href", "https://github.com/owner/repo/pull/42");
expect(summary.container.querySelector(".merge-details-card a")).toHaveAttribute("href", "https://github.com/owner/repo/pull/42");
summary.unmount();
render(
<TaskDetailModal
initialTab="definition"
task={task}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
expect(screen.queryByText("Merge Details")).toBeNull();
});
it("shows PR automation waiting label instead of Merge & Close when awaiting PR checks", () => {

View File

@@ -455,6 +455,71 @@ describe("TaskDetailModal Summary tab", () => {
expect(css).not.toMatch(/task-summary-token[^{}]*#[0-9a-fA-F]{3,8}/);
});
it("renders every Merge Details data state inside the done-only Summary tab", () => {
const linkedTask = doneTask({
mergeDetails: {
commitSha: "abcdef1234567890",
mergeConfirmed: true,
prNumber: 42,
mergedAt: "2026-07-29T12:00:00Z",
mergeCommitMessage: "feat: land summary merge details",
},
prInfo: {
url: "https://github.com/owner/repo/pull/42",
number: 42,
status: "merged",
title: "Task",
headBranch: "fusion/fn-8197",
baseBranch: "main",
commentCount: 0,
},
});
const view = render(<TaskSummaryTab task={linkedTask} />);
const summary = screen.getByTestId("task-summary-tab");
const mergeCard = summary.querySelector(".merge-details-card");
expect(screen.getByText("Merge Details")).toBeTruthy();
expect(screen.getByText("Merged successfully")).toBeTruthy();
expect(screen.getByText("Merged at")).toBeTruthy();
expect(screen.getByText("feat: land summary merge details")).toBeTruthy();
expect(screen.getByRole("link", { name: "#42" })).toHaveAttribute("href", "https://github.com/owner/repo/pull/42");
expect(mergeCard?.classList.contains("pr-card")).toBe(true);
expect(summary.contains(mergeCard)).toBe(true);
view.rerender(
<TaskSummaryTab
task={doneTask({
mergeDetails: { commitSha: "abcdef1234567890", mergeConfirmed: false, prNumber: 7 },
prInfo: undefined,
})}
/>,
);
expect(screen.getByText("Recorded without local merge confirmation")).toBeTruthy();
expect(screen.getByText("#7")).toBeTruthy();
expect(screen.queryByRole("link", { name: "#7" })).toBeNull();
expect(screen.queryByText("Merged at")).toBeNull();
expect(screen.queryByText("Message")).toBeNull();
view.rerender(<TaskSummaryTab task={doneTask({ mergeDetails: { commitSha: "abcdef1234567890" } })} />);
expect(screen.queryByText("PR")).toBeNull();
expect(screen.queryByText("Merged at")).toBeNull();
expect(screen.queryByText("Message")).toBeNull();
view.rerender(<TaskSummaryTab task={doneTask({ mergeDetails: undefined })} />);
expect(screen.getByTestId("task-summary-tab")).toBeTruthy();
expect(screen.queryByText("Merge Details")).toBeNull();
});
it("keeps relocated Merge Details inside the existing mobile pr-card containment", () => {
const { container } = render(<TaskSummaryTab task={doneTask({ mergeDetails: { commitSha: "abcdef1234567890", prNumber: 42 } })} />);
const summary = screen.getByTestId("task-summary-tab");
const mergeCard = container.querySelector(".task-summary-tab .merge-details-card.pr-card");
expect(mergeCard).toBeTruthy();
expect(summary.contains(mergeCard)).toBe(true);
expect(readDashboardStylesSource()).toMatch(/@media \(max-width: 768px\)[\s\S]*\.pr-card/);
});
it("renders graceful empty states without orphaned changed-file headings", () => {
render(
<TaskDetailModal