Skip to content
Closed
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 apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const clientSettings: ClientSettings = {
fontFamilyComposer: "",
fontFamilySans: "",
fontFamilyTerminal: "",
fontSizeChat: null,
fontSizeCode: 13,
fontSizeInterface: 16,
fontSizePrompt: 14,
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/appearanceFonts.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
19 changes: 19 additions & 0 deletions apps/web/src/appearanceFonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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`);
Expand All @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1046,7 +1046,7 @@ const ConversationTimeline = memo(function ConversationTimeline({
<TimelineRowCtx value={sharedState}>
<div className="messages-timeline-row-frame">
<div
className="@container/chat-lane chat-content-lane overflow-x-clip"
className="@container/chat-lane chat-content-lane chat-text-scope overflow-x-clip"
data-timeline-root="true"
>
<TimelineRowContent row={item} />
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type TypographySettings = Pick<
| "fontFamilyCode"
| "fontFamilyTerminal"
| "fontSizeInterface"
| "fontSizeChat"
| "fontSizePrompt"
| "fontSizeCode"
| "fontSizeTerminal"
Expand All @@ -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"]
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
60 changes: 60 additions & 0 deletions apps/web/src/components/settings/TypographySettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<SettingsRow
{...searchableSetting("chat-font-size")}
description="Your messages and the agent's replies. Follows the interface size by default."
resetAction={
value !== DEFAULT_UNIFIED_SETTINGS.fontSizeChat ? (
<SettingResetButton
label="chat font size"
onClick={() => updateSettings({ fontSizeChat: DEFAULT_UNIFIED_SETTINGS.fontSizeChat })}
/>
) : null
}
control={
<Select
value={value === null ? FOLLOW_INTERFACE_VALUE : String(value)}
onValueChange={(next) => {
if (next === FOLLOW_INTERFACE_VALUE) {
updateSettings({ fontSizeChat: null });
return;
}
const parsed = Number(next);
if (
Number.isInteger(parsed) &&
parsed >= MIN_INTERFACE_FONT_SIZE &&
parsed <= MAX_INTERFACE_FONT_SIZE
) {
updateSettings({ fontSizeChat: parsed });
}
}}
>
<SelectTrigger size="sm" className="w-28 shrink-0" aria-label="Chat font size">
<SelectValue>{value === null ? "Interface" : `${value} px`}</SelectValue>
</SelectTrigger>
<SelectPopup align="end" alignItemWithTrigger={false}>
<SelectItem hideIndicator value={FOLLOW_INTERFACE_VALUE}>
Interface
</SelectItem>
{Array.from(
{ length: MAX_INTERFACE_FONT_SIZE - MIN_INTERFACE_FONT_SIZE + 1 },
(_, index) => MIN_INTERFACE_FONT_SIZE + index,
).map((px) => (
<SelectItem hideIndicator key={px} value={String(px)}>
{px} px
</SelectItem>
))}
</SelectPopup>
</Select>
}
/>
);
}

function PromptFontRow() {
const settings = useScopedSettings();
const updateSettings = useUpdateScopedSettings();
Expand Down Expand Up @@ -254,6 +312,7 @@ function FontSettingsGroup() {
return (
<>
<InterfaceFontRow />
<ChatFontSizeRow />
<PromptFontRow />
<CodeFontRow />
<TerminalFontRow />
Expand All @@ -267,6 +326,7 @@ function SimpleFontRows() {
return (
<>
<InterfaceFontRow preview={<PromptFontPreview />} />
<ChatFontSizeRow />
<CodeFontRow
title="Monospace font"
description="Code blocks, diffs, file previews, and the terminal."
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ export const SETTINGS_SEARCH_ITEMS = [
to: "/settings/appearance",
searchTerms: ["typography family size system sans"],
},
{
id: "chat-font-size",
title: "Chat font size",
to: "/settings/appearance",
searchTerms: ["typography text size conversation messages replies responses timeline"],
},
{
id: "prompt-font",
title: "Prompt font",
Expand Down
21 changes: 17 additions & 4 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,19 @@ code {
background: var(--app-scrollbar-thumb-hover);
}

/* Chat font size. Tailwind's text utilities read these tokens, so rescaling
them here resizes conversation text without touching chrome, the prompt, or
code, which sizes itself in pixels through --font-size-code. The root only
carries the ratio; scaling applies inside the scope, so markdown elsewhere
(prompt, file previews) keeps the interface size. */
.chat-text-scope {
--chat-font-scale: var(--chat-font-size-ratio, 1);
--text-xs: calc(0.75rem * var(--chat-font-scale, 1));
--text-sm: calc(0.875rem * var(--chat-font-scale, 1));
--text-base: calc(1rem * var(--chat-font-scale, 1));
--text-lg: calc(1.125rem * var(--chat-font-scale, 1));
}

/* Chat markdown rendering */

.chat-markdown > :first-child {
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -323,6 +324,7 @@ function FontAppearanceSync() {
code: fontFamilyCode,
composer: fontFamilyComposer,
sizeInterface: fontSizeInterface,
sizeChat: fontSizeChat,
sizePrompt: fontSizePrompt,
sizeCode: fontSizeCode,
smoothing: fontSmoothing,
Expand All @@ -332,6 +334,7 @@ function FontAppearanceSync() {
fontFamilyComposer,
fontFamilySans,
fontSizeCode,
fontSizeChat,
fontSizeInterface,
fontSizePrompt,
fontSmoothing,
Expand Down
5 changes: 5 additions & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
),
Expand Down Expand Up @@ -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),
Expand Down
Loading