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
2 changes: 2 additions & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ const clientSettings: ClientSettings = {
diffLayout: "stacked",
environmentIdentificationMode: "artwork",
favorites: [],
fontFamilyChat: "",
fontFamilyCode: "",
fontFamilyComposer: "",
fontFamilySans: "",
fontFamilyTerminal: "",
fontSizeChat: 14,
fontSizeCode: 13,
fontSizeInterface: 16,
fontSizePrompt: 14,
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/appInit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,21 @@ function startClientAppearance(): void {
previous?.fontFamilySans !== settings.fontFamilySans ||
previous.fontFamilyCode !== settings.fontFamilyCode ||
previous.fontFamilyComposer !== settings.fontFamilyComposer ||
previous.fontFamilyChat !== settings.fontFamilyChat ||
previous.fontSizeInterface !== settings.fontSizeInterface ||
previous.fontSizePrompt !== settings.fontSizePrompt ||
previous.fontSizeChat !== settings.fontSizeChat ||
previous.fontSizeCode !== settings.fontSizeCode ||
previous.fontSmoothing !== settings.fontSmoothing
) {
applyAppearanceFontVariables(root, {
sans: settings.fontFamilySans,
code: settings.fontFamilyCode,
composer: settings.fontFamilyComposer,
chat: settings.fontFamilyChat,
sizeInterface: settings.fontSizeInterface,
sizePrompt: settings.fontSizePrompt,
sizeChat: settings.fontSizeChat,
sizeCode: settings.fontSizeCode,
smoothing: settings.fontSmoothing,
});
Expand Down
13 changes: 12 additions & 1 deletion apps/web/src/appearanceFonts.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {
DEFAULT_CHAT_FONT_SIZE,
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 @@ -62,13 +65,15 @@ export interface AppearanceFontPreferences {
readonly sans: string;
readonly code: string;
readonly composer: string;
readonly chat: string;
readonly sizeInterface: number;
readonly sizePrompt: number;
readonly sizeChat: number;
readonly sizeCode: number;
readonly smoothing: boolean;
}

/** Always writes sizes: interface size sets the root; prompt and code use pixels to avoid double scaling. */
/** Always writes sizes: interface size sets the root; prompt, chat and code use pixels to avoid double scaling. */
export function applyAppearanceFontVariables(
root: HTMLElement,
preferences: AppearanceFontPreferences,
Expand All @@ -77,6 +82,7 @@ export function applyAppearanceFontVariables(
["--font-sans", preferences.sans, DEFAULT_SANS_FONT_STACK],
["--font-mono", preferences.code, DEFAULT_CODE_FONT_STACK],
["--font-composer", preferences.composer, "var(--font-sans)"],
["--font-chat", preferences.chat, "var(--font-sans)"],
];
for (const [variable, custom, fallback] of families) {
const list = cssFontFamilies(custom);
Expand All @@ -89,6 +95,7 @@ export function applyAppearanceFontVariables(

root.style.fontSize = `${clampInterfaceFontSize(preferences.sizeInterface)}px`;
root.style.setProperty("--font-size-prompt", `${clampPromptFontSize(preferences.sizePrompt)}px`);
root.style.setProperty("--font-size-chat", `${clampChatFontSize(preferences.sizeChat)}px`);
const code = clampCodeFontSize(preferences.sizeCode);
root.style.setProperty("--font-size-code", `${code}px`);
root.style.setProperty("--diffs-font-size", `${code}px`);
Expand Down Expand Up @@ -118,6 +125,10 @@ function clampPromptFontSize(value: number): number {
return clampFontSize(value, MIN_PROMPT_FONT_SIZE, MAX_PROMPT_FONT_SIZE, DEFAULT_PROMPT_FONT_SIZE);
}

function clampChatFontSize(value: number): number {
return clampFontSize(value, MIN_CHAT_FONT_SIZE, MAX_CHAT_FONT_SIZE, DEFAULT_CHAT_FONT_SIZE);
}

function clampCodeFontSize(value: number): number {
return clampFontSize(value, MIN_CODE_FONT_SIZE, MAX_CODE_FONT_SIZE, DEFAULT_CODE_FONT_SIZE);
}
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/NoProjectsHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ export function NoProjectsHero() {
<Empty size="hero" className="flex-1">
<div className="w-full max-w-lg px-8 py-12">
<EmptyHeader className="max-w-none">
<EmptyTitle>What should we work on?</EmptyTitle>
<EmptyTitle>
<span className="font-chat">What should we work on?</span>
</EmptyTitle>
<EmptyDescription>
{scratchTargetEnvironmentId === null
? "Add a project to start your first thread."
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/DraftHeroHeadline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ export function DraftHeroHeadline({
{projectSelector}
</DraftHeroProjectHeading>
) : (
<h1 className="w-full text-center font-normal text-2xl text-foreground tracking-tight sm:text-3xl">
<h1 className="w-full text-center font-chat font-normal text-2xl text-foreground tracking-tight sm:text-3xl">
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
{isScratchDraft ? (
<>What should we work on?</>
) : canChooseProject ? (
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/components/chat/DraftHeroProjectHeading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ export function DraftHeroProjectHeading({
}, []);

return (
<div ref={containerRef} className="group/draft-hero relative w-full" data-inline={fitsInline}>
<div
ref={containerRef}
className="group/draft-hero relative w-full font-chat"
data-inline={fitsInline}
>
<span
aria-hidden="true"
className="pointer-events-none invisible absolute inset-x-0 top-0 h-0 overflow-hidden"
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 @@ -42,6 +42,7 @@ export function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind
>
<ChatMarkdown
text={messageText}
className="chat-message font-chat text-(length:--font-size-chat,var(--text-sm))"
cwd={ctx.markdownCwd}
threadRef={ctx.threadRef ?? undefined}
isStreaming={Boolean(row.message.streaming)}
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 @@ -904,7 +904,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {
cwd={props.markdownCwd}
threadRef={ctx.threadRef ?? undefined}
skills={props.skills}
className="text-foreground"
className="chat-message font-chat text-(length:--font-size-chat,var(--text-sm)) text-foreground"
lineBreaks
parseRawHtml={false}
renderContextReference={props.renderContextReference}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/TriageDraftHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function TriageDraftHero(props: {
const { copyToClipboard, isCopied } = useCopyToClipboard({ timeout: 1500, target: "command" });
return (
<div className="mx-auto flex w-full max-w-5xl flex-col items-center">
<h1 className="w-full text-center font-normal text-2xl text-foreground tracking-tight sm:text-3xl">
<h1 className="w-full text-center font-chat font-normal text-2xl text-foreground tracking-tight sm:text-3xl">
What went wrong?
</h1>
<p className="mt-2 flex h-6 w-full items-center justify-center gap-1.5 text-sm">
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/components/settings/SettingsFontPreviews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ export function PromptFontPreview() {
);
}

export function ChatFontPreview() {
return (
<div className="mt-1 mb-2 rounded-lg border border-border bg-background px-3 py-2 font-chat">
<p className="text-xl tracking-tight">What should we build?</p>
<p className="mt-1 text-(length:--font-size-chat,var(--text-sm)) leading-relaxed text-foreground/90">
The flaky test raced the settings write. I moved the assertion after the commit, so it now
waits on the persisted event instead of a timer.
</p>
</div>
);
}

const DIFF_PREVIEW_PATCH = [
"diff --git a/src/formatUser.ts b/src/formatUser.ts",
"--- a/src/formatUser.ts",
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,12 @@ type TypographySettings = Pick<
UnifiedSettings,
| "fontFamilySans"
| "fontFamilyComposer"
| "fontFamilyChat"
| "fontFamilyCode"
| "fontFamilyTerminal"
| "fontSizeInterface"
| "fontSizePrompt"
| "fontSizeChat"
| "fontSizeCode"
| "fontSizeTerminal"
>;
Expand All @@ -90,6 +92,10 @@ export function getChangedTypographySettingLabels(settings: TypographySettings):
settings.fontSizeInterface !== DEFAULT_UNIFIED_SETTINGS.fontSizeInterface
? ["Interface font"]
: []),
...(settings.fontFamilyChat !== DEFAULT_UNIFIED_SETTINGS.fontFamilyChat ||
settings.fontSizeChat !== DEFAULT_UNIFIED_SETTINGS.fontSizeChat
? ["Chat font"]
: []),
...(settings.fontFamilyComposer !== DEFAULT_UNIFIED_SETTINGS.fontFamilyComposer ||
settings.fontSizePrompt !== DEFAULT_UNIFIED_SETTINGS.fontSizePrompt
? ["Prompt font"]
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -655,10 +655,12 @@ export function useSettingsRestore(onRestored?: () => void) {
threadTitleInstructions: DEFAULT_UNIFIED_SETTINGS.threadTitleInstructions,
fontFamilySans: DEFAULT_UNIFIED_SETTINGS.fontFamilySans,
fontFamilyComposer: DEFAULT_UNIFIED_SETTINGS.fontFamilyComposer,
fontFamilyChat: DEFAULT_UNIFIED_SETTINGS.fontFamilyChat,
fontFamilyCode: DEFAULT_UNIFIED_SETTINGS.fontFamilyCode,
fontFamilyTerminal: DEFAULT_UNIFIED_SETTINGS.fontFamilyTerminal,
fontSizeInterface: DEFAULT_UNIFIED_SETTINGS.fontSizeInterface,
fontSizePrompt: DEFAULT_UNIFIED_SETTINGS.fontSizePrompt,
fontSizeChat: DEFAULT_UNIFIED_SETTINGS.fontSizeChat,
fontSizeCode: DEFAULT_UNIFIED_SETTINGS.fontSizeCode,
fontSizeTerminal: DEFAULT_UNIFIED_SETTINGS.fontSizeTerminal,
browserDefaultViewport: DEFAULT_UNIFIED_SETTINGS.browserDefaultViewport,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ vi.mock(import("./useScopedSettings"), async (original) => ({
}));
// The previews mount Ghostty and Shiki, which this test does not exercise.
vi.mock(import("./SettingsFontPreviews"), () => ({
ChatFontPreview: () => <div />,
CodeFontPreview: () => <div />,
PromptFontPreview: () => <div />,
TerminalFontPreview: () => <div />,
Expand Down
43 changes: 41 additions & 2 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 All @@ -27,7 +29,12 @@ import {
TYPOGRAPHY_ADVANCED_STORAGE_KEY,
} from "../../appearanceFonts";
import { AdvancedSettings } from "./AdvancedSettings";
import { CodeFontPreview, PromptFontPreview, TerminalFontPreview } from "./SettingsFontPreviews";
import {
ChatFontPreview,
CodeFontPreview,
PromptFontPreview,
TerminalFontPreview,
} from "./SettingsFontPreviews";
import { discoverInstalledFonts, FontFamilyPicker, useFontEnumeration } from "./FontFamilyPicker";
import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
import { Switch } from "../ui/switch";
Expand Down Expand Up @@ -87,6 +94,37 @@ function InterfaceFontRow({ preview }: { preview?: ReactNode }) {
);
}

function ChatFontRow() {
const settings = useScopedSettings();
const updateSettings = useUpdateScopedSettings();
const defaults = useFontDefaultFamilies();
return (
<FontFamilySettingsRow
{...searchableSetting("chat-font")}
description="Messages and the new-thread heading. Try a serif such as Georgia."
defaultFamily={defaults.interfaceFamily}
defaultValue={DEFAULT_UNIFIED_SETTINGS.fontFamilyChat}
value={settings.fontFamilyChat}
onValueChange={(fontFamilyChat) => updateSettings({ fontFamilyChat })}
onReset={() =>
updateSettings({
fontFamilyChat: DEFAULT_UNIFIED_SETTINGS.fontFamilyChat,
fontSizeChat: DEFAULT_UNIFIED_SETTINGS.fontSizeChat,
})
}
size={{
label: "Chat font size",
min: MIN_CHAT_FONT_SIZE,
max: MAX_CHAT_FONT_SIZE,
value: settings.fontSizeChat,
defaultValue: DEFAULT_UNIFIED_SETTINGS.fontSizeChat,
onChange: (fontSizeChat) => updateSettings({ fontSizeChat }),
}}
preview={<ChatFontPreview />}
/>
);
}

function PromptFontRow() {
const settings = useScopedSettings();
const updateSettings = useUpdateScopedSettings();
Expand Down Expand Up @@ -259,6 +297,7 @@ function FontSettingsGroup() {
return (
<>
<InterfaceFontRow />
<ChatFontRow />
<PromptFontRow />
<CodeFontRow />
<TerminalFontRow />
Expand All @@ -276,7 +315,7 @@ function SeparateFontsRow({
return (
<SettingsRow
{...searchableSetting("separate-fonts")}
description="Set the prompt, code, and terminal fonts separately instead of one monospace font."
description="Set the chat, prompt, code, and terminal fonts separately instead of one monospace font."
control={
<Switch
checked={checked}
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ export const SETTINGS_SEARCH_ITEMS = [
to: "/settings/appearance",
searchTerms: ["typography family size system sans"],
},
{
id: "chat-font",
title: "Chat font",
to: "/settings/appearance",
searchTerms: ["typography family size serif messages bubbles reading welcome heading"],
},
{
id: "prompt-font",
title: "Prompt font",
Expand Down Expand Up @@ -1389,6 +1395,7 @@ export function searchSettings(

// These rows only exist with separate fonts on, so a search jump to one turns them on.
const ADVANCED_TYPOGRAPHY_TARGET_IDS: ReadonlySet<string> = new Set([
"chat-font",
"prompt-font",
"terminal-font",
]);
Expand Down
27 changes: 27 additions & 0 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
-apple-system, BlinkMacSystemFont, "Inter Variable", "Segoe UI", system-ui, sans-serif;
--font-mono:
ui-monospace, "SF Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
--font-chat: var(--font-sans);
}

@theme inline {
Expand Down Expand Up @@ -2207,6 +2208,32 @@ code {
font-size: 0.875rem;
}

/* Message bodies carry the chat font size, so their fixed rem sizes become ratios of the
0.875rem body; at the default size these resolve to the same pixels as the rules above. */
.chat-markdown.chat-message h1 {
font-size: calc(1.25em / 0.875);
}

.chat-markdown.chat-message h2 {
font-size: calc(1.125em / 0.875);
}

.chat-markdown.chat-message h3 {
font-size: calc(1em / 0.875);
}

.chat-markdown.chat-message :is(h4, h5, h6, table) {
font-size: 1em;
}

.chat-markdown.chat-message section[data-footnotes] {
font-size: calc(0.75em / 0.875);
}

.chat-markdown.chat-message :is(a[data-footnote-ref], a[data-footnote-backref]) {
font-size: calc(0.6875 * var(--font-size-chat, 0.875rem) / 0.875);
}

/* Top-aligned so wrapped rows share a first line, and the outer columns sit
flush with the surrounding prose. */
.chat-markdown th,
Expand Down
Loading
Loading