FN-7974: collapse chat thinking blocks by default

Collapse Thinking reasoning blocks by default so chat transcripts stay scannable without manually closing each block.

- Remove the open attribute from TaskChatTab thinking details so blocks start collapsed
- Strengthen ChatView and TaskChatTab tests for collapsed-by-default and expand-on-click across persisted, streaming, and Task Detail surfaces
- Add a patch changeset for the operator-facing transcript UX fix

Files changed:
 .changeset/fn-7974-collapse-thinking.md            |  7 ++++++
 packages/dashboard/app/components/TaskChatTab.tsx  |  6 ++++-
 .../__tests__/ChatView.core-interactions.test.tsx  | 26 +++++++++++++++++-----
 .../app/components/__tests__/TaskChatTab.test.tsx  | 13 +++++++----
 4 files changed, 41 insertions(+), 11 deletions(-)

Fusion-Task-Id: FN-7974

Fusion-Task-Lineage: 7cd9009f-3483-444c-8024-ed6b1cec3b89

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-15 14:15:50 -07:00
parent 2179a61db9
commit d74018ff81
4 changed files with 41 additions and 11 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Chat "Thinking" reasoning blocks now start collapsed for a cleaner transcript.
category: fix
dev: Removed the `open` attribute from TaskChatTab's task-chat-thinking <details>; StandardChatSurface already collapses thinking. Regression tests assert collapsed-by-default + expand-on-click across persisted, streaming, and Task Detail chat surfaces (FN-7974).

View File

@@ -533,12 +533,16 @@ 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)
*/
function TaskChatThinking({ entries }: { entries: AgentLogEntry[] }) {
const { t } = useTranslation("app");
const combinedThinkingText = entries.map((entry) => entry.text).join("");
return (
<details className="task-chat-thinking" data-testid="task-chat-thinking" open>
<details className="task-chat-thinking" data-testid="task-chat-thinking">
<summary className="task-chat-thinking-summary">
<span>{t("taskChat.thinking", "Thinking")}</span>
<TaskChatTimestamp timestamp={getLatestEntryTimestamp(entries)} label="Thinking block timestamp" />

View File

@@ -855,7 +855,8 @@ describe("ChatView core interactions", () => {
expect(streamingMessage?.textContent).toContain("Typing");
});
it("shows thinking blocks collapsed by default", async () => {
it("keeps persisted thinking blocks collapsed until expanded", async () => {
const user = userEvent.setup();
setupMockChat({
activeSession: { id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" },
messages: [
@@ -866,9 +867,15 @@ describe("ChatView core interactions", () => {
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
const message = screen.getByTestId("chat-message-msg-001");
const details = message.querySelector("details");
const details = message.querySelector("details") as HTMLDetailsElement;
expect(details).toBeInTheDocument();
expect(details).toHaveProperty("open", false);
expect(details).not.toHaveAttribute("open");
expect(within(message).getByText("I need to think about this...")).not.toBeVisible();
await user.click(within(details).getByText("Thinking"));
expect(details).toHaveAttribute("open");
expect(within(message).getByText("I need to think about this...")).toBeVisible();
});
describe("streaming states", () => {
@@ -995,7 +1002,8 @@ describe("ChatView core interactions", () => {
expect(typingIndicator?.querySelectorAll("span").length).toBe(3);
});
it("shows thinking indicator when streaming thinking arrives before text", async () => {
it("keeps streaming thinking collapsed until expanded", async () => {
const user = userEvent.setup();
setupMockChat({
activeSession: { id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" },
messages: [
@@ -1014,9 +1022,15 @@ describe("ChatView core interactions", () => {
expect(streamingMessage?.textContent).toContain("Thinking");
// Thinking details should be rendered
const thinkingDetails = streamingMessage?.querySelector("details.chat-message-thinking");
const thinkingDetails = streamingMessage?.querySelector("details.chat-message-thinking") as HTMLDetailsElement;
expect(thinkingDetails).toBeInTheDocument();
expect(thinkingDetails?.querySelector(".chat-message-thinking-content")?.textContent).toContain("analyzing the request");
expect(thinkingDetails).not.toHaveAttribute("open");
expect(within(thinkingDetails).getByText("analyzing the request...")).not.toBeVisible();
await user.click(within(thinkingDetails).getByText("Thinking"));
expect(thinkingDetails).toHaveAttribute("open");
expect(within(thinkingDetails).getByText("analyzing the request...")).toBeVisible();
// Typing indicator dots should be rendered
const typingIndicator = streamingMessage?.querySelector(".chat-typing-indicator");

View File

@@ -997,7 +997,7 @@ describe("TaskChatTab", () => {
expect(within(standaloneEntry).getByLabelText("Tool entry timestamp")).toHaveTextContent(expectedTime);
});
it("renders thinking in an expanded-by-default collapsible block", async () => {
it("renders thinking in a collapsed-by-default expandable block", async () => {
const user = userEvent.setup();
mockLogs([
makeEntry({ agent: "triage", type: "thinking", text: "I am considering options" }),
@@ -1006,13 +1006,18 @@ describe("TaskChatTab", () => {
render(<TaskChatTab task={makeTask()} active addToast={vi.fn()} />);
const thinking = screen.getByTestId("task-chat-thinking");
expect(thinking).toHaveAttribute("open");
expect(thinking).not.toHaveAttribute("open");
expect(within(thinking).getByText("Thinking")).toBeVisible();
expect(screen.getByText("I am considering options")).toBeVisible();
expect(screen.getByText("I am considering options")).not.toBeVisible();
expect(within(thinking).getAllByTestId("task-chat-entry-thinking")).toHaveLength(1);
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();
});
@@ -1055,7 +1060,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")).toBeVisible();
expect(screen.getByText("thinking between tools")).not.toBeVisible();
});
it("appends newly streamed entries from the hook without auto-opening tool groups", () => {