From dfbb11bdd7c3f1a5575cb55d3e3abb12be025727 Mon Sep 17 00:00:00 2001
From: Bilal Bakr <62337003+Bil0000@users.noreply.github.com>
Date: Sat, 19 Sep 2026 12:05:49 +0300
Subject: [PATCH] fix(web): show plain text in collapsed thought previews
(#12377)
Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com>
---
.../components/chat/MessagesTimeline.test.tsx | 92 +++++++++++++++++++
.../src/components/chat/MessagesTimeline.tsx | 33 ++++++-
2 files changed, 124 insertions(+), 1 deletion(-)
diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx
index 3bf5f482948dd9f31fe9bb3e5486c92373e792ef..dae27ccdba473134395d2b7a89a7c13d74f5ff37 100644
--- a/apps/web/src/components/chat/MessagesTimeline.test.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx
@@ -1656,6 +1656,98 @@ describe("MessagesTimeline", () => {
expect(markup).not.toContain("tool call failed");
});
+ it.each(
+ (
+ [
+ [
+ "**Viewing image first** with *care*, ~~old~~ `code` and [context](https://example.com)",
+ "Viewing image first with care, old code and context",
+ 1,
+ ],
+ ["first paragraph\n\nsecond paragraph", "first paragraph second paragraph", 0],
+ ["- first\n- second", "first second", 0],
+ ["first \nsecond", "first second", 0],
+ ["", "image description", 0],
+ ["", "Thought", 0],
+ ["---", "Thought", 0],
+ ] as const
+ ).flatMap(([markdown, expected, strongCount]) =>
+ [false, true].map((streaming) => ({
+ markdown,
+ expected,
+ strongCount,
+ streaming,
+ })),
+ ),
+ )(
+ "shows a plain thought preview for $markdown, streaming=$streaming",
+ async ({ markdown, expected, strongCount, streaming }) => {
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
+ vi.stubGlobal("requestAnimationFrame", () => 0);
+ vi.stubGlobal("cancelAnimationFrame", () => {});
+ const turnId = TurnId.make("turn-thought");
+ const thought = buildAssistantTimelineEntry(markdown);
+ let renderer: ReactTestRenderer | undefined;
+ try {
+ await act(() => {
+ renderer = create(
+ ,
+ );
+ });
+ await act(() => renderer!.root.findByProps({ "aria-expanded": false }).props.onClick());
+ const text = renderer!.root.findByProps({
+ className: "relative min-w-0 flex-1 truncate text-secondary-label",
+ });
+ const preview = text.parent!;
+ expect(
+ text
+ .findAll(() => true)
+ .flatMap((node) => node.children)
+ .filter((child) => typeof child === "string")
+ .join(""),
+ ).toBe(
+ (streaming && expected === "Thought" ? "Thinking" : expected).repeat(streaming ? 2 : 1),
+ );
+ expect(
+ preview.findAll((node) =>
+ ["strong", "em", "del", "code", "a"].includes(String(node.type)),
+ ),
+ ).toHaveLength(0);
+ await act(() => preview.props.onClick());
+ expect(renderer!.root.findAllByType("strong")).toHaveLength(strongCount);
+ await act(() => preview.props.onClick());
+ expect(renderer!.root.findAllByType("strong")).toHaveLength(0);
+ } finally {
+ await act(() => renderer?.unmount());
+ }
+ },
+ );
+
it("renders initial thinking as the shared live activity row", () => {
const turnId = TurnId.make("turn-live");
const markup = renderToStaticMarkup(
diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx
index 8305cb1d5283ba8aad221e73ba83ab43dad65eab..42a45b9910281f72c479bdd9ed358cdbd53ff342 100644
--- a/apps/web/src/components/chat/MessagesTimeline.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.tsx
@@ -103,6 +103,9 @@ import {
} from "../../lib/diffRendering";
import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting";
import ChatMarkdown, { ChatMarkdownAssetImage } from "../ChatMarkdown";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import type { Root, RootContent } from "mdast";
import { T3Wordmark } from "../T3Wordmark";
import {
BotIcon,
@@ -2689,6 +2692,28 @@ function ThinkingTimelineRow() {
);
}
+function remarkThoughtPreview(fallback: string) {
+ return (tree: Root) => {
+ const plainText = (node: Root | RootContent): string => {
+ if (node.type === "html" || node.type === "definition") return "";
+ if ("alt" in node) return node.alt ?? "";
+ if ("value" in node) return node.value;
+ if ("children" in node) {
+ const separator = ["root", "blockquote", "list", "listItem", "table", "tableRow"].includes(
+ node.type,
+ )
+ ? " "
+ : "";
+ return node.children.map(plainText).join(separator);
+ }
+ return node.type === "break" ? " " : "";
+ };
+ tree.children = [
+ { type: "text", value: plainText(tree).replace(/\s+/g, " ").trim() || fallback },
+ ];
+ };
+}
+
/**
* Thinking inside a tool group has its own disclosure, preserved across recycling.
* A group whose row already reads "Thought" (no visible tool) skips the header.
@@ -2722,7 +2747,13 @@ function ReasoningTraceBlock({
}
const label = streaming ? "Thinking" : "Thought";
const collapsedPreview = messages.find((message) => message.text.trim().length > 0)?.text.trim();
- const headerText = expanded ? label : (collapsedPreview ?? label);
+ const headerText = expanded ? (
+ label
+ ) : (
+
+ {collapsedPreview ?? label}
+
+ );
return (
{showHeader ? (
--
2.54.0