From 335082582eb4bc4c6192c102dec31a87df8d06d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Mon, 1 Jun 2026 22:08:18 +0800 Subject: [PATCH] fix(desktop): keep thinking details hidden by default Thinking markdown should remain available only after the user opens the disclosure. The prior preview mounted markdown content while collapsed, which made internal thinking occupy visible transcript space by default. Constraint: Thinking content must stay behind the existing disclosure while preserving markdown rendering after expansion Rejected: Ten-line collapsed markdown preview | still exposes thinking content in the normal chat flow Confidence: high Scope-risk: narrow Directive: Do not render ThinkingBlock content while aria-expanded=false; keep MarkdownRenderer inside the expanded branch Tested: cd desktop && bun run test -- chatBlocks.test.tsx Tested: bun run check:desktop Tested: git diff --check Not-tested: Live desktop visual smoke with a real provider transcript Related: #660 --- desktop/src/components/chat/ThinkingBlock.tsx | 58 ++----------------- .../src/components/chat/chatBlocks.test.tsx | 20 +++++-- 2 files changed, 21 insertions(+), 57 deletions(-) diff --git a/desktop/src/components/chat/ThinkingBlock.tsx b/desktop/src/components/chat/ThinkingBlock.tsx index d43889b4..284f9a3f 100644 --- a/desktop/src/components/chat/ThinkingBlock.tsx +++ b/desktop/src/components/chat/ThinkingBlock.tsx @@ -2,20 +2,12 @@ import { useState, useEffect, useMemo, useRef } from 'react' import { useTranslation } from '../../i18n' import { MarkdownRenderer } from '../markdown/MarkdownRenderer' -const PREVIEW_LINE_LIMIT = 10 - export function ThinkingBlock({ content, isActive = false }: { content: string; isActive?: boolean }) { const t = useTranslation() const [expanded, setExpanded] = useState(false) const contentRef = useRef(null) const displayContent = useMemo(() => content.replace(/\r\n?/g, '\n').trimEnd(), [content]) const hasDisplayContent = displayContent.trim().length > 0 - const displayLines = useMemo(() => displayContent.split('\n'), [displayContent]) - const previewContent = useMemo( - () => displayLines.slice(0, PREVIEW_LINE_LIMIT).join('\n'), - [displayLines], - ) - const isPreviewTruncated = displayLines.length > PREVIEW_LINE_LIMIT useEffect(() => { if (expanded && isActive && contentRef.current) { @@ -40,29 +32,20 @@ export function ThinkingBlock({ content, isActive = false }: { content: string; {isActive && } - {hasDisplayContent && ( + {expanded && hasDisplayContent && (
setExpanded(true) : undefined} - className={`relative mt-1 rounded-lg border border-[var(--color-border)]/40 bg-[var(--color-surface-container-lowest)] p-2.5 text-[11px] text-[var(--color-text-secondary)] ${ - expanded - ? 'max-h-[300px] overflow-y-auto' - : 'thinking-preview-clamp cursor-pointer' - }`} + ref={contentRef} + data-thinking-content="expanded" + className="relative mt-1 max-h-[300px] overflow-y-auto rounded-lg border border-[var(--color-border)]/40 bg-[var(--color-surface-container-lowest)] p-2.5 text-[11px] text-[var(--color-text-secondary)]" > - {!expanded && isPreviewTruncated &&
)} @@ -89,15 +72,6 @@ const thinkingStyles = ` margin-left: 1px; animation: thinking-cursor-blink 1s step-end infinite; } -.thinking-inline-cursor { - display: inline-block; - width: 1px; - height: 0.95em; - margin-left: 3px; - vertical-align: text-bottom; - background: var(--color-text-tertiary); - animation: thinking-cursor-blink 1s step-end infinite; -} .thinking-dots::after { content: ''; animation: thinking-dots 1.4s steps(1, end) infinite; @@ -110,24 +84,4 @@ const thinkingStyles = ` .thinking-markdown > :last-child > :last-child { margin-bottom: 0; } -.thinking-preview-clamp { - max-height: calc(${PREVIEW_LINE_LIMIT} * 1.25rem + 1.25rem); - overflow: hidden; -} -.thinking-preview-fade { - position: absolute; - left: 0; - right: 0; - bottom: 0; - height: 1.75rem; - border-bottom-left-radius: 0.5rem; - border-bottom-right-radius: 0.5rem; - pointer-events: none; - background: linear-gradient(to bottom, transparent, var(--color-surface-container-lowest)); -} -.thinking-preview-active-cursor { - position: absolute; - right: 0.6rem; - bottom: 0.45rem; -} ` diff --git a/desktop/src/components/chat/chatBlocks.test.tsx b/desktop/src/components/chat/chatBlocks.test.tsx index 22985b67..25e41d3c 100644 --- a/desktop/src/components/chat/chatBlocks.test.tsx +++ b/desktop/src/components/chat/chatBlocks.test.tsx @@ -18,32 +18,42 @@ describe('chat blocks', () => { const { container } = render() expect(screen.getByText(/Thinking/)).toBeTruthy() - expect(container.textContent).toContain('this is a long internal reasoning trace') + expect(container.textContent).not.toContain('this is a long internal reasoning trace') expect(container.querySelector('.thinking-cursor')).toBeNull() }) it('does not animate inactive historical thinking blocks', () => { const { container } = render() - expect(container.querySelector('.thinking-inline-cursor')).toBeNull() + fireEvent.click(screen.getByRole('button', { name: /Thinking/ })) + + expect(container.textContent).toContain('old reasoning') + expect(container.querySelector('.thinking-cursor')).toBeNull() }) - it('renders collapsed thinking content as markdown', () => { + it('renders thinking content as markdown only after expanding', () => { const { container } = render() + expect(container.textContent).not.toContain('important') + expect(container.querySelector('strong')).toBeNull() + expect(container.querySelector('li')).toBeNull() + + fireEvent.click(screen.getByRole('button', { name: /Thinking/ })) + expect(container.querySelector('strong')?.textContent).toBe('important') expect(container.querySelector('li')?.textContent).toBe('item one') }) - it('limits collapsed thinking content to ten lines until expanded', () => { + it('hides full thinking content until expanded', () => { const content = Array.from({ length: 12 }, (_, index) => `line-${index + 1}`).join('\n') const { container } = render() - expect(container.textContent).toContain('line-10') + expect(container.textContent).not.toContain('line-1') expect(container.textContent).not.toContain('line-11') fireEvent.click(screen.getByRole('button', { name: /Thinking/ })) + expect(container.textContent).toContain('line-1') expect(container.textContent).toContain('line-11') expect(container.textContent).toContain('line-12') })