From f2f5baab74d0348a08619e01a4d8ed4aa7ef04e8 Mon Sep 17 00:00:00 2001 From: jh-block Date: Wed, 8 Apr 2026 22:35:04 +0200 Subject: [PATCH] feat: Improve the display of thinking content in the desktop UI (#8414) Signed-off-by: jh-block --- ui/desktop/src/components/GooseMessage.tsx | 34 +++-------------- ui/desktop/src/components/ThinkingContent.tsx | 37 +++++++++++++++++++ ui/desktop/src/types/message.ts | 36 ++++++++++++++---- 3 files changed, 71 insertions(+), 36 deletions(-) create mode 100644 ui/desktop/src/components/ThinkingContent.tsx diff --git a/ui/desktop/src/components/GooseMessage.tsx b/ui/desktop/src/components/GooseMessage.tsx index 71227fb3..c609e92b 100644 --- a/ui/desktop/src/components/GooseMessage.tsx +++ b/ui/desktop/src/components/GooseMessage.tsx @@ -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(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 = /([\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({
{thinkingContent && ( -
- -
- )} - - {cotText && ( -
- -
+ )} {(displayText.trim() || imagePaths.length > 0) && ( diff --git a/ui/desktop/src/components/ThinkingContent.tsx b/ui/desktop/src/components/ThinkingContent.tsx new file mode 100644 index 00000000..037d30af --- /dev/null +++ b/ui/desktop/src/components/ThinkingContent.tsx @@ -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(null); + const prevIsExpanded = useRef(isExpanded); + + useEffect(() => { + if (prevIsExpanded.current && !isExpanded) { + setManualToggle(null); + } + prevIsExpanded.current = isExpanded; + }, [isExpanded]); + + const expanded = manualToggle !== null ? manualToggle : isExpanded; + + return ( + setManualToggle(open)} className="mb-2"> + + + Thinking + + +
+ +
+
+
+ ); +} diff --git a/ui/desktop/src/types/message.ts b/ui/desktop/src/types/message.ts index 0d94effa..d63bdc76 100644 --- a/ui/desktop/src/types/message.ts +++ b/ui/desktop/src/types/message.ts @@ -94,19 +94,39 @@ export function getTextAndImageContent(message: Message): { } } + // Strip tags from assistant text — the thinking is surfaced via getThinkingContent + if (message.role === 'assistant') { + textContent = textContent.replace(/[\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 tags in assistant text content + if (message.role === 'assistant') { + for (const content of message.content) { + if (content.type === 'text') { + const regex = /([\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' })[] {