From ea8aca2818bdf81dc3106640bf74fa5df416ec08 Mon Sep 17 00:00:00 2001 From: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com> Date: Fri, 8 May 2026 20:45:53 +0200 Subject: [PATCH] feat: add opt-in session endless scroll --- api/config.py | 2 ++ static/boot.js | 2 ++ static/i18n.js | 36 +++++++++++++++++++++++++++ static/index.html | 7 ++++++ static/panels.js | 13 ++++++++++ static/ui.js | 10 ++++++-- tests/test_parallel_session_switch.py | 5 ++-- tests/test_session_endless_scroll.py | 32 ++++++++++++++++++++++++ 8 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 tests/test_session_endless_scroll.py diff --git a/api/config.py b/api/config.py index b93d9ba2..ba0e36c9 100644 --- a/api/config.py +++ b/api/config.py @@ -3682,6 +3682,7 @@ _SETTINGS_DEFAULTS = { "theme": "dark", # light | dark | system "skin": "default", # accent color skin: default | ares | mono | slate | poseidon | sisyphus | charizard "font_size": "default", # small | default | large + "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 "bot_name": os.getenv( "HERMES_WEBUI_BOT_NAME", "Hermes" @@ -3810,6 +3811,7 @@ _SETTINGS_BOOL_KEYS = { "show_thinking", "simplified_tool_calling", "api_redact_enabled", + "session_endless_scroll", } # Language codes are validated as short alphanumeric BCP-47-like tags (e.g. 'en', 'zh', 'fr') _SETTINGS_LANG_RE = __import__("re").compile(r"^[a-zA-Z]{2,10}(-[a-zA-Z0-9]{2,8})?$") diff --git a/static/boot.js b/static/boot.js index e6f1b7e7..145e61b0 100644 --- a/static/boot.js +++ b/static/boot.js @@ -1303,6 +1303,7 @@ function applyBotName(){ window._simplifiedToolCalling=s.simplified_tool_calling!==false; window._sidebarDensity=(s.sidebar_density==='detailed'?'detailed':'compact'); window._busyInputMode=(s.busy_input_mode||'queue'); + window._sessionEndlessScrollEnabled=!!s.session_endless_scroll; window._botName=s.bot_name||'Hermes'; if(s.default_model) window._defaultModel=s.default_model; // Persist default workspace so the blank new-chat page can show it @@ -1337,6 +1338,7 @@ function applyBotName(){ window._simplifiedToolCalling=true; window._sidebarDensity='compact'; window._busyInputMode='queue'; + window._sessionEndlessScrollEnabled=false; window._botName='Hermes'; _bootSettings={check_for_updates:false}; if(typeof setLocale==='function'){ diff --git a/static/i18n.js b/static/i18n.js index d32cfa84..1cfd3305 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -421,6 +421,10 @@ const LOCALES = { settings_update_check_failed: 'Update check failed', settings_label_workspace_panel_open: 'Keep workspace panel open by default', settings_desc_workspace_panel_open: 'When enabled, the workspace / file browser panel opens automatically with each new session. You can still close it manually at any time.', + + settings_label_session_endless_scroll: 'Load older messages while scrolling up', + + settings_desc_session_endless_scroll: 'When enabled, older messages load automatically as you scroll upward. When disabled, use the older-messages button.', open_in_browser: 'Open in browser', settings_dropdown_conversation: 'Conversation', settings_dropdown_appearance: 'Appearance', @@ -1442,6 +1446,10 @@ const LOCALES = { settings_updates_disabled: 'アップデート確認は無効です', settings_label_workspace_panel_open: 'ワークスペースパネルをデフォルトで開いておく', settings_desc_workspace_panel_open: '有効にすると、新しいセッションごとにワークスペース/ファイルブラウザパネルが自動で開きます。手動でいつでも閉じられます。', + + settings_label_session_endless_scroll: '上スクロールで古いメッセージを読み込む', + + settings_desc_session_endless_scroll: '有効にすると、上にスクロールしたとき古いメッセージを自動で読み込みます。無効の場合は古いメッセージボタンを使います。', open_in_browser: 'ブラウザで開く', settings_dropdown_conversation: '会話', settings_dropdown_appearance: '外観', @@ -2880,6 +2888,10 @@ const LOCALES = { settings_update_check_failed: 'Ошибка проверки обновлений', settings_label_workspace_panel_open: 'Открывать панель рабочей области по умолчанию', settings_desc_workspace_panel_open: 'При включении панель файлов будет открываться автоматически в каждой новой сессии.', + + settings_label_session_endless_scroll: 'Загружать старые сообщения при прокрутке вверх', + + settings_desc_session_endless_scroll: 'Если включено, старые сообщения загружаются автоматически при прокрутке вверх. Если выключено, используйте кнопку загрузки старых сообщений.', open_in_browser: 'Открыть в браузере', settings_section_system_title: 'System', settings_tab_appearance: 'Appearance', @@ -3823,6 +3835,10 @@ const LOCALES = { settings_update_check_failed: 'Error al comprobar actualizaciones', settings_label_workspace_panel_open: 'Mantener panel de espacio abierto', settings_desc_workspace_panel_open: 'Al activar, el panel de archivos se abre automáticamente en cada nueva sesión. Aún puedes cerrarlo manualmente.', + + settings_label_session_endless_scroll: 'Cargar mensajes antiguos al desplazarse hacia arriba', + + settings_desc_session_endless_scroll: 'Si está activado, los mensajes antiguos se cargan automáticamente al desplazarte hacia arriba. Si está desactivado, usa el botón de mensajes antiguos.', open_in_browser: 'Abrir en el navegador', settings_section_system_title: 'System', settings_tab_appearance: 'Appearance', @@ -4512,6 +4528,10 @@ const LOCALES = { settings_label_workspace_panel_open: 'Arbeitsbereich-Panel standardmäßig öffnen', settings_desc_workspace_panel_open: 'Wenn aktiviert, wird der Datei-Browser bei jeder neuen Sitzung automatisch geöffnet. Er kann jederzeit manuell geschlossen werden.', + settings_label_session_endless_scroll: 'Ältere Nachrichten beim Hochscrollen laden', + + settings_desc_session_endless_scroll: 'Wenn aktiviert, werden ältere Nachrichten beim Hochscrollen automatisch geladen. Wenn deaktiviert, nutzt du den Button für ältere Nachrichten.', + workspace_drag_hint: 'Ziehen zum Neuordnen', workspace_reorder_failed: 'Neuordnen fehlgeschlagen', open_in_browser: 'Im Browser öffnen', @@ -5730,6 +5750,10 @@ const LOCALES = { settings_update_check_failed: '更新检查失败', settings_label_workspace_panel_open: '默认保持工作区面板打开', settings_desc_workspace_panel_open: '启用后,工作区/文件浏览器面板会在每次新会话时自动打开。您仍可随时手动关闭。', + + settings_label_session_endless_scroll: '向上滚动时加载更早的消息', + + settings_desc_session_endless_scroll: '启用后,向上滚动时会自动加载更早的消息。禁用时请使用加载更早消息按钮。', open_in_browser: '在浏览器中打开', settings_section_system_title: 'System', settings_tab_appearance: 'Appearance', @@ -6136,6 +6160,10 @@ const LOCALES = { settings_update_check_failed: '更新檢查失敗', settings_label_workspace_panel_open: '預設保持工作區面板開啓', settings_desc_workspace_panel_open: '啟用後,工作區/檔案瀏覽器面板會在每次新會話時自動開啓。您仍可隨時手動關閉。', + + settings_label_session_endless_scroll: '向上捲動時載入較早訊息', + + settings_desc_session_endless_scroll: '啟用後,向上捲動時會自動載入較早訊息。停用時請使用載入較早訊息按鈕。', open_in_browser: '在瀏覽器中開啓', settings_dropdown_conversation: '對話', settings_dropdown_appearance: '外觀', @@ -7170,6 +7198,10 @@ const LOCALES = { settings_update_check_failed: 'Falha ao verificar updates', settings_label_workspace_panel_open: 'Manter painel workspace aberto por padrão', settings_desc_workspace_panel_open: 'Quando ativo, o painel workspace abre automaticamente com cada nova sessão.', + + settings_label_session_endless_scroll: 'Carregar mensagens antigas ao rolar para cima', + + settings_desc_session_endless_scroll: 'Quando ativado, mensagens antigas carregam automaticamente ao rolar para cima. Quando desativado, use o botão de mensagens antigas.', open_in_browser: 'Abrir no navegador', settings_dropdown_conversation: 'Conversa', settings_dropdown_appearance: 'Aparência', @@ -8089,6 +8121,10 @@ const LOCALES = { settings_update_check_failed: 'Update check failed', settings_label_workspace_panel_open: '기본으로 워크스페이스 패널 열기', settings_desc_workspace_panel_open: '활성화하면 새 세션마다 워크스페이스/파일 브라우저 패널이 자동으로 열립니다. 언제든지 수동으로 닫을 수 있습니다.', + + settings_label_session_endless_scroll: '위로 스크롤할 때 이전 메시지 불러오기', + + settings_desc_session_endless_scroll: '활성화하면 위로 스크롤할 때 이전 메시지를 자동으로 불러옵니다. 비활성화하면 이전 메시지 버튼을 사용합니다.', open_in_browser: '브라우저에서 열기', settings_dropdown_conversation: '대화', settings_dropdown_appearance: '외형', diff --git a/static/index.html b/static/index.html index d21c64b4..1279da83 100644 --- a/static/index.html +++ b/static/index.html @@ -863,6 +863,13 @@
When enabled, the workspace / file browser panel opens automatically with each new session. You can still close it manually at any time.
+
+ +
When enabled, older messages load automatically as you scroll upward. When disabled, use the older-messages button.
+
diff --git a/static/panels.js b/static/panels.js index 14d210d6..3946c2d6 100644 --- a/static/panels.js +++ b/static/panels.js @@ -4251,6 +4251,7 @@ function _appearancePayloadFromUi(){ theme: ($('settingsTheme')||{}).value || localStorage.getItem('hermes-theme') || 'dark', skin: ($('settingsSkin')||{}).value || localStorage.getItem('hermes-skin') || 'default', font_size: ($('settingsFontSize')||{}).value || localStorage.getItem('hermes-font-size') || 'default', + session_endless_scroll: !!($('settingsSessionEndlessScroll')||{}).checked, }; } @@ -4298,6 +4299,7 @@ async function _autosaveAppearanceSettings(payload){ if(saved&&saved.font_size){ localStorage.setItem('hermes-font-size',saved.font_size); } + window._sessionEndlessScrollEnabled=!!(saved&&saved.session_endless_scroll); _setAppearanceAutosaveStatus('saved'); }catch(e){ console.warn('[settings] appearance autosave failed', e); @@ -4470,6 +4472,15 @@ async function loadSettingsPanel(){ else if(!open&&_workspacePanelMode!=='closed') toggleWorkspacePanel(false); }; } + const endlessScrollCb=$('settingsSessionEndlessScroll'); + if(endlessScrollCb){ + endlessScrollCb.checked=!!settings.session_endless_scroll; + window._sessionEndlessScrollEnabled=endlessScrollCb.checked; + endlessScrollCb.onchange=function(){ + window._sessionEndlessScrollEnabled=this.checked; + _scheduleAppearanceAutosave(); + }; + } const resolvedLanguage=(typeof resolvePreferredLocale==='function') ? resolvePreferredLocale(settings.language, localStorage.getItem('hermes-lang')) : (settings.language || localStorage.getItem('hermes-lang') || 'en'); @@ -5126,6 +5137,7 @@ function _applySavedSettingsUi(saved, body, opts){ window._simplifiedToolCalling=body.simplified_tool_calling!==false; window._sidebarDensity=sidebarDensity==='detailed'?'detailed':'compact'; window._busyInputMode=body.busy_input_mode||'queue'; + window._sessionEndlessScrollEnabled=!!body.session_endless_scroll; window._botName=body.bot_name||'Hermes'; if(typeof applyBotName==='function') applyBotName(); if(typeof setLocale==='function') setLocale(language); @@ -5221,6 +5233,7 @@ async function saveSettings(andClose){ body.theme=theme; body.skin=skin; body.font_size=fontSize; + body.session_endless_scroll=!!($('settingsSessionEndlessScroll')||{}).checked; body.language=language; body.show_token_usage=showTokenUsage; body.show_tps=showTps; diff --git a/static/ui.js b/static/ui.js index 3e95e1b0..78e23773 100644 --- a/static/ui.js +++ b/static/ui.js @@ -185,6 +185,9 @@ function _messageRenderableMessageCount(){ function _messageHiddenBeforeCount(){ return Math.max(0,_messageRenderableMessageCount()-_currentMessageRenderWindowSize()); } +function _isSessionEndlessScrollEnabled(){ + return window._sessionEndlessScrollEnabled===true; +} function _wireMessageWindowLoadEarlierButton(){ const indicator=$('loadOlderIndicator'); if(!indicator) return; @@ -1574,8 +1577,11 @@ if (typeof window !== 'undefined') window._resetScrollDirectionTracker = _resetS } // #1360 const btn=$('scrollToBottomBtn'); if(btn) btn.style.display=_scrollPinned?'none':'flex'; - // Load older messages when scrolled near the top - if(el.scrollTop<80 && typeof _messagesTruncated!=='undefined' && _messagesTruncated && typeof _loadOlderMessages==='function'){ + // Prefetch older messages before the reader hits the hard top. Prepending + // then preserving scrollTop is seamless only if there is runway left for + // the user's continued upward wheel/touch movement. + const olderPrefetchPx=Math.max(600,el.clientHeight*1.5); + if(_isSessionEndlessScrollEnabled()&&el.scrollTop