From ab958bf80bd0041bfead18257fe914adf83764b3 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sun, 7 Sep 2025 04:22:58 +0000 Subject: [PATCH 1/9] feat(ui): render reasoning as plain italic text to match style; remove bounding container --- webview-ui/src/components/chat/ChatRow.tsx | 10 +- .../src/components/chat/ReasoningBlock.tsx | 96 ++----------------- 2 files changed, 9 insertions(+), 97 deletions(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 7b3107a2bed..0e56c96d1d9 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) @@ -1084,14 +1083,7 @@ export const ChatRowContent = ({ ) case "reasoning": - return ( - setReasoningCollapsed(!reasoningCollapsed)} - /> - ) + return case "api_req_started": return ( <> diff --git a/webview-ui/src/components/chat/ReasoningBlock.tsx b/webview-ui/src/components/chat/ReasoningBlock.tsx index baa93485f9f..3c2a7c839e2 100644 --- a/webview-ui/src/components/chat/ReasoningBlock.tsx +++ b/webview-ui/src/components/chat/ReasoningBlock.tsx @@ -1,99 +1,19 @@ -import { useCallback, useEffect, useRef, useState } from "react" -import { CaretDownIcon, CaretUpIcon, CounterClockwiseClockIcon } from "@radix-ui/react-icons" -import { useTranslation } from "react-i18next" - import MarkdownBlock from "../common/MarkdownBlock" -import { useMount } from "react-use" interface ReasoningBlockProps { content: string - elapsed?: number - isCollapsed?: boolean - onToggleCollapse?: () => void } -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([]) - - 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() - }) - - 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]) - +/** + * Render reasoning as simple italic text, matching how content is shown. + * No borders, boxes, headers, timers, or collapsible behavior. + */ +export const ReasoningBlock = ({ content }: ReasoningBlockProps) => { return ( -
-
-
- {prevThought} -
-
- {elapsedRef.current > 1000 && ( - <> - -
{t("reasoning.seconds", { count: Math.round(elapsedRef.current / 1000) })}
- - )} - {isCollapsed ? : } -
+
+
+
- {!isCollapsed && ( -
- -
- )}
) } From c557a2faa3910e606efb1848933eca80cd9fd68d Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 8 Sep 2025 10:47:46 -0600 Subject: [PATCH 2/9] feat(chat): add Reasoning heading and persistent timer; persist timing in message.metadata; render timer in UI --- src/core/webview/webviewMessageHandler.ts | 29 +++++++ src/shared/WebviewMessage.ts | 5 ++ webview-ui/src/components/chat/ChatRow.tsx | 10 ++- .../src/components/chat/ReasoningBlock.tsx | 79 ++++++++++++++++++- 4 files changed, 119 insertions(+), 4 deletions(-) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 080fbbcd943..5597180e1cc 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2865,6 +2865,35 @@ export const webviewMessageHandler = async ( } break } + case "updateMessageReasoningMeta": { + // Persist reasoning timer metadata on a specific message (by ts) + try { + const currentCline = provider.getCurrentTask() + if (!currentCline || !message.messageTs) { + break + } + const { messageIndex } = findMessageIndices(message.messageTs, currentCline) + if (messageIndex === -1) { + break + } + const msg = currentCline.clineMessages[messageIndex] as any + const existingMeta = (msg.metadata as any) || {} + const existingReasoning = existingMeta.reasoning || {} + msg.metadata = { + ...existingMeta, + reasoning: { ...existingReasoning, ...(message.reasoningMeta || {}) }, + } + + await saveTaskMessages({ + messages: currentCline.clineMessages, + taskId: currentCline.taskId, + globalStoragePath: provider.contextProxy.globalStorageUri.fsPath, + }) + } catch (error) { + console.error("[updateMessageReasoningMeta] Failed to persist reasoning metadata:", error) + } + break + } case "showMdmAuthRequiredNotification": { // Show notification that organization requires authentication vscode.window.showWarningMessage(t("common:mdm.info.organization_requires_auth")) diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 565712bfbfc..55d1ada6387 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -221,6 +221,7 @@ export interface WebviewMessage { | "queueMessage" | "removeQueuedMessage" | "editQueuedMessage" + | "updateMessageReasoningMeta" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" @@ -256,6 +257,10 @@ export interface WebviewMessage { terminalOperation?: "continue" | "abort" messageTs?: number restoreCheckpoint?: boolean + reasoningMeta?: { + startedAt?: number + elapsedMs?: number + } historyPreviewCollapsed?: boolean filters?: { type?: string; search?: string; tags?: string[] } settings?: any diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 0e56c96d1d9..67df0c2dbbb 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1083,7 +1083,15 @@ export const ChatRowContent = ({
) case "reasoning": - return + return ( + + ) case "api_req_started": return ( <> diff --git a/webview-ui/src/components/chat/ReasoningBlock.tsx b/webview-ui/src/components/chat/ReasoningBlock.tsx index 3c2a7c839e2..6e34091fcb1 100644 --- a/webview-ui/src/components/chat/ReasoningBlock.tsx +++ b/webview-ui/src/components/chat/ReasoningBlock.tsx @@ -1,16 +1,89 @@ +import React, { useEffect, useMemo, useRef, useState } from "react" +import { useTranslation } from "react-i18next" + import MarkdownBlock from "../common/MarkdownBlock" +import { vscode } from "@src/utils/vscode" interface ReasoningBlockProps { content: string + ts: number + isStreaming: boolean + isLast: boolean + metadata?: Record +} + +function formatDuration(ms: number): string { + const totalSeconds = Math.max(0, Math.floor(ms / 1000)) + const minutes = Math.floor(totalSeconds / 60) + const seconds = totalSeconds % 60 + return `${minutes}:${seconds.toString().padStart(2, "0")}` } /** - * Render reasoning as simple italic text, matching how content is shown. - * No borders, boxes, headers, timers, or collapsible behavior. + * Render reasoning with a heading and a persistent timer. + * - Heading uses i18n key chat:reasoning.thinking + * - Timer persists via message.metadata.reasoning { startedAt, elapsedMs } */ -export const ReasoningBlock = ({ content }: ReasoningBlockProps) => { +export const ReasoningBlock = ({ content, ts, isStreaming, isLast, metadata }: ReasoningBlockProps) => { + const { t } = useTranslation() + + const persisted = (metadata?.reasoning as { startedAt?: number; elapsedMs?: number } | undefined) || {} + const startedAtRef = useRef(persisted.startedAt ?? Date.now()) + const [elapsed, setElapsed] = useState(persisted.elapsedMs ?? 0) + + // Initialize startedAt on first mount if missing (persist to task) + useEffect(() => { + if (!persisted.startedAt && isLast) { + vscode.postMessage({ + type: "updateMessageReasoningMeta", + messageTs: ts, + reasoningMeta: { startedAt: startedAtRef.current }, + } as any) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ts]) + + // Tick while active (last row and streaming) + useEffect(() => { + const active = isLast && isStreaming + if (!active) return + + const tick = () => setElapsed(Date.now() - startedAtRef.current) + tick() + const id = setInterval(tick, 1000) + return () => clearInterval(id) + }, [isLast, isStreaming]) + + // Persist final elapsed when streaming stops + const wasActiveRef = useRef(false) + useEffect(() => { + const active = isLast && isStreaming + if (wasActiveRef.current && !active) { + const finalMs = Date.now() - startedAtRef.current + setElapsed(finalMs) + vscode.postMessage({ + type: "updateMessageReasoningMeta", + messageTs: ts, + reasoningMeta: { startedAt: startedAtRef.current, elapsedMs: finalMs }, + } as any) + } + wasActiveRef.current = active + }, [isLast, isStreaming, ts]) + + const displayMs = useMemo(() => { + if (isLast && isStreaming) return elapsed + return persisted.elapsedMs ?? elapsed + }, [elapsed, isLast, isStreaming, persisted.elapsedMs]) + return (
+
+
+ + {t("chat:reasoning.thinking")} +
+ {formatDuration(displayMs)} +
From 0acab3bd7b495af629e00e64b74a33b064db5553 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 8 Sep 2025 11:04:34 -0600 Subject: [PATCH 3/9] =?UTF-8?q?ui(chat):=20show=20reasoning=20timer=20as?= =?UTF-8?q?=20(=E2=9F=B2=20Ns)=20beside=20Thinking=20heading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/chat/ReasoningBlock.tsx | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/webview-ui/src/components/chat/ReasoningBlock.tsx b/webview-ui/src/components/chat/ReasoningBlock.tsx index 6e34091fcb1..01777c7b132 100644 --- a/webview-ui/src/components/chat/ReasoningBlock.tsx +++ b/webview-ui/src/components/chat/ReasoningBlock.tsx @@ -12,17 +12,10 @@ interface ReasoningBlockProps { metadata?: Record } -function formatDuration(ms: number): string { - const totalSeconds = Math.max(0, Math.floor(ms / 1000)) - const minutes = Math.floor(totalSeconds / 60) - const seconds = totalSeconds % 60 - return `${minutes}:${seconds.toString().padStart(2, "0")}` -} - /** * Render reasoning with a heading and a persistent timer. * - Heading uses i18n key chat:reasoning.thinking - * - Timer persists via message.metadata.reasoning { startedAt, elapsedMs } + * - Timer shown as "(⟲ 24s)" beside the heading and persists via message.metadata.reasoning { startedAt, elapsedMs } */ export const ReasoningBlock = ({ content, ts, isStreaming, isLast, metadata }: ReasoningBlockProps) => { const { t } = useTranslation() @@ -75,14 +68,19 @@ export const ReasoningBlock = ({ content, ts, isStreaming, isLast, metadata }: R return persisted.elapsedMs ?? elapsed }, [elapsed, isLast, isStreaming, persisted.elapsedMs]) + const seconds = Math.max(0, Math.floor((displayMs || 0) / 1000)) + const secondsLabel = t("chat:reasoning.seconds", { count: seconds }) + return (
-
-
- - {t("chat:reasoning.thinking")} -
- {formatDuration(displayMs)} +
+ + {t("chat:reasoning.thinking")} + + ( + + {secondsLabel}) +
From 8d8111f8a7c3131f7227f8b2244f2aa1ffdd58b6 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 8 Sep 2025 11:10:29 -0600 Subject: [PATCH 4/9] =?UTF-8?q?ui(chat):=20refine=20reasoning=20timer=20?= =?UTF-8?q?=E2=80=94=20remove=20brackets,=20match=20heading=20font=20size,?= =?UTF-8?q?=20align=20inline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webview-ui/src/components/chat/ReasoningBlock.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/webview-ui/src/components/chat/ReasoningBlock.tsx b/webview-ui/src/components/chat/ReasoningBlock.tsx index 01777c7b132..06e4f6b11ee 100644 --- a/webview-ui/src/components/chat/ReasoningBlock.tsx +++ b/webview-ui/src/components/chat/ReasoningBlock.tsx @@ -76,10 +76,9 @@ export const ReasoningBlock = ({ content, ts, isStreaming, isLast, metadata }: R
{t("chat:reasoning.thinking")} - - ( - - {secondsLabel}) + + + {secondsLabel}
From ea322fd885bd386313bdb7e342d0e3709f813aa3 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 8 Sep 2025 11:56:31 -0600 Subject: [PATCH 5/9] fix(ui/chat): align 'Thinking' header with Task Completed; restore body padding (px-3); make lightbulb icon yellow --- .../src/components/chat/ReasoningBlock.tsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/webview-ui/src/components/chat/ReasoningBlock.tsx b/webview-ui/src/components/chat/ReasoningBlock.tsx index 06e4f6b11ee..fbb34344b38 100644 --- a/webview-ui/src/components/chat/ReasoningBlock.tsx +++ b/webview-ui/src/components/chat/ReasoningBlock.tsx @@ -72,18 +72,22 @@ export const ReasoningBlock = ({ content, ts, isStreaming, isLast, metadata }: R const secondsLabel = t("chat:reasoning.seconds", { count: seconds }) return ( -
-
- - {t("chat:reasoning.thinking")} - - +
+
+
+ + {t("chat:reasoning.thinking")} +
+ + {secondsLabel}
-
- -
+ {(content?.trim()?.length ?? 0) > 0 && ( +
+ +
+ )}
) } From a84d41bcbdec3a6d0a7d71c498e2517cfe770afa Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 8 Sep 2025 15:57:52 -0600 Subject: [PATCH 6/9] =?UTF-8?q?refactor(chat):=20reasoning=20UI=20tidy=20?= =?UTF-8?q?=E2=80=94=20utility=20classes,=20mb-2.5,=20safer=20effect=20gua?= =?UTF-8?q?rd;=20add=20ReasoningMeta=20typing;=20reduce=20any=20casts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/webview/webviewMessageHandler.ts | 4 +-- src/shared/WebviewMessage.ts | 10 ++++--- .../src/components/chat/ReasoningBlock.tsx | 30 +++++++++++-------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 5597180e1cc..e12beadb0a4 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 55d1ada6387..e71d327bf18 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 fbb34344b38..1c5d823e59e 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}
From a34f392099dfec2436f838a6ee384cb41ffa888c Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Mon, 8 Sep 2025 18:50:10 -0500 Subject: [PATCH 7/9] fix: prevent memory leak in ReasoningBlock timer cleanup --- .../src/components/chat/ReasoningBlock.tsx | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/chat/ReasoningBlock.tsx b/webview-ui/src/components/chat/ReasoningBlock.tsx index 1c5d823e59e..0df97d654c4 100644 --- a/webview-ui/src/components/chat/ReasoningBlock.tsx +++ b/webview-ui/src/components/chat/ReasoningBlock.tsx @@ -29,6 +29,7 @@ export const ReasoningBlock = ({ content, ts, isStreaming, isLast, metadata }: R const startedAtRef = useRef(persisted.startedAt ?? Date.now()) const [elapsed, setElapsed] = useState(persisted.elapsedMs ?? 0) const postedRef = useRef(false) + const intervalRef = useRef(null) // Initialize startedAt on first mount if missing (persist to task) - guard with postedRef useEffect(() => { @@ -49,10 +50,30 @@ export const ReasoningBlock = ({ content, ts, isStreaming, isLast, metadata }: R const tick = () => setElapsed(Date.now() - startedAtRef.current) tick() - const id = setInterval(tick, 1000) - return () => clearInterval(id) + // ensure no duplicate intervals + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + intervalRef.current = window.setInterval(tick, 1000) + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + } }, [isLast, isStreaming]) + // Cleanup on unmount (safety net) + useEffect(() => { + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + } + }, []) + // Persist final elapsed when streaming stops const wasActiveRef = useRef(false) useEffect(() => { From 1be489b9e2d36ab8a8aeeafa663aa24ffe12526f Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Mon, 8 Sep 2025 19:21:06 -0500 Subject: [PATCH 8/9] refactor: simplify ReasoningBlock by removing timer persistence - Remove unnecessary persistence logic for timer state - Remove updateMessageReasoningMeta handler from webviewMessageHandler - Remove ReasoningMeta type and related fields from WebviewMessage - Keep simple ephemeral timer that runs while reasoning is active - Only show timer when elapsed time is greater than 0 --- src/core/webview/webviewMessageHandler.ts | 29 ----- src/shared/WebviewMessage.ts | 7 -- .../src/components/chat/ReasoningBlock.tsx | 104 ++++-------------- 3 files changed, 22 insertions(+), 118 deletions(-) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index e12beadb0a4..080fbbcd943 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2865,35 +2865,6 @@ export const webviewMessageHandler = async ( } break } - case "updateMessageReasoningMeta": { - // Persist reasoning timer metadata on a specific message (by ts) - try { - const currentCline = provider.getCurrentTask() - if (!currentCline || !message.messageTs) { - break - } - const { messageIndex } = findMessageIndices(message.messageTs, currentCline) - if (messageIndex === -1) { - break - } - const msg = currentCline.clineMessages[messageIndex] as { metadata?: { reasoning?: { startedAt?: number; elapsedMs?: number } } } - const existingMeta = msg.metadata || {} - const existingReasoning = existingMeta.reasoning || {} - msg.metadata = { - ...existingMeta, - reasoning: { ...existingReasoning, ...(message.reasoningMeta || {}) }, - } - - await saveTaskMessages({ - messages: currentCline.clineMessages, - taskId: currentCline.taskId, - globalStoragePath: provider.contextProxy.globalStorageUri.fsPath, - }) - } catch (error) { - console.error("[updateMessageReasoningMeta] Failed to persist reasoning metadata:", error) - } - break - } case "showMdmAuthRequiredNotification": { // Show notification that organization requires authentication vscode.window.showWarningMessage(t("common:mdm.info.organization_requires_auth")) diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index e71d327bf18..565712bfbfc 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -23,11 +23,6 @@ export interface UpdateTodoListPayload { todos: any[] } -export interface ReasoningMeta { - startedAt?: number - elapsedMs?: number -} - export type EditQueuedMessagePayload = Pick export interface WebviewMessage { @@ -226,7 +221,6 @@ export interface WebviewMessage { | "queueMessage" | "removeQueuedMessage" | "editQueuedMessage" - | "updateMessageReasoningMeta" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" @@ -262,7 +256,6 @@ export interface WebviewMessage { terminalOperation?: "continue" | "abort" messageTs?: number restoreCheckpoint?: boolean - 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 0df97d654c4..66c5e64f395 100644 --- a/webview-ui/src/components/chat/ReasoningBlock.tsx +++ b/webview-ui/src/components/chat/ReasoningBlock.tsx @@ -1,114 +1,54 @@ -import React, { useEffect, useMemo, useRef, useState } from "react" +import React, { useEffect, useRef, useState } from "react" import { useTranslation } from "react-i18next" import MarkdownBlock from "../common/MarkdownBlock" -import { vscode } from "@src/utils/vscode" - -interface ReasoningMeta { - startedAt?: number - elapsedMs?: number -} +import { Clock, Lightbulb } from "lucide-react" interface ReasoningBlockProps { content: string ts: number isStreaming: boolean isLast: boolean - metadata?: { reasoning?: ReasoningMeta } | Record + metadata?: any } /** - * Render reasoning with a heading and a persistent timer. + * Render reasoning with a heading and a simple timer. * - Heading uses i18n key chat:reasoning.thinking - * - Timer shown beside the heading and persists via message.metadata.reasoning { startedAt, elapsedMs } + * - Timer runs while reasoning is active (no persistence) */ -export const ReasoningBlock = ({ content, ts, isStreaming, isLast, metadata }: ReasoningBlockProps) => { +export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockProps) => { const { t } = useTranslation() - 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) - const intervalRef = useRef(null) + const startTimeRef = useRef(Date.now()) + const [elapsed, setElapsed] = useState(0) - // Initialize startedAt on first mount if missing (persist to task) - guard with postedRef + // Simple timer that runs while streaming useEffect(() => { - if (!persisted.startedAt && isLast && !postedRef.current) { - postedRef.current = true - vscode.postMessage({ - type: "updateMessageReasoningMeta", - messageTs: ts, - reasoningMeta: { startedAt: startedAtRef.current }, - }) - } - }, [ts, isLast, persisted.startedAt]) - - // Tick while active (last row and streaming) - useEffect(() => { - const active = isLast && isStreaming - if (!active) return - - const tick = () => setElapsed(Date.now() - startedAtRef.current) - tick() - // ensure no duplicate intervals - if (intervalRef.current) { - clearInterval(intervalRef.current) - intervalRef.current = null - } - intervalRef.current = window.setInterval(tick, 1000) - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current) - intervalRef.current = null - } + if (isLast && isStreaming) { + const tick = () => setElapsed(Date.now() - startTimeRef.current) + tick() + const id = setInterval(tick, 1000) + return () => clearInterval(id) } }, [isLast, isStreaming]) - // Cleanup on unmount (safety net) - useEffect(() => { - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current) - intervalRef.current = null - } - } - }, []) - - // Persist final elapsed when streaming stops - const wasActiveRef = useRef(false) - useEffect(() => { - const active = isLast && isStreaming - if (wasActiveRef.current && !active) { - const finalMs = Date.now() - startedAtRef.current - setElapsed(finalMs) - vscode.postMessage({ - type: "updateMessageReasoningMeta", - messageTs: ts, - reasoningMeta: { startedAt: startedAtRef.current, elapsedMs: finalMs }, - }) - } - wasActiveRef.current = active - }, [isLast, isStreaming, ts]) - - const displayMs = useMemo(() => { - if (isLast && isStreaming) return elapsed - return persisted.elapsedMs ?? elapsed - }, [elapsed, isLast, isStreaming, persisted.elapsedMs]) - - const seconds = Math.max(0, Math.floor((displayMs || 0) / 1000)) + const seconds = Math.floor(elapsed / 1000) const secondsLabel = t("chat:reasoning.seconds", { count: seconds }) return (
- + {t("chat:reasoning.thinking")}
- - - {secondsLabel} - + {elapsed > 0 && ( + + + {secondsLabel} + + )}
{(content?.trim()?.length ?? 0) > 0 && (
From d377e45f4894406ef2aa547bf63b8fd61dc26497 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Mon, 8 Sep 2025 19:51:44 -0500 Subject: [PATCH 9/9] fix: add right padding to ReasoningBlock header for proper alignment --- webview-ui/src/components/chat/ReasoningBlock.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/components/chat/ReasoningBlock.tsx b/webview-ui/src/components/chat/ReasoningBlock.tsx index 66c5e64f395..3c981126ef9 100644 --- a/webview-ui/src/components/chat/ReasoningBlock.tsx +++ b/webview-ui/src/components/chat/ReasoningBlock.tsx @@ -38,7 +38,7 @@ export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockP return (
-
+
{t("chat:reasoning.thinking")}