FN-7344: replace Activity subtabs with a dropdown

Replace the task detail Activity subtab strip with a compact selector while keeping legacy routing intact.

- Swap the Live, Feed, and Raw Logs segmented control for an Activity view dropdown.
- Preserve legacy Activity segment ids and initial-tab routing for Live, Feed, and Raw views.
- Update Activity styling, coverage, and the published CLI changeset for the dropdown behavior.

Files changed:
 .changeset/fn-7344-activity-dropdown.md            |  7 ++
 .../dashboard/app/components/TaskDetailModal.css   | 63 +++---------------
 .../dashboard/app/components/TaskDetailModal.tsx   | 56 +++++++---------
 .../TaskDetailModal.attachments-and-tabs.test.tsx  | 76 ++++++++++++----------
 .../__tests__/TaskDetailModal.css.test.ts          | 10 +--
 ...lModal.inline-editing-and-integrations.test.tsx |  2 +-
 ...skDetailModal.models-progress-workflow.test.tsx | 12 ++--
 .../__tests__/TaskDetailModal.rendering.test.tsx   |  2 +-
 ...etailModal.responsive-and-dependencies.test.tsx | 43 +++++-------
 .../TaskDetailModal.task-activity-chat.test.tsx    | 39 ++++++-----
 .../components/__tests__/TaskDetailModal.test.tsx  | 28 ++++----
 11 files changed, 153 insertions(+), 185 deletions(-)

Fusion-Task-Id: FN-7344

Fusion-Task-Lineage: cb609122-d9b6-4ef2-9cf0-35130c3dcffc

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-30 23:24:35 -07:00
parent 519f158400
commit 4ab4aae02e
11 changed files with 152 additions and 184 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Replace task Activity subtabs with a compact Live, Feed, Raw dropdown.
category: fix
dev: Keeps legacy Activity segment ids and initial-tab routing while removing the old segmented-control shell.

View File

