Skip to content

Commit e42dbe9

Browse files
committed
feat: Add collapsed/expanded states to ReasoningBlock
- Add collapsible UI with chevron icon that shows on hover - Default to collapsed state showing only last 2 lines - Move elapsed time counter next to thinking label - Add smooth transitions and gradient mask for collapsed state
1 parent 12f94fc commit e42dbe9

File tree

2 files changed

+60
-10
lines changed

2 files changed

+60
-10
lines changed

webview-ui/src/components/chat/ReasoningBlock.tsx

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import React, { useEffect, useRef, useState } from "react"
22
import { useTranslation } from "react-i18next"
3+
import { useExtensionState } from "@src/context/ExtensionStateContext"
34

45
import 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

79
interface 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
*/
2023
export 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>

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { convertTextMateToHljs } from "@src/utils/textMateToHljs"
2626

2727
export interface ExtensionStateContextType extends ExtensionState {
2828
historyPreviewCollapsed?: boolean // Add the new state property
29+
reasoningBlockCollapsed?: boolean // Add reasoning block collapsed state
2930
didHydrateState: boolean
3031
showWelcome: boolean
3132
theme: any
@@ -142,6 +143,7 @@ export interface ExtensionStateContextType extends ExtensionState {
142143
terminalCompressProgressBar?: boolean
143144
setTerminalCompressProgressBar: (value: boolean) => void
144145
setHistoryPreviewCollapsed: (value: boolean) => void
146+
setReasoningBlockCollapsed: (value: boolean) => void
145147
autoCondenseContext: boolean
146148
setAutoCondenseContext: (value: boolean) => void
147149
autoCondenseContextPercent: number
@@ -280,6 +282,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
280282
global: {},
281283
})
282284
const [includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance] = useState(true)
285+
const [reasoningBlockCollapsed, setReasoningBlockCollapsed] = useState(true) // Default to collapsed
283286

284287
const setListApiConfigMeta = useCallback(
285288
(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
@@ -317,6 +320,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
317320
if ((newState as any).includeTaskHistoryInEnhance !== undefined) {
318321
setIncludeTaskHistoryInEnhance((newState as any).includeTaskHistoryInEnhance)
319322
}
323+
// Update reasoningBlockCollapsed if present in state message
324+
if ((newState as any).reasoningBlockCollapsed !== undefined) {
325+
setReasoningBlockCollapsed((newState as any).reasoningBlockCollapsed)
326+
}
320327
// Handle marketplace data if present in state message
321328
if (newState.marketplaceItems !== undefined) {
322329
setMarketplaceItems(newState.marketplaceItems)
@@ -413,6 +420,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
413420

414421
const contextValue: ExtensionStateContextType = {
415422
...state,
423+
reasoningBlockCollapsed,
416424
didHydrateState,
417425
showWelcome,
418426
theme,
@@ -528,6 +536,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
528536
}),
529537
setHistoryPreviewCollapsed: (value) =>
530538
setState((prevState) => ({ ...prevState, historyPreviewCollapsed: value })),
539+
setReasoningBlockCollapsed,
531540
setHasOpenedModeSelector: (value) => setState((prevState) => ({ ...prevState, hasOpenedModeSelector: value })),
532541
setAutoCondenseContext: (value) => setState((prevState) => ({ ...prevState, autoCondenseContext: value })),
533542
setAutoCondenseContextPercent: (value) =>

0 commit comments

Comments
 (0)