Add blinking cursor to active streaming text

This commit is contained in:
2026-06-15 13:12:08 -07:00
parent 6cf4dc494f
commit 44385514cd

View File

@@ -546,7 +546,7 @@ const MessageBubble = React.memo(function MessageBubble({
}} }}
> >
{!isUser && msg.timeline && msg.timeline.length > 0 && ( {!isUser && msg.timeline && msg.timeline.length > 0 && (
<Timeline entries={msg.timeline} /> <Timeline entries={msg.timeline} isActiveStream={sending && msgIndex === messages.length - 1} />
)} )}
{/* {/*
Render the legacy bottom content bubble ONLY when: Render the legacy bottom content bubble ONLY when:
@@ -600,7 +600,7 @@ const MessageBubble = React.memo(function MessageBubble({
* name with a ×N counter. The flow visually mirrors what actually * name with a ×N counter. The flow visually mirrors what actually
* happened: thought → tools → thought → tools → ... → final summary. * happened: thought → tools → thought → tools → ... → final summary.
*/ */
function Timeline({ entries }: { entries: TimelineEntry[] }) { function Timeline({ entries, isActiveStream }: { entries: TimelineEntry[], isActiveStream?: boolean }) {
// Walk the entries and emit a renderable list. Adjacent same-category // Walk the entries and emit a renderable list. Adjacent same-category
// tool entries get bundled into a TimelineToolGroup; thought and // tool entries get bundled into a TimelineToolGroup; thought and
// text entries pass through as-is. // text entries pass through as-is.
@@ -634,11 +634,12 @@ function Timeline({ entries }: { entries: TimelineEntry[] }) {
return ( return (
<div style={{ marginBottom: 6 }}> <div style={{ marginBottom: 6 }}>
{items.map((item, i) => { {items.map((item, i) => {
const isLast = i === items.length - 1;
if (item.kind === "thought") { if (item.kind === "thought") {
return <TimelineText key={i} text={item.text} />; return <TimelineText key={i} text={item.text} isStreaming={isActiveStream && isLast} />;
} }
if (item.kind === "text") { if (item.kind === "text") {
return <TimelineText key={i} text={item.text} />; return <TimelineText key={i} text={item.text} isStreaming={isActiveStream && isLast} />;
} }
if (item.kind === "toolGroup") { if (item.kind === "toolGroup") {
return ( return (
@@ -660,7 +661,7 @@ function Timeline({ entries }: { entries: TimelineEntry[] }) {
* bubble so each round of multi-tool-loop output reads as a discrete * bubble so each round of multi-tool-loop output reads as a discrete
* step instead of concatenating into a wall of text. * step instead of concatenating into a wall of text.
*/ */
function TimelineText({ text }: { text: string }) { function TimelineText({ text, isStreaming }: { text: string; isStreaming?: boolean }) {
const proseWrap: React.CSSProperties = { const proseWrap: React.CSSProperties = {
overflowWrap: "anywhere", overflowWrap: "anywhere",
wordBreak: "break-word", wordBreak: "break-word",
@@ -684,7 +685,7 @@ function TimelineText({ text }: { text: string }) {
<span <span
style={proseWrap} style={proseWrap}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: renderMarkdown(stripRawToolLogs(text)), __html: renderMarkdown(stripRawToolLogs(text)) + (isStreaming ? `<span class="animate-pulse" style="display:inline-block; width:6px; height:13px; background-color:#9ca3af; vertical-align:-1px; margin-left:2px; border-radius:1px;"></span>` : ""),
}} }}
/> />
</div> </div>