FN-7306: rename task-detail Chat tab to Activity

Renames the existing task-detail activity surface while preserving the stable chat tab id.

- Move the Activity tab to the first task-detail position and make it the default for every task state.
- Keep `chat` as the compatibility tab id for modal callers, plugin APIs, and deep links.
- Update task-detail tests and add a patch changeset for the published CLI package.

Files changed:
 .changeset/fn-7306-task-detail-activity-tab.md     |  7 +++
 .../dashboard/app/components/TaskDetailModal.tsx   | 35 ++++++-------
 .../TaskDetailModal.attachments-and-tabs.test.tsx  | 57 +++++++++++-----------
 .../TaskDetailModal.definition-actions.test.tsx    | 24 ++++-----
 ...TaskDetailModal.github-tracking-header.test.tsx |  2 +-
 .../TaskDetailModal.github-tracking-stale.test.tsx |  2 +-
 ...lModal.inline-editing-and-integrations.test.tsx |  4 +-
 ...skDetailModal.models-progress-workflow.test.tsx |  2 +-
 .../__tests__/TaskDetailModal.rendering.test.tsx   |  4 +-
 ...etailModal.responsive-and-dependencies.test.tsx |  6 +--
 .../__tests__/TaskDetailModal.summary-tab.test.tsx | 33 ++++++++-----
 .../components/__tests__/TaskDetailModal.test.tsx  |  4 +-
 packages/dashboard/app/hooks/useModalManager.ts    |  4 ++
 packages/dashboard/app/plugins/types.ts            |  7 ++-
 14 files changed, 107 insertions(+), 84 deletions(-)

Fusion-Task-Id: FN-7306
Fusion-Task-Lineage: c905134c-de74-4c46-9c5a-d3fbc8200093
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-30 14:58:07 -07:00
parent 9f7215851b
commit 254e97da74
14 changed files with 107 additions and 84 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Rename the task-detail Chat tab to Activity and make it first.
category: feature
dev: Keeps the internal `chat` tab id compatible for existing deep links and plugin callers.

View File

