diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 5597180e1c..e12beadb0a 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2876,8 +2876,8 @@ export const webviewMessageHandler = async ( if (messageIndex === -1) { break } - const msg = currentCline.clineMessages[messageIndex] as any - const existingMeta = (msg.metadata as any) || {} + const msg = currentCline.clineMessages[messageIndex] as { metadata?: { reasoning?: { startedAt?: number; elapsedMs?: number } } } + const existingMeta = msg.metadata || {} const existingReasoning = existingMeta.reasoning || {} msg.metadata = { ...existingMeta, diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 55d1ada638..e71d327bf1 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -23,6 +23,11 @@ export interface UpdateTodoListPayload { todos: any[] } +export interface ReasoningMeta { + startedAt?: number + elapsedMs?: number +} + export type EditQueuedMessagePayload = Pick export interface WebviewMessage { @@ -257,10 +262,7 @@ export interface WebviewMessage { terminalOperation?: "continue" | "abort" messageTs?: number restoreCheckpoint?: boolean - reasoningMeta?: { - startedAt?: number - elapsedMs?: number - } + reasoningMeta?: ReasoningMeta historyPreviewCollapsed?: boolean filters?: { type?: string; search?: string; tags?: string[] } settings?: any diff --git a/webview-ui/src/components/chat/ReasoningBlock.tsx b/webview-ui/src/components/chat/ReasoningBlock.tsx index fbb34344b3..1c5d823e59 100644 --- a/webview-ui/src/components/chat/ReasoningBlock.tsx +++ b/webview-ui/src/components/chat/ReasoningBlock.tsx @@ -4,37 +4,43 @@ import { useTranslation } from "react-i18next" import MarkdownBlock from "../common/MarkdownBlock" import { vscode } from "@src/utils/vscode" +interface ReasoningMeta { + startedAt?: number + elapsedMs?: number +} + interface ReasoningBlockProps { content: string ts: number isStreaming: boolean isLast: boolean - metadata?: Record + metadata?: { reasoning?: ReasoningMeta } | Record } /** * Render reasoning with a heading and a persistent timer. * - Heading uses i18n key chat:reasoning.thinking - * - Timer shown as "(⟲ 24s)" beside the heading and persists via message.metadata.reasoning { startedAt, elapsedMs } + * - Timer shown beside the heading and persists via message.metadata.reasoning { startedAt, elapsedMs } */ export const ReasoningBlock = ({ content, ts, isStreaming, isLast, metadata }: ReasoningBlockProps) => { const { t } = useTranslation() - const persisted = (metadata?.reasoning as { startedAt?: number; elapsedMs?: number } | undefined) || {} + const persisted: ReasoningMeta = (metadata?.reasoning as ReasoningMeta) || {} const startedAtRef = useRef(persisted.startedAt ?? Date.now()) const [elapsed, setElapsed] = useState(persisted.elapsedMs ?? 0) + const postedRef = useRef(false) - // Initialize startedAt on first mount if missing (persist to task) + // Initialize startedAt on first mount if missing (persist to task) - guard with postedRef useEffect(() => { - if (!persisted.startedAt && isLast) { + if (!persisted.startedAt && isLast && !postedRef.current) { + postedRef.current = true vscode.postMessage({ type: "updateMessageReasoningMeta", messageTs: ts, reasoningMeta: { startedAt: startedAtRef.current }, - } as any) + }) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ts]) + }, [ts, isLast, persisted.startedAt]) // Tick while active (last row and streaming) useEffect(() => { @@ -58,7 +64,7 @@ export const ReasoningBlock = ({ content, ts, isStreaming, isLast, metadata }: R type: "updateMessageReasoningMeta", messageTs: ts, reasoningMeta: { startedAt: startedAtRef.current, elapsedMs: finalMs }, - } as any) + }) } wasActiveRef.current = active }, [isLast, isStreaming, ts]) @@ -73,13 +79,13 @@ export const ReasoningBlock = ({ content, ts, isStreaming, isLast, metadata }: R return (
-
+
- + {t("chat:reasoning.thinking")}
- + {secondsLabel}