Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ export const ChatRowContent = ({

const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration } = useExtensionState()
const { info: model } = useSelectedModel(apiConfiguration)
const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The state and are no longer used after the simplification. Should we remove this unused import and state declaration?

const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
const [showCopySuccess, setShowCopySuccess] = useState(false)
const [isEditing, setIsEditing] = useState(false)
Expand Down Expand Up @@ -1084,14 +1083,7 @@ export const ChatRowContent = ({
</div>
)
case "reasoning":
return (
<ReasoningBlock
content={message.text || ""}
elapsed={isLast && isStreaming ? Date.now() - message.ts : undefined}
isCollapsed={reasoningCollapsed}
onToggleCollapse={() => setReasoningCollapsed(!reasoningCollapsed)}
/>
)
return <ReasoningBlock content={message.text || ""} />
case "api_req_started":
return (
<>
Expand Down
96 changes: 8 additions & 88 deletions webview-ui/src/components/chat/ReasoningBlock.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null)
const elapsedRef = useRef<number>(0)
const { t } = useTranslation("chat")
const [thought, setThought] = useState<string>()
const [prevThought, setPrevThought] = useState<string>(t("chat:reasoning.thinking"))
const [isTransitioning, setIsTransitioning] = useState<boolean>(false)
const cursorRef = useRef<number>(0)
const queueRef = useRef<string[]>([])

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])

/**
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice documentation! Could we expand it slightly to explain the design rationale? For example:

* Render reasoning as simple italic text, matching how <thinking> content is shown.
* No borders, boxes, headers, timers, or collapsible behavior.
*/
export const ReasoningBlock = ({ content }: ReasoningBlockProps) => {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for this component. Even though it's simpler now, would it make sense to add basic tests to ensure it renders correctly and maintains the expected styling?

return (
<div className="bg-vscode-editor-background border border-vscode-border rounded-xs overflow-hidden">
<div
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>{t("reasoning.seconds", { count: Math.round(elapsedRef.current / 1000) })}</div>
</>
)}
{isCollapsed ? <CaretDownIcon /> : <CaretUpIcon />}
</div>
<div className="px-3 py-1">
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding accessibility support here. Since we're relying solely on visual styling (italic text) to distinguish reasoning content, could we add an ARIA label or role to help screen reader users? Something like:

<div className="italic text-muted-foreground">
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a more semantic class name for better maintainability. Instead of just , perhaps or ? This would make the styling intent clearer:

<MarkdownBlock markdown={content} />
</div>
{!isCollapsed && (
<div ref={contentRef} className="px-3 max-h-[160px] overflow-y-auto">
<MarkdownBlock markdown={content} />
</div>
)}
</div>
)
}
Loading