feat(ui): add chat font size multiplier setting

- Add chatFontSizeMultiplier to global settings with min 0.5 max 2 bounds
- Add commands: increaseChatFontSize, decreaseChatFontSize, resetChatFontSize
- Add UI Settings control with number input and reset button
- Apply font size multiplier only to ChatView container (scoped per Bruno feedback)
- Add English translations for the new setting
- Update UISettings tests

Addresses issue #8100 per feedback from Bruno in PR #8457
This commit is contained in:
Roo Code 2026-01-06 21:05:17 +00:00
parent 618aa6652b
commit aecec67f22
13 changed files with 170 additions and 2 deletions

View file

@ -207,6 +207,12 @@ export const globalSettingsSchema = z.object({
* @default "send"
*/
enterBehavior: z.enum(["send", "newline"]).optional(),
/**
* Multiplier for the chat view font size.
* - Min: 0.5, Max: 2.0
* @default 1.0
*/
chatFontSizeMultiplier: z.number().min(0.5).max(2).optional(),
profileThresholds: z.record(z.string(), z.number()).optional(),
hasOpenedModeSelector: z.boolean().optional(),
lastModeExportPath: z.string().optional(),

View file

@ -299,6 +299,7 @@ export type ExtensionState = Pick<
| "includeTaskHistoryInEnhance"
| "reasoningBlockCollapsed"
| "enterBehavior"
| "chatFontSizeMultiplier"
| "includeCurrentTime"
| "includeCurrentCost"
| "maxGitStatusFiles"

View file

@ -47,6 +47,10 @@ export const commandIds = [
"acceptInput",
"focusPanel",
"toggleAutoApprove",
"increaseChatFontSize",
"decreaseChatFontSize",
"resetChatFontSize",
] as const
export type CommandId = (typeof commandIds)[number]

View file

@ -195,6 +195,40 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
action: "toggleAutoApprove",
})
},
increaseChatFontSize: async () => {
const visibleProvider = getVisibleProviderOrLog(outputChannel)
if (!visibleProvider) {
return
}
const currentMultiplier = visibleProvider.contextProxy.getValue("chatFontSizeMultiplier") ?? 1
const newMultiplier = Math.min(2, currentMultiplier + 0.1)
await visibleProvider.contextProxy.setValue("chatFontSizeMultiplier", newMultiplier)
await visibleProvider.postStateToWebview()
},
decreaseChatFontSize: async () => {
const visibleProvider = getVisibleProviderOrLog(outputChannel)
if (!visibleProvider) {
return
}
const currentMultiplier = visibleProvider.contextProxy.getValue("chatFontSizeMultiplier") ?? 1
const newMultiplier = Math.max(0.5, currentMultiplier - 0.1)
await visibleProvider.contextProxy.setValue("chatFontSizeMultiplier", newMultiplier)
await visibleProvider.postStateToWebview()
},
resetChatFontSize: async () => {
const visibleProvider = getVisibleProviderOrLog(outputChannel)
if (!visibleProvider) {
return
}
await visibleProvider.contextProxy.setValue("chatFontSizeMultiplier", 1)
await visibleProvider.postStateToWebview()
},
})
export const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {

View file

@ -2174,6 +2174,7 @@ export class ClineProvider
historyPreviewCollapsed,
reasoningBlockCollapsed,
enterBehavior,
chatFontSizeMultiplier,
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
@ -2300,6 +2301,7 @@ export class ClineProvider
historyPreviewCollapsed: historyPreviewCollapsed ?? false,
reasoningBlockCollapsed: reasoningBlockCollapsed ?? true,
enterBehavior: enterBehavior ?? "send",
chatFontSizeMultiplier: chatFontSizeMultiplier ?? 1,
cloudUserInfo,
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
cloudAuthSkipModel: this.context.globalState.get<boolean>("roo-auth-skip-model") ?? false,
@ -2523,6 +2525,7 @@ export class ClineProvider
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,
enterBehavior: stateValues.enterBehavior ?? "send",
chatFontSizeMultiplier: stateValues.chatFontSizeMultiplier ?? 1,
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,

View file

@ -169,6 +169,21 @@
"command": "roo-cline.toggleAutoApprove",
"title": "%command.toggleAutoApprove.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.increaseChatFontSize",
"title": "%command.increaseChatFontSize.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.decreaseChatFontSize",
"title": "%command.decreaseChatFontSize.title%",
"category": "%configuration.title%"
},
{
"command": "roo-cline.resetChatFontSize",
"title": "%command.resetChatFontSize.title%",
"category": "%configuration.title%"
}
],
"menus": {

View file

@ -25,6 +25,9 @@
"command.terminal.explainCommand.title": "Explain This Command",
"command.acceptInput.title": "Accept Input/Suggestion",
"command.toggleAutoApprove.title": "Toggle Auto-Approve",
"command.increaseChatFontSize.title": "Increase Chat Font Size",
"command.decreaseChatFontSize.title": "Decrease Chat Font Size",
"command.resetChatFontSize.title": "Reset Chat Font Size",
"configuration.title": "Roo Code",
"commands.allowedCommands.description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled",
"commands.deniedCommands.description": "Command prefixes that will be automatically denied without asking for approval. In case of conflicts with allowed commands, the longest prefix match takes precedence. Add * to deny all commands.",

View file

@ -93,6 +93,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
cloudIsAuthenticated,
messageQueue = [],
showWorktreesInHomeScreen,
chatFontSizeMultiplier,
} = useExtensionState()
// Show a WarningRow when the user sends a message with a retired provider.
@ -1527,7 +1528,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
return (
<div
data-testid="chat-view"
className={isHidden ? "hidden" : "fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden"}>
className={isHidden ? "hidden" : "fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden"}
style={{ fontSize: `calc(1em * ${chatFontSizeMultiplier ?? 1})` }}>
{telemetrySetting === "unset" && <TelemetryBanner />}
{(showAnnouncement || showAnnouncementModal) && (
<Announcement

View file

@ -200,6 +200,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
openRouterImageGenerationSelectedModel,
reasoningBlockCollapsed,
enterBehavior,
chatFontSizeMultiplier,
includeCurrentTime,
includeCurrentCost,
maxGitStatusFiles,
@ -892,6 +893,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
<UISettings
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
enterBehavior={enterBehavior ?? "send"}
chatFontSizeMultiplier={chatFontSizeMultiplier ?? 1}
setCachedStateField={setCachedStateField}
/>
)}

View file

@ -1,6 +1,7 @@
import { HTMLAttributes, useMemo } from "react"
import { HTMLAttributes, useMemo, useState, useCallback, useEffect } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { RotateCcw } from "lucide-react"
import { telemetryClient } from "@/utils/TelemetryClient"
import { SetCachedStateField } from "./types"
@ -12,17 +13,27 @@ import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
interface UISettingsProps extends HTMLAttributes<HTMLDivElement> {
reasoningBlockCollapsed: boolean
enterBehavior: "send" | "newline"
chatFontSizeMultiplier: number
setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
}
export const UISettings = ({
reasoningBlockCollapsed,
enterBehavior,
chatFontSizeMultiplier,
setCachedStateField,
...props
}: UISettingsProps) => {
const { t } = useAppTranslation()
// Local state for the input value to allow typing freely
const [localMultiplier, setLocalMultiplier] = useState(chatFontSizeMultiplier.toString())
// Sync local state when prop changes (e.g., from commands)
useEffect(() => {
setLocalMultiplier(chatFontSizeMultiplier.toString())
}, [chatFontSizeMultiplier])
// Detect platform for dynamic modifier key display
const primaryMod = useMemo(() => {
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
@ -48,6 +59,45 @@ export const UISettings = ({
})
}
const handleFontSizeMultiplierChange = useCallback(
(value: string) => {
setLocalMultiplier(value)
// Parse and validate the value
const numValue = parseFloat(value)
if (!isNaN(numValue)) {
// Clamp the value between 0.5 and 2
const clampedValue = Math.max(0.5, Math.min(2, numValue))
setCachedStateField("chatFontSizeMultiplier", clampedValue)
// Track telemetry event
telemetryClient.capture("ui_settings_chat_font_size_changed", {
multiplier: clampedValue,
})
}
},
[setCachedStateField],
)
const handleFontSizeMultiplierBlur = useCallback(() => {
// On blur, ensure the display value matches the clamped value
const numValue = parseFloat(localMultiplier)
if (isNaN(numValue)) {
setLocalMultiplier(chatFontSizeMultiplier.toString())
} else {
const clampedValue = Math.max(0.5, Math.min(2, numValue))
setLocalMultiplier(clampedValue.toString())
}
}, [localMultiplier, chatFontSizeMultiplier])
const handleResetFontSize = useCallback(() => {
setCachedStateField("chatFontSizeMultiplier", 1)
setLocalMultiplier("1")
// Track telemetry event
telemetryClient.capture("ui_settings_chat_font_size_reset", {})
}, [setCachedStateField])
return (
<div {...props}>
<SectionHeader>{t("settings:sections.ui")}</SectionHeader>
@ -91,6 +141,43 @@ export const UISettings = ({
</div>
</div>
</SearchableSetting>
{/* Chat Font Size Multiplier Setting */}
<SearchableSetting
settingId="ui-chat-font-size"
section="ui"
label={t("settings:ui.chatFontSizeMultiplier.label")}>
<div className="flex flex-col gap-1">
<label className="font-medium" htmlFor="chat-font-size-input">
{t("settings:ui.chatFontSizeMultiplier.label")}
</label>
<div className="flex items-center gap-2">
<input
id="chat-font-size-input"
type="number"
min="0.5"
max="2"
step="0.1"
value={localMultiplier}
onChange={(e) => handleFontSizeMultiplierChange(e.target.value)}
onBlur={handleFontSizeMultiplierBlur}
className="w-20 px-2 py-1 bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded"
data-testid="chat-font-size-input"
/>
<button
onClick={handleResetFontSize}
className="flex items-center gap-1 px-2 py-1 text-sm bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground hover:bg-vscode-button-secondaryHoverBackground rounded"
title={t("settings:ui.chatFontSizeMultiplier.reset")}
data-testid="chat-font-size-reset-button">
<RotateCcw className="w-3 h-3" />
{t("settings:ui.chatFontSizeMultiplier.reset")}
</button>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:ui.chatFontSizeMultiplier.description")}
</div>
</div>
</SearchableSetting>
</div>
</Section>
</div>

View file

@ -6,6 +6,7 @@ describe("UISettings", () => {
const defaultProps = {
reasoningBlockCollapsed: false,
enterBehavior: "send" as const,
chatFontSizeMultiplier: 1,
setCachedStateField: vi.fn(),
}

View file

@ -126,6 +126,8 @@ export interface ExtensionStateContextType extends ExtensionState {
setReasoningBlockCollapsed: (value: boolean) => void
enterBehavior?: "send" | "newline"
setEnterBehavior: (value: "send" | "newline") => void
chatFontSizeMultiplier?: number
setChatFontSizeMultiplier: (value: number) => void
autoCondenseContext: boolean
setAutoCondenseContext: (value: boolean) => void
autoCondenseContextPercent: number
@ -236,6 +238,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
historyPreviewCollapsed: false, // Initialize the new state (default to expanded)
reasoningBlockCollapsed: true, // Default to collapsed
enterBehavior: "send", // Default: Enter sends, Shift+Enter creates newline
chatFontSizeMultiplier: 1, // Default: 1x multiplier (no scaling)
cloudUserInfo: null,
cloudIsAuthenticated: false,
cloudOrganizations: [],
@ -586,6 +589,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setState((prevState) => ({ ...prevState, reasoningBlockCollapsed: value })),
enterBehavior: state.enterBehavior ?? "send",
setEnterBehavior: (value) => setState((prevState) => ({ ...prevState, enterBehavior: value })),
chatFontSizeMultiplier: state.chatFontSizeMultiplier ?? 1,
setChatFontSizeMultiplier: (value) => setState((prevState) => ({ ...prevState, chatFontSizeMultiplier: value })),
setHasOpenedModeSelector: (value) => setState((prevState) => ({ ...prevState, hasOpenedModeSelector: value })),
setAutoCondenseContext: (value) => setState((prevState) => ({ ...prevState, autoCondenseContext: value })),
setAutoCondenseContextPercent: (value) =>

View file

@ -163,6 +163,11 @@
"requireCtrlEnterToSend": {
"label": "Require {{primaryMod}}+Enter to send messages",
"description": "When enabled, you must press {{primaryMod}}+Enter to send messages instead of just Enter"
},
"chatFontSizeMultiplier": {
"label": "Chat Font Size Multiplier",
"description": "Adjust the font size in the chat view. Values range from 0.5 (smaller) to 2 (larger). Default is 1.",
"reset": "Reset"
}
},
"prompts": {