diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 51c3d31daae85..df61117c46883 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -45,6 +45,7 @@ const clientSettings: ClientSettings = { fontFamilyComposer: "", fontFamilySans: "", fontFamilyTerminal: "", + fontSizeChat: null, fontSizeCode: 13, fontSizeInterface: 16, fontSizePrompt: 14, diff --git a/apps/web/src/appearanceFonts.test.ts b/apps/web/src/appearanceFonts.test.ts new file mode 100644 index 0000000000000..d027447c4074b --- /dev/null +++ b/apps/web/src/appearanceFonts.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { chatFontScale } from "./appearanceFonts"; + +describe("chatFontScale", () => { + it("follows the interface when unset or equal to it", () => { + expect(chatFontScale(null, 16)).toBeNull(); + expect(chatFontScale(16, 16)).toBeNull(); + }); + + it("scales chat text relative to the interface size", () => { + expect(chatFontScale(20, 16)).toBe(1.25); + expect(chatFontScale(12, 16)).toBe(0.75); + expect(chatFontScale(18, 12)).toBe(1.5); + }); + + it("clamps out-of-range sizes before scaling", () => { + expect(chatFontScale(40, 16)).toBe(20 / 16); + expect(chatFontScale(20, 40)).toBeNull(); + }); +}); diff --git a/apps/web/src/appearanceFonts.ts b/apps/web/src/appearanceFonts.ts index c816836ae8aed..3a3228769075f 100644 --- a/apps/web/src/appearanceFonts.ts +++ b/apps/web/src/appearanceFonts.ts @@ -64,6 +64,7 @@ export interface AppearanceFontPreferences { readonly code: string; readonly composer: string; readonly sizeInterface: number; + readonly sizeChat: number | null; readonly sizePrompt: number; readonly sizeCode: number; readonly smoothing: boolean; @@ -93,6 +94,12 @@ export function applyAppearanceFontVariables( } root.style.fontSize = `${clampInterfaceFontSize(preferences.sizeInterface)}px`; + const chatScale = chatFontScale(preferences.sizeChat, preferences.sizeInterface); + if (chatScale === null) { + root.style.removeProperty("--chat-font-size-ratio"); + } else { + root.style.setProperty("--chat-font-size-ratio", String(chatScale)); + } root.style.setProperty("--font-size-prompt", `${clampPromptFontSize(preferences.sizePrompt)}px`); const code = clampCodeFontSize(preferences.sizeCode); root.style.setProperty("--font-size-code", `${code}px`); @@ -119,6 +126,18 @@ function clampInterfaceFontSize(value: number): number { ); } +/** + * Chat text keeps its rem-based sizes, so a chat size is applied as a ratio to + * the interface size rather than as pixels. Null means chat follows the + * interface. + */ +export function chatFontScale(sizeChat: number | null, sizeInterface: number): number | null { + if (sizeChat === null) return null; + const chat = clampInterfaceFontSize(sizeChat); + const base = clampInterfaceFontSize(sizeInterface); + return chat === base ? null : chat / base; +} + function clampPromptFontSize(value: number): number { return clampFontSize(value, MIN_PROMPT_FONT_SIZE, MAX_PROMPT_FONT_SIZE, DEFAULT_PROMPT_FONT_SIZE); } diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 19b1aeb55a5d5..9d863b0c09334 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1046,7 +1046,7 @@ const ConversationTimeline = memo(function ConversationTimeline({
diff --git a/apps/web/src/components/settings/SettingsPanels.logic.ts b/apps/web/src/components/settings/SettingsPanels.logic.ts index f7d60578989a6..1fdd6998ba91a 100644 --- a/apps/web/src/components/settings/SettingsPanels.logic.ts +++ b/apps/web/src/components/settings/SettingsPanels.logic.ts @@ -79,6 +79,7 @@ type TypographySettings = Pick< | "fontFamilyCode" | "fontFamilyTerminal" | "fontSizeInterface" + | "fontSizeChat" | "fontSizePrompt" | "fontSizeCode" | "fontSizeTerminal" @@ -90,6 +91,7 @@ export function getChangedTypographySettingLabels(settings: TypographySettings): settings.fontSizeInterface !== DEFAULT_UNIFIED_SETTINGS.fontSizeInterface ? ["Interface font"] : []), + ...(settings.fontSizeChat !== DEFAULT_UNIFIED_SETTINGS.fontSizeChat ? ["Chat font size"] : []), ...(settings.fontFamilyComposer !== DEFAULT_UNIFIED_SETTINGS.fontFamilyComposer || settings.fontSizePrompt !== DEFAULT_UNIFIED_SETTINGS.fontSizePrompt ? ["Prompt font"] diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index ec1e510ad160f..0912233621e37 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -635,6 +635,7 @@ export function useSettingsRestore(onRestored?: () => void) { fontFamilyCode: DEFAULT_UNIFIED_SETTINGS.fontFamilyCode, fontFamilyTerminal: DEFAULT_UNIFIED_SETTINGS.fontFamilyTerminal, fontSizeInterface: DEFAULT_UNIFIED_SETTINGS.fontSizeInterface, + fontSizeChat: DEFAULT_UNIFIED_SETTINGS.fontSizeChat, fontSizePrompt: DEFAULT_UNIFIED_SETTINGS.fontSizePrompt, fontSizeCode: DEFAULT_UNIFIED_SETTINGS.fontSizeCode, fontSizeTerminal: DEFAULT_UNIFIED_SETTINGS.fontSizeTerminal, diff --git a/apps/web/src/components/settings/TypographySettings.tsx b/apps/web/src/components/settings/TypographySettings.tsx index f991a49bbd439..7282ef81f6415 100644 --- a/apps/web/src/components/settings/TypographySettings.tsx +++ b/apps/web/src/components/settings/TypographySettings.tsx @@ -82,6 +82,64 @@ function InterfaceFontRow({ preview }: { preview?: ReactNode }) { ); } +const FOLLOW_INTERFACE_VALUE = "interface"; + +function ChatFontSizeRow() { + const settings = useScopedSettings(); + const updateSettings = useUpdateScopedSettings(); + const value = settings.fontSizeChat; + return ( + updateSettings({ fontSizeChat: DEFAULT_UNIFIED_SETTINGS.fontSizeChat })} + /> + ) : null + } + control={ + + } + /> + ); +} + function PromptFontRow() { const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); @@ -254,6 +312,7 @@ function FontSettingsGroup() { return ( <> + @@ -267,6 +326,7 @@ function SimpleFontRows() { return ( <> } /> + :first-child { @@ -1822,21 +1835,21 @@ code { } :is(.chat-markdown, .composer-tiptap) h1 { - font-size: 1.25rem; + font-size: calc(1.25rem * var(--chat-font-scale, 1)); } :is(.chat-markdown, .composer-tiptap) h2 { - font-size: 1.125rem; + font-size: calc(1.125rem * var(--chat-font-scale, 1)); } :is(.chat-markdown, .composer-tiptap) h3 { - font-size: 1rem; + font-size: calc(1rem * var(--chat-font-scale, 1)); } :is(.chat-markdown, .composer-tiptap) h4, :is(.chat-markdown, .composer-tiptap) h5, :is(.chat-markdown, .composer-tiptap) h6 { - font-size: 0.875rem; + font-size: calc(0.875rem * var(--chat-font-scale, 1)); } :is(.chat-markdown, .composer-tiptap) h6 { diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 3d73c01104a24..8baecb0869c12 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -313,6 +313,7 @@ function FontAppearanceSync() { const fontFamilyCode = useClientSettings((settings) => settings.fontFamilyCode); const fontFamilyComposer = useClientSettings((settings) => settings.fontFamilyComposer); const fontSizeInterface = useClientSettings((settings) => settings.fontSizeInterface); + const fontSizeChat = useClientSettings((settings) => settings.fontSizeChat); const fontSizePrompt = useClientSettings((settings) => settings.fontSizePrompt); const fontSizeCode = useClientSettings((settings) => settings.fontSizeCode); const fontSmoothing = useClientSettings((settings) => settings.fontSmoothing); @@ -323,6 +324,7 @@ function FontAppearanceSync() { code: fontFamilyCode, composer: fontFamilyComposer, sizeInterface: fontSizeInterface, + sizeChat: fontSizeChat, sizePrompt: fontSizePrompt, sizeCode: fontSizeCode, smoothing: fontSmoothing, @@ -332,6 +334,7 @@ function FontAppearanceSync() { fontFamilyComposer, fontFamilySans, fontSizeCode, + fontSizeChat, fontSizeInterface, fontSizePrompt, fontSmoothing, diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 9f84636d12bea..2e8dde1376ced 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -401,6 +401,10 @@ export const ClientSettingsSchema = Schema.Struct({ fontSizePrompt: PromptFontSize.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_PROMPT_FONT_SIZE)), ), + // Null follows the interface size. + fontSizeChat: Schema.NullOr(InterfaceFontSize).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), fontSizeCode: CodeFontSize.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_CODE_FONT_SIZE)), ), @@ -1710,6 +1714,7 @@ export const ClientSettingsPatch = Schema.Struct({ onboardingCompletedAt: Schema.optionalKey(Schema.NullOr(Schema.String)), fontSizeInterface: Schema.optionalKey(InterfaceFontSize), fontSizePrompt: Schema.optionalKey(PromptFontSize), + fontSizeChat: Schema.optionalKey(Schema.NullOr(InterfaceFontSize)), fontSizeCode: Schema.optionalKey(CodeFontSize), fontSizeTerminal: Schema.optionalKey(TerminalFontSize), fontFamilyCode: Schema.optionalKey(FontFamilyPreference),