UI: Render reasoning as plain italic (match <thinking>) (#7752)

Co-authored-by: Roo Code <roomote@roocode.com>
Co-authored-by: Hannes Rudolph <hrudolph@gmail.com>
Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com>
This commit is contained in:
roomote[bot] 2025-09-09 13:18:33 -04:00 committed by GitHub
parent 39030bf273
commit bbd3d9883b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 42 additions and 81 deletions

View file

@ -118,7 +118,6 @@ export const ChatRowContent = ({
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration } = useExtensionState()
const { info: model } = useSelectedModel(apiConfiguration)
const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
const [showCopySuccess, setShowCopySuccess] = useState(false)
const [isEditing, setIsEditing] = useState(false)
@ -1087,9 +1086,10 @@ export const ChatRowContent = ({
return (
<ReasoningBlock
content={message.text || ""}
elapsed={isLast && isStreaming ? Date.now() - message.ts : undefined}
isCollapsed={reasoningCollapsed}
onToggleCollapse={() => setReasoningCollapsed(!reasoningCollapsed)}
ts={message.ts}
isStreaming={isStreaming}
isLast={isLast}
metadata={message.metadata as any}
/>
)
case "api_req_started":

View file

@ -1,96 +1,57 @@
import { useCallback, useEffect, useRef, useState } from "react"
import { CaretDownIcon, CaretUpIcon, CounterClockwiseClockIcon } from "@radix-ui/react-icons"
import React, { useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import MarkdownBlock from "../common/MarkdownBlock"
import { useMount } from "react-use"
import { Clock, Lightbulb } from "lucide-react"
interface ReasoningBlockProps {
content: string
elapsed?: number
isCollapsed?: boolean
onToggleCollapse?: () => void
ts: number
isStreaming: boolean
isLast: boolean
metadata?: any
}
export const ReasoningBlock = ({ content, elapsed, isCollapsed = false, onToggleCollapse }: ReasoningBlockProps) => {
const contentRef = useRef<HTMLDivElement>(null)
const elapsedRef = useRef<number>(0)
const { t } = useTranslation("chat")
const [thought, setThought] = useState<string>()
const [prevThought, setPrevThought] = useState<string>(t("chat:reasoning.thinking"))
const [isTransitioning, setIsTransitioning] = useState<boolean>(false)
const cursorRef = useRef<number>(0)
const queueRef = useRef<string[]>([])
/**
* Render reasoning with a heading and a simple timer.
* - Heading uses i18n key chat:reasoning.thinking
* - Timer runs while reasoning is active (no persistence)
*/
export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockProps) => {
const { t } = useTranslation()
const startTimeRef = useRef<number>(Date.now())
const [elapsed, setElapsed] = useState<number>(0)
// Simple timer that runs while streaming
useEffect(() => {
if (contentRef.current && !isCollapsed) {
contentRef.current.scrollTop = contentRef.current.scrollHeight
if (isLast && isStreaming) {
const tick = () => setElapsed(Date.now() - startTimeRef.current)
tick()
const id = setInterval(tick, 1000)
return () => clearInterval(id)
}
}, [content, isCollapsed])
}, [isLast, isStreaming])
useEffect(() => {
if (elapsed) {
elapsedRef.current = elapsed
}
}, [elapsed])
// Process the transition queue.
const processNextTransition = useCallback(() => {
const nextThought = queueRef.current.pop()
queueRef.current = []
if (nextThought) {
setIsTransitioning(true)
}
setTimeout(() => {
if (nextThought) {
setPrevThought(nextThought)
setIsTransitioning(false)
}
setTimeout(() => processNextTransition(), 500)
}, 200)
}, [])
useMount(() => {
processNextTransition()
})
useEffect(() => {
if (content.length - cursorRef.current > 160) {
setThought("... " + content.slice(cursorRef.current))
cursorRef.current = content.length
}
}, [content])
useEffect(() => {
if (thought && thought !== prevThought) {
queueRef.current.push(thought)
}
}, [thought, prevThought])
const seconds = Math.floor(elapsed / 1000)
const secondsLabel = t("chat:reasoning.seconds", { count: seconds })
return (
<div className="bg-vscode-editor-background border border-vscode-border rounded-xs overflow-hidden">
<div
className="flex items-center justify-between gap-1 px-3 py-2 cursor-pointer text-muted-foreground"
onClick={onToggleCollapse}>
<div
className={`truncate flex-1 transition-opacity duration-200 ${isTransitioning ? "opacity-0" : "opacity-100"}`}>
{prevThought}
</div>
<div className="flex flex-row items-center gap-1">
{elapsedRef.current > 1000 && (
<>
<CounterClockwiseClockIcon className="scale-80" />
<div>{t("reasoning.seconds", { count: Math.round(elapsedRef.current / 1000) })}</div>
</>
)}
{isCollapsed ? <CaretDownIcon /> : <CaretUpIcon />}
<div className="py-1">
<div className="flex items-center justify-between mb-2.5 pr-2">
<div className="flex items-center gap-2">
<Lightbulb className="w-4" />
<span className="font-bold text-vscode-foreground">{t("chat:reasoning.thinking")}</span>
</div>
{elapsed > 0 && (
<span className="text-vscode-foreground tabular-nums flex items-center gap-1">
<Clock className="w-4" />
{secondsLabel}
</span>
)}
</div>
{!isCollapsed && (
<div ref={contentRef} className="px-3 max-h-[160px] overflow-y-auto">
{(content?.trim()?.length ?? 0) > 0 && (
<div className="px-3 italic text-vscode-descriptionForeground">
<MarkdownBlock markdown={content} />
</div>
)}