From 2232845134a9de8d066bf3852725d720e1467572 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 26 Sep 2026 08:46:41 -0400 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=92=AD=20feat:=20Bring=20the=20Stream?= =?UTF-8?q?ing=20Thought=20Peek=20Back=20Under=20a=20Collapsed=20Live=20Ca?= =?UTF-8?q?rd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #14546 previewed streaming reasoning as its trailing sentences in a short fading window under the thought's header. The live fold (#16118) put that thought inside a collapsed card, which unmounts its rows, so the peek went with them and one throttled sentence on the header stood for a paragraph of live reasoning. A collapsed live card whose tail is a streaming thought now renders the same peek under its header, outside the fold, in the cursor's place. It gives way to the rows when the card opens and to the cursor when a call, not a thought, is at the tail. --- .../Messages/Content/ActivityPhaseGroup.tsx | 36 +++++++++--- .../Chat/Messages/Content/Parts/Reasoning.tsx | 3 +- .../Chat/Messages/Content/Parts/index.ts | 7 ++- .../__tests__/ActivityPhaseGroup.test.tsx | 55 +++++++++++++++++++ .../ContentParts.integration.test.tsx | 3 + 5 files changed, 94 insertions(+), 10 deletions(-) diff --git a/client/src/components/Chat/Messages/Content/ActivityPhaseGroup.tsx b/client/src/components/Chat/Messages/Content/ActivityPhaseGroup.tsx index d9520e4e2f1..7d7eccb671b 100644 --- a/client/src/components/Chat/Messages/Content/ActivityPhaseGroup.tsx +++ b/client/src/components/Chat/Messages/Content/ActivityPhaseGroup.tsx @@ -31,7 +31,7 @@ import { StackedToolIcons } from './ToolOutput'; import { getSourceDomains } from './sources'; import { mapAttachments } from '~/utils/map'; import SearchVerticals from './verticals'; -import { AttachmentGroup } from './Parts'; +import { AttachmentGroup, StreamingThoughtPeek } from './Parts'; import { cn } from '~/utils'; /** Matches `EXPAND_TRANSITION` so the panel and the label ticker resolve on @@ -682,13 +682,32 @@ export default function ActivityPhaseGroup({ * right for the streaming-markdown cursor, wrong here — so `after:!static` * puts that one pseudo-element back in flow for the slot to center; the * `!` is what outranks the dot rule's three-class selector. */ - const cursor = showCursor ? ( -
-
- ) : null; + /** The thought streaming at the tail of a collapsed live card, shown the + * way an unfolded thought shows it: the trailing sentences in a short + * fading window under the header (#14546). The fold had swallowed that + * peek with the rows, leaving one throttled sentence on the header to + * stand for a paragraph of live reasoning. It takes the cursor's place: + * moving text is its own sign the run is alive. */ + const streamingThought = useMemo(() => { + if (!isLive || isExpanded || liveParts == null) { + return ''; + } + const tail = liveParts[liveParts.length - 1]; + if (tail?.type !== ContentTypes.THINK) { + return ''; + } + return typeof tail.think === 'string' ? tail.think : (tail.think?.value ?? ''); + }, [isLive, isExpanded, liveParts]); + const thoughtPeek = + streamingThought.trim() !== '' ? : null; + const cursor = + showCursor && thoughtPeek == null ? ( +
+
+ ) : null; /** `AttachmentGroup` drops `web_search` attachments, and the nested segment * renders with `hideAttachments` so its own `WebSearch` row stands down * for this hoist — so without `SearchVerticals` here a phase containing a @@ -827,6 +846,7 @@ export default function ActivityPhaseGroup({ return ( <> {group} + {thoughtPeek} {media} {cursor} diff --git a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx index 8565ea28a0f..b94c70c46e4 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx @@ -65,7 +65,7 @@ const PEEK_FADE = * out loud" treatment popularized by Grok). Decorative only (aria-hidden); the * toggle button above it provides the accessible control. */ -const StreamingThoughtPeek = memo(({ text }: { text: string }) => { +export const StreamingThoughtPeek = memo(({ text }: { text: string }) => { const ref = useRef(null); const fontSize = useAtomValue(fontSizeAtom); const peek = useMemo(() => lastSentences(text), [text]); @@ -88,6 +88,7 @@ const StreamingThoughtPeek = memo(({ text }: { text: string }) => {