11import React , { useEffect , useRef , useState } from "react"
22import { useTranslation } from "react-i18next"
3+ import { useExtensionState } from "@src/context/ExtensionStateContext"
34
45import MarkdownBlock from "../common/MarkdownBlock"
5- import { Lightbulb } from "lucide-react"
6+ import { Lightbulb , ChevronDown , ChevronRight } from "lucide-react"
7+ import { cn } from "@/lib/utils"
68
79interface ReasoningBlockProps {
810 content : string
@@ -16,12 +18,23 @@ interface ReasoningBlockProps {
1618 * Render reasoning with a heading and a simple timer.
1719 * - Heading uses i18n key chat:reasoning.thinking
1820 * - Timer runs while reasoning is active (no persistence)
21+ * - Can be collapsed to show only last 2 lines of content
1922 */
2023export const ReasoningBlock = ( { content, isStreaming, isLast } : ReasoningBlockProps ) => {
2124 const { t } = useTranslation ( )
25+ const { reasoningBlockCollapsed } = useExtensionState ( )
26+
27+ // Initialize collapsed state based on global setting (default to collapsed)
28+ const [ isCollapsed , setIsCollapsed ] = useState ( reasoningBlockCollapsed !== false )
2229
2330 const startTimeRef = useRef < number > ( Date . now ( ) )
2431 const [ elapsed , setElapsed ] = useState < number > ( 0 )
32+ const contentRef = useRef < HTMLDivElement > ( null )
33+
34+ // Update collapsed state when global setting changes
35+ useEffect ( ( ) => {
36+ setIsCollapsed ( reasoningBlockCollapsed !== false )
37+ } , [ reasoningBlockCollapsed ] )
2538
2639 // Simple timer that runs while streaming
2740 useEffect ( ( ) => {
@@ -36,22 +49,50 @@ export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockP
3649 const seconds = Math . floor ( elapsed / 1000 )
3750 const secondsLabel = t ( "chat:reasoning.seconds" , { count : seconds } )
3851
52+ const handleToggle = ( ) => {
53+ setIsCollapsed ( ! isCollapsed )
54+ }
55+
3956 return (
40- < div >
41- < div className = "flex items-center justify-between mb-2.5 pr-2" >
57+ < div className = "group" >
58+ < div
59+ className = "flex items-center justify-between mb-2.5 pr-2 cursor-pointer select-none"
60+ onClick = { handleToggle } >
4261 < div className = "flex items-center gap-2" >
4362 < Lightbulb className = "w-4" />
4463 < span className = "font-bold text-vscode-foreground" > { t ( "chat:reasoning.thinking" ) } </ span >
64+ { elapsed > 0 && (
65+ < span className = "text-sm text-vscode-descriptionForeground tabular-nums" > { secondsLabel } </ span >
66+ ) }
67+ </ div >
68+ < div className = "opacity-0 group-hover:opacity-100 transition-opacity" >
69+ { isCollapsed ? < ChevronRight className = "w-4" /> : < ChevronDown className = "w-4" /> }
4570 </ div >
46- { elapsed > 0 && (
47- < span className = "text-sm text-vscode-descriptionForeground tabular-nums flex items-center gap-1" >
48- { secondsLabel }
49- </ span >
50- ) }
5171 </ div >
5272 { ( content ?. trim ( ) ?. length ?? 0 ) > 0 && (
53- < div className = "border-l border-vscode-descriptionForeground/20 ml-2 pl-4 pb-1 text-vscode-descriptionForeground" >
54- < MarkdownBlock markdown = { content } />
73+ < div
74+ ref = { contentRef }
75+ className = { cn (
76+ "border-l border-vscode-descriptionForeground/20 ml-2 pl-4 pb-1 text-vscode-descriptionForeground" ,
77+ isCollapsed && "relative overflow-hidden" ,
78+ ) }
79+ style = {
80+ isCollapsed
81+ ? {
82+ maxHeight : "3em" , // Approximately 2 lines
83+ maskImage : "linear-gradient(to top, transparent 0%, black 30%)" ,
84+ WebkitMaskImage : "linear-gradient(to top, transparent 0%, black 30%)" ,
85+ }
86+ : undefined
87+ } >
88+ { isCollapsed ? (
89+ // When collapsed, render content in a container that shows bottom-aligned text
90+ < div className = "flex flex-col justify-end" style = { { minHeight : "3em" } } >
91+ < MarkdownBlock markdown = { content } />
92+ </ div >
93+ ) : (
94+ < MarkdownBlock markdown = { content } />
95+ ) }
5596 </ div >
5697 ) }
5798 </ div >
0 commit comments