feat(webchat): Add chat font size setting

Adds a configurable font size option for the webchat interface.

Changes:
- Add chatFontSize setting to UiSettings (small/medium/large/x-large)
- Add --font-size-chat CSS variable in base.css
- Update chat text styles to use the CSS variable
- Add applyChatFontSize function to apply setting to document
- Add font size dropdown control in chat controls UI
- Add styles for the font size control

Resolves #3027
This commit is contained in:
Josh Lewis 2026-01-27 17:34:05 -06:00
parent 0b1c8db0ca
commit 169ff8b8d9
7 changed files with 116 additions and 2 deletions

View File

@ -84,6 +84,7 @@
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;
--font-body: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-display: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-size-chat: 14px; /* Adjustable via UI settings */
/* Shadows - Richer with subtle color */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);

View File

@ -325,6 +325,53 @@
color: rgba(16, 24, 40, 0.3);
}
/* Font size control */
.chat-controls__font-size {
display: flex;
align-items: center;
gap: 6px;
}
.chat-controls__font-icon {
display: flex;
align-items: center;
justify-content: center;
color: var(--muted);
}
.chat-controls__font-icon svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 2px;
}
.chat-controls__font-select {
padding: 4px 8px;
font-size: 12px;
border-radius: 6px;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.06);
color: var(--text);
cursor: pointer;
min-width: 80px;
}
.chat-controls__font-select:hover {
border-color: var(--border-strong);
}
.chat-controls__font-select:focus {
outline: none;
border-color: var(--accent);
}
:root[data-theme="light"] .chat-controls__font-select {
background: #ffffff;
border-color: var(--border);
}
.btn--icon:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.2);

View File

@ -19,7 +19,7 @@
}
.chat-text {
font-size: 14px;
font-size: var(--font-size-chat, 14px);
line-height: 1.5;
word-wrap: break-word;
overflow-wrap: break-word;

View File

@ -9,6 +9,7 @@ import { syncUrlWithSessionKey } from "./app-settings";
import type { SessionsListResult } from "./types";
import type { ThemeMode } from "./theme";
import type { ThemeTransitionContext } from "./theme-transition";
import type { ChatFontSize } from "./storage";
export function renderTab(state: AppViewState, tab: Tab) {
const href = pathForTab(tab, state.basePath);
@ -128,6 +129,44 @@ export function renderChatControls(state: AppViewState) {
>
${focusIcon}
</button>
<span class="chat-controls__separator">|</span>
${renderFontSizeControl(state)}
</div>
`;
}
const FONT_SIZE_OPTIONS: Array<{ value: ChatFontSize; label: string }> = [
{ value: "small", label: "Small" },
{ value: "medium", label: "Medium" },
{ value: "large", label: "Large" },
{ value: "x-large", label: "X-Large" },
];
function renderFontSizeControl(state: AppViewState) {
const currentSize = state.settings.chatFontSize ?? "medium";
const textIcon = html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h16v3"></path><path d="M9 20h6"></path><path d="M12 4v16"></path></svg>`;
return html`
<div class="chat-controls__font-size">
<span class="chat-controls__font-icon" title="Font size">${textIcon}</span>
<select
class="chat-controls__font-select"
.value=${currentSize}
@change=${(e: Event) => {
const next = (e.target as HTMLSelectElement).value as ChatFontSize;
state.applySettings({
...state.settings,
chatFontSize: next,
});
}}
title="Chat font size"
>
${FONT_SIZE_OPTIONS.map(
(opt) =>
html`<option value=${opt.value} ?selected=${opt.value === currentSize}>
${opt.label}
</option>`,
)}
</select>
</div>
`;
}

View File

@ -17,6 +17,7 @@ const createHost = (tab: Tab): SettingsHost => ({
theme: "system",
chatFocusMode: false,
chatShowThinking: true,
chatFontSize: "medium",
splitRatio: 0.6,
navCollapsed: false,
navGroupsCollapsed: {},

View File

@ -10,7 +10,7 @@ import { loadPresence } from "./controllers/presence";
import { loadSessions } from "./controllers/sessions";
import { loadSkills } from "./controllers/skills";
import { inferBasePathFromPathname, normalizeBasePath, normalizePath, pathForTab, tabFromPath, type Tab } from "./navigation";
import { saveSettings, type UiSettings } from "./storage";
import { saveSettings, CHAT_FONT_SIZE_PX, type UiSettings, type ChatFontSize } from "./storage";
import { resolveTheme, type ResolvedTheme, type ThemeMode } from "./theme";
import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition";
import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
@ -46,6 +46,7 @@ export function applySettings(host: SettingsHost, next: UiSettings) {
host.theme = next.theme;
applyResolvedTheme(host, resolveTheme(next.theme));
}
applyChatFontSize(normalized.chatFontSize);
host.applySessionKey = host.settings.lastActiveSessionKey;
}
@ -191,6 +192,7 @@ export function inferBasePath() {
export function syncThemeWithSettings(host: SettingsHost) {
host.theme = host.settings.theme ?? "system";
applyResolvedTheme(host, resolveTheme(host.theme));
applyChatFontSize(host.settings.chatFontSize ?? "medium");
}
export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) {
@ -201,6 +203,12 @@ export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme)
root.style.colorScheme = resolved;
}
export function applyChatFontSize(size: ChatFontSize) {
if (typeof document === "undefined") return;
const px = CHAT_FONT_SIZE_PX[size] ?? CHAT_FONT_SIZE_PX.medium;
document.documentElement.style.setProperty("--font-size-chat", `${px}px`);
}
export function attachThemeListener(host: SettingsHost) {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
host.themeMedia = window.matchMedia("(prefers-color-scheme: dark)");

View File

@ -2,6 +2,15 @@ const KEY = "moltbot.control.settings.v1";
import type { ThemeMode } from "./theme";
export type ChatFontSize = "small" | "medium" | "large" | "x-large";
export const CHAT_FONT_SIZE_PX: Record<ChatFontSize, number> = {
small: 13,
medium: 14,
large: 16,
"x-large": 18,
};
export type UiSettings = {
gatewayUrl: string;
token: string;
@ -10,6 +19,7 @@ export type UiSettings = {
theme: ThemeMode;
chatFocusMode: boolean;
chatShowThinking: boolean;
chatFontSize: ChatFontSize; // Chat text font size
splitRatio: number; // Sidebar split ratio (0.4 to 0.7, default 0.6)
navCollapsed: boolean; // Collapsible sidebar state
navGroupsCollapsed: Record<string, boolean>; // Which nav groups are collapsed
@ -29,6 +39,7 @@ export function loadSettings(): UiSettings {
theme: "system",
chatFocusMode: false,
chatShowThinking: true,
chatFontSize: "medium",
splitRatio: 0.6,
navCollapsed: false,
navGroupsCollapsed: {},
@ -69,6 +80,13 @@ export function loadSettings(): UiSettings {
typeof parsed.chatShowThinking === "boolean"
? parsed.chatShowThinking
: defaults.chatShowThinking,
chatFontSize:
parsed.chatFontSize === "small" ||
parsed.chatFontSize === "medium" ||
parsed.chatFontSize === "large" ||
parsed.chatFontSize === "x-large"
? parsed.chatFontSize
: defaults.chatFontSize,
splitRatio:
typeof parsed.splitRatio === "number" &&
parsed.splitRatio >= 0.4 &&