From 804532d4af4358c7a77c15579ca356ee61c94c35 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 10 Sep 2025 23:52:30 +0000 Subject: [PATCH] feat: add collapsible reasoning blocks with auto-expand setting - Added autoExpandReasoningBlocks configuration setting to control default expansion state - Created CollapsibleReasoningBlock component that wraps ReasoningBlock with collapsible UI - Updated ChatRow to use CollapsibleReasoningBlock instead of ReasoningBlock - Added preview text display when reasoning block is collapsed - Integrated with ExtensionStateContext for settings management Fixes #7873 --- packages/types/src/global-settings.ts | 1 + src/core/webview/webviewMessageHandler.ts | 4 + src/shared/ExtensionMessage.ts | 1 + src/shared/WebviewMessage.ts | 1 + webview-ui/src/components/chat/ChatRow.tsx | 4 +- .../chat/CollapsibleReasoningBlock.tsx | 82 +++++++++++++++++++ 6 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 webview-ui/src/components/chat/CollapsibleReasoningBlock.tsx diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 7e79855f7e..d82dbf46f3 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -67,6 +67,7 @@ export const globalSettingsSchema = z.object({ alwaysAllowFollowupQuestions: z.boolean().optional(), followupAutoApproveTimeoutMs: z.number().optional(), alwaysAllowUpdateTodoList: z.boolean().optional(), + autoExpandReasoningBlocks: z.boolean().optional(), allowedCommands: z.array(z.string()).optional(), deniedCommands: z.array(z.string()).optional(), commandExecutionTimeout: z.number().optional(), diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index a6e8e73a6a..8b8f6c95ed 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1553,6 +1553,10 @@ export const webviewMessageHandler = async ( await updateGlobalState("alwaysAllowFollowupQuestions", message.bool ?? false) await provider.postStateToWebview() break + case "autoExpandReasoningBlocks": + await updateGlobalState("autoExpandReasoningBlocks", message.bool ?? false) + await provider.postStateToWebview() + break case "followupAutoApproveTimeoutMs": await updateGlobalState("followupAutoApproveTimeoutMs", message.value) await provider.postStateToWebview() diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index aaddc520cb..84cd4f63df 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -229,6 +229,7 @@ export type ExtensionState = Pick< | "alwaysAllowExecute" | "alwaysAllowUpdateTodoList" | "followupAutoApproveTimeoutMs" + | "autoExpandReasoningBlocks" | "allowedCommands" | "deniedCommands" | "allowedMaxRequests" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 93d0b9bc45..a16ef2dfe6 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -49,6 +49,7 @@ export interface WebviewMessage { | "alwaysAllowFollowupQuestions" | "alwaysAllowUpdateTodoList" | "followupAutoApproveTimeoutMs" + | "autoExpandReasoningBlocks" | "webviewDidLaunch" | "newTask" | "askResponse" diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 23ec50af37..c408dbdcda 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -24,7 +24,7 @@ import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock" import CodeAccordian from "../common/CodeAccordian" import CodeBlock from "../common/CodeBlock" import MarkdownBlock from "../common/MarkdownBlock" -import { ReasoningBlock } from "./ReasoningBlock" +import { CollapsibleReasoningBlock } from "./CollapsibleReasoningBlock" import Thumbnails from "../common/Thumbnails" import ImageBlock from "../common/ImageBlock" @@ -1084,7 +1084,7 @@ export const ChatRowContent = ({ ) case "reasoning": return ( - = ({ + content, + ts, + isStreaming, + isLast, + metadata, +}) => { + const extensionState = useContext(ExtensionStateContext) + const autoExpand = extensionState?.autoExpandReasoningBlocks ?? false + + // Start with the configured default state + const [isOpen, setIsOpen] = useState(autoExpand) + + // Update when the setting changes + useEffect(() => { + setIsOpen(autoExpand) + }, [autoExpand]) + + // Extract first line or preview of the reasoning content + const getPreviewText = () => { + if (!content) return "Thinking..." + const lines = content.split("\n").filter((line) => line.trim()) + if (lines.length === 0) return "Thinking..." + + // Get first meaningful line (skip empty lines) + const firstLine = lines[0] + const maxLength = 100 + + if (firstLine.length > maxLength) { + return firstLine.substring(0, maxLength) + "..." + } + return firstLine + (lines.length > 1 ? "..." : "") + } + + return ( + +
+ +
+ {isOpen ? ( + + ) : ( + + )} + Reasoning + {!isOpen && ( + + {getPreviewText()} + + )} +
+
+ + +
+ +
+
+
+
+ ) +}