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: 1 addition & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ button.extension-widget-trigger:focus-visible {
padding: 3px 12px 8px;
color: var(--text-muted);
font-family: var(--font-mono);
font-size: 14px;
font-size: var(--extension-widget-font-size, 14px);
line-height: 1.45;
overflow-wrap: anywhere;
white-space: pre-wrap;
Expand Down
12 changes: 11 additions & 1 deletion components/ChatAppearance.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const settingsPanel = await readFile(new URL("./SettingsPanel.tsx", import.meta.
const globals = await readFile(new URL("../app/globals.css", import.meta.url), "utf8");
const chatAppearanceHook = await readFile(new URL("../hooks/useChatAppearance.ts", import.meta.url), "utf8");
const jiti = createJiti(import.meta.url);
const { clampChatContentWidth, clampChatContentFontSize } = await jiti.import("../hooks/useChatAppearance.ts");
const { clampChatContentWidth, clampChatContentFontSize, clampExtensionWidgetFontSize } = await jiti.import("../hooks/useChatAppearance.ts");

const widthVariable = /var\(--chat-content-max-width, 820px\)/g;

Expand Down Expand Up @@ -49,3 +49,13 @@ test("chat font size preserves the default and bounds stored or supplied values"
assert.equal(clampChatContentFontSize(18.7), 19);
assert.equal(clampChatContentFontSize(30), 24);
});

test("extension widget font size preserves the default and bounds stored or supplied values", () => {
for (const value of [undefined, null, "invalid", Infinity, NaN]) {
assert.equal(clampExtensionWidgetFontSize(value), 14);
}
assert.equal(clampExtensionWidgetFontSize(8), 12);
assert.equal(clampExtensionWidgetFontSize("18"), 18);
assert.equal(clampExtensionWidgetFontSize(18.7), 19);
assert.equal(clampExtensionWidgetFontSize(30), 24);
});
4 changes: 2 additions & 2 deletions components/SettingsPanel.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ test("groups chat display controls together without row backgrounds", () => {

assert.doesNotMatch(appearanceSection, /settings-chat-content/);
assert.match(chatSection, /className="settings-chat-options"/);
assert.equal((chatSection.match(/className="settings-chat-option(?: |")/g) ?? []).length, 4);
assert.equal((chatSection.match(/className="settings-chat-option(?: |")/g) ?? []).length, 5);
assert.equal((chatSection.match(/<ConfigSwitch/g) ?? []).length, 2);
for (const key of ["thinkingExpandedDefault", "chatContentWidth", "chatContentFontSize", "quoteSelection"]) {
for (const key of ["thinkingExpandedDefault", "chatContentWidth", "chatContentFontSize", "extensionWidgetFontSize", "quoteSelection"]) {
assert.match(chatSection, new RegExp(`t\\("settings\\.${key}"\\)`));
}
assert.doesNotMatch(panelSource, /ThinkingIcon|settings-thinking-/);
Expand Down
33 changes: 32 additions & 1 deletion components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {
CHAT_CONTENT_FONT_SIZE_DEFAULT,
CHAT_CONTENT_FONT_SIZE_MAX,
CHAT_CONTENT_FONT_SIZE_MIN,
EXTENSION_WIDGET_FONT_SIZE_DEFAULT,
EXTENSION_WIDGET_FONT_SIZE_MAX,
EXTENSION_WIDGET_FONT_SIZE_MIN,
useChatAppearance,
} from "@/hooks/useChatAppearance";
import { sendAgentCommand } from "@/lib/agent-client";
Expand Down Expand Up @@ -72,7 +75,7 @@ function ThemeIcon({ preference }: { preference: ThemePreference }) {
function GeneralSettings({ sessionId, onSessionReloaded, quoteSelectionEnabled, onQuoteSelectionChange }: Pick<Props, "sessionId" | "onSessionReloaded" | "quoteSelectionEnabled" | "onQuoteSelectionChange">) {
const { locale, setLocale, supportedLocales, t } = useI18n();
const { preference, setThemePreference } = useTheme();
const { width: chatContentWidth, setWidth: setChatContentWidth, fontSize, setFontSize } = useChatAppearance();
const { width: chatContentWidth, setWidth: setChatContentWidth, fontSize, setFontSize, extensionWidgetFontSize, setExtensionWidgetFontSize } = useChatAppearance();
const [shellSettings, setShellSettings] = useState<ShellToolSettingsResponse | null>(null);
const [shellSaving, setShellSaving] = useState(false);
const [shellError, setShellError] = useState<string | null>(null);
Expand Down Expand Up @@ -220,6 +223,34 @@ function GeneralSettings({ sessionId, onSessionReloaded, quoteSelectionEnabled,
onChange={(event) => setFontSize(Number(event.target.value))}
/>
</div>
<div className="settings-chat-option settings-chat-range-option">
<div className="settings-chat-range-header">
<label htmlFor="settings-extension-widget-font-size">{t("settings.extensionWidgetFontSize")}</label>
<output htmlFor="settings-extension-widget-font-size">{extensionWidgetFontSize}px</output>
<ConfigButton
variant="ghost"
size="small"
className="settings-chat-reset"
title={t("settings.resetExtensionWidgetFontSize")}
aria-label={t("settings.resetExtensionWidgetFontSize")}
disabled={extensionWidgetFontSize === EXTENSION_WIDGET_FONT_SIZE_DEFAULT}
onClick={() => setExtensionWidgetFontSize(EXTENSION_WIDGET_FONT_SIZE_DEFAULT)}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8M3 3v5h5" />
</svg>
</ConfigButton>
</div>
<input
id="settings-extension-widget-font-size"
type="range"
min={EXTENSION_WIDGET_FONT_SIZE_MIN}
max={EXTENSION_WIDGET_FONT_SIZE_MAX}
step={1}
value={extensionWidgetFontSize}
onChange={(event) => setExtensionWidgetFontSize(Number(event.target.value))}
/>
</div>
<div className="settings-chat-option settings-chat-switch-option">
<span>{t("settings.quoteSelection")}</span>
<ConfigSwitch
Expand Down
42 changes: 35 additions & 7 deletions hooks/useChatAppearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,21 @@ export const CHAT_CONTENT_FONT_SIZE_DEFAULT = 14;
export const CHAT_CONTENT_FONT_SIZE_MIN = 12;
export const CHAT_CONTENT_FONT_SIZE_MAX = 24;
export const CHAT_CONTENT_FONT_SIZE_STORAGE_KEY = "pi-chat-content-font-size";
export const EXTENSION_WIDGET_FONT_SIZE_DEFAULT = 14;
export const EXTENSION_WIDGET_FONT_SIZE_MIN = 12;
export const EXTENSION_WIDGET_FONT_SIZE_MAX = 24;
export const EXTENSION_WIDGET_FONT_SIZE_STORAGE_KEY = "pi-extension-widget-font-size";

interface ChatAppearance {
width: number;
fontSize: number;
extensionWidgetFontSize: number;
}

const DEFAULT_APPEARANCE: ChatAppearance = {
width: CHAT_CONTENT_WIDTH_DEFAULT,
fontSize: CHAT_CONTENT_FONT_SIZE_DEFAULT,
extensionWidgetFontSize: EXTENSION_WIDGET_FONT_SIZE_DEFAULT,
};
let appearance: ChatAppearance | null = null;
const listeners = new Set<() => void>();
Expand All @@ -35,6 +41,12 @@ export function clampChatContentFontSize(value: unknown): number {
return Math.max(CHAT_CONTENT_FONT_SIZE_MIN, Math.min(CHAT_CONTENT_FONT_SIZE_MAX, Math.round(size)));
}

export function clampExtensionWidgetFontSize(value: unknown): number {
const size = Number(value ?? EXTENSION_WIDGET_FONT_SIZE_DEFAULT);
if (!Number.isFinite(size)) return EXTENSION_WIDGET_FONT_SIZE_DEFAULT;
return Math.max(EXTENSION_WIDGET_FONT_SIZE_MIN, Math.min(EXTENSION_WIDGET_FONT_SIZE_MAX, Math.round(size)));
}

function readStoredPreference(key: string): string | null {
try {
return window.localStorage.getItem(key);
Expand All @@ -43,10 +55,11 @@ function readStoredPreference(key: string): string | null {
}
}

function applyAppearance({ width, fontSize }: ChatAppearance): void {
function applyAppearance({ width, fontSize, extensionWidgetFontSize }: ChatAppearance): void {
if (typeof document === "undefined") return;
document.documentElement.style.setProperty("--chat-content-max-width", `${width}px`);
document.documentElement.style.setProperty("--chat-content-font-size", `${fontSize}px`);
document.documentElement.style.setProperty("--extension-widget-font-size", `${extensionWidgetFontSize}px`);
}

function getSnapshot(): ChatAppearance {
Expand All @@ -55,6 +68,7 @@ function getSnapshot(): ChatAppearance {
appearance = {
width: clampChatContentWidth(readStoredPreference(CHAT_CONTENT_WIDTH_STORAGE_KEY)),
fontSize: clampChatContentFontSize(readStoredPreference(CHAT_CONTENT_FONT_SIZE_STORAGE_KEY)),
extensionWidgetFontSize: clampExtensionWidgetFontSize(readStoredPreference(EXTENSION_WIDGET_FONT_SIZE_STORAGE_KEY)),
};
applyAppearance(appearance);
}
Expand All @@ -66,15 +80,28 @@ function subscribe(listener: () => void): () => void {
return () => { listeners.delete(listener); };
}

function clampByKey(key: keyof ChatAppearance, value: number): number {
switch (key) {
case "width": return clampChatContentWidth(value);
case "fontSize": return clampChatContentFontSize(value);
case "extensionWidgetFontSize": return clampExtensionWidgetFontSize(value);
}
}

function storageKeyFor(key: keyof ChatAppearance): string {
switch (key) {
case "width": return CHAT_CONTENT_WIDTH_STORAGE_KEY;
case "fontSize": return CHAT_CONTENT_FONT_SIZE_STORAGE_KEY;
case "extensionWidgetFontSize": return EXTENSION_WIDGET_FONT_SIZE_STORAGE_KEY;
}
}

function setPreference(key: keyof ChatAppearance, value: number): void {
const nextValue = key === "width" ? clampChatContentWidth(value) : clampChatContentFontSize(value);
const nextValue = clampByKey(key, value);
appearance = { ...getSnapshot(), [key]: nextValue };
applyAppearance(appearance);
try {
window.localStorage.setItem(
key === "width" ? CHAT_CONTENT_WIDTH_STORAGE_KEY : CHAT_CONTENT_FONT_SIZE_STORAGE_KEY,
String(nextValue),
);
window.localStorage.setItem(storageKeyFor(key), String(nextValue));
} catch {
// Best-effort browser preference persistence.
}
Expand All @@ -83,8 +110,9 @@ function setPreference(key: keyof ChatAppearance, value: number): void {

const setWidth = (value: number) => setPreference("width", value);
const setFontSize = (value: number) => setPreference("fontSize", value);
const setExtensionWidgetFontSize = (value: number) => setPreference("extensionWidgetFontSize", value);

export function useChatAppearance() {
const snapshot = useSyncExternalStore(subscribe, getSnapshot, () => DEFAULT_APPEARANCE);
return { ...snapshot, setWidth, setFontSize };
return { ...snapshot, setWidth, setFontSize, setExtensionWidgetFontSize };
}
2 changes: 2 additions & 0 deletions lib/i18n/messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const enLocale: LocalePlugin = {
"settings.chatContentFontSize": "Chat font size",
"settings.resetChatContentWidth": "Reset chat content width",
"settings.resetChatContentFontSize": "Reset chat font size",
"settings.extensionWidgetFontSize": "Extension widget font size",
"settings.resetExtensionWidgetFontSize": "Reset extension widget font size",
"settings.chatContentWidthDescription": "Set the maximum width of messages and the composer.",
"settings.themeLight": "Light",
"settings.themeDark": "Dark",
Expand Down
2 changes: 2 additions & 0 deletions lib/i18n/messages/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const zhCNLocale: LocalePlugin = {
"settings.chatContentFontSize": "聊天字体大小",
"settings.resetChatContentWidth": "重置聊天内容宽度",
"settings.resetChatContentFontSize": "重置聊天字体大小",
"settings.extensionWidgetFontSize": "扩展组件字体大小",
"settings.resetExtensionWidgetFontSize": "重置扩展组件字体大小",
"settings.chatContentWidthDescription": "设置消息内容和输入框的最大宽度。",
"settings.themeLight": "浅色",
"settings.themeDark": "深色",
Expand Down
2 changes: 2 additions & 0 deletions lib/i18n/messages/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const zhTWLocale: LocalePlugin = {
"settings.chatContentFontSize": "聊天字體大小",
"settings.resetChatContentWidth": "重設聊天內容寬度",
"settings.resetChatContentFontSize": "重設聊天字體大小",
"settings.extensionWidgetFontSize": "擴充功能小工具字體大小",
"settings.resetExtensionWidgetFontSize": "重設擴充功能小工具字體大小",
"settings.chatContentWidthDescription": "設定訊息內容和輸入框的最大寬度。",
"settings.themeLight": "淺色",
"settings.themeDark": "深色",
Expand Down
Loading