From b3fd1a2e232f059b9ba6d480795445624ff8206c Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Fri, 28 Feb 2025 09:08:58 -0800 Subject: [PATCH] Prettier thinking blocks --- webview-ui/src/components/chat/ChatRow.tsx | 59 +++------ .../src/components/chat/ReasoningBlock.tsx | 117 +++++++++++------- webview-ui/src/index.css | 2 + 3 files changed, 89 insertions(+), 89 deletions(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 4017ccf318..1533bba3a8 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -16,7 +16,7 @@ import { vscode } from "../../utils/vscode" import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian" import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock" import MarkdownBlock from "../common/MarkdownBlock" -import ReasoningBlock from "./ReasoningBlock" +import { ReasoningBlock } from "./ReasoningBlock" import Thumbnails from "../common/Thumbnails" import McpResourceRow from "../mcp/McpResourceRow" import McpToolRow from "../mcp/McpToolRow" @@ -25,12 +25,12 @@ import { CheckpointSaved } from "./checkpoints/CheckpointSaved" interface ChatRowProps { message: ClineMessage - isExpanded: boolean - onToggleExpand: () => void lastModifiedMessage?: ClineMessage + isExpanded: boolean isLast: boolean - onHeightChange: (isTaller: boolean) => void isStreaming: boolean + onToggleExpand: () => void + onHeightChange: (isTaller: boolean) => void } interface ChatRowContentProps extends Omit {} @@ -43,10 +43,7 @@ const ChatRow = memo( const prevHeightRef = useRef(0) const [chatrow, { height }] = useSize( -
+
, ) @@ -75,33 +72,32 @@ export default ChatRow export const ChatRowContent = ({ message, - isExpanded, - onToggleExpand, lastModifiedMessage, + isExpanded, isLast, isStreaming, + onToggleExpand, }: ChatRowContentProps) => { const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState() - const [reasoningCollapsed, setReasoningCollapsed] = useState(false) + const [reasoningCollapsed, setReasoningCollapsed] = useState(true) - // Auto-collapse reasoning when new messages arrive - useEffect(() => { - if (!isLast && message.say === "reasoning") { - setReasoningCollapsed(true) - } - }, [isLast, message.say]) const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => { if (message.text !== null && message.text !== undefined && message.say === "api_req_started") { const info: ClineApiReqInfo = JSON.parse(message.text) return [info.cost, info.cancelReason, info.streamingFailedMessage] } + return [undefined, undefined, undefined] }, [message.text, message.say]) - // when resuming task, last wont be api_req_failed but a resume_task message, so api_req_started will show loading spinner. that's why we just remove the last api_req_started that failed without streaming anything + + // When resuming task, last wont be api_req_failed but a resume_task + // message, so api_req_started will show loading spinner. That's why we just + // remove the last api_req_started that failed without streaming anything. const apiRequestFailedMessage = isLast && lastModifiedMessage?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried ? lastModifiedMessage?.text : undefined + const isCommandExecuting = isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING) @@ -428,32 +424,6 @@ export const ChatRowContent = ({ /> ) - // case "inspectSite": - // const isInspecting = - // isLast && lastModifiedMessage?.say === "inspect_site_result" && !lastModifiedMessage?.images - // return ( - // <> - //
- // {isInspecting ? : toolIcon("inspect")} - // - // {message.type === "ask" ? ( - // <>Roo wants to inspect this website: - // ) : ( - // <>Roo is inspecting this website: - // )} - // - //
- //
- // - //
- // - // ) case "switchMode": return ( <> @@ -501,6 +471,7 @@ export const ChatRowContent = ({ return ( setReasoningCollapsed(!reasoningCollapsed)} /> diff --git a/webview-ui/src/components/chat/ReasoningBlock.tsx b/webview-ui/src/components/chat/ReasoningBlock.tsx index 0c9971f269..fa12899092 100644 --- a/webview-ui/src/components/chat/ReasoningBlock.tsx +++ b/webview-ui/src/components/chat/ReasoningBlock.tsx @@ -1,70 +1,97 @@ -import React, { useEffect, useRef } from "react" -import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock" +import { useCallback, useEffect, useRef, useState } from "react" +import { CaretDownIcon, CaretUpIcon, CounterClockwiseClockIcon } from "@radix-ui/react-icons" + import MarkdownBlock from "../common/MarkdownBlock" +import { useMount } from "react-use" interface ReasoningBlockProps { content: string + elapsed?: number isCollapsed?: boolean onToggleCollapse?: () => void - autoHeight?: boolean } -const ReasoningBlock: React.FC = ({ - content, - isCollapsed = false, - onToggleCollapse, - autoHeight = false, -}) => { +export const ReasoningBlock = ({ content, elapsed, isCollapsed = false, onToggleCollapse }: ReasoningBlockProps) => { const contentRef = useRef(null) + const elapsedRef = useRef(0) + const [thought, setThought] = useState() + const [prevThought, setPrevThought] = useState("Thinking") + const [isTransitioning, setIsTransitioning] = useState(false) + const cursorRef = useRef(0) + const queueRef = useRef([]) - // Scroll to bottom when content updates 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]) + return ( -
+
- Reasoning - + className="flex items-center justify-between gap-1 px-3 py-2 cursor-pointer text-muted-foreground" + onClick={onToggleCollapse}> +
+ {prevThought} +
+
+ {elapsedRef.current > 1000 && ( + <> + +
{Math.round(elapsedRef.current / 1000)}s
+ + )} + {isCollapsed ? : } +
{!isCollapsed && ( -
-
- -
+
+
)}
) } - -export default ReasoningBlock diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index 53025be01a..74c8463b37 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -64,6 +64,8 @@ --color-vscode-editor-foreground: var(--vscode-editor-foreground); --color-vscode-editor-background: var(--vscode-editor-background); + --color-vscode-editorGroup-border: var(--vscode-editorGroup-border); + --color-vscode-button-foreground: var(--vscode-button-foreground); --color-vscode-button-background: var(--vscode-button-background); --color-vscode-button-secondaryForeground: var(--vscode-button-secondaryForeground);