diff --git a/.changeset/fn-7307-activity-segments.md b/.changeset/fn-7307-activity-segments.md new file mode 100644 index 0000000000..9e5cbd5f11 --- /dev/null +++ b/.changeset/fn-7307-activity-segments.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add Activity segments for current task activity, Feed, and Raw Logs. +category: feature +dev: Task detail keeps legacy initialTab="logs" compatibility by routing to Activity → Feed. diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index e2b050abe5..35651a7cc2 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -119,7 +119,7 @@ Task detail workflow badges share the board badge's slight token-based icon-to-l background: var(--text-muted); } -/* When the Agent Log tab is active, switch to flex layout so the log section +/* When Activity → Raw Logs is active, switch to flex layout so the log section can stretch to fill the remaining space above the action bar. The detail-body itself should NOT scroll; the agent-log-viewer scrolls instead. */ .detail-body--agent-log { @@ -128,7 +128,7 @@ Task detail workflow badges share the board badge's slight token-based icon-to-l overflow-y: hidden; } -/* Chat mirrors the Agent Log fill-height layout: the modal body does not scroll; +/* Activity → Current mirrors the Raw Logs fill-height layout: the modal body does not scroll; the transcript owns internal scrolling while the composer stays visible. */ .detail-body--chat { display: flex; @@ -793,7 +793,7 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P color: var(--color-error); } -/* Agent Log tab: stretch to fill the remaining modal body height +/* Activity → Raw Logs: stretch to fill the remaining modal body height so the log viewer uses all available space above the action bar. */ .detail-section--agent-log { display: flex; @@ -2353,36 +2353,54 @@ FNXC:TaskDetailTabs 2026-06-26-00:35: font-weight: 600; } -/* === Log Subview Toggle === */ -.log-subview-toggle { +/* === Activity Segmented Control === */ +/* +FNXC:TaskDetailActivity 2026-06-30-22:15: +The Activity top-level tab owns 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. +*/ +.activity-segmented-control { display: inline-flex; + max-inline-size: 100%; gap: 0; background: var(--card); - border: 1px solid var(--border); + border: var(--border-width) solid var(--border); border-radius: var(--radius-md); - padding: 2px; + padding: calc(var(--space-xs) / 4); margin-bottom: var(--space-md); + overflow-x: auto; + overflow-y: hidden; + overscroll-behavior-inline: contain; + touch-action: pan-x pan-y; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; } -.log-subview-btn { - padding: 4px 12px; +.activity-segment { + flex: 0 0 auto; + 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: 12px; + font-size: var(--font-size-sm); font-weight: 500; font-family: inherit; + touch-action: pan-x pan-y; transition: color var(--transition-fast), background var(--transition-fast); } -.log-subview-btn:hover { +.activity-segment:hover { color: var(--text); background: var(--card-hover); } -.log-subview-btn-active { +.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); diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index efc219e77d..05f638d96f 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -192,24 +192,32 @@ function formatDurationCompact(ageMs: number): string { } type TabId = "summary" | "definition" | "chat" | "logs" | "changes" | "review" | "pr" | "comments" | "model" | "workflow" | "documents" | "stats" | "routing" | "retries" | "terminal" | `plugin-${string}`; +type ActivitySegment = "current" | "feed" | "raw-logs"; /* 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: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. +FNXC:TaskDetailActivity 2026-06-30-22:15: +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 Activity → Current. Legacy `initialTab="logs"` now routes to Activity → Feed because Logs no longer renders as a top-level task-detail tab. */ function resolveDefaultTab(initialTab: TabId | undefined, _column: ColumnId): TabId { if (initialTab === "retries") { return "definition"; } + if (initialTab === "logs") { + return "chat"; + } if (initialTab) { return initialTab; } return "chat"; } +function resolveDefaultActivitySegment(initialTab: TabId | undefined): ActivitySegment { + return initialTab === "logs" ? "feed" : "current"; +} + // Lazy-load the terminal so xterm + addons stay out of the main bundle (U11). const LazySessionTerminal = lazy(() => import("./SessionTerminal").then((m) => ({ default: m.SessionTerminal })), @@ -516,6 +524,7 @@ export function TaskDetailContent({ const columnLabel = useColumnLabel(); const fileBrowser = useFileBrowser(); const [activeTab, setActiveTab] = useState(() => resolveDefaultTab(initialTab, task.column)); + const [activitySegment, setActivitySegment] = useState(() => resolveDefaultActivitySegment(initialTab)); const [chatExpanded, setChatExpanded] = useState(false); // ── CLI agent session (U11) ──────────────────────────────────────────────── @@ -646,6 +655,7 @@ export function TaskDetailContent({ // Sync activeTab when the caller changes initialTab (e.g. opening a different tab) useEffect(() => { setActiveTab(resolveDefaultTab(initialTab, task.column)); + setActivitySegment(resolveDefaultActivitySegment(initialTab)); if (initialTab === "retries") { setRetriesExpanded(true); } @@ -668,7 +678,6 @@ export function TaskDetailContent({ setDescriptionExpanded(false); }, [task.column, task.id]); - const [logSubview, setLogSubview] = useState<"activity" | "agent-log">("activity"); const [highlightStallCode, setHighlightStallCode] = useState(null); const [descriptionExpanded, setDescriptionExpanded] = useState(false); const [titleOverflows, setTitleOverflows] = useState(false); @@ -807,7 +816,7 @@ export function TaskDetailContent({ ); useEffect(() => { - if (activeTab !== "logs" || logSubview !== "activity") { + if (activeTab !== "chat" || activitySegment !== "feed") { setHighlightStallCode(null); return; } @@ -820,7 +829,7 @@ export function TaskDetailContent({ if (highlighted && typeof highlighted.scrollIntoView === "function") { highlighted.scrollIntoView({ block: "nearest", behavior: "smooth" }); } - }, [activeTab, logSubview, highlightStallCode]); + }, [activeTab, activitySegment, highlightStallCode]); const [refineFeedback, setRefineFeedback] = useState(""); const [isRefining, setIsRefining] = useState(false); @@ -828,10 +837,10 @@ export function TaskDetailContent({ const [isEditing, setIsEditing] = useState(false); useEffect(() => { - if (activeTab !== "chat" || isEditing) { + if (activeTab !== "chat" || activitySegment !== "current" || isEditing) { setChatExpanded(false); } - }, [activeTab, isEditing]); + }, [activeTab, activitySegment, isEditing]); const [editTitle, setEditTitle] = useState(task.title || ""); const [editDescription, setEditDescription] = useState(task.description || ""); @@ -955,7 +964,7 @@ export function TaskDetailContent({ activeTaskIdRef.current = task.id; }, [task.id]); - // Merged project settings for effective model resolution in Agent Log header + // Merged project settings for effective model resolution in Raw Logs header const [settings, setSettings] = useState(undefined); const [globalSettings, setGlobalSettings] = useState(null); @@ -1012,7 +1021,7 @@ export function TaskDetailContent({ let cancelled = false; /* FNXC:ModelResolution 2026-06-27-10:52: - Task-detail model displays are task-scoped because project model lanes moved into workflow setting values. Fetch the effective settings for the selected task so Workflow, Chat, Agent Log, and Model editor surfaces resolve the same Executor/Reviewer/Planning models the engine uses. + Task-detail model displays are task-scoped because project model lanes moved into workflow setting values. Fetch the effective settings for the selected task so Workflow, Activity → Raw Logs, and Model editor surfaces resolve the same Executor/Reviewer/Planning models the engine uses. */ fetchTaskEffectiveSettings(task.id, projectId) .catch(() => fetchSettings(projectId)) @@ -1774,7 +1783,7 @@ export function TaskDetailContent({ loadingMore: agentLogLoadingMore, } = useAgentLogs( task.id, - activeTab === "logs" && logSubview === "agent-log", + activeTab === "chat" && activitySegment === "raw-logs", projectId, ); const requestClose = useCallback(() => { @@ -2792,7 +2801,7 @@ export function TaskDetailContent({ )} -
+
{isEditing ? (
{t("taskDetail.tabs.definition", "Plan")} - {(task.column === "in-progress" || task.column === "in-review" || task.column === "done") && (
) : activeTab === "chat" ? ( -
- setChatExpanded((value) => !value)} - effectiveModels={{ - triage: toTaskChatModelInfo(resolveEffectivePlanning(workingTask, agentLogEntries, settings)), - executor: toTaskChatModelInfo(resolveEffectiveExecutor(workingTask, agentLogEntries, assignedAgent, settings)), - reviewer: toTaskChatModelInfo(resolveEffectiveValidator(workingTask, agentLogEntries, assignedAgent, settings)), - merger: toTaskChatModelInfo(resolveEffectiveValidator(workingTask, agentLogEntries, assignedAgent, settings)), - }} - /> -
- ) : activeTab === "logs" ? ( -
-
+
+ {/* + FNXC:TaskDetailActivity 2026-06-30-22:15: + Activity owns the existing steering/current view, Feed, and Raw Logs inside one segmented control. The later planner-model Chat tab is intentionally out of scope, so the stable top-level tab id remains `chat` while the legacy `logs` id lands on the Feed segment. + */} +
+
- {logSubview === "agent-log" ? ( + {activitySegment === "current" ? ( + setChatExpanded((value) => !value)} + effectiveModels={{ + triage: toTaskChatModelInfo(resolveEffectivePlanning(workingTask, agentLogEntries, settings)), + executor: toTaskChatModelInfo(resolveEffectiveExecutor(workingTask, agentLogEntries, assignedAgent, settings)), + reviewer: toTaskChatModelInfo(resolveEffectiveValidator(workingTask, agentLogEntries, assignedAgent, settings)), + merger: toTaskChatModelInfo(resolveEffectiveValidator(workingTask, agentLogEntries, assignedAgent, settings)), + }} + /> + ) : activitySegment === "raw-logs" ? ( ) : ( -
-

{t("taskDetail.logs.activityHeading", "Activity")}

+
+

{t("taskDetail.activity.feedHeading", "Feed")}

{(workingTask as typeof workingTask & { activityLogTruncatedCount?: number }).activityLogTruncatedCount ? (
{t("taskDetail.logs.truncated", "Showing the most recent {{count}} activity entries.", { count: workingTask.log.length })} @@ -3426,8 +3446,8 @@ export function TaskDetailContent({ type="button" className="btn btn-sm detail-in-review-stall-jump" onClick={() => { - setActiveTab("logs"); - setLogSubview("activity"); + setActiveTab("chat"); + setActivitySegment("feed"); setHighlightStallCode(workingTask.inReviewStall?.code ?? null); }} > @@ -3474,8 +3494,8 @@ export function TaskDetailContent({ type="button" className="btn btn-sm detail-in-review-stall-jump" onClick={() => { - setActiveTab("logs"); - setLogSubview("activity"); + setActiveTab("chat"); + setActivitySegment("feed"); setHighlightStallCode(workingTask.stalePausedReview?.code ?? null); }} > diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx index 30fc23f6ad..0817aaf98e 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx @@ -368,8 +368,9 @@ describe("TaskDetailModal", () => { />, ); - // Click Logs tab — Activity is the default subview - fireEvent.click(screen.getByText("Logs")); + // Click Activity tab — Activity is the default subview + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Feed" })); const activityList = container.querySelector(".detail-activity-list"); expect(activityList).toBeTruthy(); @@ -453,27 +454,23 @@ describe("TaskDetailModal", () => { ); expect(screen.getByText("Plan")).toBeTruthy(); - expect(screen.getByText("Logs")).toBeTruthy(); - // Logs subview controls should NOT be visible on the default Activity tab. - expect(container.querySelector(".log-subview-toggle")).toBeNull(); + expect(screen.queryByRole("button", { name: "Logs" })).toBeNull(); + expect(container.querySelector(".activity-segmented-control")).toBeTruthy(); expect(screen.queryByText("Agent Log")).toBeNull(); - // Chat content should be visible by default. + const segments = screen.getAllByRole("tab"); + expect(segments.map((segment) => segment.textContent)).toEqual(["Current", "Feed", "Raw Logs"]); + expect(screen.getByRole("tab", { name: "Current" })).toHaveAttribute("aria-selected", "true"); expect(container.querySelector(".detail-section--chat")).toBeTruthy(); expect(container.querySelector("[data-testid='task-chat-tab']")).toBeTruthy(); - // Activity section should NOT be visible initially. expect(container.querySelector(".detail-activity")).toBeNull(); - // Agent log viewer should not be visible. expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull(); - // After clicking Logs tab, the subview toggle buttons should appear. - fireEvent.click(screen.getByText("Logs")); - const logSubviewToggle = container.querySelector(".log-subview-toggle"); - expect(logSubviewToggle).toBeTruthy(); - expect(logSubviewToggle!.textContent).toContain("Activity"); - expect(logSubviewToggle!.textContent).toContain("Agent Log"); + fireEvent.click(screen.getByRole("tab", { name: "Feed" })); + expect(container.querySelector(".detail-activity")).toBeTruthy(); + expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true"); }); - it("switches to Activity subview via Logs tab and shows activity feed", () => { + it("switches to Feed segment via Activity tab and shows activity feed", () => { const { container } = render( { />, ); - // Click Logs tab — Activity is the default subview - fireEvent.click(screen.getByText("Logs")); + // Click Activity tab — Activity is the default subview + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Feed" })); // Activity section should be visible expect(container.querySelector(".detail-activity")).toBeTruthy(); @@ -502,7 +500,7 @@ describe("TaskDetailModal", () => { expect(container.querySelector(".markdown-body")).toBeNull(); }); - it("Activity subview renders log entries correctly", () => { + it("Feed segment renders log entries correctly", () => { const { container } = render( { />, ); - // Click Logs tab — Activity is the default subview - fireEvent.click(screen.getByText("Logs")); + // Click Activity tab — Activity is the default subview + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Feed" })); const activityList = container.querySelector(".detail-activity-list"); expect(activityList).toBeTruthy(); @@ -538,7 +537,7 @@ describe("TaskDetailModal", () => { expect(logEntries[2].textContent).toContain("Created task"); }); - it("Activity subview keeps action/outcome rendering intact", () => { + it("Feed segment keeps action/outcome rendering intact", () => { const { container } = render( { />, ); - fireEvent.click(screen.getByText("Logs")); + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Feed" })); const actions = container.querySelectorAll(".detail-log-action"); const outcomes = container.querySelectorAll(".detail-log-outcome"); @@ -581,7 +581,7 @@ describe("TaskDetailModal", () => { expect(timestampRule).not.toContain("color: var(--text);"); }); - it("Activity subview shows empty state when no logs", () => { + it("Feed segment shows empty state when no logs", () => { const { container } = render( { />, ); - // Click Logs tab — Activity is the default subview - fireEvent.click(screen.getByText("Logs")); + // Click Activity tab — Activity is the default subview + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Feed" })); // Activity section should be visible expect(container.querySelector(".detail-activity")).toBeTruthy(); @@ -606,7 +607,7 @@ describe("TaskDetailModal", () => { expect(container.querySelector(".detail-activity-list")).toBeNull(); }); - it("can switch between all tabs and Logs subviews", () => { + it("can switch between all tabs and Activity segments", () => { const { container } = render( { expect(container.querySelector(".markdown-body")).toBeTruthy(); expect(container.querySelector(".detail-activity")).toBeNull(); - // Switch to Logs tab (Activity subview is default) - fireEvent.click(screen.getByText("Logs")); + // Switch to Activity tab (Feed segment is default) + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Feed" })); expect(container.querySelector(".detail-activity")).toBeTruthy(); expect(container.querySelector(".markdown-body")).toBeNull(); - // Switch to Agent Log subview within Logs tab - fireEvent.click(screen.getByText("Agent Log")); + // Switch to Raw Activity segment within Activity tab + fireEvent.click(screen.getByText("Raw Logs")); 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(container.querySelector(".log-subview-toggle .log-subview-btn")!); + // Switch back to Feed segment within Activity tab. + fireEvent.click(screen.getByRole("tab", { name: "Feed" })); expect(container.querySelector(".detail-activity")).toBeTruthy(); expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull(); @@ -654,7 +656,7 @@ describe("TaskDetailModal", () => { }); - it("switches to Agent Log subview via Logs tab and back", async () => { + it("switches to Raw Activity segment via Activity tab and back", async () => { const { useAgentLogs } = await import("../../hooks/useAgentLogs"); const mockUseAgentLogs = vi.mocked(useAgentLogs); @@ -670,9 +672,10 @@ describe("TaskDetailModal", () => { />, ); - // Click Logs tab, then Agent Log subview - fireEvent.click(screen.getByText("Logs")); - fireEvent.click(screen.getByText("Agent Log")); + // 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")); // Agent log viewer should appear expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeTruthy(); @@ -687,7 +690,7 @@ describe("TaskDetailModal", () => { expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull(); }); - it("passes enabled=true to useAgentLogs only when Logs → Agent Log subview is active", async () => { + it("passes enabled=true to useAgentLogs only when Activity → Raw Logs segment is active", async () => { const { useAgentLogs } = await import("../../hooks/useAgentLogs"); const mockUseAgentLogs = vi.mocked(useAgentLogs); mockUseAgentLogs.mockClear(); @@ -708,13 +711,14 @@ describe("TaskDetailModal", () => { const initialCall = mockUseAgentLogs.mock.calls[mockUseAgentLogs.mock.calls.length - 1]; expect(initialCall[1]).toBe(false); - // Switch to Logs tab (Activity subview is default) — enabled should still be false - fireEvent.click(screen.getByText("Logs")); + // Switch to Activity tab (Feed segment is default) — enabled should still be false + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Feed" })); const afterLogsClick = mockUseAgentLogs.mock.calls[mockUseAgentLogs.mock.calls.length - 1]; expect(afterLogsClick[1]).toBe(false); - // Switch to Agent Log subview — enabled should become true - fireEvent.click(screen.getByText("Agent Log")); + // Switch to Raw Activity segment — enabled should become true + fireEvent.click(screen.getByText("Raw Logs")); const afterAgentLog = mockUseAgentLogs.mock.calls[mockUseAgentLogs.mock.calls.length - 1]; expect(afterAgentLog[1]).toBe(true); }); @@ -743,7 +747,7 @@ describe("TaskDetailModal", () => { expect(container.querySelector(".markdown-body")).toBeNull(); }); - it("shows correct top-level tabs including Logs", async () => { + it("shows correct top-level tabs without Logs", async () => { const { container } = render( { ); // For an in-progress task (no workflow steps, no merge commit), the - // top-level tabs are: Activity, Plan, Logs, Changes, Review, Comments, + // top-level tabs are: Activity, Plan, Changes, Review, Comments, // Artifacts, Model, Workflow, Stats, Routing. - const tabTexts = ["Activity", "Plan", "Logs", "Changes", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing"]; + const tabTexts = ["Activity", "Plan", "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("Activity"); expect(tabs[1].textContent).toBe("Plan"); - expect(tabs[2].textContent).toBe("Logs"); + expect(tabs[2].textContent).toBe("Changes"); + expect(screen.queryByRole("button", { name: "Logs" })).toBeNull(); - expect(container.querySelectorAll(".detail-tab").length).toBe(11); + expect(container.querySelectorAll(".detail-tab").length).toBe(10); // Workflow tab should always appear even when no workflow steps are configured expect(screen.getByText("Workflow")).toBeInTheDocument(); // Commits tab should NOT appear for non-done tasks @@ -985,10 +990,11 @@ describe("TaskDetailModal", () => { />, ); - expect(screen.getByRole("button", { name: "Logs" })).toHaveClass("detail-tab-active"); + expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active"); + expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true"); 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(); + expect(container.querySelector(".detail-activity")).toBeTruthy(); }); it("FN-6574 renders Definition-only content when initialTab requests definition", () => { @@ -1040,8 +1046,9 @@ describe("TaskDetailModal", () => { expect(chatSection).toBeTruthy(); expect(chatSection!.querySelector("[data-testid='task-chat-tab']")).toBeTruthy(); - fireEvent.click(screen.getByRole("button", { name: "Logs" })); - fireEvent.click(screen.getByText("Agent Log")); + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Feed" })); + fireEvent.click(screen.getByText("Raw Logs")); expect(container.querySelector(".detail-body--chat")).toBeNull(); expect(container.querySelector(".detail-section--chat")).toBeNull(); expect(container.querySelector(".detail-body--agent-log")).toBeTruthy(); @@ -1069,8 +1076,8 @@ describe("TaskDetailModal", () => { }); }); - describe("Agent Log full-height layout", () => { - it("applies detail-body--agent-log class when Logs → Agent Log subview is active", () => { + describe("Raw Logs full-height layout", () => { + it("applies detail-body--agent-log class when Activity → Raw Logs segment is active", () => { const { container } = render( { // Initially, detail-body should NOT have the agent-log modifier expect(container.querySelector(".detail-body--agent-log")).toBeNull(); - // Switch to Logs tab, then Agent Log subview - fireEvent.click(screen.getByText("Logs")); - expect(container.querySelector(".detail-body--agent-log")).toBeNull(); // Activity subview default + // Switch to Activity tab, then Raw Activity segment + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Feed" })); + expect(container.querySelector(".detail-body--agent-log")).toBeNull(); // Feed segment default - fireEvent.click(screen.getByText("Agent Log")); + fireEvent.click(screen.getByText("Raw Logs")); // detail-body should now have the agent-log modifier class expect(container.querySelector(".detail-body--agent-log")).toBeTruthy(); @@ -1115,9 +1123,10 @@ describe("TaskDetailModal", () => { />, ); - // Switch to Logs tab, then Agent Log subview - fireEvent.click(screen.getByText("Logs")); - fireEvent.click(screen.getByText("Agent Log")); + // 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")); // The section wrapping AgentLogViewer should have the full-height class const section = container.querySelector(".detail-section--agent-log"); @@ -1138,9 +1147,10 @@ describe("TaskDetailModal", () => { />, ); - // Switch to Logs tab, then Agent Log subview first - fireEvent.click(screen.getByText("Logs")); - fireEvent.click(screen.getByText("Agent Log")); + // 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")); expect(container.querySelector(".detail-body--agent-log")).toBeTruthy(); // Now enter edit mode via the pencil button in the header diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.css.test.ts b/packages/dashboard/app/components/__tests__/TaskDetailModal.css.test.ts index 311abaca54..48eac251b2 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.css.test.ts +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.css.test.ts @@ -15,4 +15,15 @@ describe("TaskDetailModal CSS contract", () => { expect(css).toMatch(/\.detail-tabs\s*\{[^}]*touch-action\s*:\s*pan-x\s+pan-y\s*;/); 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 () => { + 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).not.toContain(".log-subview-toggle"); + expect(css).not.toContain(".log-subview-btn"); + }); }); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx index 9f9fdf49ae..ff3436eb3b 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx @@ -2112,7 +2112,8 @@ describe("TaskDetailModal", () => { expect(container.querySelector(".markdown-body")).toBeTruthy(); }, { timeout: 3000 }); - fireEvent.click(screen.getByText("Logs")); + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Feed" })); const activityList = container.querySelector(".detail-activity-list"); expect(activityList).toBeTruthy(); @@ -2258,9 +2259,9 @@ describe("TaskDetailModal", () => { /> ); - // Only standard tabs should be visible (Definition, Logs, etc.) + // Only standard tabs should be visible (Activity, Plan, etc.) without the legacy top-level Logs tab. expect(screen.getByText("Plan")).toBeDefined(); - expect(screen.getByText("Logs")).toBeDefined(); + expect(screen.queryByRole("button", { name: "Logs" })).toBeNull(); // Plugin tabs should not exist expect(screen.queryByText("Plugin A Tab")).toBeNull(); }); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.models-progress-workflow.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.models-progress-workflow.test.tsx index 03a36df9d4..5ef4bb9e14 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.models-progress-workflow.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.models-progress-workflow.test.tsx @@ -24,7 +24,7 @@ import { TaskDetailModal, TaskDetailContent } from "../TaskDetailModal"; setupTaskDetailModalHooks(); describe("TaskDetailModal", () => { - describe("Agent Log model resolution", () => { + describe("Raw Logs model resolution", () => { // AgentLogViewer only renders the model header when entries.length > 0, // so we mock useAgentLogs to return at least one entry. const mockLogEntry = { timestamp: "2026-01-01T00:00:00Z", taskId: "FN-099", text: "hello", type: "text" as const }; @@ -100,8 +100,8 @@ describe("TaskDetailModal", () => { } async function openAgentLogAndExpandModelDetails(container: HTMLElement) { - fireEvent.click(screen.getByText("Logs")); - fireEvent.click(screen.getByText("Agent Log")); + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" })); await waitFor(() => { const header = container.querySelector("[data-testid='agent-log-model-header']"); @@ -116,7 +116,7 @@ describe("TaskDetailModal", () => { return container.querySelector("[data-testid='agent-log-model-header']") as HTMLElement; } - it("uses task effective settings success path for Agent Log model display", async () => { + it("uses task effective settings success path for Raw Logs model display", async () => { const { fetchTaskEffectiveSettings, fetchSettings } = await import("../../api"); const { useAgentLogs } = await import("../../hooks/useAgentLogs"); @@ -574,8 +574,8 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByText("Logs")); - fireEvent.click(screen.getByText("Agent Log")); + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" })); await waitFor(() => { const header = container.querySelector("[data-testid='agent-log-model-header']"); expect(header).toBeTruthy(); @@ -631,8 +631,8 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByText("Logs")); - fireEvent.click(screen.getByText("Agent Log")); + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" })); await waitFor(() => { const header = container.querySelector("[data-testid='agent-log-model-header']"); expect(header).toBeTruthy(); @@ -872,7 +872,7 @@ describe("TaskDetailModal", () => { expect(segments[1].getAttribute("data-tooltip")).toBe("Add tests (in-progress)"); }); - it("step progress only renders in Definition tab, not in Agent Log subview", () => { + it("step progress only renders in Definition tab, not in Raw Logs segment", () => { const { container } = render( { // Should be visible in Definition tab expect(container.querySelector(".detail-step-progress")).toBeTruthy(); - // Switch to Logs tab, then Agent Log subview - fireEvent.click(screen.getByText("Logs")); - fireEvent.click(screen.getByText("Agent Log")); + // Switch to Activity tab, then Raw Logs segment + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" })); - // Should not be visible in Agent Log subview + // Should not be visible in Raw Logs segment expect(container.querySelector(".detail-step-progress")).toBeNull(); }); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx index 790b2eea30..9bc60929ef 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx @@ -2428,7 +2428,8 @@ describe("TaskDetailModal", () => { expect(container.querySelector(".markdown-body")).toBeTruthy(); }, { timeout: 3000 }); - fireEvent.click(screen.getByText("Logs")); + fireEvent.click(screen.getByRole("button", { name: "Activity" })); + fireEvent.click(screen.getByRole("tab", { name: "Feed" })); const activityList = container.querySelector(".detail-activity-list"); expect(activityList).toBeTruthy(); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.test.tsx index d21458e498..035dd340f0 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.test.tsx @@ -265,8 +265,8 @@ describe("TaskDetailModal GitHub tracking CTA", () => { }); }); -describe("TaskDetailModal Logs activity loading", () => { - function renderLogsModal(task: ReturnType | Record) { +describe("TaskDetailModal Activity feed loading", () => { + function renderActivityFeedModal(task: ReturnType | Record) { return render( { vi.mocked(fetchTaskDetail).mockReset(); vi.mocked(fetchTaskDetail).mockImplementationOnce(() => new Promise(() => {})); - renderLogsModal(makeSlimTask()); + renderActivityFeedModal(makeSlimTask()); expect(await screen.findByRole("status")).toHaveTextContent("Loading activity…"); expect(screen.queryByText("(no activity)")).not.toBeInTheDocument(); }); - it("shows activity loading when switching to Logs before slim task detail resolves", async () => { + it("shows activity loading when switching to Activity Feed before slim task detail resolves", async () => { const user = userEvent.setup(); const { fetchTaskDetail } = await import("../../api"); vi.mocked(fetchTaskDetail).mockReset(); @@ -320,7 +320,8 @@ describe("TaskDetailModal Logs activity loading", () => { />, ); - await user.click(screen.getByRole("button", { name: "Logs" })); + await user.click(screen.getByRole("button", { name: "Activity" })); + await user.click(screen.getByRole("tab", { name: "Feed" })); expect(await screen.findByRole("status")).toHaveTextContent("Loading activity…"); expect(screen.queryByText("(no activity)")).not.toBeInTheDocument(); }); @@ -330,7 +331,7 @@ describe("TaskDetailModal Logs activity loading", () => { vi.mocked(fetchTaskDetail).mockReset(); vi.mocked(fetchTaskDetail).mockResolvedValueOnce(makeTask({ id: "FN-6040", prompt: "# Loaded", log: [] })); - renderLogsModal(makeSlimTask()); + renderActivityFeedModal(makeSlimTask()); expect(await screen.findByText("(no activity)")).toBeInTheDocument(); expect(screen.queryByRole("status")).not.toBeInTheDocument(); @@ -348,7 +349,7 @@ describe("TaskDetailModal Logs activity loading", () => { ], })); - const { container } = renderLogsModal(makeSlimTask()); + const { container } = renderActivityFeedModal(makeSlimTask()); await screen.findByText("newer entry"); const actions = Array.from(container.querySelectorAll(".detail-log-action")).map((node) => node.textContent); @@ -366,7 +367,7 @@ describe("TaskDetailModal Logs activity loading", () => { activityLogTruncatedCount: 25, } as any)); - renderLogsModal(makeSlimTask()); + renderActivityFeedModal(makeSlimTask()); expect(await screen.findByText("Showing the most recent 1 activity entries.")).toBeInTheDocument(); expect(screen.getByText("kept entry")).toBeInTheDocument(); @@ -430,8 +431,8 @@ describe("TaskDetailModal Chat task merge", () => { }); }); -describe("TaskDetailModal Logs agent loading", () => { - it("shows the Agent Log loading indicator when entering the subview", async () => { +describe("TaskDetailModal Raw Logs agent loading", () => { + it("shows the Raw Logs loading indicator when entering the segment", async () => { const user = userEvent.setup(); const { useAgentLogs } = await import("../../hooks/useAgentLogs"); const mockUseAgentLogs = vi.mocked(useAgentLogs); @@ -458,8 +459,9 @@ describe("TaskDetailModal Logs agent loading", () => { />, ); - await user.click(screen.getByRole("button", { name: "Logs" })); - await user.click(screen.getByRole("button", { name: "Agent Log" })); + 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" })); expect(screen.getByText("Loading agent logs…")).toBeInTheDocument(); expect(screen.queryByText("No agent output yet.")).not.toBeInTheDocument(); @@ -688,8 +690,8 @@ describe("TaskDetailModal in-review stall diagnostics", () => { expect(screen.getByText("Open the Review tab to see which step is blocking, then fix the failure or override the step.")).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "View activity log" })); - expect(screen.getByRole("button", { name: "Logs" })).toHaveClass("detail-tab-active"); - expect(document.querySelector(".log-subview-toggle .log-subview-btn-active")).toHaveTextContent("Activity"); + expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active"); + expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true"); const highlighted = document.querySelector(".detail-log-entry--stall-highlight .detail-log-action"); expect(highlighted?.textContent).toContain("In-review stall surfaced [merge-blocker]"); }); diff --git a/packages/dashboard/app/hooks/useModalManager.ts b/packages/dashboard/app/hooks/useModalManager.ts index 3346ef1455..8629aa1bb3 100644 --- a/packages/dashboard/app/hooks/useModalManager.ts +++ b/packages/dashboard/app/hooks/useModalManager.ts @@ -6,8 +6,8 @@ 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. +FNXC:TaskDetailActivity 2026-06-30-22:15: +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. Legacy `logs` callers are also preserved by TaskDetailModal, which opens Activity → Feed instead of rendering a top-level Logs tab. */ export type DetailTaskTab = | "summary" diff --git a/packages/dashboard/app/plugins/types.ts b/packages/dashboard/app/plugins/types.ts index e71102e6d8..2d48d9ef57 100644 --- a/packages/dashboard/app/plugins/types.ts +++ b/packages/dashboard/app/plugins/types.ts @@ -14,8 +14,8 @@ import type { Task, TaskDetail, WorkflowStep } from "@fusion/core"; /** * 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. + * FNXC:TaskDetailActivity 2026-06-30-22:15: + * 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. Legacy `logs` requests remain accepted by the host and route to Activity → Feed because Logs is no longer a visible top-level tab. */ export type DetailTaskTab = "summary" | "chat" | "definition" | "logs" | "changes" | "comments" | "model" | "workflow" | "pr" | "retries";