diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 7b3107a2bed0..67df0c2dbbba 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -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 ( setReasoningCollapsed(!reasoningCollapsed)} + ts={message.ts} + isStreaming={isStreaming} + isLast={isLast} + metadata={message.metadata as any} /> ) case "api_req_started": diff --git a/webview-ui/src/components/chat/ReasoningBlock.tsx b/webview-ui/src/components/chat/ReasoningBlock.tsx index baa93485f9fd..3c981126ef98 100644 --- a/webview-ui/src/components/chat/ReasoningBlock.tsx +++ b/webview-ui/src/components/chat/ReasoningBlock.tsx @@ -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(null) - const elapsedRef = useRef(0) - const { t } = useTranslation("chat") - const [thought, setThought] = useState() - const [prevThought, setPrevThought] = useState(t("chat:reasoning.thinking")) - const [isTransitioning, setIsTransitioning] = useState(false) - const cursorRef = useRef(0) - const queueRef = useRef([]) +/** + * 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() - useEffect(() => { - if (contentRef.current && !isCollapsed) { - contentRef.current.scrollTop = contentRef.current.scrollHeight - } - }, [content, isCollapsed]) - - 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() - }) + const startTimeRef = useRef(Date.now()) + const [elapsed, setElapsed] = useState(0) + // Simple timer that runs while streaming useEffect(() => { - if (content.length - cursorRef.current > 160) { - setThought("... " + content.slice(cursorRef.current)) - cursorRef.current = content.length + if (isLast && isStreaming) { + const tick = () => setElapsed(Date.now() - startTimeRef.current) + tick() + const id = setInterval(tick, 1000) + return () => clearInterval(id) } - }, [content]) + }, [isLast, isStreaming]) - 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 ( -
-
-
- {prevThought} -
-
- {elapsedRef.current > 1000 && ( - <> - -
{t("reasoning.seconds", { count: Math.round(elapsedRef.current / 1000) })}
- - )} - {isCollapsed ? : } +
+
+
+ + {t("chat:reasoning.thinking")}
+ {elapsed > 0 && ( + + + {secondsLabel} + + )}
- {!isCollapsed && ( -
+ {(content?.trim()?.length ?? 0) > 0 && ( +
)}