Skip to content

Commit

Permalink
Merge pull request #1256 from RooVetGit/cte/pretty-thinking
Browse files Browse the repository at this point in the history
Prettier thinking blocks
  • Loading branch information
cte authored Feb 28, 2025
2 parents b6a9bc9 + 360e47d commit 8fa770e
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 89 deletions.
5 changes: 5 additions & 0 deletions .changeset/young-hornets-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": patch
---

Prettier thinking blocks
59 changes: 15 additions & 44 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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<ChatRowProps, "onHeightChange"> {}
Expand All @@ -43,10 +43,7 @@ const ChatRow = memo(
const prevHeightRef = useRef(0)

const [chatrow, { height }] = useSize(
<div
style={{
padding: "10px 6px 10px 15px",
}}>
<div className="px-[15px] py-[10px] pr-[6px]">
<ChatRowContent {...props} />
</div>,
)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -428,32 +424,6 @@ export const ChatRowContent = ({
/>
</>
)
// case "inspectSite":
// const isInspecting =
// isLast && lastModifiedMessage?.say === "inspect_site_result" && !lastModifiedMessage?.images
// return (
// <>
// <div style={headerStyle}>
// {isInspecting ? <ProgressIndicator /> : toolIcon("inspect")}
// <span style={{ fontWeight: "bold" }}>
// {message.type === "ask" ? (
// <>Roo wants to inspect this website:</>
// ) : (
// <>Roo is inspecting this website:</>
// )}
// </span>
// </div>
// <div
// style={{
// borderRadius: 3,
// border: "1px solid var(--vscode-editorGroup-border)",
// overflow: "hidden",
// backgroundColor: CODE_BLOCK_BG_COLOR,
// }}>
// <CodeBlock source={`${"```"}shell\n${tool.path}\n${"```"}`} forceWrap={true} />
// </div>
// </>
// )
case "switchMode":
return (
<>
Expand Down Expand Up @@ -501,6 +471,7 @@ export const ChatRowContent = ({
return (
<ReasoningBlock
content={message.text || ""}
elapsed={isLast && isStreaming ? Date.now() - message.ts : undefined}
isCollapsed={reasoningCollapsed}
onToggleCollapse={() => setReasoningCollapsed(!reasoningCollapsed)}
/>
Expand Down
117 changes: 72 additions & 45 deletions webview-ui/src/components/chat/ReasoningBlock.tsx
Original file line number Diff line number Diff line change
@@ -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<ReasoningBlockProps> = ({
content,
isCollapsed = false,
onToggleCollapse,
autoHeight = false,
}) => {
export const ReasoningBlock = ({ content, elapsed, isCollapsed = false, onToggleCollapse }: ReasoningBlockProps) => {
const contentRef = useRef<HTMLDivElement>(null)
const elapsedRef = useRef<number>(0)
const [thought, setThought] = useState<string>()
const [prevThought, setPrevThought] = useState<string>("Thinking")
const [isTransitioning, setIsTransitioning] = useState<boolean>(false)
const cursorRef = useRef<number>(0)
const queueRef = useRef<string[]>([])

// 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 (
<div
style={{
backgroundColor: CODE_BLOCK_BG_COLOR,
border: "1px solid var(--vscode-editorGroup-border)",
borderRadius: "3px",
overflow: "hidden",
}}>
<div className="bg-vscode-editor-background border border-vscode-border rounded-xs overflow-hidden">
<div
onClick={onToggleCollapse}
style={{
padding: "8px 12px",
cursor: "pointer",
userSelect: "none",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
borderBottom: isCollapsed ? "none" : "1px solid var(--vscode-editorGroup-border)",
}}>
<span style={{ fontWeight: "bold" }}>Reasoning</span>
<span className={`codicon codicon-chevron-${isCollapsed ? "right" : "down"}`}></span>
className="flex items-center justify-between gap-1 px-3 py-2 cursor-pointer text-muted-foreground"
onClick={onToggleCollapse}>
<div
className={`truncate flex-1 transition-opacity duration-200 ${isTransitioning ? "opacity-0" : "opacity-100"}`}>
{prevThought}
</div>
<div className="flex flex-row items-center gap-1">
{elapsedRef.current > 1000 && (
<>
<CounterClockwiseClockIcon className="scale-80" />
<div>{Math.round(elapsedRef.current / 1000)}s</div>
</>
)}
{isCollapsed ? <CaretDownIcon /> : <CaretUpIcon />}
</div>
</div>
{!isCollapsed && (
<div
ref={contentRef}
style={{
padding: "8px 12px",
maxHeight: autoHeight ? "none" : "160px",
overflowY: "auto",
}}>
<div
style={{
fontSize: "13px",
opacity: 0.9,
}}>
<MarkdownBlock markdown={content} />
</div>
<div ref={contentRef} className="px-3 max-h-[160px] overflow-y-auto">
<MarkdownBlock markdown={content} />
</div>
)}
</div>
)
}

export default ReasoningBlock
2 changes: 2 additions & 0 deletions webview-ui/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 8fa770e

Please sign in to comment.