Merge pull request #2225 into stage-353

Add extra-large Appearance font size option (franksong2702)
This commit is contained in:
Hermes Agent
2026-05-14 03:43:52 +00:00
8 changed files with 108 additions and 10 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Added
- **PR #2225** by @franksong2702 (refs #2224) — Adds an Extra Large option to Settings → Appearance → Font size for tablet and large-desktop readability. The new `xlarge` value is accepted by the persisted settings contract, appears alongside the existing Small / Default / Large picker options, and scales the same key UI text surfaces already covered by the font-size preference: sidebar session rows, chat message bodies/headings/code/tables, the composer textarea, workspace file rows, and app-level em/rem text.
### Fixed
- **PR #2227** by @theh4v0c (closes #2223 — critical) — Context compression no longer destroys session history. The previous implementation renamed `old_sid.json``new_sid.json` before the new compressed session had been saved, destroying the only persistent copy of the full conversation. When the summarisation LLM call also failed, the user was left with zero recoverable messages and the bug report `Summary generation was unavailable. N message(s) were removed to free context space but could not be summarized.` text with no way to scroll back. The fix removes the destructive `old_path.rename(new_path)` call: `old_sid.json` is preserved intact as an immutable pre-compression archive, `new_sid.json` is created fresh via `s.save()`, and `parent_session_id` is set on the continuation session so the frontend can traverse the lineage chain back to the original. Even when summarisation or `s.save()` fails, the original conversation file survives on disk. New 106-line regression test file covers the no-rename invariant, parent_session_id stamping, and marker-only-result preservation.
+2 -2
View File
@@ -3912,7 +3912,7 @@ _SETTINGS_DEFAULTS = {
"whats_new_summary_enabled": False, # show an LLM-written What's New summary before diff links
"theme": "dark", # light | dark | system
"skin": "default", # accent color skin: default | ares | mono | slate | poseidon | sisyphus | charizard
"font_size": "default", # small | default | large
"font_size": "default", # small | default | large | xlarge
"session_jump_buttons": False, # show Start/End transcript jump pills
"session_endless_scroll": False, # auto-load older transcript pages while scrolling upward
"language": "en", # UI locale code; must match a key in static/i18n.js LOCALES
@@ -4027,7 +4027,7 @@ _SETTINGS_ALLOWED_KEYS = set(_SETTINGS_DEFAULTS.keys()) - {
_SETTINGS_ENUM_VALUES = {
"send_key": {"enter", "ctrl+enter"},
"sidebar_density": {"compact", "detailed"},
"font_size": {"small", "default", "large"},
"font_size": {"small", "default", "large", "xlarge"},
"auto_title_refresh_every": {"0", "5", "10", "20"},
"busy_input_mode": {"queue", "interrupt", "steer"},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

+11
View File
@@ -516,6 +516,7 @@ const LOCALES = {
font_size_small: 'Small',
font_size_default: 'Default',
font_size_large: 'Large',
font_size_xlarge: 'Extra Large',
settings_autosave_saving: 'Saving…',
settings_autosave_saved: 'Saved',
settings_autosave_failed: 'Save failed',
@@ -1657,6 +1658,7 @@ const LOCALES = {
font_size_small: 'Piccolo',
font_size_default: 'Predefinito',
font_size_large: 'Grande',
font_size_xlarge: 'Molto grande',
settings_autosave_saving: 'Salvataggio…',
settings_autosave_saved: 'Salvato',
settings_autosave_failed: 'Salvataggio fallito',
@@ -2790,6 +2792,7 @@ const LOCALES = {
font_size_small: '小',
font_size_default: 'デフォルト',
font_size_large: '大',
font_size_xlarge: '特大',
settings_autosave_saving: '保存中…',
settings_autosave_saved: '保存しました',
settings_autosave_failed: '保存失敗',
@@ -3663,6 +3666,7 @@ const LOCALES = {
font_size_small: 'Маленький',
font_size_default: 'Стандарт',
font_size_large: 'Большой',
font_size_xlarge: 'Очень большой',
settings_autosave_saving: 'Сохранение…',
settings_autosave_saved: 'Сохранено',
settings_autosave_failed: 'Не удалось сохранить',
@@ -4810,6 +4814,7 @@ const LOCALES = {
font_size_small: 'Pequeño',
font_size_default: 'Por defecto',
font_size_large: 'Grande',
font_size_xlarge: 'Extra grande',
settings_autosave_saving: 'Guardando…',
settings_autosave_saved: 'Guardado',
settings_autosave_failed: 'Error al guardar',
@@ -5865,6 +5870,7 @@ const LOCALES = {
font_size_small: 'Small',
font_size_default: 'Default',
font_size_large: 'Large',
font_size_xlarge: 'Extra Large',
settings_autosave_saving: 'Wird gespeichert…',
settings_autosave_saved: 'Gespeichert',
settings_autosave_failed: 'Speichern fehlgeschlagen',
@@ -6961,6 +6967,7 @@ const LOCALES = {
font_size_small: '小',
font_size_default: '默认',
font_size_large: '大',
font_size_xlarge: '超大',
settings_autosave_saving: '保存中…',
settings_autosave_saved: '已保存',
settings_autosave_failed: '保存失败',
@@ -8042,6 +8049,7 @@ const LOCALES = {
font_size_small: '\u5c0f',
font_size_default: '\u9810\u8a2d',
font_size_large: '\u5927',
font_size_xlarge: '\u8d85\u5927',
settings_autosave_saving: '\u5132\u5b58\u4e2d…',
settings_autosave_saved: '\u5df2\u5132\u5b58',
settings_autosave_failed: '\u5132\u5b58\u5931\u6557',
@@ -9260,6 +9268,7 @@ const LOCALES = {
font_size_small: 'Pequeno',
font_size_default: 'Padrão',
font_size_large: 'Grande',
font_size_xlarge: 'Extra grande',
settings_autosave_saving: 'Salvando…',
settings_autosave_saved: 'Salvo',
settings_autosave_failed: 'Falha ao salvar',
@@ -10297,6 +10306,7 @@ const LOCALES = {
font_size_small: '작게',
font_size_default: '기본',
font_size_large: '크게',
font_size_xlarge: '매우 크게',
settings_autosave_saving: '저장 중…',
settings_autosave_saved: '저장됨',
settings_autosave_failed: '저장 실패',
@@ -11350,6 +11360,7 @@ const LOCALES = {
font_size_small: 'Petit',
font_size_default: 'Défaut',
font_size_large: 'Grand',
font_size_xlarge: 'Très grand',
settings_autosave_saving: 'Économie…',
settings_autosave_saved: 'Enregistré',
settings_autosave_failed: 'Échec de l\'enregistrement',
+7 -1
View File
@@ -858,7 +858,7 @@
</div>
<div class="settings-field">
<label data-i18n="settings_label_font_size">Font size</label>
<div id="fontSizePickerGrid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:4px">
<div id="fontSizePickerGrid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(96px,1fr));gap:8px;margin-top:4px">
<button type="button" data-font-size-val="small" onclick="_pickFontSize('small')" class="font-size-pick-btn" style="border:1px solid var(--border2);border-radius:10px;padding:10px 8px;text-align:center;cursor:pointer;background:none;transition:all .15s">
<div style="width:100%;height:40px;border-radius:6px;background:var(--surface);border:1px solid var(--border);margin-bottom:6px;display:flex;align-items:center;justify-content:center">
<span style="font-size:10px;font-weight:600;color:var(--muted)">Aa</span>
@@ -877,6 +877,12 @@
</div>
<span style="font-size:12px;font-weight:500;color:var(--text)" data-i18n="font_size_large">Large</span>
</button>
<button type="button" data-font-size-val="xlarge" onclick="_pickFontSize('xlarge')" class="font-size-pick-btn" style="border:1px solid var(--border2);border-radius:10px;padding:10px 8px;text-align:center;cursor:pointer;background:none;transition:all .15s">
<div style="width:100%;height:40px;border-radius:6px;background:var(--surface);border:1px solid var(--border);margin-bottom:6px;display:flex;align-items:center;justify-content:center">
<span style="font-size:20px;font-weight:600;color:var(--muted)">Aa</span>
</div>
<span style="font-size:12px;font-weight:500;color:var(--text)" data-i18n="font_size_xlarge">Extra Large</span>
</button>
</div>
<input type="hidden" id="settingsFontSize" value="default">
</div>
+17 -1
View File
@@ -19,7 +19,7 @@
}
/* ── Font size modifiers ── */
/* ── Font size preference: scale key UI text elements ── */
/* Default is 14px (no attribute needed). Small=12px, Large=16px. */
/* Default is 14px (no attribute needed). Small=12px, Large=16px, Extra Large=18px. */
/* We override the px values directly on key containers since most of the */
/* stylesheet uses hardcoded px — changing :root font-size alone only affects */
/* the small number of em/rem values. */
@@ -27,44 +27,60 @@
/* Sidebar session list */
:root[data-font-size="small"] .session-item { font-size: 11px; }
:root[data-font-size="large"] .session-item { font-size: 15px; }
:root[data-font-size="xlarge"] .session-item { font-size: 17px; }
:root[data-font-size="small"] .session-meta { font-size: 10px; }
:root[data-font-size="large"] .session-meta { font-size: 13px; }
:root[data-font-size="xlarge"] .session-meta { font-size: 15px; }
:root[data-font-size="small"] .session-title-input { font-size: 11px; }
:root[data-font-size="large"] .session-title-input { font-size: 15px; }
:root[data-font-size="xlarge"] .session-title-input { font-size: 17px; }
/* Chat message body */
:root[data-font-size="small"] .msg-body { font-size: 12px; }
:root[data-font-size="large"] .msg-body { font-size: 16px; }
:root[data-font-size="xlarge"] .msg-body { font-size: 18px; }
:root[data-font-size="small"] .msg-body h1 { font-size: 21px; }
:root[data-font-size="large"] .msg-body h1 { font-size: 27px; }
:root[data-font-size="xlarge"] .msg-body h1 { font-size: 30px; }
:root[data-font-size="small"] .msg-body h2 { font-size: 17px; }
:root[data-font-size="large"] .msg-body h2 { font-size: 23px; }
:root[data-font-size="xlarge"] .msg-body h2 { font-size: 26px; }
:root[data-font-size="small"] .msg-body h3 { font-size: 15px; }
:root[data-font-size="large"] .msg-body h3 { font-size: 20px; }
:root[data-font-size="xlarge"] .msg-body h3 { font-size: 22px; }
:root[data-font-size="small"] .msg-body h4 { font-size: 13px; }
:root[data-font-size="large"] .msg-body h4 { font-size: 17px; }
:root[data-font-size="xlarge"] .msg-body h4 { font-size: 19px; }
:root[data-font-size="small"] .msg-body h5 { font-size: 12px; }
:root[data-font-size="large"] .msg-body h5 { font-size: 16px; }
:root[data-font-size="xlarge"] .msg-body h5 { font-size: 18px; }
:root[data-font-size="small"] .msg-body h6 { font-size: 11px; }
:root[data-font-size="large"] .msg-body h6 { font-size: 15px; }
:root[data-font-size="xlarge"] .msg-body h6 { font-size: 17px; }
:root[data-font-size="small"] .msg-body code { font-size: 10.5px; }
:root[data-font-size="large"] .msg-body code { font-size: 14.5px; }
:root[data-font-size="xlarge"] .msg-body code { font-size: 16px; }
:root[data-font-size="small"] .msg-body pre code { font-size: 11px; }
:root[data-font-size="large"] .msg-body pre code { font-size: 15px; }
:root[data-font-size="xlarge"] .msg-body pre code { font-size: 16.5px; }
:root[data-font-size="small"] .msg-body table { font-size: 11px; }
:root[data-font-size="large"] .msg-body table { font-size: 14px; }
:root[data-font-size="xlarge"] .msg-body table { font-size: 16px; }
/* Composer textarea (default is 16px in stylesheet) */
:root[data-font-size="small"] #msg { font-size: 14px; }
:root[data-font-size="large"] #msg { font-size: 18px; }
:root[data-font-size="xlarge"] #msg { font-size: 20px; }
/* Workspace file tree */
:root[data-font-size="small"] .file-item { font-size: 11px; }
:root[data-font-size="large"] .file-item { font-size: 14px; }
:root[data-font-size="xlarge"] .file-item { font-size: 16px; }
/* App-level base — keeps em/rem values scaling correctly */
:root[data-font-size="small"] { font-size: 12px; }
:root[data-font-size="large"] { font-size: 16px; }
:root[data-font-size="xlarge"] { font-size: 18px; }
/* ── Dark mode — navy-black + gold accent matching Hermes terminal ── */
:root.dark {
--bg:#0D0D1A;--sidebar:#141425;--border:#2A2A45;--border2:rgba(255,255,255,0.14);
+67 -6
View File
@@ -1,4 +1,4 @@
"""Tests for font size setting (#833) — 3-toggle Small/Default/Large in Appearance."""
"""Tests for font size setting (#833) — Small/Default/Large/Extra Large in Appearance."""
import os
import re
@@ -9,7 +9,7 @@ def _read(name):
class TestFontSizeCssModifiers:
"""CSS must define font-size overrides for small and large via data attribute."""
"""CSS must define font-size overrides via data attribute."""
def test_small_font_size_rule_exists(self):
css = _read("static/style.css")
@@ -23,14 +23,26 @@ class TestFontSizeCssModifiers:
"style.css must have :root[data-font-size=\"large\"] font-size rule"
)
def test_small_is_smaller_than_default(self):
def test_extra_large_font_size_rule_exists(self):
css = _read("static/style.css")
assert 'data-font-size="xlarge"' in css, (
"style.css must have :root[data-font-size=\"xlarge\"] font-size rule"
)
def test_small_large_and_xlarge_scale_from_default(self):
css = _read("static/style.css")
# Match both compact {font-size:12px} and spaced { font-size: 12px; } formats
m_small = re.search(r':root\[data-font-size="small"\][^{]*\{[^}]*font-size:\s*(\d+)px', css)
m_large = re.search(r':root\[data-font-size="large"\][^{]*\{[^}]*font-size:\s*(\d+)px', css)
assert m_small and m_large, "Both small and large font-size rules must set px values"
m_xlarge = re.search(r':root\[data-font-size="xlarge"\][^{]*\{[^}]*font-size:\s*(\d+)px', css)
assert m_small and m_large and m_xlarge, (
"Small, large, and extra-large font-size rules must set px values"
)
assert int(m_small.group(1)) < 14, "Small font size must be < 14px (default)"
assert int(m_large.group(1)) > 14, "Large font size must be > 14px (default)"
assert int(m_xlarge.group(1)) > int(m_large.group(1)), (
"Extra Large font size must be larger than Large"
)
class TestFontSizeBootScript:
@@ -57,11 +69,15 @@ class TestFontSizeBootScript:
"Font size picker buttons must have font-size-pick-btn class"
)
def test_three_font_size_values_present(self):
def test_font_size_values_present(self):
html = _read("static/index.html")
assert 'data-font-size-val="small"' in html, "Small button must exist"
assert 'data-font-size-val="default"' in html, "Default button must exist"
assert 'data-font-size-val="large"' in html, "Large button must exist"
assert 'data-font-size-val="xlarge"' in html, "Extra Large button must exist"
assert 'data-i18n="font_size_xlarge"' in html, (
"Extra Large button must use the font_size_xlarge i18n key"
)
def test_font_size_picker_not_duplicated(self):
"""Regression guard: the font size picker grid must appear exactly once
@@ -153,7 +169,13 @@ class TestFontSizeI18nCoverage:
block = src[start:end if end > 0 else start + 20000]
return set(re.findall(r"(\w[\w_]+):", block))
REQUIRED_KEYS = {"settings_label_font_size", "font_size_small", "font_size_default", "font_size_large"}
REQUIRED_KEYS = {
"settings_label_font_size",
"font_size_small",
"font_size_default",
"font_size_large",
"font_size_xlarge",
}
def test_all_locales_have_font_size_keys(self):
src = _read("static/i18n.js")
@@ -173,6 +195,21 @@ class TestFontSizeI18nCoverage:
count = src.count("font_size_large")
assert count >= 6, f"font_size_large must appear in all 6 locales, found {count}"
def test_font_size_extra_large_key_in_all_locales(self):
src = _read("static/i18n.js")
count = src.count("font_size_xlarge")
assert count >= 6, f"font_size_xlarge must appear in all locales, found {count}"
class TestFontSizeSettingsValidation:
"""The backend settings contract must accept the persisted xlarge value."""
def test_config_allows_extra_large_font_size(self):
config = _read("api/config.py")
assert '"font_size": {"small", "default", "large", "xlarge"}' in config, (
"api/config.py must accept xlarge as a persisted font_size value"
)
class TestFontSizeCssTargetedOverrides:
"""CSS must override px-unit text in key UI elements, not just :root font-size.
@@ -192,6 +229,11 @@ class TestFontSizeCssTargetedOverrides:
assert ':root[data-font-size="large"] .msg-body' in css, \
"Chat message text must be explicitly scaled for large"
def test_msg_body_overridden_for_extra_large(self):
css = _read("static/style.css")
assert ':root[data-font-size="xlarge"] .msg-body' in css, \
"Chat message text must be explicitly scaled for extra large"
def test_session_item_overridden_for_small(self):
css = _read("static/style.css")
assert ':root[data-font-size="small"] .session-item' in css, \
@@ -202,6 +244,11 @@ class TestFontSizeCssTargetedOverrides:
assert ':root[data-font-size="large"] .session-item' in css, \
"Sidebar session list text must be explicitly scaled for large"
def test_session_item_overridden_for_extra_large(self):
css = _read("static/style.css")
assert ':root[data-font-size="xlarge"] .session-item' in css, \
"Sidebar session list text must be explicitly scaled for extra large"
def test_composer_overridden_for_small(self):
css = _read("static/style.css")
assert ':root[data-font-size="small"] #msg' in css, \
@@ -217,6 +264,15 @@ class TestFontSizeCssTargetedOverrides:
assert m and int(m.group(1)) != 16, \
"Large composer font-size must differ from default (16px) to have visible effect"
def test_composer_overridden_for_extra_large(self):
css = _read("static/style.css")
assert ':root[data-font-size="xlarge"] #msg' in css, \
"Composer textarea must be explicitly scaled for extra large"
m_large = re.search(r':root\[data-font-size="large"\] #msg \{ font-size: (\d+)px', css)
m_xlarge = re.search(r':root\[data-font-size="xlarge"\] #msg \{ font-size: (\d+)px', css)
assert m_large and m_xlarge and int(m_xlarge.group(1)) > int(m_large.group(1)), \
"Extra Large composer font-size must be larger than Large"
def test_file_item_overridden_for_small(self):
css = _read("static/style.css")
assert ':root[data-font-size="small"] .file-item' in css, \
@@ -226,3 +282,8 @@ class TestFontSizeCssTargetedOverrides:
css = _read("static/style.css")
assert ':root[data-font-size="large"] .file-item' in css, \
"Workspace file tree text must be explicitly scaled for large"
def test_file_item_overridden_for_extra_large(self):
css = _read("static/style.css")
assert ':root[data-font-size="xlarge"] .file-item' in css, \
"Workspace file tree text must be explicitly scaled for extra large"