Skip to content
Open
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 @@ -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(),
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 @@ -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") ?? {}
Expand Down
1 change: 1 addition & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ export type ExtensionState = Pick<
| "includeTaskHistoryInEnhance"
| "reasoningBlockCollapsed"
> & {
chatMessageFontSize?: string
version: string
clineMessages: ClineMessage[]
currentTaskItem?: HistoryItem
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export interface WebviewMessage {
| "profileThresholds"
| "setHistoryPreviewCollapsed"
| "setReasoningBlockCollapsed"
| "chatMessageFontSize"
| "openExternal"
| "filterMarketplaceItems"
| "marketplaceButtonClicked"
Expand Down
21 changes: 19 additions & 2 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,37 @@ interface ChatRowProps {
onFollowUpUnmount?: () => void
isFollowUpAnswered?: boolean
editable?: boolean
chatMessageFontSize?: string
}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
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>,
)
Expand Down
3 changes: 3 additions & 0 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
soundVolume,
cloudIsAuthenticated,
messageQueue = [],
chatMessageFontSize,
} = useExtensionState()

const messagesRef = useRef(messages)
Expand Down Expand Up @@ -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" &&
Expand Down Expand Up @@ -1576,6 +1578,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
alwaysAllowUpdateTodoList,
enableButtons,
primaryButtonText,
chatMessageFontSize,
],
)

Expand Down
3 changes: 3 additions & 0 deletions webview-ui/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
openRouterImageApiKey,
openRouterImageGenerationSelectedModel,
reasoningBlockCollapsed,
chatMessageFontSize,
} = cachedState

const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -782,6 +784,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
{activeTab === "ui" && (
<UISettings
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
chatMessageFontSize={chatMessageFontSize || "default"}
setCachedStateField={setCachedStateField}
/>
)}
Expand Down
37 changes: 35 additions & 2 deletions webview-ui/src/components/settings/UISettings.tsx
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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) => {
Expand All @@ -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>
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe("UISettings", () => {
const defaultProps = {
reasoningBlockCollapsed: false,
setCachedStateField: vi.fn(),
chatMessageFontSize: "default",
}

it("renders the collapse thinking checkbox", () => {
Expand Down
10 changes: 10 additions & 0 deletions webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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 })),
Expand Down Expand Up @@ -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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: The new state property 'chatMessageFontSize' is added; consider improving type safety instead of using 'as any' in the update block.

setChatMessageFontSize((newState as any).chatMessageFontSize)
}
// Handle marketplace data if present in state message
if (newState.marketplaceItems !== undefined) {
setMarketplaceItems(newState.marketplaceItems)
Expand Down Expand Up @@ -559,6 +567,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
},
includeTaskHistoryInEnhance,
setIncludeTaskHistoryInEnhance,
chatMessageFontSize,
setChatMessageFontSize,
}

return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
Expand Down
Loading