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
2 changes: 2 additions & 0 deletions apps/web/src/appInit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ function startClientAppearance(): void {
previous.fontFamilyCode !== settings.fontFamilyCode ||
previous.fontFamilyComposer !== settings.fontFamilyComposer ||
previous.fontSizeInterface !== settings.fontSizeInterface ||
previous.fontSizeChat !== settings.fontSizeChat ||
previous.fontSizePrompt !== settings.fontSizePrompt ||
previous.fontSizeCode !== settings.fontSizeCode ||
previous.fontSmoothing !== settings.fontSmoothing ||
Expand All @@ -105,6 +106,7 @@ function startClientAppearance(): void {
code: settings.fontFamilyCode,
composer: settings.fontFamilyComposer,
sizeInterface: settings.fontSizeInterface,
sizeChat: settings.fontSizeChat,
sizePrompt: settings.fontSizePrompt,
sizeCode: settings.fontSizeCode,
smoothing: settings.fontSmoothing,
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/appearanceFonts.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe("applyAppearanceFontVariables", () => {
code: "",
composer: "",
sizeInterface: 20,
sizeChat: null,
sizePrompt: 14,
sizeCode: 13,
smoothing: true,
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/appearanceFonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import {
DEFAULT_CODE_FONT_SIZE,
DEFAULT_INTERFACE_FONT_SIZE,
DEFAULT_PROMPT_FONT_SIZE,
MAX_CHAT_FONT_SIZE,
MAX_CODE_FONT_SIZE,
MAX_INTERFACE_FONT_SIZE,
MAX_PROMPT_FONT_SIZE,
MIN_CHAT_FONT_SIZE,
MIN_CODE_FONT_SIZE,
MIN_INTERFACE_FONT_SIZE,
MIN_PROMPT_FONT_SIZE,
Expand Down Expand Up @@ -63,6 +65,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 +96,14 @@ export function applyAppearanceFontVariables(
"--font-size-prompt",
preferences.separate ? `${clampPromptFontSize(preferences.sizePrompt)}px` : "var(--text-sm)",
);
if (preferences.sizeChat === null) {
root.style.removeProperty("--font-size-chat");
} else {
root.style.setProperty(
"--font-size-chat",
`${clampFontSize(preferences.sizeChat, MIN_CHAT_FONT_SIZE, MAX_CHAT_FONT_SIZE, MIN_CHAT_FONT_SIZE)}px`,
);
}
const code = clampCodeFontSize(preferences.sizeCode);
root.style.setProperty("--font-size-code", `${code}px`);
root.style.setProperty("--diffs-font-size", `${code}px`);
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/chat/TimelineAssistantRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind
listRef={ctx.listRef}
>
<ChatMarkdown
className="chat-message-markdown"
text={messageText}
cwd={ctx.markdownCwd}
threadRef={ctx.threadRef ?? undefined}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/TimelineUserMessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1190,7 +1190,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {
cwd={props.markdownCwd}
threadRef={ctx.threadRef ?? undefined}
skills={props.skills}
className="text-foreground"
className="chat-message-markdown text-foreground"
lineBreaks
parseRawHtml={false}
renderContextReference={props.renderContextReference}
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 @@ -688,6 +688,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
61 changes: 61 additions & 0 deletions apps/web/src/components/settings/TypographySettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import type { ReactNode } from "react";
import { useEffect, useEffectEvent, useRef, useState } from "react";
import {
DEFAULT_UNIFIED_SETTINGS,
MAX_CHAT_FONT_SIZE,
MAX_CODE_FONT_SIZE,
MAX_INTERFACE_FONT_SIZE,
MAX_PROMPT_FONT_SIZE,
MAX_TERMINAL_FONT_SIZE,
MIN_CHAT_FONT_SIZE,
MIN_CODE_FONT_SIZE,
MIN_INTERFACE_FONT_SIZE,
MIN_PROMPT_FONT_SIZE,
Expand Down Expand Up @@ -96,6 +98,64 @@ function InterfaceFontRow({ preview }: { preview?: ReactNode }) {
);
}

function ChatFontSizeRow() {
const settings = useScopedSettings();
const updateSettings = useUpdateScopedSettings();
return (
<SettingsRow
{...searchableSetting("chat-font-size")}
description="User messages and agent replies. Default follows the interface font size."
resetAction={
settings.fontSizeChat !== DEFAULT_UNIFIED_SETTINGS.fontSizeChat ? (
<SettingResetButton
label="chat font size"
onClick={() => updateSettings({ fontSizeChat: DEFAULT_UNIFIED_SETTINGS.fontSizeChat })}
/>
) : null
}
control={
<Select
value={settings.fontSizeChat === null ? "default" : String(settings.fontSizeChat)}
onValueChange={(next) => {
if (next === "default") {
updateSettings({ fontSizeChat: null });
return;
}
if (typeof next !== "string") return;
const parsed = Number(next);
if (
Number.isInteger(parsed) &&
parsed >= MIN_CHAT_FONT_SIZE &&
parsed <= MAX_CHAT_FONT_SIZE
) {
updateSettings({ fontSizeChat: parsed });
}
}}
>
<SelectTrigger size="sm" className="w-28" aria-label="Chat font size">
<SelectValue>
{settings.fontSizeChat === null ? "Default" : `${settings.fontSizeChat} px`}
</SelectValue>
</SelectTrigger>
<SelectPopup align="end" alignItemWithTrigger={false}>
<SelectItem hideIndicator value="default">
Default
</SelectItem>
{Array.from(
{ length: MAX_CHAT_FONT_SIZE - MIN_CHAT_FONT_SIZE + 1 },
(_, index) => MIN_CHAT_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 @@ -346,6 +406,7 @@ export function TypographySection() {
return (
<SettingsSection id="typography" title="Typography">
{showAdvanced ? <FontSettingsGroup /> : <SimpleFontRows />}
<ChatFontSizeRow />
<WordWrapRow />
<AdvancedSettings
targetIds={[searchableSetting("separate-fonts").id, searchableSetting("font-smoothing").id]}
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 @@ -381,6 +381,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 messages user assistant agent output reply response text size large"],
},
{
id: "prompt-font",
title: "Prompt font",
Expand Down
22 changes: 13 additions & 9 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -1867,6 +1867,11 @@ code {

/* Chat markdown rendering */

.chat-message-markdown {
--chat-markdown-font-unit: calc(var(--font-size-chat, var(--text-sm)) / 0.875);
font-size: var(--font-size-chat, var(--text-sm));
}

.chat-markdown > :first-child {
margin-top: 0;
}
Expand Down Expand Up @@ -1903,21 +1908,21 @@ code {
}

:is(.chat-markdown, .composer-tiptap) h1 {
font-size: 1.25rem;
font-size: calc(var(--chat-markdown-font-unit, 1rem) * 1.25);
}

:is(.chat-markdown, .composer-tiptap) h2 {
font-size: 1.125rem;
font-size: calc(var(--chat-markdown-font-unit, 1rem) * 1.125);
}

:is(.chat-markdown, .composer-tiptap) h3 {
font-size: 1rem;
font-size: calc(var(--chat-markdown-font-unit, 1rem) * 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(var(--chat-markdown-font-unit, 1rem) * 0.875);
}

:is(.chat-markdown, .composer-tiptap) h6 {
Expand All @@ -1928,8 +1933,8 @@ code {
/* Reset for nested uls under a widened ol. --list-gutter is an inherited
custom property, so without this a task-list under a multi-digit ordered
list would inherit the outer gutter instead of its own default. */
--list-gutter: 1.25rem;
padding-inline-start: 1.25rem;
--list-gutter: calc(var(--chat-markdown-font-unit, 1rem) * 1.25);
padding-inline-start: var(--list-gutter);
list-style-type: disc;
}

Expand All @@ -1939,7 +1944,7 @@ code {
outside painting the marker past the padding box. Reset it here too so a
nested ol without its own widened marker doesn't inherit the outer one. */
:is(.chat-markdown, .composer-tiptap) ol {
--list-gutter: 1.25rem;
--list-gutter: calc(var(--chat-markdown-font-unit, 1rem) * 1.25);
padding-inline-start: var(--list-gutter, 1.25rem);
list-style-type: decimal;
}
Expand Down Expand Up @@ -2227,8 +2232,7 @@ code {
border-collapse: collapse;
overflow-wrap: normal;
word-break: normal;
/* One step under 15px prose. */
font-size: 0.875rem;
font-size: calc(var(--chat-markdown-font-unit, 1rem) * 0.875);
}

/* Top-aligned so wrapped rows share a first line, and the outer columns sit
Expand Down
7 changes: 7 additions & 0 deletions docs/user/appearance.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ Android uses **Material You Layout** by default unless you have turned it off in
It changes shapes, spacing, and controls independently
of the selected theme.

## Chat text size

On web and desktop, use **Settings → Appearance → Typography → Chat font size** to set user
messages and agent replies from 12 to 32 px without enlarging the sidebar or composer. Code
blocks keep their own font size. **Default** or resetting the setting restores chat text that
follows the interface font size.

## Composer context

Git-backed projects show branch and worktree controls below the composer while you create a thread.
Expand Down
11 changes: 11 additions & 0 deletions packages/contracts/src/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,17 @@ describe("resolveIdleCompaction", () => {
});
});

describe("ClientSettings chat font size", () => {
it("preserves inherited sizes for older settings and round-trips explicit sizes and reset", () => {
const legacy = decodeClientSettings({});
expect(legacy.fontSizeChat).toBeNull();
for (const fontSizeChat of [32, null]) {
const stored = encodeClientSettings({ ...legacy, fontSizeChat });
expect(decodeClientSettings(stored).fontSizeChat).toBe(fontSizeChat);
}
});
});

describe("ClientSettings notifications", () => {
it("requires opt-in when existing settings omit notification preferences", () => {
expect(decodeClientSettings({}).notificationMode).toBe("off");
Expand Down
9 changes: 9 additions & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ export const InterfaceFontSize = Schema.Int.check(
export type InterfaceFontSize = typeof InterfaceFontSize.Type;
export const DEFAULT_INTERFACE_FONT_SIZE: InterfaceFontSize = 16;

export const MIN_CHAT_FONT_SIZE = 12;
export const MAX_CHAT_FONT_SIZE = 32;
export const ChatFontSize = Schema.Int.check(
Schema.isBetween({ minimum: MIN_CHAT_FONT_SIZE, maximum: MAX_CHAT_FONT_SIZE }),
);
export type ChatFontSize = typeof ChatFontSize.Type;

export const MIN_PROMPT_FONT_SIZE = 12;
export const MAX_PROMPT_FONT_SIZE = 20;
export const PromptFontSize = Schema.Int.check(
Expand Down Expand Up @@ -467,6 +474,7 @@ export const ClientSettingsSchema = Schema.Struct({
fontSizeInterface: InterfaceFontSize.pipe(
Schema.withDecodingDefault(Effect.succeed(DEFAULT_INTERFACE_FONT_SIZE)),
),
fontSizeChat: Schema.NullOr(ChatFontSize).pipe(Schema.withDecodingDefault(Effect.succeed(null))),
fontSizePrompt: PromptFontSize.pipe(
Schema.withDecodingDefault(Effect.succeed(DEFAULT_PROMPT_FONT_SIZE)),
),
Expand Down Expand Up @@ -2006,6 +2014,7 @@ export const ClientSettingsPatch = Schema.Struct({
glassOpacity: Schema.optionalKey(GlassOpacity),
onboardingCompletedAt: Schema.optionalKey(Schema.NullOr(Schema.String)),
fontSizeInterface: Schema.optionalKey(InterfaceFontSize),
fontSizeChat: Schema.optionalKey(Schema.NullOr(ChatFontSize)),
fontSizePrompt: Schema.optionalKey(PromptFontSize),
fontSizeCode: Schema.optionalKey(CodeFontSize),
fontSizeTerminal: Schema.optionalKey(TerminalFontSize),
Expand Down
Loading