feat(ui): add sidebar density mode to session list (#764)
Adds compact/detailed toggle for the session list sidebar. Compact is the default (no behavior change for existing users). Detailed mode shows message count and model; profile names only appear when mixing sessions across profiles. Fixes #673 Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# Hermes Web UI -- Changelog
|
||||
|
||||
## [v0.50.112] — 2026-04-20
|
||||
|
||||
### Added
|
||||
- **Sidebar density mode for the session list** — new Settings option toggles the left session list between a compact default and a detailed view that shows message count and model. Profile names only appear in detailed mode when "Show active profile only" is disabled. (#673)
|
||||
|
||||
## [v0.50.111] — 2026-04-20
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1256,6 +1256,7 @@ _SETTINGS_DEFAULTS = {
|
||||
"sound_enabled": False, # play notification sound when assistant finishes
|
||||
"notifications_enabled": False, # browser notification when tab is in background
|
||||
"bubble_layout": False, # right-aligned user / left-aligned assistant chat bubbles
|
||||
"sidebar_density": "compact", # compact | detailed
|
||||
"password_hash": None, # PBKDF2-HMAC-SHA256 hash; None = auth disabled
|
||||
}
|
||||
_SETTINGS_LEGACY_DROP_KEYS = {"assistant_language"}
|
||||
@@ -1351,6 +1352,7 @@ def load_settings() -> dict:
|
||||
_SETTINGS_ALLOWED_KEYS = set(_SETTINGS_DEFAULTS.keys()) - {"password_hash"}
|
||||
_SETTINGS_ENUM_VALUES = {
|
||||
"send_key": {"enter", "ctrl+enter"},
|
||||
"sidebar_density": {"compact", "detailed"},
|
||||
}
|
||||
_SETTINGS_BOOL_KEYS = {
|
||||
"onboarding_completed",
|
||||
|
||||
@@ -757,6 +757,7 @@ function applyBotName(){
|
||||
window._showCliSessions=!!s.show_cli_sessions;
|
||||
window._soundEnabled=!!s.sound_enabled;
|
||||
window._notificationsEnabled=!!s.notifications_enabled;
|
||||
window._sidebarDensity=(s.sidebar_density==='detailed'?'detailed':'compact');
|
||||
window._botName=s.bot_name||'Hermes';
|
||||
const appearance=_normalizeAppearance(s.theme,s.skin);
|
||||
localStorage.setItem('hermes-theme',appearance.theme);
|
||||
@@ -778,6 +779,7 @@ function applyBotName(){
|
||||
window._showCliSessions=false;
|
||||
window._soundEnabled=false;
|
||||
window._notificationsEnabled=false;
|
||||
window._sidebarDensity='compact';
|
||||
window._botName='Hermes';
|
||||
_bootSettings={check_for_updates:false};
|
||||
document.body.classList.remove('bubble-layout');
|
||||
|
||||
@@ -195,6 +195,7 @@ const LOCALES = {
|
||||
settings_label_language: 'Language',
|
||||
settings_label_token_usage: 'Show token usage',
|
||||
settings_label_bubble_layout: 'Chat bubble layout',
|
||||
settings_label_sidebar_density: 'Sidebar density',
|
||||
settings_label_cli_sessions: 'Show agent sessions',
|
||||
settings_label_sync_insights: 'Sync to insights',
|
||||
settings_label_check_updates: 'Check for updates',
|
||||
@@ -249,6 +250,7 @@ const LOCALES = {
|
||||
personal_memory: 'Personal memory',
|
||||
current_task_list: 'Current task list',
|
||||
workspace_desc: 'Add and switch workspaces for your sessions.',
|
||||
session_meta_messages: (n) => `${n} msg${n === 1 ? '' : 's'}`,
|
||||
new_profile: 'New profile',
|
||||
transcript: 'Transcript',
|
||||
download_transcript: 'Download as Markdown',
|
||||
@@ -260,6 +262,9 @@ const LOCALES = {
|
||||
settings_desc_notifications: 'Show a system notification when a response completes while the app is in the background.',
|
||||
settings_desc_token_usage: 'Displays input/output token count below each assistant reply. Also toggled with /usage.',
|
||||
settings_desc_bubble_layout: 'Right-align user messages and left-align assistant replies. Off by default to keep code blocks and tool output full-width.',
|
||||
settings_sidebar_density_compact: 'Compact',
|
||||
settings_sidebar_density_detailed: 'Detailed',
|
||||
settings_desc_sidebar_density: 'Controls how much metadata the session list shows in the left sidebar.',
|
||||
settings_desc_cli_sessions: 'Merges sessions from the Hermes CLI (state.db) into the session list. Click a CLI session to import it and continue the conversation.',
|
||||
settings_desc_sync_insights: 'Mirrors WebUI token usage to state.db so hermes /insights includes browser session data. Off by default.',
|
||||
settings_desc_check_updates: 'Show a banner when newer versions of the WebUI or Agent are available. Runs a background git fetch periodically.',
|
||||
@@ -623,6 +628,7 @@ const LOCALES = {
|
||||
settings_label_language: 'Язык',
|
||||
settings_label_token_usage: 'Показывать использование токенов',
|
||||
settings_label_bubble_layout: 'Раскладка пузырьков чата',
|
||||
settings_label_sidebar_density: 'Плотность боковой панели',
|
||||
settings_label_cli_sessions: 'Показывать сеансы агента',
|
||||
settings_label_sync_insights: 'Синхронизировать с Insights',
|
||||
settings_label_check_updates: 'Проверять обновления',
|
||||
@@ -694,6 +700,7 @@ const LOCALES = {
|
||||
personal_memory: 'Личная память',
|
||||
current_task_list: 'Текущий список задач',
|
||||
workspace_desc: 'Добавляйте рабочие пространства и переключайтесь между ними в своих сеансах.',
|
||||
session_meta_messages: (n) => `${n} сообщ.`,
|
||||
new_profile: 'Новый профиль',
|
||||
transcript: 'Транскрипт',
|
||||
download_transcript: 'Скачать как Markdown',
|
||||
@@ -704,6 +711,9 @@ const LOCALES = {
|
||||
settings_desc_notifications: 'Показывать системное уведомление, когда ответ готов, а вкладка находится в фоне.',
|
||||
settings_desc_token_usage: 'Показывает количество входных и выходных токенов под каждым ответом помощника. Также переключается через /usage.',
|
||||
settings_desc_bubble_layout: 'Выравнивает сообщения пользователя справа, а ответы помощника слева. Выключено по умолчанию, чтобы блоки кода и вывод инструментов занимали всю ширину.',
|
||||
settings_sidebar_density_compact: 'Компактно',
|
||||
settings_sidebar_density_detailed: 'Подробно',
|
||||
settings_desc_sidebar_density: 'Управляет тем, сколько метаданных показывается в списке сеансов на левой панели.',
|
||||
settings_desc_cli_sessions: 'Объединяет сеансы из Hermes CLI (state.db) в список сеансов. Нажмите на CLI-сеанс, чтобы импортировать его и продолжить разговор.',
|
||||
settings_desc_sync_insights: 'Синхронизирует использование токенов WebUI в state.db, чтобы Hermes /insights включал данные браузерных сеансов. Выключено по умолчанию.',
|
||||
settings_desc_check_updates: 'Показывает баннер, когда доступны более новые версии WebUI или Agent. Периодически выполняет git fetch в фоне.',
|
||||
@@ -1079,6 +1089,7 @@ const LOCALES = {
|
||||
settings_label_language: 'Idioma',
|
||||
settings_label_token_usage: 'Mostrar uso de tokens',
|
||||
settings_label_bubble_layout: 'Disposición en burbujas',
|
||||
settings_label_sidebar_density: 'Densidad de la barra lateral',
|
||||
settings_label_cli_sessions: 'Mostrar sesiones de CLI',
|
||||
settings_label_sync_insights: 'Sincronizar con insights',
|
||||
settings_label_check_updates: 'Buscar actualizaciones',
|
||||
@@ -1133,6 +1144,7 @@ const LOCALES = {
|
||||
personal_memory: 'Memoria personal',
|
||||
current_task_list: 'Lista de tareas actual',
|
||||
workspace_desc: 'Añade y cambia espacios de trabajo para tus sesiones.',
|
||||
session_meta_messages: (n) => `${n} mens.`,
|
||||
new_profile: 'Nuevo perfil',
|
||||
transcript: 'Transcripción',
|
||||
download_transcript: 'Descargar como Markdown',
|
||||
@@ -1144,6 +1156,9 @@ const LOCALES = {
|
||||
settings_desc_notifications: 'Muestra una notificación del sistema cuando una respuesta termina mientras la pestaña está en segundo plano.',
|
||||
settings_desc_token_usage: 'Muestra el conteo de tokens de entrada/salida debajo de cada respuesta del asistente. También se puede alternar con /usage.',
|
||||
settings_desc_bubble_layout: 'Alinea los mensajes del usuario a la derecha y las respuestas del asistente a la izquierda. Desactivado por defecto para mantener los bloques de código y la salida de herramientas a ancho completo.',
|
||||
settings_sidebar_density_compact: 'Compacta',
|
||||
settings_sidebar_density_detailed: 'Detallada',
|
||||
settings_desc_sidebar_density: 'Controla cuántos metadatos muestra la lista de sesiones en la barra lateral izquierda.',
|
||||
settings_desc_cli_sessions: 'Fusiona las sesiones del CLI de Hermes (state.db) en la lista de sesiones. Haz clic en una sesión de CLI para importarla y continuar la conversación.',
|
||||
settings_desc_sync_insights: 'Refleja el uso de tokens de la WebUI en state.db para que hermes /insights incluya datos de sesiones del navegador. Desactivado por defecto.',
|
||||
settings_desc_check_updates: 'Muestra un banner cuando haya versiones más nuevas de la WebUI o del Agent. Ejecuta periódicamente un git fetch en segundo plano.',
|
||||
@@ -1506,6 +1521,8 @@ const LOCALES = {
|
||||
settings_label_skin: 'Skin',
|
||||
settings_label_language: 'Sprache',
|
||||
settings_label_token_usage: 'Token-Verbrauch anzeigen',
|
||||
settings_label_bubble_layout: 'Chat-Bubble-Layout',
|
||||
settings_label_sidebar_density: 'Seitenleistendichte',
|
||||
settings_label_cli_sessions: 'Agent-Sitzungen anzeigen',
|
||||
settings_label_sync_insights: 'Mit Insights synchronisieren',
|
||||
settings_label_check_updates: 'Nach Updates suchen',
|
||||
@@ -1549,6 +1566,7 @@ const LOCALES = {
|
||||
personal_memory: 'Persönliches Gedächtnis',
|
||||
current_task_list: 'Aktuelle Aufgabenliste',
|
||||
workspace_desc: 'Workspaces hinzufügen und wechseln.',
|
||||
session_meta_messages: (n) => `${n} Nachr.`,
|
||||
new_profile: 'Neues Profil',
|
||||
transcript: 'Protokoll',
|
||||
download_transcript: 'Als Markdown herunterladen',
|
||||
@@ -1559,6 +1577,10 @@ const LOCALES = {
|
||||
settings_label_notifications: 'Browser-Benachrichtigungen',
|
||||
settings_desc_notifications: 'Zeigt eine Systembenachrichtigung an, wenn eine Antwort fertiggestellt wird, während der Tab im Hintergrund ist.',
|
||||
settings_desc_token_usage: 'Zeigt die Anzahl der Input/Output-Token unter jeder Antwort des Assistenten an. Auch umschaltbar mit /usage.',
|
||||
settings_desc_bubble_layout: 'Richtet Benutzernachrichten rechts und Assistentenantworten links aus. Standardmäßig deaktiviert, damit Codeblöcke und Tool-Ausgaben die volle Breite behalten.',
|
||||
settings_sidebar_density_compact: 'Kompakt',
|
||||
settings_sidebar_density_detailed: 'Detailliert',
|
||||
settings_desc_sidebar_density: 'Steuert, wie viele Metadaten die Sitzungsliste in der linken Seitenleiste anzeigt.',
|
||||
settings_desc_cli_sessions: 'Fügt Sitzungen aus der Hermes CLI (state.db) in die Sitzungsliste ein. Klicken Sie auf eine CLI-Sitzung, um sie zu importieren und das Gespräch fortzusetzen.',
|
||||
settings_desc_sync_insights: 'Spiegelt den WebUI-Token-Verbrauch in die state.db, sodass hermes /insights Browser-Sitzungsdaten enthält. Standardmäßig aus.',
|
||||
settings_desc_check_updates: 'Zeigt ein Banner an, wenn neuere Versionen der WebUI oder des Agenten verfügbar sind. Führt regelmäßig einen Git-Fetch im Hintergrund aus.',
|
||||
@@ -1734,6 +1756,7 @@ const LOCALES = {
|
||||
settings_label_language: '\u8bed\u8a00',
|
||||
settings_label_token_usage: '\u663e\u793a token \u7528\u91cf',
|
||||
settings_label_bubble_layout: '聊天气泡布局',
|
||||
settings_label_sidebar_density: '侧边栏密度',
|
||||
settings_label_cli_sessions: '\u663e\u793a CLI \u4f1a\u8bdd',
|
||||
settings_label_sync_insights: '\u540c\u6b65\u5230 insights',
|
||||
settings_label_check_updates: '\u68c0\u67e5\u66f4\u65b0',
|
||||
@@ -1780,6 +1803,7 @@ const LOCALES = {
|
||||
personal_memory: '个人记忆',
|
||||
current_task_list: '当前任务列表',
|
||||
workspace_desc: '为你的会话添加并切换工作区。',
|
||||
session_meta_messages: (n) => `${n} 条消息`,
|
||||
new_profile: '新配置',
|
||||
transcript: '记录',
|
||||
download_transcript: '下载为 Markdown',
|
||||
@@ -1802,6 +1826,9 @@ const LOCALES = {
|
||||
settings_desc_notifications: '当标签页在后台时,回复完成后显示系统通知。',
|
||||
settings_desc_token_usage: '在助手每次回复下方显示输入/输出 token 数量。也可以用 /usage 切换。',
|
||||
settings_desc_bubble_layout: '开启后将用户消息右对齐、助手消息左对齐。默认关闭,以保持代码块和工具输出为全宽显示。',
|
||||
settings_sidebar_density_compact: '紧凑',
|
||||
settings_sidebar_density_detailed: '详细',
|
||||
settings_desc_sidebar_density: '控制左侧会话列表展示多少元信息。',
|
||||
settings_desc_cli_sessions: '将 Hermes CLI(state.db)中的会话合并到会话列表。点击某个 CLI 会话可导入并继续对话。',
|
||||
settings_desc_sync_insights: '将 WebUI token 使用情况同步到 state.db,使 hermes /insights 包含浏览器会话数据。默认关闭。',
|
||||
settings_desc_check_updates: '当有更新的 WebUI 或助手版本时显示横幅。会在后台定期执行 git fetch。',
|
||||
@@ -2145,6 +2172,8 @@ const LOCALES = {
|
||||
settings_label_skin: '\u76ae\u819a',
|
||||
settings_label_language: '\u8a9d\u8a00',
|
||||
settings_label_token_usage: '\u986f\u793a token \u7528\u91cf',
|
||||
settings_label_bubble_layout: '聊天泡泡版面',
|
||||
settings_label_sidebar_density: '側邊欄密度',
|
||||
settings_label_cli_sessions: '\u986f\u793a CLI \u6703\u8a71',
|
||||
settings_label_sync_insights: '\u540c\u6b65\u5230 insights',
|
||||
settings_label_check_updates: '\u6aa2\u67e5\u66f4\u65b0',
|
||||
@@ -2186,6 +2215,7 @@ const LOCALES = {
|
||||
save_skill: '\u5132\u5b58\u6280\u80fd',
|
||||
personal_memory: '\u500b\u4eba\u8a18\u61b6',
|
||||
current_task_list: '\u76ee\u524d\u4efb\u52d9\u6e05\u55ae',
|
||||
session_meta_messages: (n) => `${n} 則訊息`,
|
||||
new_profile: '\u65b0\u914d\u7f6e\u6a94',
|
||||
transcript: '\u8a18\u9304',
|
||||
download_transcript: '\u4e0b\u8f09\u8a18\u9304',
|
||||
@@ -2207,6 +2237,10 @@ const LOCALES = {
|
||||
settings_desc_sound: '\u52a9\u624b\u5b8c\u6210\u56de\u7b54\u6642\u64a9\u653e\u8072\u97f3\u3002',
|
||||
settings_desc_notifications: '\u7576\u5206\u9801\u5728\u5f8c\u81ea\u6642\uff0c\u6709\u56de\u7b54\u5b8c\u6210\u6e05\u55ae\u6703\u986f\u793a\u7cfb\u7d71\u901a\u77e5\u3002',
|
||||
settings_desc_token_usage: '\u5728\u52a9\u624b\u6bcf\u6b21\u56de\u7b54\u4e0b\u65b9\u986f\u793a Input/Output token \u6578\u91cf\u3002\u4e5f\u53ef\u4ee5\u7528 /usage \u5207\u63db\u3002',
|
||||
settings_desc_bubble_layout: '開啟後將使用者訊息靠右、助理訊息靠左。預設關閉,以保持程式碼區塊與工具輸出為全寬顯示。',
|
||||
settings_sidebar_density_compact: '精簡',
|
||||
settings_sidebar_density_detailed: '詳細',
|
||||
settings_desc_sidebar_density: '控制左側對話清單要顯示多少額外資訊。',
|
||||
settings_desc_cli_sessions: '\u5c07 Hermes CLI (\u7684 state.db) \u4e2d\u7684\u6703\u8a71\u6dfb\u52a0\u5230\u6703\u8a71\u6e05\u55ae\u3002\u9ede\u64ca\u4e00\u500b CLI \u6703\u8a71\u5c07\u5c0e\u5165\u5b83\u7a0b\u5f0f\u4e26\u7e7c\u7e8c\u5b58\u5c0d\u8a71\u3002',
|
||||
settings_desc_sync_insights: '\u5c07 WebUI token \u4f7f\u7528\u60c5\u6cc1\u540c\u6b65\u5230 state.db\uff0c\u8a93 hermes /insights \u5305\u542b\u700f\u89bd\u5668\u6703\u8a71\u6578\u64da\u3002\u9810\u8a2d\u70b8\u555f\u7528\u3002',
|
||||
settings_desc_check_updates: '\u7576\u6709\u66f4\u65b0\u7684 WebUI \u6216\u52a9\u624b\u7248\u672c\u6642\u986f\u793a\u6a19\u8a18\u3002\u5c07\u5728\u5f8c\u81ea\u6b63\u5e38\u57f7\u884c Git-Fetch\u3002',
|
||||
|
||||
@@ -561,6 +561,14 @@
|
||||
</label>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_bubble_layout">Right-align user messages and left-align assistant replies. Off by default to keep code blocks and tool output full-width.</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="settingsSidebarDensity" data-i18n="settings_label_sidebar_density">Sidebar density</label>
|
||||
<select id="settingsSidebarDensity" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px">
|
||||
<option value="compact" data-i18n="settings_sidebar_density_compact">Compact</option>
|
||||
<option value="detailed" data-i18n="settings_sidebar_density_detailed">Detailed</option>
|
||||
</select>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_sidebar_density">Controls how much metadata the session list shows in the left sidebar.</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="settingsShowCliSessions" style="width:15px;height:15px;accent-color:var(--accent)">
|
||||
|
||||
@@ -1267,6 +1267,11 @@ async function loadSettingsPanel(){
|
||||
if(notifCb){notifCb.checked=!!settings.notifications_enabled;notifCb.addEventListener('change',_markSettingsDirty,{once:false});}
|
||||
const bubbleCb=$('settingsBubbleLayout');
|
||||
if(bubbleCb){bubbleCb.checked=!!settings.bubble_layout;bubbleCb.addEventListener('change',_markSettingsDirty,{once:false});}
|
||||
const sidebarDensitySel=$('settingsSidebarDensity');
|
||||
if(sidebarDensitySel){
|
||||
sidebarDensitySel.value=settings.sidebar_density==='detailed'?'detailed':'compact';
|
||||
sidebarDensitySel.addEventListener('change',_markSettingsDirty,{once:false});
|
||||
}
|
||||
// Bot name
|
||||
const botNameField=$('settingsBotName');
|
||||
if(botNameField){botNameField.value=settings.bot_name||'Hermes';botNameField.addEventListener('input',_markSettingsDirty,{once:false});}
|
||||
@@ -1293,12 +1298,13 @@ function _setSettingsAuthButtonsVisible(active){
|
||||
}
|
||||
|
||||
function _applySavedSettingsUi(saved, body, opts){
|
||||
const {sendKey,showTokenUsage,showCliSessions,theme,skin,language}=opts;
|
||||
const {sendKey,showTokenUsage,showCliSessions,theme,skin,language,sidebarDensity}=opts;
|
||||
window._sendKey=sendKey||'enter';
|
||||
window._showTokenUsage=showTokenUsage;
|
||||
window._showCliSessions=showCliSessions;
|
||||
window._soundEnabled=body.sound_enabled;
|
||||
window._notificationsEnabled=body.notifications_enabled;
|
||||
window._sidebarDensity=sidebarDensity==='detailed'?'detailed':'compact';
|
||||
window._botName=body.bot_name||'Hermes';
|
||||
document.body.classList.toggle('bubble-layout', !!body.bubble_layout);
|
||||
if(typeof applyBotName==='function') applyBotName();
|
||||
@@ -1328,6 +1334,7 @@ async function saveSettings(andClose){
|
||||
const theme=($('settingsTheme')||{}).value||'dark';
|
||||
const skin=($('settingsSkin')||{}).value||'default';
|
||||
const language=($('settingsLanguage')||{}).value||'en';
|
||||
const sidebarDensity=($('settingsSidebarDensity')||{}).value==='detailed'?'detailed':'compact';
|
||||
const body={};
|
||||
if(model) body.default_model=model;
|
||||
|
||||
@@ -1342,6 +1349,7 @@ async function saveSettings(andClose){
|
||||
body.sound_enabled=!!($('settingsSoundEnabled')||{}).checked;
|
||||
body.notifications_enabled=!!($('settingsNotificationsEnabled')||{}).checked;
|
||||
body.bubble_layout=!!($('settingsBubbleLayout')||{}).checked;
|
||||
body.sidebar_density=sidebarDensity;
|
||||
document.body.classList.toggle('bubble-layout', body.bubble_layout);
|
||||
const botName=(($('settingsBotName')||{}).value||'').trim();
|
||||
body.bot_name=botName||'Hermes';
|
||||
@@ -1349,7 +1357,7 @@ async function saveSettings(andClose){
|
||||
if(pw && pw.trim()){
|
||||
try{
|
||||
const saved=await api('/api/settings',{method:'POST',body:JSON.stringify({...body,_set_password:pw.trim()})});
|
||||
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,skin,language});
|
||||
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,skin,language,sidebarDensity});
|
||||
showToast(t(saved.auth_just_enabled?'settings_saved_pw':'settings_saved_pw_updated'));
|
||||
_hideSettingsPanel();
|
||||
return;
|
||||
@@ -1357,7 +1365,7 @@ async function saveSettings(andClose){
|
||||
}
|
||||
try{
|
||||
const saved=await api('/api/settings',{method:'POST',body:JSON.stringify(body)});
|
||||
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,skin,language});
|
||||
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,skin,language,sidebarDensity});
|
||||
showToast(t('settings_saved'));
|
||||
_hideSettingsPanel();
|
||||
}catch(e){
|
||||
|
||||
@@ -618,6 +618,21 @@ function renderSessionListFromCache(){
|
||||
const tsMs=_sessionTimestampMs(s);
|
||||
titleRow.appendChild(title);
|
||||
sessionText.appendChild(titleRow);
|
||||
const density=(window._sidebarDensity==='detailed'?'detailed':'compact');
|
||||
if(density==='detailed'){
|
||||
const metaBits=[];
|
||||
const msgCount=typeof s.message_count==='number'?s.message_count:0;
|
||||
const msgLabel=(typeof t==='function')
|
||||
? t('session_meta_messages', msgCount)
|
||||
: `${msgCount} msg${msgCount===1?'':'s'}`;
|
||||
metaBits.push(msgLabel);
|
||||
if(s.model) metaBits.push(s.model);
|
||||
if(_showAllProfiles&&s.profile) metaBits.push(s.profile);
|
||||
const meta=document.createElement('div');
|
||||
meta.className='session-meta';
|
||||
meta.textContent=metaBits.join(' · ');
|
||||
sessionText.appendChild(meta);
|
||||
}
|
||||
// Append tag chips after the title text
|
||||
for(const tag of tags){
|
||||
const chip=document.createElement('span');
|
||||
|
||||
@@ -186,6 +186,8 @@
|
||||
.session-title-row{display:flex;align-items:center;gap:6px;min-width:0;}
|
||||
.session-title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);}
|
||||
.session-item.active .session-title{color:var(--accent-text);}
|
||||
.session-meta{font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.session-item.active .session-meta{color:var(--accent-text);opacity:.8;}
|
||||
.session-time{display:none;}
|
||||
/* ── Session action trigger + dropdown ── */
|
||||
.session-actions{position:absolute;right:6px;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .15s ease;}
|
||||
|
||||
@@ -3,9 +3,9 @@ Tests for fixes:
|
||||
|
||||
- #569: docker_init.bash auto-detects WANTED_UID/WANTED_GID from mounted workspace
|
||||
so macOS users (UID 501) don't need to manually set the env var.
|
||||
- #579: Topbar message count already filters tool messages (role !== 'tool') —
|
||||
confirmed present. Closing as already fixed by #584 which removed the
|
||||
sidebar meta row (the only place raw message_count was ever displayed).
|
||||
- #579: Topbar message count already filters tool messages (role !== 'tool').
|
||||
The legacy raw sidebar count was removed by #584, and later reintroduced
|
||||
in a gated detailed-density mode by #673.
|
||||
"""
|
||||
import pathlib
|
||||
import re
|
||||
@@ -143,15 +143,21 @@ def test_579_topbar_filters_tool_messages():
|
||||
)
|
||||
|
||||
|
||||
def test_579_sidebar_no_longer_shows_raw_count():
|
||||
"""sessions.js must not reference message_count in the render path (#579).
|
||||
def test_579_sidebar_count_is_gated_behind_detailed_density():
|
||||
"""sessions.js may only show sidebar count inside detailed density mode.
|
||||
|
||||
After PR #584, the sidebar no longer shows message_count at all,
|
||||
eliminating the inconsistency between sidebar (raw) and topbar (filtered).
|
||||
PR #584 removed the always-visible raw sidebar count to avoid mismatching the
|
||||
topbar's filtered count. PR #673 later reintroduced message_count as
|
||||
optional metadata, but only when the user explicitly opts into detailed
|
||||
sidebar density.
|
||||
"""
|
||||
sessions_js = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
||||
# message_count should not appear in the client-side session renderer
|
||||
assert "message_count" not in sessions_js, (
|
||||
"sessions.js must not reference message_count — "
|
||||
"the meta row that displayed it was removed in PR #584"
|
||||
assert "const density=(window._sidebarDensity==='detailed'?'detailed':'compact');" in sessions_js, (
|
||||
"sessions.js must normalize sidebar density before rendering metadata"
|
||||
)
|
||||
assert "if(density==='detailed'){" in sessions_js, (
|
||||
"sessions.js must gate sidebar metadata behind detailed density mode"
|
||||
)
|
||||
assert "typeof s.message_count==='number'?s.message_count:0" in sessions_js, (
|
||||
"message_count may be rendered only inside the detailed-density branch"
|
||||
)
|
||||
|
||||
181
tests/test_issue673.py
Normal file
181
tests/test_issue673.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
Tests for issue #673 — sidebar density mode for the session list.
|
||||
|
||||
Covers:
|
||||
- api/config.py: sidebar_density registered in defaults + enum validation
|
||||
- static/index.html: settingsSidebarDensity field and i18n wiring present
|
||||
- static/boot.js: boot path applies window._sidebarDensity with compact default
|
||||
- static/panels.js: load/save settings wire sidebar_density correctly
|
||||
- static/sessions.js: detailed mode renders message count + model, and profile
|
||||
only when the "show all profiles" toggle is active
|
||||
- static/i18n.js: locale keys exist for all shipped locales
|
||||
- Integration: GET/POST /api/settings round-trip sidebar_density
|
||||
"""
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import unittest
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).parent.parent
|
||||
CONFIG_PY = (REPO_ROOT / "api" / "config.py").read_text(encoding="utf-8")
|
||||
INDEX_HTML = (REPO_ROOT / "static" / "index.html").read_text(encoding="utf-8")
|
||||
BOOT_JS = (REPO_ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
PANELS_JS = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
SESSIONS_JS = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
||||
STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||||
I18N_JS = (REPO_ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||
|
||||
from tests._pytest_port import BASE
|
||||
|
||||
|
||||
def _get(path):
|
||||
with urllib.request.urlopen(BASE + path, timeout=10) as r:
|
||||
return json.loads(r.read()), r.status
|
||||
|
||||
|
||||
def _post(path, body=None):
|
||||
data = json.dumps(body or {}).encode()
|
||||
req = urllib.request.Request(
|
||||
BASE + path, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
return json.loads(r.read()), r.status
|
||||
except urllib.error.HTTPError as e:
|
||||
return json.loads(e.read()), e.code
|
||||
|
||||
|
||||
class TestSidebarDensityConfig(unittest.TestCase):
|
||||
def test_sidebar_density_in_defaults(self):
|
||||
self.assertIn('"sidebar_density"', CONFIG_PY)
|
||||
|
||||
def test_sidebar_density_default_is_compact(self):
|
||||
self.assertRegex(CONFIG_PY, r'"sidebar_density"\s*:\s*"compact"')
|
||||
|
||||
def test_sidebar_density_in_enum_values(self):
|
||||
self.assertIn('"sidebar_density": {"compact", "detailed"}', CONFIG_PY)
|
||||
|
||||
|
||||
class TestSidebarDensityHTML(unittest.TestCase):
|
||||
def test_settings_select_present(self):
|
||||
self.assertIn('id="settingsSidebarDensity"', INDEX_HTML)
|
||||
|
||||
def test_i18n_wiring_present(self):
|
||||
for key in (
|
||||
'data-i18n="settings_label_sidebar_density"',
|
||||
'data-i18n="settings_desc_sidebar_density"',
|
||||
'data-i18n="settings_sidebar_density_compact"',
|
||||
'data-i18n="settings_sidebar_density_detailed"',
|
||||
):
|
||||
self.assertIn(key, INDEX_HTML)
|
||||
|
||||
|
||||
class TestSidebarDensityBootAndPanels(unittest.TestCase):
|
||||
def test_boot_applies_sidebar_density(self):
|
||||
self.assertIn(
|
||||
"window._sidebarDensity=(s.sidebar_density==='detailed'?'detailed':'compact');",
|
||||
BOOT_JS,
|
||||
)
|
||||
|
||||
def test_boot_fallback_is_compact(self):
|
||||
self.assertIn("window._sidebarDensity='compact';", BOOT_JS)
|
||||
|
||||
def test_settings_panel_reads_sidebar_density(self):
|
||||
self.assertIn("settingsSidebarDensity", PANELS_JS)
|
||||
self.assertIn(
|
||||
"settings.sidebar_density==='detailed'?'detailed':'compact'",
|
||||
PANELS_JS,
|
||||
)
|
||||
|
||||
def test_settings_save_writes_sidebar_density(self):
|
||||
self.assertIn("body.sidebar_density=sidebarDensity;", PANELS_JS)
|
||||
self.assertIn(
|
||||
"window._sidebarDensity=sidebarDensity==='detailed'?'detailed':'compact';",
|
||||
PANELS_JS,
|
||||
)
|
||||
|
||||
|
||||
class TestSidebarDensitySessionRendering(unittest.TestCase):
|
||||
def test_detailed_mode_branch_present(self):
|
||||
self.assertIn(
|
||||
"const density=(window._sidebarDensity==='detailed'?'detailed':'compact');",
|
||||
SESSIONS_JS,
|
||||
)
|
||||
self.assertIn("if(density==='detailed')", SESSIONS_JS)
|
||||
|
||||
def test_detailed_mode_uses_message_count_and_model(self):
|
||||
self.assertIn("typeof s.message_count==='number'?s.message_count:0", SESSIONS_JS)
|
||||
self.assertIn("if(s.model) metaBits.push(s.model);", SESSIONS_JS)
|
||||
self.assertIn("t('session_meta_messages', msgCount)", SESSIONS_JS)
|
||||
|
||||
def test_profile_only_when_show_all_profiles(self):
|
||||
self.assertIn(
|
||||
"if(_showAllProfiles&&s.profile) metaBits.push(s.profile);", SESSIONS_JS
|
||||
)
|
||||
|
||||
def test_session_meta_css_hook_present(self):
|
||||
self.assertIn(".session-meta", STYLE_CSS)
|
||||
|
||||
|
||||
class TestSidebarDensityI18N(unittest.TestCase):
|
||||
def _extract_locale_block(self, start_marker, end_marker):
|
||||
start = I18N_JS.find(start_marker)
|
||||
end = I18N_JS.find(end_marker, start)
|
||||
self.assertGreater(start, -1)
|
||||
self.assertGreater(end, start)
|
||||
return I18N_JS[start:end]
|
||||
|
||||
def test_all_locale_blocks_have_sidebar_density_keys(self):
|
||||
locale_ranges = [
|
||||
("\n en: {", "\n ru: {"),
|
||||
("\n ru: {", "\n es: {"),
|
||||
("\n es: {", "\n de: {"),
|
||||
("\n de: {", "\n zh: {"),
|
||||
("\n zh: {", "\n // Traditional Chinese (zh-Hant)"),
|
||||
("\n // Traditional Chinese (zh-Hant)\n 'zh-Hant': {", "\n};"),
|
||||
]
|
||||
required = (
|
||||
"settings_label_sidebar_density",
|
||||
"settings_desc_sidebar_density",
|
||||
"settings_sidebar_density_compact",
|
||||
"settings_sidebar_density_detailed",
|
||||
"session_meta_messages",
|
||||
)
|
||||
for start, end in locale_ranges:
|
||||
block = self._extract_locale_block(start, end)
|
||||
for key in required:
|
||||
self.assertIn(key, block, f"{key} missing from locale block {start}")
|
||||
|
||||
|
||||
class TestSidebarDensitySettingsAPI(unittest.TestCase):
|
||||
def test_sidebar_density_default_is_compact(self):
|
||||
try:
|
||||
data, status = _get("/api/settings")
|
||||
except OSError:
|
||||
self.skipTest("Server not running on test server port")
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(data.get("sidebar_density"), "compact")
|
||||
|
||||
def test_sidebar_density_round_trips_detailed(self):
|
||||
try:
|
||||
_, status = _post("/api/settings", {"sidebar_density": "detailed"})
|
||||
except OSError:
|
||||
self.skipTest("Server not running on test server port")
|
||||
self.assertEqual(status, 200)
|
||||
data, _ = _get("/api/settings")
|
||||
self.assertEqual(data.get("sidebar_density"), "detailed")
|
||||
_post("/api/settings", {"sidebar_density": "compact"})
|
||||
|
||||
def test_invalid_sidebar_density_is_ignored(self):
|
||||
try:
|
||||
_post("/api/settings", {"sidebar_density": "compact"})
|
||||
data, status = _post("/api/settings", {"sidebar_density": "nope"})
|
||||
except OSError:
|
||||
self.skipTest("Server not running on test server port")
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(data.get("sidebar_density"), "compact")
|
||||
current, _ = _get("/api/settings")
|
||||
self.assertEqual(current.get("sidebar_density"), "compact")
|
||||
Reference in New Issue
Block a user