mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add chat message font size setting
- Add chatMessageFontSize property to ExtensionState and GlobalSettings - Add UI dropdown in settings with 5 size options (Default, Extra Small, Small, Large, Extra Large) - Apply font size scaling to ChatRow component (90%, 95%, 100%, 105%, 110%) - Add message handler to persist the setting - Include tests for the new functionality Users can now control the font size for chat messages in ChatView/Virtuoso content without affecting other UI elements
This commit is contained in:
parent
270dce5050
commit
6249077cde
10 changed files with 204 additions and 4 deletions
|
|
@ -148,6 +148,7 @@ export const globalSettingsSchema = z.object({
|
|||
includeTaskHistoryInEnhance: z.boolean().optional(),
|
||||
historyPreviewCollapsed: z.boolean().optional(),
|
||||
reasoningBlockCollapsed: z.boolean().optional(),
|
||||
chatMessageFontSize: z.string().optional(),
|
||||
profileThresholds: z.record(z.string(), z.number()).optional(),
|
||||
hasOpenedModeSelector: z.boolean().optional(),
|
||||
lastModeExportPath: z.string().optional(),
|
||||
|
|
|
|||
|
|
@ -1621,6 +1621,10 @@ export const webviewMessageHandler = async (
|
|||
await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true)
|
||||
// No need to call postStateToWebview here as the UI already updated optimistically
|
||||
break
|
||||
case "chatMessageFontSize":
|
||||
await updateGlobalState("chatMessageFontSize", message.text ?? "default")
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "toggleApiConfigPin":
|
||||
if (message.text) {
|
||||
const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@ export type ExtensionState = Pick<
|
|||
| "includeTaskHistoryInEnhance"
|
||||
| "reasoningBlockCollapsed"
|
||||
> & {
|
||||
chatMessageFontSize?: string
|
||||
version: string
|
||||
clineMessages: ClineMessage[]
|
||||
currentTaskItem?: HistoryItem
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ export interface WebviewMessage {
|
|||
| "profileThresholds"
|
||||
| "setHistoryPreviewCollapsed"
|
||||
| "setReasoningBlockCollapsed"
|
||||
| "chatMessageFontSize"
|
||||
| "openExternal"
|
||||
| "filterMarketplaceItems"
|
||||
| "marketplaceButtonClicked"
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ interface ChatRowProps {
|
|||
onFollowUpUnmount?: () => void
|
||||
isFollowUpAnswered?: boolean
|
||||
editable?: boolean
|
||||
chatMessageFontSize?: string
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
|
|
@ -84,13 +85,29 @@ interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange"> {}
|
|||
|
||||
const ChatRow = memo(
|
||||
(props: ChatRowProps) => {
|
||||
const { isLast, onHeightChange, message } = props
|
||||
const { isLast, onHeightChange, message, chatMessageFontSize } = props
|
||||
// Store the previous height to compare with the current height
|
||||
// This allows us to detect changes without causing re-renders
|
||||
const prevHeightRef = useRef(0)
|
||||
|
||||
// Calculate the font size scale based on the setting
|
||||
const fontSizeScale = useMemo(() => {
|
||||
switch (chatMessageFontSize) {
|
||||
case "extra-small":
|
||||
return "90%"
|
||||
case "small":
|
||||
return "95%"
|
||||
case "large":
|
||||
return "105%"
|
||||
case "extra-large":
|
||||
return "110%"
|
||||
default:
|
||||
return "100%"
|
||||
}
|
||||
}, [chatMessageFontSize])
|
||||
|
||||
const [chatrow, { height }] = useSize(
|
||||
<div className="px-[15px] py-[10px] pr-[6px]">
|
||||
<div className="px-[15px] py-[10px] pr-[6px]" style={{ fontSize: fontSizeScale }}>
|
||||
<ChatRowContent {...props} />
|
||||
</div>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
soundVolume,
|
||||
cloudIsAuthenticated,
|
||||
messageQueue = [],
|
||||
chatMessageFontSize,
|
||||
} = useExtensionState()
|
||||
|
||||
const messagesRef = useRef(messages)
|
||||
|
|
@ -1541,6 +1542,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
onBatchFileResponse={handleBatchFileResponse}
|
||||
onFollowUpUnmount={handleFollowUpUnmount}
|
||||
isFollowUpAnswered={messageOrGroup.isAnswered === true || messageOrGroup.ts === currentFollowUpTs}
|
||||
chatMessageFontSize={chatMessageFontSize}
|
||||
editable={
|
||||
messageOrGroup.type === "ask" &&
|
||||
messageOrGroup.ask === "tool" &&
|
||||
|
|
@ -1576,6 +1578,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
alwaysAllowUpdateTodoList,
|
||||
enableButtons,
|
||||
primaryButtonText,
|
||||
chatMessageFontSize,
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
openRouterImageApiKey,
|
||||
openRouterImageGenerationSelectedModel,
|
||||
reasoningBlockCollapsed,
|
||||
chatMessageFontSize,
|
||||
} = cachedState
|
||||
|
||||
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
|
||||
|
|
@ -384,6 +385,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
vscode.postMessage({ type: "updateSupportPrompt", values: customSupportPrompts || {} })
|
||||
vscode.postMessage({ type: "includeTaskHistoryInEnhance", bool: includeTaskHistoryInEnhance ?? true })
|
||||
vscode.postMessage({ type: "setReasoningBlockCollapsed", bool: reasoningBlockCollapsed ?? true })
|
||||
vscode.postMessage({ type: "chatMessageFontSize", text: chatMessageFontSize || "default" })
|
||||
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
|
||||
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
|
||||
vscode.postMessage({ type: "profileThresholds", values: profileThresholds })
|
||||
|
|
@ -782,6 +784,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
{activeTab === "ui" && (
|
||||
<UISettings
|
||||
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
|
||||
chatMessageFontSize={chatMessageFontSize || "default"}
|
||||
setCachedStateField={setCachedStateField}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { HTMLAttributes } from "react"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
|
||||
import { Glasses } from "lucide-react"
|
||||
import { telemetryClient } from "@/utils/TelemetryClient"
|
||||
|
||||
|
|
@ -11,10 +11,16 @@ import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
|
|||
|
||||
interface UISettingsProps extends HTMLAttributes<HTMLDivElement> {
|
||||
reasoningBlockCollapsed: boolean
|
||||
chatMessageFontSize: string
|
||||
setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
|
||||
}
|
||||
|
||||
export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...props }: UISettingsProps) => {
|
||||
export const UISettings = ({
|
||||
reasoningBlockCollapsed,
|
||||
chatMessageFontSize,
|
||||
setCachedStateField,
|
||||
...props
|
||||
}: UISettingsProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const handleReasoningBlockCollapsedChange = (value: boolean) => {
|
||||
|
|
@ -26,6 +32,15 @@ export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...pr
|
|||
})
|
||||
}
|
||||
|
||||
const handleChatMessageFontSizeChange = (value: string) => {
|
||||
setCachedStateField("chatMessageFontSize", value)
|
||||
|
||||
// Track telemetry event
|
||||
telemetryClient.capture("ui_settings_chat_font_size_changed", {
|
||||
size: value,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<SectionHeader>
|
||||
|
|
@ -49,6 +64,24 @@ export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...pr
|
|||
{t("settings:ui.collapseThinking.description")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat Message Font Size Setting */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="font-medium">Chat Message Font Size</label>
|
||||
<VSCodeDropdown
|
||||
value={chatMessageFontSize || "default"}
|
||||
onChange={(e: any) => handleChatMessageFontSizeChange(e.target.value)}
|
||||
data-testid="chat-font-size-dropdown">
|
||||
<VSCodeOption value="default">Default</VSCodeOption>
|
||||
<VSCodeOption value="extra-small">Extra Small (90%)</VSCodeOption>
|
||||
<VSCodeOption value="small">Small (95%)</VSCodeOption>
|
||||
<VSCodeOption value="large">Large (105%)</VSCodeOption>
|
||||
<VSCodeOption value="extra-large">Extra Large (110%)</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1">
|
||||
Adjust the font size for messages in the chat view
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
// npx vitest run src/components/settings/__tests__/UISettings.fontsize.spec.tsx
|
||||
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import "@testing-library/jest-dom"
|
||||
import { UISettings } from "../UISettings"
|
||||
import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"
|
||||
import React from "react"
|
||||
|
||||
// Mock vscode API
|
||||
const mockPostMessage = vi.fn()
|
||||
;(global as any).vscode = {
|
||||
postMessage: mockPostMessage,
|
||||
}
|
||||
|
||||
// Mock the useTranslation hook
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
Trans: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
}))
|
||||
|
||||
// Mock VSCode webview UI toolkit components
|
||||
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
|
||||
VSCodeDropdown: ({ children, value, onChange }: any) => (
|
||||
<select role="combobox" value={value} onChange={(e) => onChange && onChange(e)}>
|
||||
{children}
|
||||
</select>
|
||||
),
|
||||
VSCodeOption: ({ children, value }: any) => (
|
||||
<option role="option" value={value}>
|
||||
{children}
|
||||
</option>
|
||||
),
|
||||
VSCodeCheckbox: ({ children }: any) => <label>{children}</label>,
|
||||
}))
|
||||
|
||||
// Mock useExtensionState hook to provide consistent state
|
||||
vi.mock("@src/context/ExtensionStateContext", async () => {
|
||||
const actual = await vi.importActual("@src/context/ExtensionStateContext")
|
||||
return {
|
||||
...actual,
|
||||
useExtensionState: () => ({
|
||||
chatMessageFontSize: "default",
|
||||
setChatMessageFontSize: vi.fn(),
|
||||
soundEnabled: true,
|
||||
setSoundEnabled: vi.fn(),
|
||||
soundVolume: 0.5,
|
||||
setSoundVolume: vi.fn(),
|
||||
diffEnabled: true,
|
||||
setDiffEnabled: vi.fn(),
|
||||
browserActionType: "auto",
|
||||
setBrowserActionType: vi.fn(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe("UISettings - Chat Message Font Size", () => {
|
||||
const defaultProps = {
|
||||
expandedRows: {},
|
||||
setExpandedRows: vi.fn(),
|
||||
isHidden: false,
|
||||
hideAnnouncement: vi.fn(),
|
||||
soundEnabled: true,
|
||||
soundVolume: 0.5,
|
||||
diffEnabled: true,
|
||||
allowedCommands: [],
|
||||
deniedCommands: [],
|
||||
historyPreviewCollapsed: false,
|
||||
chatMessageFontSize: "default",
|
||||
reasoningBlockCollapsed: false,
|
||||
setCachedStateField: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should render chat message font size dropdown with all options", () => {
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<UISettings {...defaultProps} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
|
||||
// Check that the dropdown is rendered
|
||||
const dropdown = screen.getByRole("combobox")
|
||||
expect(dropdown).toBeInTheDocument()
|
||||
|
||||
// Check that all font size options are available
|
||||
const options = screen.getAllByRole("option")
|
||||
const optionTexts = options.map((opt) => opt.textContent)
|
||||
|
||||
expect(optionTexts).toContain("Default")
|
||||
expect(optionTexts).toContain("Extra Small (90%)")
|
||||
expect(optionTexts).toContain("Small (95%)")
|
||||
expect(optionTexts).toContain("Large (105%)")
|
||||
expect(optionTexts).toContain("Extra Large (110%)")
|
||||
})
|
||||
|
||||
it("should display the current font size setting", () => {
|
||||
const props = { ...defaultProps, chatMessageFontSize: "large" }
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<UISettings {...props} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
|
||||
const dropdown = screen.getByRole("combobox")
|
||||
expect(dropdown).toHaveValue("large")
|
||||
})
|
||||
|
||||
it("should handle undefined chatMessageFontSize gracefully", () => {
|
||||
// Use type assertion to test undefined case
|
||||
const props = { ...defaultProps, chatMessageFontSize: undefined as any }
|
||||
render(
|
||||
<ExtensionStateContextProvider>
|
||||
<UISettings {...props} />
|
||||
</ExtensionStateContextProvider>,
|
||||
)
|
||||
|
||||
const dropdown = screen.getByRole("combobox")
|
||||
// Should default to "default" when undefined
|
||||
expect(dropdown).toHaveValue("default")
|
||||
})
|
||||
})
|
||||
|
|
@ -158,6 +158,8 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setMaxDiagnosticMessages: (value: number) => void
|
||||
includeTaskHistoryInEnhance?: boolean
|
||||
setIncludeTaskHistoryInEnhance: (value: boolean) => void
|
||||
chatMessageFontSize?: string
|
||||
setChatMessageFontSize: (value: string) => void
|
||||
}
|
||||
|
||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
|
|
@ -266,6 +268,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
maxDiagnosticMessages: 50,
|
||||
openRouterImageApiKey: "",
|
||||
openRouterImageGenerationSelectedModel: "",
|
||||
chatMessageFontSize: "default", // Default font size
|
||||
})
|
||||
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
|
|
@ -285,6 +288,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
global: {},
|
||||
})
|
||||
const [includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance] = useState(true)
|
||||
const [chatMessageFontSize, setChatMessageFontSize] = useState("default")
|
||||
|
||||
const setListApiConfigMeta = useCallback(
|
||||
(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
|
||||
|
|
@ -322,6 +326,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
if ((newState as any).includeTaskHistoryInEnhance !== undefined) {
|
||||
setIncludeTaskHistoryInEnhance((newState as any).includeTaskHistoryInEnhance)
|
||||
}
|
||||
// Update chatMessageFontSize if present in state message
|
||||
if ((newState as any).chatMessageFontSize !== undefined) {
|
||||
setChatMessageFontSize((newState as any).chatMessageFontSize)
|
||||
}
|
||||
// Handle marketplace data if present in state message
|
||||
if (newState.marketplaceItems !== undefined) {
|
||||
setMarketplaceItems(newState.marketplaceItems)
|
||||
|
|
@ -559,6 +567,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
},
|
||||
includeTaskHistoryInEnhance,
|
||||
setIncludeTaskHistoryInEnhance,
|
||||
chatMessageFontSize,
|
||||
setChatMessageFontSize,
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue