From 3cc1d9373f0067dd03d39a759771a3979efcf008 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 4 Aug 2026 01:54:22 -0700 Subject: [PATCH] FN-8780: expand Activity thinking blocks by default Make Task Detail Activity reasoning immediately readable in every workflow column. - Default newly mounted thinking segments to expanded state - Preserve user collapse choices while streamed reasoning updates - Add coverage for default, toggle, and streaming behavior Files changed: .changeset/fn-8780-activity-thinking-open.md | 7 ++ packages/dashboard/app/components/TaskChatTab.tsx | 25 ++----- .../app/components/__tests__/TaskChatTab.test.tsx | 79 +++++++++------------- 3 files changed, 45 insertions(+), 66 deletions(-) Fusion-Task-Id: FN-8780 Fusion-Task-Lineage: 51a0be0d-0bc8-4e1f-ac1d-b30583d7b202 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8780-activity-thinking-open.md | 7 ++ .../dashboard/app/components/TaskChatTab.tsx | 25 ++---- .../components/__tests__/TaskChatTab.test.tsx | 87 ++++++++----------- 3 files changed, 49 insertions(+), 70 deletions(-) create mode 100644 .changeset/fn-8780-activity-thinking-open.md diff --git a/.changeset/fn-8780-activity-thinking-open.md b/.changeset/fn-8780-activity-thinking-open.md new file mode 100644 index 0000000000..23e42e38ae --- /dev/null +++ b/.changeset/fn-8780-activity-thinking-open.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Expand Task Detail Activity thinking blocks by default. +category: fix +dev: Preserves per-block collapse state while streamed reasoning updates arrive. diff --git a/packages/dashboard/app/components/TaskChatTab.tsx b/packages/dashboard/app/components/TaskChatTab.tsx index 73c28e3605..701c6f5103 100644 --- a/packages/dashboard/app/components/TaskChatTab.tsx +++ b/packages/dashboard/app/components/TaskChatTab.tsx @@ -1,5 +1,5 @@ import type { AgentLogEntry, AgentRole, SteeringComment, Task, TaskDetail } from "@fusion/core"; -import { isCompleteColumnRole, isReviewColumnRole, isWipColumnRole } from "../utils/columnRoles"; +import { isCompleteColumnRole, isWipColumnRole } from "../utils/columnRoles"; import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; @@ -586,13 +586,10 @@ function TaskChatToolGroup({ entries }: { entries: AgentLogEntry[] }) { } /* - FNXC:Chat-Thinking 2026-07-15-10:33: - Chat thinking (reasoning) blocks render collapsed by default so the response is scannable without manually closing each block; the summary remains an expand-on-click affordance. (FN-7974) - - FNXC:Chat-Thinking 2026-07-16-18:05: - FN-8171 keeps the scannable collapsed default for idle task columns, but opens Live Activity thinking for in-progress and in-review tasks so operators can follow active or awaiting-review reasoning at a glance. The expanded block remains user-collapsible. +FNXC:Chat-Thinking 2026-08-04-08:15: +FN-8780 requires every newly mounted Task Detail Activity thinking segment to start expanded, regardless of workflow column, so operators can read reasoning immediately. State remains controlled after mount: the summary still lets operators collapse or reopen a segment, and stable segment identity preserves that choice during streaming. */ -function TaskChatThinking({ entries, defaultOpen = false }: { entries: AgentLogEntry[]; defaultOpen?: boolean }) { +function TaskChatThinking({ entries, defaultOpen = true }: { entries: AgentLogEntry[]; defaultOpen?: boolean }) { const { t } = useTranslation("app"); const [open, setOpen] = useState(defaultOpen); const combinedThinkingText = entries.map((entry) => entry.text).join(""); @@ -622,12 +619,12 @@ function TaskChatThinking({ entries, defaultOpen = false }: { entries: AgentLogE ); } -function TaskChatSegmentView({ segment, defaultOpen }: { segment: TaskChatSegment; defaultOpen?: boolean }) { +function TaskChatSegmentView({ segment }: { segment: TaskChatSegment }) { if (segment.kind === "tool") { return ; } if (segment.kind === "thinking") { - return ; + return ; } return ; } @@ -672,14 +669,6 @@ export function TaskChatTab({ task, columnFlags, projectId, active, addToast, on const [optimisticMessages, setOptimisticMessages] = useState([]); const [isTranscriptAtBottom, setIsTranscriptAtBottom] = useState(true); const isTranscriptAtBottomRef = useRef(true); - /* - FNXC:WorkflowResolvedColumns 2026-07-30-02:10 (batch-dashboard-app): - WIP and REVIEW roles, resolved. This decides whether the thinking transcript is expanded by - default — open while work is live, collapsed once it is not. Keyed on the literals, a renamed - board collapsed it for every card, so an operator watching an active run had to expand it by hand - on every task, every time. - */ - const thinkingDefaultOpen = isWipColumnRole(columnFlags, task.column) || isReviewColumnRole(columnFlags, task.column); const transcriptRef = useRef(null); const previousEntryCountRef = useRef(0); const previousScrollHeightRef = useRef(0); @@ -1103,7 +1092,7 @@ export function TaskChatTab({ task, columnFlags, projectId, active, addToast, on A genuinely new segment always has a different startIndex, so identity stays correct without the volatile suffix. */ const segmentKey = `${segment.kind}-${segment.startIndex}`; - return ; + return ; })} diff --git a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx index 8037469f94..062cf7e563 100644 --- a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx @@ -1149,7 +1149,26 @@ describe("TaskChatTab", () => { expect(within(standaloneEntry).getByLabelText("Tool entry timestamp")).toHaveTextContent(expectedTime); }); - it("renders thinking in a collapsed-by-default expandable block for inactive tasks", async () => { + it.each([ + ["WIP", "in-progress"], + ["review", "in-review"], + ["idle", "todo"], + ["planning", "triage"], + ["terminal", "done"], + ["archived", "archived"], + ] as const)("defaults thinking blocks open for %s tasks", (_state, column) => { + mockLogs([ + makeEntry({ agent: "executor", type: "thinking", text: "Immediately readable reasoning" }), + ]); + + render(); + + const thinking = screen.getByTestId("task-chat-thinking"); + expect(thinking).toHaveAttribute("open"); + expect(screen.getByText("Immediately readable reasoning")).toBeVisible(); + }); + + it("lets users collapse and reopen initially expanded thinking blocks", async () => { const user = userEvent.setup(); mockLogs([ makeEntry({ agent: "triage", type: "thinking", text: "I am considering options" }), @@ -1158,57 +1177,20 @@ describe("TaskChatTab", () => { render(); const thinking = screen.getByTestId("task-chat-thinking"); - expect(thinking).not.toHaveAttribute("open"); + expect(thinking).toHaveAttribute("open"); expect(within(thinking).getByText("Thinking")).toBeVisible(); - expect(screen.getByText("I am considering options")).not.toBeVisible(); + expect(screen.getByText("I am considering options")).toBeVisible(); expect(within(thinking).getAllByTestId("task-chat-entry-thinking")).toHaveLength(1); await user.click(within(thinking).getByText("Thinking")); + expect(thinking).not.toHaveAttribute("open"); + expect(screen.getByText("I am considering options")).not.toBeVisible(); + + await user.click(within(thinking).getByText("Thinking")); + expect(thinking).toHaveAttribute("open"); expect(screen.getByText("I am considering options")).toBeVisible(); - - await user.click(within(thinking).getByText("Thinking")); - - expect(thinking).not.toHaveAttribute("open"); - expect(screen.getByText("I am considering options")).not.toBeVisible(); - }); - - it.each(["in-progress", "in-review"] as const)("defaults thinking blocks open for %s tasks", (column) => { - mockLogs([ - makeEntry({ agent: "executor", type: "thinking", text: "Live reasoning" }), - ]); - - render(); - - expect(screen.getByTestId("task-chat-thinking")).toHaveAttribute("open"); - }); - - it.each(["todo", "done", "triage", "archived"] as const)("keeps thinking blocks collapsed for %s tasks", (column) => { - mockLogs([ - makeEntry({ agent: "executor", type: "thinking", text: "Historical reasoning" }), - ]); - - render(); - - expect(screen.getByTestId("task-chat-thinking")).not.toHaveAttribute("open"); - }); - - it("lets users collapse auto-expanded thinking blocks", async () => { - const user = userEvent.setup(); - mockLogs([ - makeEntry({ agent: "executor", type: "thinking", text: "Active reasoning" }), - ]); - - render(); - - const thinking = screen.getByTestId("task-chat-thinking"); - expect(thinking).toHaveAttribute("open"); - - await user.click(within(thinking).getByText("Thinking")); - - expect(thinking).not.toHaveAttribute("open"); - expect(screen.getByText("Active reasoning")).not.toBeVisible(); }); it("renders consecutive thinking entries as one continuous section", () => { @@ -1220,6 +1202,7 @@ describe("TaskChatTab", () => { render(); const thinking = screen.getByTestId("task-chat-thinking"); + expect(thinking).toHaveAttribute("open"); const summary = thinking.querySelector("summary"); expect(summary).toBeTruthy(); expect(within(summary as HTMLElement).getByText("Thinking")).toBeVisible(); @@ -1234,7 +1217,7 @@ describe("TaskChatTab", () => { FNXC:TaskChatSegmentKey 2026-07-22-12:00: Segment keys must be kind + startIndex only. Embedding entries.length remounted the actively streaming segment on every appended entry, collapsing an expanded thinking block mid-stream. */ - it("keeps an expanded thinking block expanded while entries stream into the same segment", async () => { + it("keeps a user-collapsed thinking block collapsed while entries stream into the same segment", async () => { const user = userEvent.setup(); mockLogs([ makeEntry({ agent: "executor", type: "thinking", text: "First streamed thought" }), @@ -1243,9 +1226,9 @@ describe("TaskChatTab", () => { const { rerender } = render(); const thinking = screen.getByTestId("task-chat-thinking"); - expect(thinking).not.toHaveAttribute("open"); - await user.click(within(thinking).getByText("Thinking")); expect(thinking).toHaveAttribute("open"); + await user.click(within(thinking).getByText("Thinking")); + expect(thinking).not.toHaveAttribute("open"); mockLogs([ makeEntry({ agent: "executor", type: "thinking", text: "First streamed thought" }), @@ -1253,8 +1236,8 @@ describe("TaskChatTab", () => { ]); rerender(); - expect(screen.getByTestId("task-chat-thinking")).toHaveAttribute("open"); - expect(screen.getByText(/Second streamed thought/)).toBeVisible(); + expect(screen.getByTestId("task-chat-thinking")).not.toHaveAttribute("open"); + expect(screen.getByText(/Second streamed thought/)).not.toBeVisible(); }); it("gives a genuinely new segment a fresh instance with defaultOpen applied", async () => { @@ -1302,7 +1285,7 @@ describe("TaskChatTab", () => { expect(within(toolGroups[1]).getByLabelText("Tool names")).toHaveTextContent("second tool"); expect(screen.getAllByTestId("task-chat-entry-text")).toHaveLength(1); expect(screen.getByText("plain response")).toBeVisible(); - expect(screen.getByText("thinking between tools")).not.toBeVisible(); + expect(screen.getByText("thinking between tools")).toBeVisible(); }); it("appends newly streamed entries from the hook without auto-opening tool groups", () => { @@ -2962,7 +2945,7 @@ describe("TaskChatTab", () => { const source = readFileSync(resolve(__dirname, "../TaskDetailModal.tsx"), "utf8"); const taskChatMounts = source.match(/