Skip to content

Commit 2b57cdb

Browse files
brunobergherellipsis-dev[bot]roomotemrubens
authored andcommitted
ux: Collapse thinking blocks by default (but control all of them with a keyboard shortcut) (RooCodeInc#8254)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: Roo Code <[email protected]> Co-authored-by: Matt Rubens <[email protected]>
1 parent 3fca4fe commit 2b57cdb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+364
-25
lines changed

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const globalSettingsSchema = z.object({
147147
enhancementApiConfigId: z.string().optional(),
148148
includeTaskHistoryInEnhance: z.boolean().optional(),
149149
historyPreviewCollapsed: z.boolean().optional(),
150+
reasoningBlockCollapsed: z.boolean().optional(),
150151
profileThresholds: z.record(z.string(), z.number()).optional(),
151152
hasOpenedModeSelector: z.boolean().optional(),
152153
lastModeExportPath: z.string().optional(),

src/core/webview/ClineProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,6 +1796,7 @@ export class ClineProvider
17961796
maxTotalImageSize,
17971797
terminalCompressProgressBar,
17981798
historyPreviewCollapsed,
1799+
reasoningBlockCollapsed,
17991800
cloudUserInfo,
18001801
cloudIsAuthenticated,
18011802
sharingEnabled,
@@ -1929,6 +1930,7 @@ export class ClineProvider
19291930
terminalCompressProgressBar: terminalCompressProgressBar ?? true,
19301931
hasSystemPromptOverride,
19311932
historyPreviewCollapsed: historyPreviewCollapsed ?? false,
1933+
reasoningBlockCollapsed: reasoningBlockCollapsed ?? true,
19321934
cloudUserInfo,
19331935
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
19341936
cloudOrganizations,
@@ -2143,6 +2145,7 @@ export class ClineProvider
21432145
maxTotalImageSize: stateValues.maxTotalImageSize ?? 20,
21442146
maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5,
21452147
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
2148+
reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,
21462149
cloudUserInfo,
21472150
cloudIsAuthenticated,
21482151
sharingEnabled,

src/core/webview/webviewMessageHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,6 +1617,10 @@ export const webviewMessageHandler = async (
16171617
await updateGlobalState("historyPreviewCollapsed", message.bool ?? false)
16181618
// No need to call postStateToWebview here as the UI already updated optimistically
16191619
break
1620+
case "setReasoningBlockCollapsed":
1621+
await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true)
1622+
// No need to call postStateToWebview here as the UI already updated optimistically
1623+
break
16201624
case "toggleApiConfigPin":
16211625
if (message.text) {
16221626
const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ export type ExtensionState = Pick<
287287
| "maxDiagnosticMessages"
288288
| "openRouterImageGenerationSelectedModel"
289289
| "includeTaskHistoryInEnhance"
290+
| "reasoningBlockCollapsed"
290291
> & {
291292
version: string
292293
clineMessages: ClineMessage[]

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export interface WebviewMessage {
194194
| "focusPanelRequest"
195195
| "profileThresholds"
196196
| "setHistoryPreviewCollapsed"
197+
| "setReasoningBlockCollapsed"
197198
| "openExternal"
198199
| "filterMarketplaceItems"
199200
| "marketplaceButtonClicked"

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import {
6060
PocketKnife,
6161
FolderTree,
6262
TerminalSquare,
63+
MessageCircle,
6364
} from "lucide-react"
6465
import { cn } from "@/lib/utils"
6566

@@ -1118,14 +1119,20 @@ export const ChatRowContent = ({
11181119
case "text":
11191120
return (
11201121
<div>
1121-
<Markdown markdown={message.text} partial={message.partial} />
1122-
{message.images && message.images.length > 0 && (
1123-
<div style={{ marginTop: "10px" }}>
1124-
{message.images.map((image, index) => (
1125-
<ImageBlock key={index} imageData={image} />
1126-
))}
1127-
</div>
1128-
)}
1122+
<div style={headerStyle}>
1123+
<MessageCircle className="w-4" aria-label="Speech bubble icon" />
1124+
<span style={{ fontWeight: "bold" }}>{t("chat:text.rooSaid")}</span>
1125+
</div>
1126+
<div className="pl-6">
1127+
<Markdown markdown={message.text} partial={message.partial} />
1128+
{message.images && message.images.length > 0 && (
1129+
<div style={{ marginTop: "10px" }}>
1130+
{message.images.map((image, index) => (
1131+
<ImageBlock key={index} imageData={image} />
1132+
))}
1133+
</div>
1134+
)}
1135+
</div>
11291136
</div>
11301137
)
11311138
case "user_feedback":

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

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import React, { useEffect, useRef, useState } from "react"
1+
import { 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, ChevronUp } from "lucide-react"
7+
import { cn } from "@/lib/utils"
68

79
interface ReasoningBlockProps {
810
content: string
@@ -12,18 +14,20 @@ interface ReasoningBlockProps {
1214
metadata?: any
1315
}
1416

15-
/**
16-
* Render reasoning with a heading and a simple timer.
17-
* - Heading uses i18n key chat:reasoning.thinking
18-
* - Timer runs while reasoning is active (no persistence)
19-
*/
2017
export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockProps) => {
2118
const { t } = useTranslation()
19+
const { reasoningBlockCollapsed } = useExtensionState()
20+
21+
const [isCollapsed, setIsCollapsed] = useState(reasoningBlockCollapsed)
2222

2323
const startTimeRef = useRef<number>(Date.now())
2424
const [elapsed, setElapsed] = useState<number>(0)
25+
const contentRef = useRef<HTMLDivElement>(null)
26+
27+
useEffect(() => {
28+
setIsCollapsed(reasoningBlockCollapsed)
29+
}, [reasoningBlockCollapsed])
2530

26-
// Simple timer that runs while streaming
2731
useEffect(() => {
2832
if (isLast && isStreaming) {
2933
const tick = () => setElapsed(Date.now() - startTimeRef.current)
@@ -36,21 +40,35 @@ export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockP
3640
const seconds = Math.floor(elapsed / 1000)
3741
const secondsLabel = t("chat:reasoning.seconds", { count: seconds })
3842

43+
const handleToggle = () => {
44+
setIsCollapsed(!isCollapsed)
45+
}
46+
3947
return (
40-
<div>
41-
<div className="flex items-center justify-between mb-2.5 pr-2">
48+
<div className="group">
49+
<div
50+
className="flex items-center justify-between mb-2.5 pr-2 cursor-pointer select-none"
51+
onClick={handleToggle}>
4252
<div className="flex items-center gap-2">
4353
<Lightbulb className="w-4" />
4454
<span className="font-bold text-vscode-foreground">{t("chat:reasoning.thinking")}</span>
55+
{elapsed > 0 && (
56+
<span className="text-sm text-vscode-descriptionForeground mt-0.5">{secondsLabel}</span>
57+
)}
58+
</div>
59+
<div className="flex items-center gap-2">
60+
<ChevronUp
61+
className={cn(
62+
"w-4 transition-all opacity-0 group-hover:opacity-100",
63+
isCollapsed && "-rotate-180",
64+
)}
65+
/>
4566
</div>
46-
{elapsed > 0 && (
47-
<span className="text-sm text-vscode-descriptionForeground tabular-nums flex items-center gap-1">
48-
{secondsLabel}
49-
</span>
50-
)}
5167
</div>
52-
{(content?.trim()?.length ?? 0) > 0 && (
53-
<div className="border-l border-vscode-descriptionForeground/20 ml-2 pl-4 pb-1 text-vscode-descriptionForeground">
68+
{(content?.trim()?.length ?? 0) > 0 && !isCollapsed && (
69+
<div
70+
ref={contentRef}
71+
className="border-l border-vscode-descriptionForeground/20 ml-2 pl-4 pb-1 text-vscode-descriptionForeground">
5472
<MarkdownBlock markdown={content} />
5573
</div>
5674
)}

webview-ui/src/components/common/MarkdownBlock.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,12 @@ const StyledMarkdown = styled.div`
146146
}
147147
}
148148
149+
h1 {
150+
font-size: 1.65em;
151+
font-weight: 700;
152+
margin: 1.35em 0 0.5em;
153+
}
154+
149155
h2 {
150156
font-size: 1.35em;
151157
font-weight: 500;

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
MessageSquare,
2525
LucideIcon,
2626
SquareSlash,
27+
Glasses,
2728
} from "lucide-react"
2829

2930
import type { ProviderSettings, ExperimentId, TelemetrySetting } from "@roo-code/types"
@@ -66,6 +67,7 @@ import { About } from "./About"
6667
import { Section } from "./Section"
6768
import PromptsSettings from "./PromptsSettings"
6869
import { SlashCommandsSettings } from "./SlashCommandsSettings"
70+
import { UISettings } from "./UISettings"
6971

7072
export const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden"
7173
export const settingsTabList =
@@ -88,6 +90,7 @@ const sectionNames = [
8890
"contextManagement",
8991
"terminal",
9092
"prompts",
93+
"ui",
9194
"experimental",
9295
"language",
9396
"about",
@@ -191,6 +194,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
191194
includeTaskHistoryInEnhance,
192195
openRouterImageApiKey,
193196
openRouterImageGenerationSelectedModel,
197+
reasoningBlockCollapsed,
194198
} = cachedState
195199

196200
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
@@ -364,6 +368,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
364368
vscode.postMessage({ type: "updateCondensingPrompt", text: customCondensingPrompt || "" })
365369
vscode.postMessage({ type: "updateSupportPrompt", values: customSupportPrompts || {} })
366370
vscode.postMessage({ type: "includeTaskHistoryInEnhance", bool: includeTaskHistoryInEnhance ?? true })
371+
vscode.postMessage({ type: "setReasoningBlockCollapsed", bool: reasoningBlockCollapsed ?? true })
367372
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
368373
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
369374
vscode.postMessage({ type: "profileThresholds", values: profileThresholds })
@@ -458,6 +463,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
458463
{ id: "contextManagement", icon: Database },
459464
{ id: "terminal", icon: SquareTerminal },
460465
{ id: "prompts", icon: MessageSquare },
466+
{ id: "ui", icon: Glasses },
461467
{ id: "experimental", icon: FlaskConical },
462468
{ id: "language", icon: Globe },
463469
{ id: "about", icon: Info },
@@ -757,6 +763,14 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
757763
/>
758764
)}
759765

766+
{/* UI Section */}
767+
{activeTab === "ui" && (
768+
<UISettings
769+
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
770+
setCachedStateField={setCachedStateField}
771+
/>
772+
)}
773+
760774
{/* Experimental Section */}
761775
{activeTab === "experimental" && (
762776
<ExperimentalSettings
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { HTMLAttributes } from "react"
2+
import { useAppTranslation } from "@/i18n/TranslationContext"
3+
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
4+
import { Glasses } from "lucide-react"
5+
import { telemetryClient } from "@/utils/TelemetryClient"
6+
7+
import { SetCachedStateField } from "./types"
8+
import { SectionHeader } from "./SectionHeader"
9+
import { Section } from "./Section"
10+
import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
11+
12+
interface UISettingsProps extends HTMLAttributes<HTMLDivElement> {
13+
reasoningBlockCollapsed: boolean
14+
setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
15+
}
16+
17+
export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...props }: UISettingsProps) => {
18+
const { t } = useAppTranslation()
19+
20+
const handleReasoningBlockCollapsedChange = (value: boolean) => {
21+
setCachedStateField("reasoningBlockCollapsed", value)
22+
23+
// Track telemetry event
24+
telemetryClient.capture("ui_settings_collapse_thinking_changed", {
25+
enabled: value,
26+
})
27+
}
28+
29+
return (
30+
<div {...props}>
31+
<SectionHeader>
32+
<div className="flex items-center gap-2">
33+
<Glasses className="w-4" />
34+
<div>{t("settings:sections.ui")}</div>
35+
</div>
36+
</SectionHeader>
37+
38+
<Section>
39+
<div className="space-y-6">
40+
{/* Collapse Thinking Messages Setting */}
41+
<div className="flex flex-col gap-1">
42+
<VSCodeCheckbox
43+
checked={reasoningBlockCollapsed}
44+
onChange={(e: any) => handleReasoningBlockCollapsedChange(e.target.checked)}
45+
data-testid="collapse-thinking-checkbox">
46+
<span className="font-medium">{t("settings:ui.collapseThinking.label")}</span>
47+
</VSCodeCheckbox>
48+
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
49+
{t("settings:ui.collapseThinking.description")}
50+
</div>
51+
</div>
52+
</div>
53+
</Section>
54+
</div>
55+
)
56+
}

0 commit comments

Comments
 (0)