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(/