@@ -2434,13 +2434,10 @@ FNXC:TaskDetailTabs 2026-06-26-00:35:
font-weight: 600;
}
/* === Activity Segmented Control === */
/* === Activity View Selector === */
/*
FNXC:TaskDetailActivity 2026-06-30-23:55:
The Activity top-level tab owns Live (internal `current`), Feed, and Raw Logs as an in-content segmented control. Keep this selector visually quieter than the top-level tab strip and horizontally reachable on narrow modal and embedded task-detail surfaces; the Activity-wide expand control must sit beside it on every segment.
FNXC:TaskDetailActivity 2026-06-30-12:00:
Selected Activity segments must stay the same height as inactive siblings while using a slightly smaller label. Keep sizing on the base segment contract so visual active-state styling cannot alter the Live/Feed/Raw Logs row height.
FNXC:TaskDetailActivity 2026-06-30-15:50:
The Activity top-level tab owns Live (internal `current`), Feed, and Raw as one dropdown rather than in-content subtabs. Keep the selector compact and token-styled on desktop/mobile, with the Activity-wide expand control beside it on every view.
*/
.activity-toolbar {
display: flex;
@@ -2450,54 +2447,13 @@ Selected Activity segments must stay the same height as inactive siblings while
margin-bottom: var(--space-md);
}
.activity-segmented-control {
display: inline-flex;
max-inline-size: 100%;
gap: 0;
background: var(--card);
border: var(--border-width) solid var(--border);
border-radius: var(--radius-md);
padding: calc(var(--space-xs) / 4);
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-inline: contain;
touch-action: pan-x pan-y;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
}
.activity-segment {
box-sizing: border-box;
flex: 0 0 auto;
.activity-view-select {
min-inline-size: calc(var(--space-2xl) + var(--space-xl) + var(--space-lg));
min-block-size: var(--space-2xl);
padding: calc(var(--space-xs) / 2) var(--space-md);
background: none;
border: none;
border-radius: var(--radius-sm);
color: var(--text-muted);
cursor: pointer;
font-size: var(--font-size-xs);
font-weight: 500;
padding-block: calc(var(--space-xs) / 2);
padding-inline: var(--space-sm) var(--space-xl);
font-size: var(--font-size-sm);
line-height: var(--line-height-tight);
font-family: inherit;
touch-action: pan-x pan-y;
transition: color var(--transition-fast), background var(--transition-fast);
}
.activity-segment:hover {
color: var(--text);
background: var(--card-hover);
}
.activity-segment:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.activity-segment-active {
color: var(--text);
background: var(--surface);
box-shadow: var(--shadow-sm);
}
.activity-expand-toggle {
@@ -2524,8 +2480,9 @@ Selected Activity segments must stay the same height as inactive siblings while
align-items: stretch;
}
.activity-segmented-control {
.activity-view-select {
flex: 1 1 auto;
min-inline-size: 0;
}
.activity-expand-toggle {

View File

@@ -1,5 +1,5 @@
import "./TaskDetailModal.css";
import React, { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import React, { Suspense, lazy, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2, AlertTriangle, Sparkles, Maximize2, Minimize2 } from "lucide-react";
import { useModalResizePersist } from "../hooks/useModalResizePersist";
@@ -213,6 +213,9 @@ The first Activity segment keeps the stable internal `current` id for legacy seg
FNXC:TaskDetailActivity 2026-06-30-23:55:
The first Activity segment is user-facing Live while legacy internals remain `current` and explicit `initialTab="chat"` continues landing there for compatibility.
FNXC:TaskDetailActivity 2026-06-30-15:50:
Activity view switching uses one dropdown labeled for Live, Feed, and Raw while retaining the internal `current`, `feed`, and `raw-logs` segment ids. Legacy `chat` and `logs` initial-tab routing remains compatible so older links still open Activity → Live or Activity → Feed.
*/
function resolveDefaultTab(initialTab: TabId | undefined, column: ColumnId, taskDetailChatFirst = false): TabId {
if (initialTab === "retries") {
@@ -548,6 +551,7 @@ export function TaskDetailContent({
workflowFieldDefs: workflowFieldDefsProp,
}: TaskDetailContentProps) {
const { t } = useTranslation("app");
const activitySelectorId = useId();
const columnLabel = useColumnLabel();
const fileBrowser = useFileBrowser();
const [activeTab, setActiveTab] = useState<TabId>(() => resolveDefaultTab(initialTab, task.column, taskDetailChatFirst));
@@ -3412,41 +3416,29 @@ export function TaskDetailContent({
<div className={`detail-section detail-section--activity${activitySegment === "current" || isActivityExpanded ? " detail-section--chat" : ""}${activitySegment === "raw-logs" ? " detail-section--agent-log" : ""}`}>
{/*
FNXC:TaskDetailPlannerChat 2026-06-30-22:30:
Activity owns the existing steering/current view, Feed, and Raw Logs inside one segmented control. The stable Activity tab id remains `chat`, legacy `logs` callers land on Feed, and Raw Logs is the only segment that enables raw agent-log fetching. Planner-model conversation belongs to the separate `planner-chat` tab and must not route into steering comments.
Activity owns the existing steering/current view, Feed, and raw agent logs inside one compact selector. The stable Activity tab id remains `chat`, legacy `logs` callers land on Feed, and Raw is the only selector option that enables raw agent-log fetching. Planner-model conversation belongs to the separate `planner-chat` tab and must not route into steering comments.
FNXC:TaskDetailActivity 2026-06-30-23:55:
The first Activity segment is user-facing Live but keeps the legacy `current` segment id. Activity expansion is segment-wide, so the same reachable toggle must remain present on Live, Feed, and Raw Logs without fetching Raw Logs outside the Raw Logs segment.
The first Activity segment is user-facing Live but keeps the legacy `current` segment id. Activity expansion is segment-wide, so the same reachable toggle must remain present on Live, Feed, and Raw without fetching Raw outside the Raw segment.
FNXC:TaskDetailActivity 2026-06-30-15:50:
Replacing the in-content subtabs with a native dropdown removes the horizontal tab shell on mobile while preserving keyboard operation and the legacy Activity segment ids (`current`, `feed`, `raw-logs`).
*/}
<div className="activity-toolbar">
<div className="activity-segmented-control" role="tablist" aria-label={t("taskDetail.activity.segmentsLabel", "Activity views")}>
<button
type="button"
role="tab"
aria-selected={activitySegment === "current"}
className={`activity-segment${activitySegment === "current" ? " activity-segment-active" : ""}`}
onClick={() => setActivitySegment("current")}
>
{t("taskDetail.activity.current", "Live")}
</button>
<button
type="button"
role="tab"
aria-selected={activitySegment === "feed"}
className={`activity-segment${activitySegment === "feed" ? " activity-segment-active" : ""}`}
onClick={() => setActivitySegment("feed")}
>
{t("taskDetail.activity.feed", "Feed")}
</button>
<button
type="button"
role="tab"
aria-selected={activitySegment === "raw-logs"}
className={`activity-segment${activitySegment === "raw-logs" ? " activity-segment-active" : ""}`}
onClick={() => setActivitySegment("raw-logs")}
>
{t("taskDetail.activity.rawLogs", "Raw Logs")}
</button>
</div>
<label className="visually-hidden" htmlFor={activitySelectorId}>
{t("taskDetail.activity.selectorLabel", "Activity view")}
</label>
<select
id={activitySelectorId}
className="select activity-view-select"
value={activitySegment}
onChange={(event) => setActivitySegment(event.target.value as ActivitySegment)}
aria-label={t("taskDetail.activity.selectorLabel", "Activity view")}
>
<option value="current">{t("taskDetail.activity.current", "Live")}</option>
<option value="feed">{t("taskDetail.activity.feed", "Feed")}</option>
<option value="raw-logs">{t("taskDetail.activity.raw", "Raw")}</option>
</select>
<button
type="button"
className="btn btn-icon btn-sm activity-expand-toggle"

View File

@@ -30,10 +30,18 @@ FNXC:TaskDetailTabs 2026-06-17-08:20:
FN-7306 labels the stable internal `chat` tab as Activity, while later Chat-first detail work keeps that legacy `chat` id only for explicit Activity requests. Definition-tab regression coverage must prove omitted non-done task details now land on planner Chat, Activity remains selectable, and explicit `initialTab="definition"` still opens the Definition surface for prompt, GitHub tracking, and dependency sections.
FNXC:TaskDetailPlannerChat 2026-06-30-23:58:
Omitted non-done TaskDetailModal renders open the top-level planner Chat first/default. Activity controls (`Live`, `Feed`, `Raw Logs`, and the activity expand toggle) are intentionally mounted only after selecting Activity or using an explicit legacy Activity tab request.
Omitted non-done TaskDetailModal renders open the top-level planner Chat first/default. Activity controls (`Live`, `Feed`, `Raw`, and the activity expand toggle) are intentionally mounted only after selecting Activity or using an explicit legacy Activity tab request.
*/
setupTaskDetailModalHooks();
function activitySelector(): HTMLSelectElement {
return screen.getByRole("combobox", { name: "Activity view" }) as HTMLSelectElement;
}
function selectActivityView(value: "current" | "feed" | "raw-logs") {
fireEvent.change(activitySelector(), { target: { value } });
}
describe("TaskDetailModal", () => {
describe("paste image upload", () => {
it("uploads an image when pasting clipboard image data", async () => {
@@ -381,7 +389,7 @@ describe("TaskDetailModal", () => {
// Select Activity before asserting its segmented controls
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
selectActivityView("feed");
const activityList = container.querySelector(".detail-activity-list");
expect(activityList).toBeTruthy();
@@ -472,21 +480,23 @@ describe("TaskDetailModal", () => {
expect(screen.getByTestId("task-planner-chat-expand-toggle")).toHaveAttribute("aria-label", "Collapse planner chat");
expect(container.querySelector(".task-detail-content")).toHaveClass("task-detail-content--planner-chat-expanded");
expect(container.querySelector(".activity-segmented-control")).toBeNull();
expect(container.querySelector(".activity-segment")).toBeNull();
expect(screen.queryByTestId("task-chat-expand-toggle")).toBeNull();
expect(screen.queryByText("Agent Log")).toBeNull();
expect(screen.queryByRole("tab", { name: "Live" })).toBeNull();
expect(screen.queryByRole("tab", { name: "Current" })).toBeNull();
expect(screen.queryByRole("combobox", { name: "Activity view" })).toBeNull();
expect(container.querySelector(".detail-section--chat")).toBeNull();
expect(container.querySelector("[data-testid='task-chat-tab']")).toBeNull();
expect(container.querySelector(".detail-activity")).toBeNull();
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
expect(container.querySelector(".activity-segmented-control")).toBeTruthy();
expect(screen.getByRole("tab", { name: "Live" })).toHaveAttribute("aria-selected", "true");
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
expect(container.querySelector(".activity-segmented-control")).toBeNull();
expect(container.querySelector(".activity-segment")).toBeNull();
expect(Array.from(activitySelector().options).map((option) => option.textContent?.trim())).toEqual(["Live", "Feed", "Raw"]);
expect(activitySelector().value).toBe("current");
selectActivityView("feed");
expect(container.querySelector(".detail-activity")).toBeTruthy();
expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true");
expect(activitySelector().value).toBe("feed");
});
it("switches to Feed segment via Activity tab and shows activity feed", () => {
@@ -509,7 +519,7 @@ describe("TaskDetailModal", () => {
// Select Activity before asserting its segmented controls
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
selectActivityView("feed");
// Activity section should be visible
expect(container.querySelector(".detail-activity")).toBeTruthy();
@@ -540,7 +550,7 @@ describe("TaskDetailModal", () => {
// Select Activity before asserting its segmented controls
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
selectActivityView("feed");
const activityList = container.querySelector(".detail-activity-list");
expect(activityList).toBeTruthy();
@@ -577,7 +587,7 @@ describe("TaskDetailModal", () => {
);
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
selectActivityView("feed");
const actions = Array.from(container.querySelectorAll(".detail-log-action")).map((entry) => entry.textContent);
const outcomes = Array.from(container.querySelectorAll(".detail-log-outcome")).map((entry) => entry.textContent);
@@ -604,7 +614,7 @@ describe("TaskDetailModal", () => {
);
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
selectActivityView("feed");
const actions = container.querySelectorAll(".detail-log-action");
const outcomes = container.querySelectorAll(".detail-log-outcome");
@@ -644,7 +654,7 @@ describe("TaskDetailModal", () => {
// Select Activity before asserting its segmented controls
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
selectActivityView("feed");
// Activity section should be visible
expect(container.querySelector(".detail-activity")).toBeTruthy();
@@ -678,17 +688,17 @@ describe("TaskDetailModal", () => {
// Select Activity, then Feed segment
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
selectActivityView("feed");
expect(container.querySelector(".detail-activity")).toBeTruthy();
expect(container.querySelector(".markdown-body")).toBeNull();
// Switch to Raw Activity segment within Activity tab
fireEvent.click(screen.getByText("Raw Logs"));
selectActivityView("raw-logs");
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeTruthy();
expect(container.querySelector(".detail-activity")).toBeNull();
// Switch back to Feed segment within Activity tab.
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
selectActivityView("feed");
expect(container.querySelector(".detail-activity")).toBeTruthy();
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull();
@@ -722,8 +732,8 @@ describe("TaskDetailModal", () => {
// Click Activity tab, then Raw Activity segment
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
fireEvent.click(screen.getByText("Raw Logs"));
selectActivityView("feed");
selectActivityView("raw-logs");
// Agent log viewer should appear
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeTruthy();
@@ -761,12 +771,12 @@ describe("TaskDetailModal", () => {
// Select Activity and Feed — Raw Logs fetching stays disabled
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
selectActivityView("feed");
const afterLogsClick = mockUseAgentLogs.mock.calls[mockUseAgentLogs.mock.calls.length - 1];
expect(afterLogsClick[1]).toBe(false);
// Switch to Raw Activity segment — enabled should become true
fireEvent.click(screen.getByText("Raw Logs"));
selectActivityView("raw-logs");
const afterAgentLog = mockUseAgentLogs.mock.calls[mockUseAgentLogs.mock.calls.length - 1];
expect(afterAgentLog[1]).toBe(true);
});
@@ -941,20 +951,20 @@ describe("TaskDetailModal", () => {
expect(screen.queryByTestId("task-chat-expand-toggle")).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
expect(screen.getByRole("tab", { name: "Live" })).toHaveAttribute("aria-selected", "true");
expect(activitySelector().value).toBe("current");
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-label", "Expand activity to full modal");
fireEvent.click(screen.getByTestId("task-chat-expand-toggle"));
expect(content).toHaveClass("task-detail-content--chat-expanded");
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-label", "Collapse activity");
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
selectActivityView("feed");
expect(content).toHaveClass("task-detail-content--chat-expanded");
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-pressed", "true");
expect(screen.getByText("Expanded feed entry")).toBeInTheDocument();
expect(container.querySelector(".detail-activity-list")).toBeTruthy();
fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" }));
selectActivityView("raw-logs");
expect(content).toHaveClass("task-detail-content--chat-expanded");
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-pressed", "true");
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeTruthy();
@@ -989,7 +999,7 @@ describe("TaskDetailModal", () => {
);
expect(container.querySelector(".task-detail-content")).not.toHaveClass("task-detail-content--chat-expanded");
expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true");
expect(activitySelector().value).toBe("feed");
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-label", "Expand activity to full modal");
});
@@ -1177,7 +1187,7 @@ describe("TaskDetailModal", () => {
);
expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active");
expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true");
expect(activitySelector().value).toBe("feed");
expect(container.querySelector(".detail-tabs .detail-tab:first-child")).toHaveTextContent("Chat");
expect(container.querySelector(".detail-section--chat")).toBeNull();
expect(container.querySelector(".detail-activity")).toBeTruthy();
@@ -1238,8 +1248,8 @@ describe("TaskDetailModal", () => {
expect(chatBody).not.toHaveClass("detail-body--agent-log");
expect(chatSection).toBeTruthy();
expect(chatSection!.querySelector("[data-testid='task-chat-tab']")).toBeTruthy();
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
fireEvent.click(screen.getByText("Raw Logs"));
selectActivityView("feed");
selectActivityView("raw-logs");
expect(container.querySelector(".detail-body--chat")).toBeNull();
expect(container.querySelector(".detail-section--chat")).toBeNull();
expect(container.querySelector(".detail-body--agent-log")).toBeTruthy();
@@ -1286,10 +1296,10 @@ describe("TaskDetailModal", () => {
// Switch to Activity tab, then Raw Activity segment
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
selectActivityView("feed");
expect(container.querySelector(".detail-body--agent-log")).toBeNull(); // Feed segment default
fireEvent.click(screen.getByText("Raw Logs"));
selectActivityView("raw-logs");
// detail-body should now have the agent-log modifier class
expect(container.querySelector(".detail-body--agent-log")).toBeTruthy();
@@ -1316,8 +1326,8 @@ describe("TaskDetailModal", () => {
// Switch to Activity tab, then Raw Activity segment
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
fireEvent.click(screen.getByText("Raw Logs"));
selectActivityView("feed");
selectActivityView("raw-logs");
// The section wrapping AgentLogViewer should have the full-height class
const section = container.querySelector(".detail-section--agent-log");
@@ -1340,8 +1350,8 @@ describe("TaskDetailModal", () => {
// Switch to Activity tab, then Raw Activity segment first
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
fireEvent.click(screen.getByText("Raw Logs"));
selectActivityView("feed");
selectActivityView("raw-logs");
expect(container.querySelector(".detail-body--agent-log")).toBeTruthy();
// Now enter edit mode via the pencil button in the header

View File

@@ -16,13 +16,13 @@ describe("TaskDetailModal CSS contract", () => {
expect(css).toMatch(/\.detail-tab\s*\{[^}]*flex-shrink\s*:\s*0\s*;/);
});
it("FN-7307 keeps Activity segments reachable on narrow task-detail surfaces", async () => {
it("FN-7344 keeps the Activity dropdown reachable on narrow task-detail surfaces", async () => {
const css = await loadAllAppCssBaseOnly();
expect(css).toMatch(/\.activity-segmented-control\s*\{[^}]*max-inline-size\s*:\s*100%\s*;/);
expect(css).toMatch(/\.activity-segmented-control\s*\{[^}]*overflow-x\s*:\s*auto\s*;/);
expect(css).toMatch(/\.activity-segmented-control\s*\{[^}]*touch-action\s*:\s*pan-x\s+pan-y\s*;/);
expect(css).toMatch(/\.activity-segment\s*\{[^}]*flex\s*:\s*0\s+0\s+auto\s*;/);
expect(css).toMatch(/\.activity-view-select\s*\{[^}]*min-inline-size\s*:\s*calc\(var\(--space-2xl\) \+ var\(--space-xl\) \+ var\(--space-lg\)\)\s*;/);
expect(css).toMatch(/\.activity-view-select\s*\{[^}]*min-block-size\s*:\s*var\(--space-2xl\)\s*;/);
expect(css).not.toContain(".activity-segmented-control");
expect(css).not.toContain(".activity-segment");
expect(css).not.toContain(".log-subview-toggle");
expect(css).not.toContain(".log-subview-btn");
});

View File

@@ -2254,7 +2254,7 @@ describe("TaskDetailModal", () => {
}, { timeout: 3000 });
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
fireEvent.change(screen.getByRole("combobox", { name: "Activity view" }), { target: { value: "feed" } });
const activityList = container.querySelector(".detail-activity-list");
expect(activityList).toBeTruthy();

View File

@@ -23,6 +23,10 @@ import { TaskDetailModal, TaskDetailContent } from "../TaskDetailModal";
setupTaskDetailModalHooks();
function selectActivityView(value: "current" | "feed" | "raw-logs") {
fireEvent.change(screen.getByRole("combobox", { name: "Activity view" }), { target: { value } });
}
describe("TaskDetailModal", () => {
describe("Raw Logs model resolution", () => {
// AgentLogViewer only renders the model header when entries.length > 0,
@@ -101,7 +105,7 @@ describe("TaskDetailModal", () => {
async function openAgentLogAndExpandModelDetails(container: HTMLElement) {
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" }));
selectActivityView("raw-logs");
await waitFor(() => {
const header = container.querySelector("[data-testid='agent-log-model-header']");
@@ -575,7 +579,7 @@ describe("TaskDetailModal", () => {
);
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" }));
selectActivityView("raw-logs");
await waitFor(() => {
const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy();
@@ -632,7 +636,7 @@ describe("TaskDetailModal", () => {
);
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" }));
selectActivityView("raw-logs");
await waitFor(() => {
const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy();
@@ -896,7 +900,7 @@ describe("TaskDetailModal", () => {
// Switch to Activity tab, then Raw Logs segment
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" }));
selectActivityView("raw-logs");
// Should not be visible in Raw Logs segment
expect(container.querySelector(".detail-step-progress")).toBeNull();

View File

@@ -2429,7 +2429,7 @@ describe("TaskDetailModal", () => {
}, { timeout: 3000 });
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
fireEvent.change(screen.getByRole("combobox", { name: "Activity view" }), { target: { value: "feed" } });
const activityList = container.querySelector(".detail-activity-list");
expect(activityList).toBeTruthy();

View File

@@ -308,34 +308,23 @@ describe("TaskDetailModal", () => {
expect(detailBodyBlock).not.toContain("overflow: hidden;");
});
it("keeps Activity segment tabs equal-height and horizontally reachable", () => {
it("keeps the Activity dropdown compact and reachable on mobile", () => {
const css = readDashboardStylesSource();
const controlBlock = getExactCssRuleBlock(css, ".activity-segmented-control");
const segmentBlock = getExactCssRuleBlock(css, ".activity-segment");
const activeSegmentBlock = getExactCssRuleBlock(css, ".activity-segment-active");
const mobileBlock = getCssAtRuleBlockContainingExactRule(css, "@media (max-width: 768px)", ".activity-segmented-control");
const mobileControlBlock = getExactCssRuleBlock(mobileBlock, ".activity-segmented-control");
const selectorBlock = getExactCssRuleBlock(css, ".activity-view-select");
const mobileBlock = getCssAtRuleBlockContainingExactRule(css, "@media (max-width: 768px)", ".activity-view-select");
const mobileSelectorBlock = getExactCssRuleBlock(mobileBlock, ".activity-view-select");
expectHorizontalTabScroller(controlBlock, "base .activity-segmented-control");
expectTabTouchAction(segmentBlock, "base .activity-segment");
expect(segmentBlock).toContain("box-sizing: border-box;");
expect(segmentBlock).toContain("flex: 0 0 auto;");
expect(segmentBlock).toContain("min-block-size: var(--space-2xl);");
expect(segmentBlock).toContain("font-size: var(--font-size-xs);");
expect(segmentBlock).toContain("line-height: var(--line-height-tight);");
expect(segmentBlock).toContain("padding: calc(var(--space-xs) / 2) var(--space-md);");
expect(segmentBlock).toContain("border: none;");
expect(activeSegmentBlock).toContain("color: var(--text);");
expect(activeSegmentBlock).toContain("background: var(--surface);");
expect(activeSegmentBlock).toContain("box-shadow: var(--shadow-sm);");
expect(activeSegmentBlock).not.toContain("padding:");
expect(activeSegmentBlock).not.toContain("min-block-size:");
expect(activeSegmentBlock).not.toContain("line-height:");
expect(activeSegmentBlock).not.toContain("font-size:");
expect(activeSegmentBlock).not.toContain("font-weight:");
expect(activeSegmentBlock).not.toContain("border:");
expect(mobileControlBlock).toContain("flex: 1 1 auto;");
expect(css).not.toMatch(/\.task-detail-content--embedded\s+\.activity-segment(?:ed-control)?\s*\{/);
expect(selectorBlock).toContain("min-inline-size: calc(var(--space-2xl) + var(--space-xl) + var(--space-lg));");
expect(selectorBlock).toContain("min-block-size: var(--space-2xl);");
expect(selectorBlock).toContain("padding-block: calc(var(--space-xs) / 2);");
expect(selectorBlock).toContain("padding-inline: var(--space-sm) var(--space-xl);");
expect(selectorBlock).toContain("font-size: var(--font-size-sm);");
expect(selectorBlock).toContain("line-height: var(--line-height-tight);");
expect(mobileSelectorBlock).toContain("flex: 1 1 auto;");
expect(mobileSelectorBlock).toContain("min-inline-size: 0;");
expect(css).not.toContain(".activity-segmented-control");
expect(css).not.toContain(".activity-segment");
expect(css).not.toMatch(/\.task-detail-content--embedded\s+\.activity-view-select\s*\{/);
});
it("renders responsive structural classes (modal-lg, overlay, spacer, tabs, detail-body)", () => {

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, within } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React, { type ComponentProps } from "react";
import type { AgentLogEntry } from "@fusion/core";
@@ -49,8 +49,12 @@ function topLevelTabLabels(): string[] {
.map((button) => button.textContent?.trim() ?? "");
}
function activitySegmentButtons(): HTMLButtonElement[] {
return screen.getAllByRole("tab", { name: /^(Live|Feed|Raw Logs)$/ }) as HTMLButtonElement[];
function activitySelector(): HTMLSelectElement {
return screen.getByRole("combobox", { name: "Activity view" }) as HTMLSelectElement;
}
function activitySelectorLabels(): string[] {
return Array.from(activitySelector().options).map((option) => option.textContent?.trim() ?? "");
}
function mockRawLogs(entries: AgentLogEntry[]) {
@@ -66,7 +70,7 @@ function mockRawLogs(entries: AgentLogEntry[]) {
}
describe("TaskDetailModal Activity and planner Chat tab integration", () => {
it("defaults to Activity first while segmenting Live, Feed, and Raw Logs without duplicate panels on desktop", async () => {
it("defaults to Activity first while selecting Live, Feed, and Raw without duplicate panels on desktop", async () => {
const user = userEvent.setup();
mockRawLogs([
{ timestamp: "2026-06-30T20:03:00.000Z", taskId: "FN-7315", type: "text", agent: "executor", text: "raw executor line" },
@@ -78,9 +82,11 @@ describe("TaskDetailModal Activity and planner Chat tab integration", () => {
expect(screen.getAllByRole("button", { name: "Chat" })).toHaveLength(1);
expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active");
expect(screen.queryByTestId("task-planner-chat-panel")).not.toBeInTheDocument();
expect(activitySegmentButtons().map((button) => button.textContent?.trim())).toEqual(["Live", "Feed", "Raw Logs"]);
expect(activitySegmentButtons().every((button) => (button.textContent ?? "").trim().length > 0)).toBe(true);
expect(screen.getByRole("tab", { name: "Live" })).toHaveAttribute("aria-selected", "true");
expect(activitySelectorLabels()).toEqual(["Live", "Feed", "Raw"]);
expect(activitySelector().value).toBe("current");
expect(document.querySelector(".activity-segmented-control")).toBeNull();
expect(document.querySelector(".activity-segment")).toBeNull();
expect(screen.queryByRole("tablist", { name: "Activity views" })).not.toBeInTheDocument();
expect(screen.getAllByRole("form", { name: "Task activity composer" })).toHaveLength(1);
expect(screen.queryByText(/^Steering comment$/)).not.toBeInTheDocument();
expect(screen.queryByText("Send operational guidance to the active task through steering comments.")).not.toBeInTheDocument();
@@ -88,18 +94,18 @@ describe("TaskDetailModal Activity and planner Chat tab integration", () => {
expect(screen.queryByRole("heading", { name: "Feed" })).not.toBeInTheDocument();
expect(screen.queryByTestId("agent-log-viewer")).not.toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "Feed" }));
await user.selectOptions(activitySelector(), "feed");
expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true");
expect(activitySelector().value).toBe("feed");
expect(screen.queryByRole("form", { name: "Task activity composer" })).not.toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Feed" })).toBeInTheDocument();
expect(screen.getByText("Posted update")).toBeInTheDocument();
expect(screen.queryByText("Existing steering guidance")).not.toBeInTheDocument();
expect(screen.queryByTestId("agent-log-viewer")).not.toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "Raw Logs" }));
await user.selectOptions(activitySelector(), "raw-logs");
expect(screen.getByRole("tab", { name: "Raw Logs" })).toHaveAttribute("aria-selected", "true");
expect(activitySelector().value).toBe("raw-logs");
expect(screen.queryByRole("form", { name: "Task activity composer" })).not.toBeInTheDocument();
expect(screen.queryByRole("heading", { name: "Feed" })).not.toBeInTheDocument();
expect(screen.getByTestId("agent-log-viewer")).toBeInTheDocument();
@@ -124,7 +130,7 @@ describe("TaskDetailModal Activity and planner Chat tab integration", () => {
expect(topLevelTabLabels().slice(0, 2)).toEqual(["Chat", "Activity"]);
expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active");
expect(screen.getByRole("tab", { name: "Live" })).toHaveAttribute("aria-selected", "true");
expect(activitySelector().value).toBe("current");
expect(screen.queryByTestId("task-planner-chat-panel")).not.toBeInTheDocument();
rerender(
@@ -161,7 +167,7 @@ describe("TaskDetailModal Activity and planner Chat tab integration", () => {
expect(topLevelTabLabels().slice(0, 2)).toEqual(["Chat", "Activity"]);
expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active");
expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true");
expect(activitySelector().value).toBe("feed");
expect(screen.getByRole("heading", { name: "Feed" })).toBeInTheDocument();
});
@@ -188,12 +194,11 @@ describe("TaskDetailModal Activity and planner Chat tab integration", () => {
expect(topLevelTabLabels().slice(0, 3)).toEqual(["Activity", "Chat", "Summary"]);
expect(screen.getByRole("button", { name: "Summary" })).toHaveClass("detail-tab-active");
expect(screen.queryByRole("tab", { name: "Live" })).not.toBeInTheDocument();
expect(screen.queryByRole("combobox", { name: "Activity view" })).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Activity" }));
const segmentGroup = screen.getByRole("tablist", { name: "Activity views" });
expect(within(segmentGroup).getAllByRole("tab").map((tab) => tab.textContent?.trim())).toEqual(["Live", "Feed", "Raw Logs"]);
expect(within(segmentGroup).getAllByRole("tab")).toHaveLength(3);
expect(activitySelectorLabels()).toEqual(["Live", "Feed", "Raw"]);
expect(screen.queryByRole("tablist", { name: "Activity views" })).not.toBeInTheDocument();
expect(screen.getByText("No agent output yet. Live messages from Planner, Executor, Reviewer, and Merger agents will appear here.")).toBeInTheDocument();
expect(screen.getAllByRole("form", { name: "Task refinement composer" })).toHaveLength(1);
expect(screen.queryByRole("form", { name: "Refinement request" })).not.toBeInTheDocument();

View File

@@ -35,6 +35,10 @@ vi.mock("../BranchGroupCard", () => ({
setupTaskDetailModalHooks();
async function selectActivityView(user: ReturnType<typeof userEvent.setup>, value: "current" | "feed" | "raw-logs") {
await user.selectOptions(screen.getByRole("combobox", { name: "Activity view" }), value);
}
function renderSummarizeTitleModal(overrides: Parameters<typeof makeTask>[0] = {}, props: Partial<ComponentProps<typeof TaskDetailModal>> = {}) {
const addToast = props.addToast ?? vi.fn();
const onTaskUpdated = props.onTaskUpdated ?? vi.fn();
@@ -123,7 +127,7 @@ describe("TaskDetailModal planner Chat tab", () => {
renderTask("in-progress", "chat");
expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active");
expect(screen.getByRole("tab", { name: "Live" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("combobox", { name: "Activity view" })).toHaveValue("current");
});
it("routes explicit planner-chat requests to the new Chat tab", () => {
@@ -461,7 +465,7 @@ describe("TaskDetailModal Activity feed loading", () => {
);
await user.click(screen.getByRole("button", { name: "Activity" }));
await user.click(screen.getByRole("tab", { name: "Feed" }));
await selectActivityView(user, "feed");
expect(await screen.findByRole("status")).toHaveTextContent("Loading activity…");
expect(screen.queryByText("(no activity)")).not.toBeInTheDocument();
});
@@ -542,20 +546,20 @@ describe("TaskDetailModal Chat task merge", () => {
/>,
);
expect(screen.getByRole("tab", { name: "Live" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("combobox", { name: "Activity view" })).toHaveValue("current");
expect(screen.getAllByRole("form", { name: "Task activity composer" })).toHaveLength(1);
expect(screen.queryByText(/^Steering comment$/)).not.toBeInTheDocument();
expect(screen.queryByText("Send operational guidance to the active task through steering comments.")).not.toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "Feed" }));
await selectActivityView(user, "feed");
expect(screen.queryByRole("form", { name: "Task activity composer" })).not.toBeInTheDocument();
expect(screen.getByText("Started work")).toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "Raw Logs" }));
await selectActivityView(user, "raw-logs");
expect(screen.queryByRole("form", { name: "Task activity composer" })).not.toBeInTheDocument();
expect(screen.getByTestId("agent-log-viewer")).toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "Live" }));
await selectActivityView(user, "current");
const input = screen.getByLabelText("Message active agent session");
await user.type(input, "Please keep the current approach");
await user.click(screen.getByRole("button", { name: "Send" }));
@@ -586,11 +590,11 @@ describe("TaskDetailModal Chat task merge", () => {
expect(screen.getAllByRole("form", { name: "Task activity composer" })).toHaveLength(1);
expect(screen.getByText("No agent output yet. Live messages from Planner, Executor, Reviewer, and Merger agents will appear here.")).toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "Feed" }));
await selectActivityView(user, "feed");
expect(screen.queryByRole("form", { name: "Task activity composer" })).not.toBeInTheDocument();
expect(screen.getByText("(no activity)")).toBeInTheDocument();
await user.click(screen.getByRole("tab", { name: "Raw Logs" }));
await selectActivityView(user, "raw-logs");
expect(screen.queryByRole("form", { name: "Task activity composer" })).not.toBeInTheDocument();
});
@@ -679,8 +683,8 @@ describe("TaskDetailModal Raw Logs agent loading", () => {
);
await user.click(screen.getByRole("button", { name: "Activity" }));
await user.click(screen.getByRole("tab", { name: "Feed" }));
await user.click(screen.getByRole("tab", { name: "Raw Logs" }));
await selectActivityView(user, "feed");
await selectActivityView(user, "raw-logs");
expect(screen.getByText("Loading agent logs…")).toBeInTheDocument();
expect(screen.queryByText("No agent output yet.")).not.toBeInTheDocument();
@@ -720,7 +724,7 @@ describe("TaskDetailModal Raw Logs agent loading", () => {
);
await user.click(screen.getByRole("button", { name: "Activity" }));
await user.click(screen.getByRole("tab", { name: "Raw Logs" }));
await selectActivityView(user, "raw-logs");
expect(screen.getByTestId("agent-log-viewer")).toBeInTheDocument();
expect(screen.getByTestId("agent-log-summary")).toHaveTextContent("Showing 2 of 5 entries");
@@ -974,7 +978,7 @@ describe("TaskDetailModal in-review stall diagnostics", () => {
await user.click(screen.getByRole("button", { name: "View activity log" }));
expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active");
expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("combobox", { name: "Activity view" })).toHaveValue("feed");
const highlighted = document.querySelector(".detail-log-entry--stall-highlight .detail-log-action");
expect(highlighted?.textContent).toContain("In-review stall surfaced [merge-blocker]");
});