Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export const globalSettingsSchema = z.object({
enhancementApiConfigId: z.string().optional(),
includeTaskHistoryInEnhance: z.boolean().optional(),
historyPreviewCollapsed: z.boolean().optional(),
reasoningBlockCollapsed: z.boolean().optional(),
profileThresholds: z.record(z.string(), z.number()).optional(),
hasOpenedModeSelector: z.boolean().optional(),
lastModeExportPath: z.string().optional(),
Expand Down
3 changes: 3 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1792,6 +1792,7 @@ export class ClineProvider
maxTotalImageSize,
terminalCompressProgressBar,
historyPreviewCollapsed,
reasoningBlockCollapsed,
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
Expand Down Expand Up @@ -1925,6 +1926,7 @@ export class ClineProvider
terminalCompressProgressBar: terminalCompressProgressBar ?? true,
hasSystemPromptOverride,
historyPreviewCollapsed: historyPreviewCollapsed ?? false,
reasoningBlockCollapsed: reasoningBlockCollapsed ?? true,
cloudUserInfo,
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
cloudOrganizations,
Expand Down Expand Up @@ -2139,6 +2141,7 @@ export class ClineProvider
maxTotalImageSize: stateValues.maxTotalImageSize ?? 20,
maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5,
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
Expand Down
4 changes: 4 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,10 @@ export const webviewMessageHandler = async (
await updateGlobalState("historyPreviewCollapsed", message.bool ?? false)
// No need to call postStateToWebview here as the UI already updated optimistically
break
case "setReasoningBlockCollapsed":
await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true)
// No need to call postStateToWebview here as the UI already updated optimistically
break
case "toggleApiConfigPin":
if (message.text) {
const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}
Expand Down
1 change: 1 addition & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export type ExtensionState = Pick<
| "maxDiagnosticMessages"
| "openRouterImageGenerationSelectedModel"
| "includeTaskHistoryInEnhance"
| "reasoningBlockCollapsed"
> & {
version: string
clineMessages: ClineMessage[]
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export interface WebviewMessage {
| "focusPanelRequest"
| "profileThresholds"
| "setHistoryPreviewCollapsed"
| "setReasoningBlockCollapsed"
| "openExternal"
| "filterMarketplaceItems"
| "marketplaceButtonClicked"
Expand Down
23 changes: 15 additions & 8 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {
PocketKnife,
FolderTree,
TerminalSquare,
MessageCircle,
} from "lucide-react"
import { cn } from "@/lib/utils"

Expand Down Expand Up @@ -1118,14 +1119,20 @@ export const ChatRowContent = ({
case "text":
return (
<div>
<Markdown markdown={message.text} partial={message.partial} />
{message.images && message.images.length > 0 && (
<div style={{ marginTop: "10px" }}>
{message.images.map((image, index) => (
<ImageBlock key={index} imageData={image} />
))}
</div>
)}
<div style={headerStyle}>
<MessageCircle className="w-4" aria-label="Speech bubble icon" />
<span style={{ fontWeight: "bold" }}>{t("chat:text.rooSaid")}</span>
</div>
<div className="pl-6">
<Markdown markdown={message.text} partial={message.partial} />
{message.images && message.images.length > 0 && (
<div style={{ marginTop: "10px" }}>
{message.images.map((image, index) => (
<ImageBlock key={index} imageData={image} />
))}
</div>
)}
</div>
</div>
)
case "user_feedback":
Expand Down
52 changes: 35 additions & 17 deletions webview-ui/src/components/chat/ReasoningBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React, { useEffect, useRef, useState } from "react"
import { useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { useExtensionState } from "@src/context/ExtensionStateContext"

import MarkdownBlock from "../common/MarkdownBlock"
import { Lightbulb } from "lucide-react"
import { Lightbulb, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"

interface ReasoningBlockProps {
content: string
Expand All @@ -12,18 +14,20 @@ interface ReasoningBlockProps {
metadata?: any
}

/**
* Render reasoning with a heading and a simple timer.
* - Heading uses i18n key chat:reasoning.thinking
* - Timer runs while reasoning is active (no persistence)
*/
export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockProps) => {
const { t } = useTranslation()
const { reasoningBlockCollapsed } = useExtensionState()

const [isCollapsed, setIsCollapsed] = useState(reasoningBlockCollapsed)

const startTimeRef = useRef<number>(Date.now())
const [elapsed, setElapsed] = useState<number>(0)
const contentRef = useRef<HTMLDivElement>(null)

useEffect(() => {
setIsCollapsed(reasoningBlockCollapsed)
}, [reasoningBlockCollapsed])

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

const handleToggle = () => {
setIsCollapsed(!isCollapsed)
}

return (
<div>
<div className="flex items-center justify-between mb-2.5 pr-2">
<div className="group">
<div
className="flex items-center justify-between mb-2.5 pr-2 cursor-pointer select-none"
onClick={handleToggle}>
<div className="flex items-center gap-2">
<Lightbulb className="w-4" />
<span className="font-bold text-vscode-foreground">{t("chat:reasoning.thinking")}</span>
{elapsed > 0 && (
<span className="text-sm text-vscode-descriptionForeground mt-0.5">{secondsLabel}</span>
)}
</div>
<div className="flex items-center gap-2">
<ChevronUp
className={cn(
"w-4 transition-all opacity-0 group-hover:opacity-100",
isCollapsed && "-rotate-180",
)}
/>
</div>
{elapsed > 0 && (
<span className="text-sm text-vscode-descriptionForeground tabular-nums flex items-center gap-1">
{secondsLabel}
</span>
)}
</div>
{(content?.trim()?.length ?? 0) > 0 && (
<div className="border-l border-vscode-descriptionForeground/20 ml-2 pl-4 pb-1 text-vscode-descriptionForeground">
{(content?.trim()?.length ?? 0) > 0 && !isCollapsed && (
<div
ref={contentRef}
className="border-l border-vscode-descriptionForeground/20 ml-2 pl-4 pb-1 text-vscode-descriptionForeground">
<MarkdownBlock markdown={content} />
</div>
)}
Expand Down
6 changes: 6 additions & 0 deletions webview-ui/src/components/common/MarkdownBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ const StyledMarkdown = styled.div`
}
}

h1 {
font-size: 1.65em;
font-weight: 700;
margin: 1.35em 0 0.5em;
}

h2 {
font-size: 1.35em;
font-weight: 500;
Expand Down
14 changes: 14 additions & 0 deletions webview-ui/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
MessageSquare,
LucideIcon,
SquareSlash,
Glasses,
} from "lucide-react"

import type { ProviderSettings, ExperimentId, TelemetrySetting } from "@roo-code/types"
Expand Down Expand Up @@ -66,6 +67,7 @@ import { About } from "./About"
import { Section } from "./Section"
import PromptsSettings from "./PromptsSettings"
import { SlashCommandsSettings } from "./SlashCommandsSettings"
import { UISettings } from "./UISettings"

export const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden"
export const settingsTabList =
Expand All @@ -88,6 +90,7 @@ const sectionNames = [
"contextManagement",
"terminal",
"prompts",
"ui",
"experimental",
"language",
"about",
Expand Down Expand Up @@ -191,6 +194,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
includeTaskHistoryInEnhance,
openRouterImageApiKey,
openRouterImageGenerationSelectedModel,
reasoningBlockCollapsed,
} = cachedState

const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
Expand Down Expand Up @@ -364,6 +368,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
vscode.postMessage({ type: "updateCondensingPrompt", text: customCondensingPrompt || "" })
vscode.postMessage({ type: "updateSupportPrompt", values: customSupportPrompts || {} })
vscode.postMessage({ type: "includeTaskHistoryInEnhance", bool: includeTaskHistoryInEnhance ?? true })
vscode.postMessage({ type: "setReasoningBlockCollapsed", bool: reasoningBlockCollapsed ?? true })
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
vscode.postMessage({ type: "profileThresholds", values: profileThresholds })
Expand Down Expand Up @@ -458,6 +463,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
{ id: "contextManagement", icon: Database },
{ id: "terminal", icon: SquareTerminal },
{ id: "prompts", icon: MessageSquare },
{ id: "ui", icon: Glasses },
{ id: "experimental", icon: FlaskConical },
{ id: "language", icon: Globe },
{ id: "about", icon: Info },
Expand Down Expand Up @@ -757,6 +763,14 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
/>
)}

{/* UI Section */}
{activeTab === "ui" && (
<UISettings
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
setCachedStateField={setCachedStateField}
/>
)}

{/* Experimental Section */}
{activeTab === "experimental" && (
<ExperimentalSettings
Expand Down
56 changes: 56 additions & 0 deletions webview-ui/src/components/settings/UISettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { HTMLAttributes } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { Glasses } from "lucide-react"
import { telemetryClient } from "@/utils/TelemetryClient"

import { SetCachedStateField } from "./types"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
import { ExtensionStateContextType } from "@/context/ExtensionStateContext"

interface UISettingsProps extends HTMLAttributes<HTMLDivElement> {
reasoningBlockCollapsed: boolean
setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
}

export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...props }: UISettingsProps) => {
const { t } = useAppTranslation()

const handleReasoningBlockCollapsedChange = (value: boolean) => {
setCachedStateField("reasoningBlockCollapsed", value)

// Track telemetry event
telemetryClient.capture("ui_settings_collapse_thinking_changed", {
enabled: value,
})
}

return (
<div {...props}>
<SectionHeader>
<div className="flex items-center gap-2">
<Glasses className="w-4" />
<div>{t("settings:sections.ui")}</div>
</div>
</SectionHeader>

<Section>
<div className="space-y-6">
{/* Collapse Thinking Messages Setting */}
<div className="flex flex-col gap-1">
<VSCodeCheckbox
checked={reasoningBlockCollapsed}
onChange={(e: any) => handleReasoningBlockCollapsedChange(e.target.checked)}
data-testid="collapse-thinking-checkbox">
<span className="font-medium">{t("settings:ui.collapseThinking.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
{t("settings:ui.collapseThinking.description")}
</div>
</div>
</div>
</Section>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render, fireEvent, waitFor } from "@testing-library/react"
import { describe, it, expect, vi } from "vitest"
import { UISettings } from "../UISettings"

describe("UISettings", () => {
const defaultProps = {
reasoningBlockCollapsed: false,
setCachedStateField: vi.fn(),
}

it("renders the collapse thinking checkbox", () => {
const { getByTestId } = render(<UISettings {...defaultProps} />)
const checkbox = getByTestId("collapse-thinking-checkbox")
expect(checkbox).toBeTruthy()
})

it("displays the correct initial state", () => {
const { getByTestId } = render(<UISettings {...defaultProps} reasoningBlockCollapsed={true} />)
const checkbox = getByTestId("collapse-thinking-checkbox") as HTMLInputElement
expect(checkbox.checked).toBe(true)
})

it("calls setCachedStateField when checkbox is toggled", async () => {
const setCachedStateField = vi.fn()
const { getByTestId } = render(<UISettings {...defaultProps} setCachedStateField={setCachedStateField} />)

const checkbox = getByTestId("collapse-thinking-checkbox")
fireEvent.click(checkbox)

await waitFor(() => {
expect(setCachedStateField).toHaveBeenCalledWith("reasoningBlockCollapsed", true)
})
})

it("updates checkbox state when prop changes", () => {
const { getByTestId, rerender } = render(<UISettings {...defaultProps} reasoningBlockCollapsed={false} />)
const checkbox = getByTestId("collapse-thinking-checkbox") as HTMLInputElement
expect(checkbox.checked).toBe(false)

rerender(<UISettings {...defaultProps} reasoningBlockCollapsed={true} />)
expect(checkbox.checked).toBe(true)
})
})
5 changes: 5 additions & 0 deletions webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export interface ExtensionStateContextType extends ExtensionState {
terminalCompressProgressBar?: boolean
setTerminalCompressProgressBar: (value: boolean) => void
setHistoryPreviewCollapsed: (value: boolean) => void
setReasoningBlockCollapsed: (value: boolean) => void
autoCondenseContext: boolean
setAutoCondenseContext: (value: boolean) => void
autoCondenseContextPercent: number
Expand Down Expand Up @@ -240,6 +241,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
terminalZdotdir: false, // Default ZDOTDIR handling setting
terminalCompressProgressBar: true, // Default to compress progress bar output
historyPreviewCollapsed: false, // Initialize the new state (default to expanded)
reasoningBlockCollapsed: true, // Default to collapsed
cloudUserInfo: null,
cloudIsAuthenticated: false,
cloudOrganizations: [],
Expand Down Expand Up @@ -416,6 +418,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode

const contextValue: ExtensionStateContextType = {
...state,
reasoningBlockCollapsed: state.reasoningBlockCollapsed ?? true,
didHydrateState,
showWelcome,
theme,
Expand Down Expand Up @@ -532,6 +535,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
}),
setHistoryPreviewCollapsed: (value) =>
setState((prevState) => ({ ...prevState, historyPreviewCollapsed: value })),
setReasoningBlockCollapsed: (value) =>
setState((prevState) => ({ ...prevState, reasoningBlockCollapsed: value })),
setHasOpenedModeSelector: (value) => setState((prevState) => ({ ...prevState, hasOpenedModeSelector: value })),
setAutoCondenseContext: (value) => setState((prevState) => ({ ...prevState, autoCondenseContext: value })),
setAutoCondenseContextPercent: (value) =>
Expand Down
Loading
Loading