@@ -194,20 +194,20 @@ function formatDurationCompact(ageMs: number): string {
type TabId = "summary" | "definition" | "chat" | "logs" | "changes" | "review" | "pr" | "comments" | "model" | "workflow" | "documents" | "stats" | "routing" | "retries" | "terminal" | `plugin-${string}`;
/*
FNXC:TaskDetailSummaryTab 2026-06-27-00:00:
Done tasks land on Summary instead of Chat so completed work opens on the completion overview. Chat stays the implicit default for every other column, and explicit tab requests continue to win for done tasks.
FNXC:TaskDetailActivityTab 2026-06-30-00:00:
The existing task activity/steering surface keeps the stable internal `chat` tab id for deep-link/plugin compatibility, but its top-level user-facing label is Activity. Activity is the implicit default for every task column, including done tasks; Summary remains explicitly available for completed work until a later subtask adds the separate planner-model Chat surface.
FNXC:TaskDetailSummaryTab 2026-06-27-00:00:
Only an omitted initial tab is the implicit default. Preserve explicit `initialTab="chat"` requests from plugins and task-detail entrypoints so done tasks can still deep-link directly to Chat.
FNXC:TaskDetailActivityTab 2026-06-30-00:00:
Only an omitted initial tab is the implicit default. Preserve explicit `initialTab="chat"` requests from plugins and task-detail entrypoints so existing links continue to open the renamed Activity surface.
*/
function resolveDefaultTab(initialTab: TabId | undefined, column: ColumnId): TabId {
function resolveDefaultTab(initialTab: TabId | undefined, _column: ColumnId): TabId {
if (initialTab === "retries") {
return "definition";
}
if (initialTab) {
return initialTab;
}
return column === "done" ? "summary" : "chat";
return "chat";
}
// Lazy-load the terminal so xterm + addons stay out of the main bundle (U11).
@@ -501,8 +501,8 @@ export function TaskDetailContent({
autoMergeEnabled: autoMergeEnabledProp,
onOpenWorkflowEditor,
/**
* FNXC:TaskDetailTabs 2026-06-17-00:00:
* FN-6532 makes Chat the default task-detail view when no caller supplies an explicit initial tab.
* FNXC:TaskDetailActivityTab 2026-06-30-00:00:
* The Activity tab is still addressed as `chat` internally so existing callers and deep links do not break while the label/order changes ahead of the future planner Chat tab.
*/
initialTab,
mobileHeaderMode = "close",
@@ -3136,12 +3136,15 @@ export function TaskDetailContent({
<>
<div className="detail-tabs">
{/*
FNXC:TaskDetailTabs 2026-06-17-00:00:
FN-6532 requires Chat to be the first task-detail tab while preserving every explicit tab entrypoint.
FNXC:TaskDetailSummaryTab 2026-06-27-00:00:
Done tasks expose Summary as the first tab because the implicit Chat default resolves there for completed work; non-done tasks keep Chat first and never render an empty Summary shell.
FNXC:TaskDetailActivityTab 2026-06-30-00:00:
The existing task activity/steering surface is now labelled Activity and always renders first. Keep the `chat` tab id because a later subtask will add the separate planner-model Chat surface; this rename must not break existing `initialTab="chat"` callers.
*/}
<button
className={`detail-tab${activeTab === "chat" ? " detail-tab-active" : ""}`}
onClick={() => setActiveTab("chat")}
>
{t("taskDetail.tabs.activity", "Activity")}
</button>
{task.column === "done" && (
<button
className={`detail-tab${activeTab === "summary" ? " detail-tab-active" : ""}`}
@@ -3150,12 +3153,6 @@ export function TaskDetailContent({
{t("taskDetail.tabs.summary", "Summary")}
</button>
)}
<button
className={`detail-tab${activeTab === "chat" ? " detail-tab-active" : ""}`}
onClick={() => setActiveTab("chat")}
>
{t("taskDetail.tabs.chat", "Chat")}
</button>
<button
className={`detail-tab${activeTab === "definition" ? " detail-tab-active" : ""}`}
onClick={() => setActiveTab("definition")}

View File

@@ -19,7 +19,7 @@ import { TaskDetailModal, TaskDetailContent } from "../TaskDetailModal";
/*
FNXC:TaskDetailTabs 2026-06-17-08:20:
FN-6532 made Chat the default TaskDetailModal tab. Definition-tab regression coverage must prove both the no-`initialTab` Chat landing state and the explicit `initialTab="definition"` Definition surface for prompt, GitHub tracking, and dependency sections.
FN-7306 labels the stable internal `chat` tab as Activity and keeps it as the default TaskDetailModal tab. Definition-tab regression coverage must prove both the no-`initialTab` Activity landing state and the explicit `initialTab="definition"` Definition surface for prompt, GitHub tracking, and dependency sections.
*/
setupTaskDetailModalHooks();
@@ -439,7 +439,7 @@ describe("TaskDetailModal", () => {
});
describe("tab toggle", () => {
it("defaults to the Chat tab", () => {
it("defaults to the Activity tab", () => {
const { container } = render(
<TaskDetailModal
task={makeTask({ prompt: "# Hello\n\nContent" })}
@@ -452,11 +452,10 @@ describe("TaskDetailModal", () => {
/>,
);
expect(screen.getByText("Definition")).toBeTruthy();
expect(screen.getByText("Plan")).toBeTruthy();
expect(screen.getByText("Logs")).toBeTruthy();
// Activity and Agent Log are subviews inside the Logs tab, not top-level tabs.
// They should NOT be visible on the default Chat tab.
expect(screen.queryByText("Activity")).toBeNull();
// Logs subview controls should NOT be visible on the default Activity tab.
expect(container.querySelector(".log-subview-toggle")).toBeNull();
expect(screen.queryByText("Agent Log")).toBeNull();
// Chat content should be visible by default.
expect(container.querySelector(".detail-section--chat")).toBeTruthy();
@@ -638,8 +637,8 @@ describe("TaskDetailModal", () => {
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeTruthy();
expect(container.querySelector(".detail-activity")).toBeNull();
// Switch back to Activity subview within Logs tab
fireEvent.click(screen.getByText("Activity"));
// Switch back to Activity subview within Logs tab.
fireEvent.click(container.querySelector<HTMLButtonElement>(".log-subview-toggle .log-subview-btn")!);
expect(container.querySelector(".detail-activity")).toBeTruthy();
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull();
@@ -649,7 +648,7 @@ describe("TaskDetailModal", () => {
expect(container.querySelector(".detail-activity")).toBeNull();
// Switch back to Definition tab
fireEvent.click(screen.getByText("Definition"));
fireEvent.click(screen.getByText("Plan"));
expect(container.querySelector(".markdown-body")).toBeTruthy();
expect(container.querySelector(".detail-activity")).toBeNull();
@@ -681,7 +680,7 @@ describe("TaskDetailModal", () => {
expect(container.querySelector(".markdown-body")).toBeNull();
// Click Definition tab to go back
fireEvent.click(screen.getByText("Definition"));
fireEvent.click(screen.getByText("Plan"));
// Definition content should reappear
expect(container.querySelector(".markdown-body")).toBeTruthy();
@@ -705,7 +704,7 @@ describe("TaskDetailModal", () => {
/>,
);
// Default: Chat tab active → enabled should be false
// Default: Activity tab active → enabled should be false
const initialCall = mockUseAgentLogs.mock.calls[mockUseAgentLogs.mock.calls.length - 1];
expect(initialCall[1]).toBe(false);
@@ -758,18 +757,17 @@ describe("TaskDetailModal", () => {
);
// For an in-progress task (no workflow steps, no merge commit), the
// top-level tabs are: Chat, Definition, Logs, Changes, Review, Comments,
// top-level tabs are: Activity, Plan, Logs, Changes, Review, Comments,
// Artifacts, Model, Workflow, Stats, Routing.
const tabTexts = ["Chat", "Definition", "Logs", "Changes", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing"];
const tabTexts = ["Activity", "Plan", "Logs", "Changes", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing"];
const tabs = screen.getAllByRole("button").filter((b) =>
tabTexts.includes(b.textContent || "")
);
expect(tabs.map((tab) => tab.textContent)).toEqual(tabTexts);
expect(tabs[0].textContent).toBe("Chat");
expect(tabs[1].textContent).toBe("Definition");
expect(tabs[0].textContent).toBe("Activity");
expect(tabs[1].textContent).toBe("Plan");
expect(tabs[2].textContent).toBe("Logs");
// Activity and Agent Log are NOT top-level tabs (they are subviews inside Logs)
expect(container.querySelectorAll(".detail-tab").length).toBe(11);
// Workflow tab should always appear even when no workflow steps are configured
expect(screen.getByText("Workflow")).toBeInTheDocument();
@@ -841,7 +839,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Chat" }));
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
const content = container.querySelector(".task-detail-content");
const titleRow = container.querySelector(".detail-title-row");
expect(content).not.toHaveClass("task-detail-content--chat-expanded");
@@ -942,7 +940,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Chat" }));
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByTestId("task-chat-expand-toggle"));
expect(container.querySelector(".task-detail-content")).toHaveClass("task-detail-content--chat-expanded");
@@ -951,7 +949,7 @@ describe("TaskDetailModal", () => {
expect(screen.queryByTestId("task-chat-expand-toggle")).toBeNull();
});
it("FN-6532 defaults to Chat first while preserving explicit tab requests", () => {
it("FN-6532 defaults to Activity first while preserving explicit tab requests", () => {
const { container, rerender } = render(
<TaskDetailModal
task={makeTask({ prompt: "# Hello\n\nContent" })}
@@ -965,10 +963,10 @@ describe("TaskDetailModal", () => {
);
const tabs = Array.from(container.querySelectorAll<HTMLButtonElement>(".detail-tab"));
expect(tabs.map((tab) => tab.textContent)).toEqual(expect.arrayContaining(["Chat", "Definition"]));
expect(tabs[0]).toHaveTextContent("Chat");
const chatTab = screen.getByRole("button", { name: "Chat" });
const definitionTab = screen.getByRole("button", { name: "Definition" });
expect(tabs.map((tab) => tab.textContent)).toEqual(expect.arrayContaining(["Activity", "Plan"]));
expect(tabs[0]).toHaveTextContent("Activity");
const chatTab = screen.getByRole("button", { name: "Activity" });
const definitionTab = screen.getByRole("button", { name: "Plan" });
expect(chatTab.compareDocumentPosition(definitionTab) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(chatTab).toHaveClass("detail-tab-active");
expect(definitionTab).not.toHaveClass("detail-tab-active");
@@ -988,7 +986,8 @@ describe("TaskDetailModal", () => {
);
expect(screen.getByRole("button", { name: "Logs" })).toHaveClass("detail-tab-active");
expect(screen.getByRole("button", { name: "Chat" })).not.toHaveClass("detail-tab-active");
expect(container.querySelector(".detail-tabs .detail-tab:first-child")).toHaveTextContent("Activity");
expect(container.querySelector(".detail-tabs .detail-tab:first-child")).not.toHaveClass("detail-tab-active");
expect(container.querySelector(".detail-section--chat")).toBeNull();
});
@@ -1010,8 +1009,8 @@ describe("TaskDetailModal", () => {
/>,
);
expect(screen.getByRole("button", { name: "Definition" })).toHaveClass("detail-tab-active");
expect(screen.getByRole("button", { name: "Chat" })).not.toHaveClass("detail-tab-active");
expect(screen.getByRole("button", { name: "Plan" })).toHaveClass("detail-tab-active");
expect(screen.getByRole("button", { name: "Activity" })).not.toHaveClass("detail-tab-active");
expect(container.querySelector(".detail-section--chat")).toBeNull();
expect(screen.getByText("Definition body unique text.")).toBeInTheDocument();
expect(screen.getByText("GitHub tracking")).toBeInTheDocument();
@@ -1021,7 +1020,7 @@ describe("TaskDetailModal", () => {
expect(container).toHaveTextContent("FN-200");
});
it("FN-6347 applies chat modifiers only while the Chat tab is active", () => {
it("FN-6347 applies chat modifiers only while the Activity tab is active", () => {
const { container } = render(
<TaskDetailModal
task={makeTask({ prompt: "# Hello\n\nContent" })}
@@ -1061,7 +1060,7 @@ describe("TaskDetailModal", () => {
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Chat" }));
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
expect(container.querySelector(".detail-body--chat")).toBeTruthy();
fireEvent.click(screen.getByLabelText("Edit task"));
@@ -1097,7 +1096,7 @@ describe("TaskDetailModal", () => {
expect(container.querySelector(".detail-body--agent-log")).toBeTruthy();
// Switch back to Definition tab
fireEvent.click(screen.getByText("Definition"));
fireEvent.click(screen.getByText("Plan"));
// modifier class should be removed
expect(container.querySelector(".detail-body--agent-log")).toBeNull();

View File

@@ -222,10 +222,10 @@ describe("TaskDetailModal", () => {
);
// In-progress tasks show exactly 11 tabs:
// Chat, Plan, Logs, Changes, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
// Activity, Plan, Logs, Changes, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
const tabs = container.querySelectorAll(".detail-tab");
expect(tabs.length).toBe(11);
expect(tabs[0].textContent).toBe("Chat");
expect(tabs[0].textContent).toBe("Activity");
expect(tabs[1].textContent).toBe("Plan");
expect(tabs[2].textContent).toBe("Logs");
expect(tabs[3].textContent).toBe("Changes");
@@ -257,7 +257,7 @@ 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("Chat");
expect(tabs[0].textContent).toBe("Activity");
expect(tabs[1].textContent).toBe("Plan");
expect(tabs[2].textContent).toBe("Logs");
expect(tabs[3].textContent).toBe("Changes");
@@ -287,11 +287,11 @@ describe("TaskDetailModal", () => {
/>,
);
// Done task with commit SHA: Summary, Chat, Plan, Logs, Changes, Review, Comments, Artifacts, Model, Workflow, Stats, Routing (12 tabs, no Commits)
// Done task with commit SHA: Activity, Summary, Plan, Logs, 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("Summary");
expect(tabs[1].textContent).toBe("Chat");
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");
@@ -327,8 +327,8 @@ 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("Summary");
expect(tabs[1].textContent).toBe("Chat");
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");
@@ -358,9 +358,9 @@ describe("TaskDetailModal", () => {
);
const triageTabs = triageContainer.querySelectorAll(".detail-tab");
expect(triageTabs.length).toBe(10); // Chat, Plan, Logs, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
expect(triageTabs.length).toBe(10); // Activity, Plan, Logs, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
expect(Array.from(triageTabs).map(t => t.textContent)).toEqual([
"Chat", "Plan", "Logs", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
"Activity", "Plan", "Logs", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
]);
const { container: todoContainer } = render(
@@ -377,9 +377,9 @@ describe("TaskDetailModal", () => {
);
const todoTabs = todoContainer.querySelectorAll(".detail-tab");
expect(todoTabs.length).toBe(10); // Chat, Plan, Logs, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
expect(todoTabs.length).toBe(10); // Activity, Plan, Logs, Review, Comments, Artifacts, Model, Workflow, Stats, Routing
expect(Array.from(todoTabs).map(t => t.textContent)).toEqual([
"Chat", "Plan", "Logs", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
"Activity", "Plan", "Logs", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing",
]);
});

View File

@@ -1,6 +1,6 @@
/*
FNXC:TaskDetailTabs 2026-06-17-08:20:
FN-6532 made Chat the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Chat landing state.
FN-7306 labels the stable internal `chat` tab as Activity and keeps it as the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Activity landing state.
*/
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";

View File

@@ -1,6 +1,6 @@
/*
FNXC:TaskDetailTabs 2026-06-17-08:20:
FN-6532 made Chat the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Chat landing state.
FN-7306 labels the stable internal `chat` tab as Activity and keeps it as the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Activity landing state.
*/
import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";

View File

@@ -1,6 +1,6 @@
/*
FNXC:TaskDetailTabs 2026-06-17-08:20:
FN-6532 made Chat the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Chat landing state.
FN-7306 labels the stable internal `chat` tab as Activity and keeps it as the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Activity landing state.
*/
import { describe, it, expect, vi } from "vitest";
import { useState } from "react";
@@ -2259,7 +2259,7 @@ describe("TaskDetailModal", () => {
);
// Only standard tabs should be visible (Definition, Logs, etc.)
expect(screen.getByText("Definition")).toBeDefined();
expect(screen.getByText("Plan")).toBeDefined();
expect(screen.getByText("Logs")).toBeDefined();
// Plugin tabs should not exist
expect(screen.queryByText("Plugin A Tab")).toBeNull();

View File

@@ -1,6 +1,6 @@
/*
FNXC:TaskDetailTabs 2026-06-17-08:20:
FN-6532 made Chat the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Chat landing state.
FN-7306 labels the stable internal `chat` tab as Activity and keeps it as the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Activity landing state.
*/
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";

View File

@@ -1,6 +1,6 @@
/*
FNXC:TaskDetailTabs 2026-06-17-08:20:
FN-6532 made Chat the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Chat landing state.
FN-7306 labels the stable internal `chat` tab as Activity and keeps it as the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Activity landing state.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
@@ -886,7 +886,7 @@ describe("TaskDetailModal", () => {
expect(container.querySelector(".task-detail-content--embedded")).toBeTruthy();
expect(screen.queryByRole("button", { name: "Close" })).toBeNull();
expect(screen.getByRole("button", { name: "Definition" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Plan" })).toBeInTheDocument();
});
it("renders header close control for embedded floating task details", () => {

View File

@@ -1,6 +1,6 @@
/*
FNXC:TaskDetailTabs 2026-06-17-08:20:
FN-6532 made Chat the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Chat landing state.
FN-7306 labels the stable internal `chat` tab as Activity and keeps it as the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Activity landing state.
*/
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
@@ -225,8 +225,8 @@ describe("TaskDetailModal", () => {
expect(container.querySelectorAll(".detail-timestamp-item").length).toBe(2);
const tabs = container.querySelectorAll(".detail-tab");
expect(Array.from(tabs).map((tab) => tab.textContent?.trim())).toEqual([
"Chat",
"Definition",
"Activity",
"Plan",
"Logs",
"Changes",
"Review",

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { render, screen, within } from "@testing-library/react";
import { fireEvent, render, screen, within } from "@testing-library/react";
import type { Column, ModelPricingOverrides, TaskTokenUsage } from "@fusion/core";
import {
makeTask,
@@ -76,7 +76,7 @@ function doneTask(overrides = {}) {
}
describe("TaskDetailModal Summary tab", () => {
it("lands done tasks on Summary and renders completion, changed-files, and agent-work sections", () => {
it("lands done tasks on Activity first and keeps Summary accessible", () => {
const { container } = render(
<TaskDetailModal
task={doneTask()}
@@ -89,7 +89,15 @@ describe("TaskDetailModal Summary tab", () => {
/>,
);
expect(container.querySelector(".detail-tabs")?.firstElementChild?.textContent).toBe("Activity");
const activityButton = screen.getByRole("button", { name: "Activity" });
expectButtonActive(activityButton);
expect(screen.queryByRole("button", { name: "Chat" })).toBeNull();
expect(screen.queryByText("Completion summary")).toBeNull();
expect(container.querySelector(".detail-section--chat [data-testid='task-chat-tab']")).toBeTruthy();
const summaryButton = screen.getByRole("button", { name: "Summary" });
fireEvent.click(summaryButton);
expectButtonActive(summaryButton);
expect(screen.getByText("Completion summary")).toBeTruthy();
expect(screen.getByText("summary")).toBeTruthy();
@@ -99,10 +107,9 @@ describe("TaskDetailModal Summary tab", () => {
expect(screen.getByText("Preflight")).toBeTruthy();
expect(screen.getByText("Code Review")).toBeTruthy();
expect(screen.getByText("Agents retried this task 1 time.")).toBeTruthy();
expect(container.querySelector(".detail-tabs")?.firstElementChild?.textContent).toBe("Summary");
});
it("honors explicit Chat for done tasks", () => {
it("honors explicit initialTab=\"chat\" for done tasks", () => {
render(
<TaskDetailModal
task={doneTask()}
@@ -116,7 +123,7 @@ describe("TaskDetailModal Summary tab", () => {
/>,
);
expectButtonActive(screen.getByRole("button", { name: "Chat" }));
expectButtonActive(screen.getByRole("button", { name: "Activity" }));
expect(screen.queryByText("Completion summary")).toBeNull();
});
@@ -153,7 +160,7 @@ describe("TaskDetailModal Summary tab", () => {
expect(screen.queryByText("Completion summary")).toBeNull();
});
it("does not render Summary for non-done columns and still defaults to Chat", () => {
it("does not render Summary for non-done columns and still defaults to Activity", () => {
for (const column of ["in-progress", "in-review", "todo"] as Column[]) {
const rendered = render(
<TaskDetailModal
@@ -167,7 +174,7 @@ describe("TaskDetailModal Summary tab", () => {
/>,
);
expect(screen.queryByRole("button", { name: "Summary" })).toBeNull();
expectButtonActive(screen.getByRole("button", { name: "Chat" }));
expectButtonActive(screen.getByRole("button", { name: "Activity" }));
rendered.unmount();
}
});
@@ -359,6 +366,7 @@ describe("TaskDetailModal Summary tab", () => {
const full = render(
<TaskDetailContent
task={task}
initialTab="summary"
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
@@ -374,6 +382,7 @@ describe("TaskDetailModal Summary tab", () => {
<TaskDetailContent
task={task}
embedded
initialTab="summary"
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
@@ -417,6 +426,7 @@ describe("TaskDetailModal Summary tab", () => {
retrySummary: { total: 0 },
})}
onClose={noop}
initialTab="summary"
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
@@ -451,8 +461,8 @@ describe("TaskDetailModal Summary tab", () => {
expect(summaryButton.classList.contains("detail-tab")).toBe(true);
});
it("resolves the done-task Summary default in embedded TaskDetailContent", () => {
render(
it("resolves the done-task Activity default in embedded TaskDetailContent", () => {
const { container } = render(
<TaskDetailContent
task={doneTask()}
embedded
@@ -464,7 +474,8 @@ describe("TaskDetailModal Summary tab", () => {
/>,
);
expectButtonActive(screen.getByRole("button", { name: "Summary" }));
expect(screen.getByText("Completion summary")).toBeTruthy();
expectButtonActive(screen.getByRole("button", { name: "Activity" }));
expect(container.querySelector(".detail-tabs")?.firstElementChild?.textContent).toBe("Activity");
expect(screen.queryByText("Completion summary")).toBeNull();
});
});

View File

@@ -1,6 +1,6 @@
/*
FNXC:TaskDetailTabs 2026-06-17-08:20:
FN-6532 made Chat the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Chat landing state.
FN-7306 labels the stable internal `chat` tab as Activity and keeps it as the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Activity landing state.
*/
import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
@@ -689,7 +689,7 @@ describe("TaskDetailModal in-review stall diagnostics", () => {
await user.click(screen.getByRole("button", { name: "View activity log" }));
expect(screen.getByRole("button", { name: "Logs" })).toHaveClass("detail-tab-active");
expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("log-subview-btn-active");
expect(document.querySelector(".log-subview-toggle .log-subview-btn-active")).toHaveTextContent("Activity");
const highlighted = document.querySelector(".detail-log-entry--stall-highlight .detail-log-action");
expect(highlighted?.textContent).toContain("In-review stall surfaced [merge-blocker]");
});

View File

@@ -5,6 +5,10 @@ import type { SectionId } from "../components/SettingsModal";
import type { ToastType } from "./useToast";
import { removeScopedItem } from "../utils/projectStorage";
/*
FNXC:TaskDetailActivityTab 2026-06-30-00:00:
Keep `chat` as the public initial-tab id for the renamed Activity task-detail tab so existing dashboard callers and deep links remain compatible until the future planner Chat tab ships under its own contract.
*/
export type DetailTaskTab =
| "summary"
| "chat"

View File

@@ -11,7 +11,12 @@
import type { ReactNode } from "react";
import type { Task, TaskDetail, WorkflowStep } from "@fusion/core";
/** Tab identifiers for the task detail modal. Mirrors the dashboard's local enum. */
/**
* Tab identifiers for the task detail modal. Mirrors the dashboard's local enum.
*
* FNXC:TaskDetailActivityTab 2026-06-30-00:00:
* Plugins should continue passing `chat` to open the renamed Activity tab; the id is stable compatibility surface, while the top-level label is no longer Chat.
*/
export type DetailTaskTab = "summary" | "chat" | "definition" | "logs" | "changes" | "comments" | "model" | "workflow" | "pr" | "retries";
export type PluginToastType = "success" | "error" | "warning" | "info";