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:
7
.changeset/fn-7974-collapse-thinking.md
Normal file
7
.changeset/fn-7974-collapse-thinking.md
Normal 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).
|
||||||
@@ -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[] }) {
|
function TaskChatThinking({ entries }: { entries: AgentLogEntry[] }) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const combinedThinkingText = entries.map((entry) => entry.text).join("");
|
const combinedThinkingText = entries.map((entry) => entry.text).join("");
|
||||||
|
|
||||||
return (
|
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">
|
<summary className="task-chat-thinking-summary">
|
||||||
<span>{t("taskChat.thinking", "Thinking")}</span>
|
<span>{t("taskChat.thinking", "Thinking")}</span>
|
||||||
<TaskChatTimestamp timestamp={getLatestEntryTimestamp(entries)} label="Thinking block timestamp" />
|
<TaskChatTimestamp timestamp={getLatestEntryTimestamp(entries)} label="Thinking block timestamp" />
|
||||||
|
|||||||
@@ -855,7 +855,8 @@ describe("ChatView core interactions", () => {
|
|||||||
expect(streamingMessage?.textContent).toContain("Typing");
|
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({
|
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" },
|
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: [
|
messages: [
|
||||||
@@ -866,9 +867,15 @@ describe("ChatView core interactions", () => {
|
|||||||
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||||
|
|
||||||
const message = screen.getByTestId("chat-message-msg-001");
|
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).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", () => {
|
describe("streaming states", () => {
|
||||||
@@ -995,7 +1002,8 @@ describe("ChatView core interactions", () => {
|
|||||||
expect(typingIndicator?.querySelectorAll("span").length).toBe(3);
|
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({
|
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" },
|
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: [
|
messages: [
|
||||||
@@ -1014,9 +1022,15 @@ describe("ChatView core interactions", () => {
|
|||||||
expect(streamingMessage?.textContent).toContain("Thinking");
|
expect(streamingMessage?.textContent).toContain("Thinking");
|
||||||
|
|
||||||
// Thinking details should be rendered
|
// 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).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
|
// Typing indicator dots should be rendered
|
||||||
const typingIndicator = streamingMessage?.querySelector(".chat-typing-indicator");
|
const typingIndicator = streamingMessage?.querySelector(".chat-typing-indicator");
|
||||||
|
|||||||
@@ -997,7 +997,7 @@ describe("TaskChatTab", () => {
|
|||||||
expect(within(standaloneEntry).getByLabelText("Tool entry timestamp")).toHaveTextContent(expectedTime);
|
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();
|
const user = userEvent.setup();
|
||||||
mockLogs([
|
mockLogs([
|
||||||
makeEntry({ agent: "triage", type: "thinking", text: "I am considering options" }),
|
makeEntry({ agent: "triage", type: "thinking", text: "I am considering options" }),
|
||||||
@@ -1006,13 +1006,18 @@ describe("TaskChatTab", () => {
|
|||||||
render(<TaskChatTab task={makeTask()} active addToast={vi.fn()} />);
|
render(<TaskChatTab task={makeTask()} active addToast={vi.fn()} />);
|
||||||
|
|
||||||
const thinking = screen.getByTestId("task-chat-thinking");
|
const thinking = screen.getByTestId("task-chat-thinking");
|
||||||
expect(thinking).toHaveAttribute("open");
|
expect(thinking).not.toHaveAttribute("open");
|
||||||
expect(within(thinking).getByText("Thinking")).toBeVisible();
|
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);
|
expect(within(thinking).getAllByTestId("task-chat-entry-thinking")).toHaveLength(1);
|
||||||
|
|
||||||
await user.click(within(thinking).getByText("Thinking"));
|
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(thinking).not.toHaveAttribute("open");
|
||||||
expect(screen.getByText("I am considering options")).not.toBeVisible();
|
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(within(toolGroups[1]).getByLabelText("Tool names")).toHaveTextContent("second tool");
|
||||||
expect(screen.getAllByTestId("task-chat-entry-text")).toHaveLength(1);
|
expect(screen.getAllByTestId("task-chat-entry-text")).toHaveLength(1);
|
||||||
expect(screen.getByText("plain response")).toBeVisible();
|
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", () => {
|
it("appends newly streamed entries from the hook without auto-opening tool groups", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user