feat: Improve the display of thinking content in the desktop UI (#8414)

Signed-off-by: jh-block <jhugo@block.xyz>
This commit is contained in:
jh-block
2026-04-08 22:35:04 +02:00
committed by GitHub
parent 8b1007eee0
commit f2f5baab74
3 changed files with 71 additions and 36 deletions
+6 -28
View File
@@ -2,6 +2,7 @@ import { useMemo, useRef } from 'react';
import ImagePreview from './ImagePreview';
import { formatMessageTimestamp } from '../utils/timeUtils';
import MarkdownContent from './MarkdownContent';
import ThinkingContent from './ThinkingContent';
import ToolCallWithResponse from './ToolCallWithResponse';
import {
getTextAndImageContent,
@@ -47,27 +48,9 @@ export default function GooseMessage({
}: GooseMessageProps) {
const contentRef = useRef<HTMLDivElement | null>(null);
let { textContent, imagePaths } = getTextAndImageContent(message);
const { textContent: displayText, imagePaths } = getTextAndImageContent(message);
const thinkingContent = getThinkingContent(message);
const splitChainOfThought = (text: string): { displayText: string; cotText: string | null } => {
const regex = /<think>([\s\S]*?)<\/think>/i;
const match = text.match(regex);
if (!match) {
return { displayText: text, cotText: null };
}
const cotRaw = match[1].trim();
const displayText = text.replace(regex, '').trim();
return {
displayText,
cotText: cotRaw || null,
};
};
const { displayText, cotText } = splitChainOfThought(textContent);
const timestamp = useMemo(() => formatMessageTimestamp(message.created), [message.created]);
const toolRequests = getToolRequests(message);
const messageIndex = messages.findIndex((msg) => msg.id === message.id);
@@ -132,15 +115,10 @@ export default function GooseMessage({
<div className="goose-message flex w-[90%] justify-start min-w-0">
<div className="flex flex-col w-full min-w-0">
{thinkingContent && (
<div className="mb-2 text-xs text-gray-400/70 italic">
<MarkdownContent content={thinkingContent} />
</div>
)}
{cotText && (
<div className="mb-2 text-sm text-gray-400 italic">
<MarkdownContent content={cotText} />
</div>
<ThinkingContent
content={thinkingContent}
isExpanded={isStreaming && !displayText.trim() && imagePaths.length === 0 && toolRequests.length === 0}
/>
)}
{(displayText.trim() || imagePaths.length > 0) && (
@@ -0,0 +1,37 @@
import { useState, useEffect, useRef } from 'react';
import MarkdownContent from './MarkdownContent';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
import Expand from './ui/Expand';
interface ThinkingContentProps {
content: string;
isExpanded: boolean;
}
export default function ThinkingContent({ content, isExpanded }: ThinkingContentProps) {
const [manualToggle, setManualToggle] = useState<boolean | null>(null);
const prevIsExpanded = useRef(isExpanded);
useEffect(() => {
if (prevIsExpanded.current && !isExpanded) {
setManualToggle(null);
}
prevIsExpanded.current = isExpanded;
}, [isExpanded]);
const expanded = manualToggle !== null ? manualToggle : isExpanded;
return (
<Collapsible open={expanded} onOpenChange={(open) => setManualToggle(open)} className="mb-2">
<CollapsibleTrigger className="flex items-center gap-1.5 text-xs text-text-secondary hover:text-text-primary transition-colors cursor-pointer">
<Expand size={3} isExpanded={expanded} />
<span className="italic">Thinking</span>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-1 ml-[18px] text-xs text-text-secondary italic">
<MarkdownContent content={content} />
</div>
</CollapsibleContent>
</Collapsible>
);
}
+28 -8
View File
@@ -94,19 +94,39 @@ export function getTextAndImageContent(message: Message): {
}
}
// Strip <think> tags from assistant text — the thinking is surfaced via getThinkingContent
if (message.role === 'assistant') {
textContent = textContent.replace(/<think>[\s\S]*?<\/think>/gi, '');
}
return { textContent, imagePaths };
}
export function getThinkingContent(message: Message): string | null {
const thinkingContents = message.content
.filter((content) => content.type === 'thinking')
.map((content) => {
if ('thinking' in content) return content.thinking;
return '';
})
.filter((text) => text.length > 0);
const parts: string[] = [];
return thinkingContents.length > 0 ? thinkingContents.join('') : null;
// Structured thinking content blocks
for (const content of message.content) {
if (content.type === 'thinking' && 'thinking' in content && content.thinking) {
parts.push(content.thinking);
}
}
// Inline <think> tags in assistant text content
if (message.role === 'assistant') {
for (const content of message.content) {
if (content.type === 'text') {
const regex = /<think>([\s\S]*?)<\/think>/gi;
let match;
while ((match = regex.exec(content.text)) !== null) {
const text = match[1].trim();
if (text) parts.push(text);
}
}
}
}
return parts.length > 0 ? parts.join('') : null;
}
export function getToolRequests(message: Message): (ToolRequest & { type: 'toolRequest' })[] {