From 01404ac0621edf4f88f3992d3c8688dffd7f7c75 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sat, 25 Apr 2026 17:50:58 -0700 Subject: [PATCH] v0.50.211: compact timestamps, adaptive title refresh, settings picker fix (#1061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Shorten session sidebar relative time labels * feat: adaptive session title refresh based on conversation evolution Addresses #869 — the 'Optional' part: adapt session names to current conversation context instead of only generating once from the first exchange. Backend (api/streaming.py): - Add _latest_exchange_snippets() to extract last user+assistant pair - Add _count_exchanges() to count user messages - Add _get_title_refresh_interval() to read the setting - Add _run_background_title_refresh() — refreshes title from latest exchange with LLM, skips if title is unchanged or user manually renamed - Add _maybe_schedule_title_refresh() — checks exchange count and schedules refresh after stream_end (non-blocking) Config (api/config.py): - Add auto_title_refresh_every setting (default '0' = off) - Enum validation: {'0', '5', '10', '20'} Frontend: - Settings UI dropdown (static/index.html) - Wire up load/save in panels.js - i18n keys for all 6 locales (en/ru/es/de/zh/zh-Hant) Default: off. Opt-in via Settings > Conversation > Adaptive title refresh. * test: add 37 tests for adaptive title refresh helpers Covers all five new functions introduced in this PR: _count_exchanges, _latest_exchange_snippets, _get_title_refresh_interval, _run_background_title_refresh, _maybe_schedule_title_refresh Co-authored-by: bergeouss * fix(settings): show selected state on theme/skin/font-size picker cards The CSS rule `#mainSettings .theme-pick-btn { border-color: var(--border) !important }` was overriding the inline `style.borderColor = "var(--accent)"` set by `_syncThemePicker()` and siblings — `!important` beats inline styles. Active cards showed no visual highlight. Fix: move to `.active` CSS class with `border-color:var(--accent)!important` so the active rule wins over the base rule, and clear the stale inline borderColor/boxShadow from the sync functions. 5 regression tests added. Closes #1057 * fix: rename test file to match PR number, fix stale issue reference * docs: v0.50.211 release notes and version bump Compact sidebar timestamps, adaptive title refresh (opt-in), settings picker fix. * docs(changelog): correct settings tab for adaptive title refresh The v0.50.211 entry for #1058 said "Settings → Appearance" but the toggle is actually rendered inside settingsPanePreferences (the Preferences tab) per static/index.html:604+. The commit message also had the wrong tab ("Conversation"). Updated CHANGELOG to match the actual UI surface so users can find the toggle. Co-Authored-By: Claude Opus 4.7 (1M context) * fix: create state dir before writing settings file save_settings() called SETTINGS_FILE.write_text() without ensuring the parent directory exists. In fresh environments (CI, first run without HERMES_WEBUI_STATE_DIR set) this raised FileNotFoundError. Add mkdir(parents=True, exist_ok=True) before the write. --------- Co-authored-by: Pavol Biely Co-authored-by: bergeouss Co-authored-by: nesquena-hermes Co-authored-by: Nathan Esquenazi Co-authored-by: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 11 + ROADMAP.md | 2 +- TESTING.md | 2 +- api/config.py | 3 + api/streaming.py | 135 +++++++ static/boot.js | 18 +- static/i18n.js | 108 ++--- static/index.html | 10 + static/panels.js | 7 + static/sessions.js | 4 +- static/style.css | 4 + tests/test_1058_adaptive_title_refresh.py | 382 ++++++++++++++++++ .../test_1059_settings_picker_active_state.py | 84 ++++ tests/test_auth_session_persistence.py | 17 +- tests/test_chinese_locale.py | 2 +- tests/test_russian_locale.py | 2 +- tests/test_session_sidebar_relative_time.py | 18 +- 17 files changed, 730 insertions(+), 79 deletions(-) create mode 100644 tests/test_1058_adaptive_title_refresh.py create mode 100644 tests/test_1059_settings_picker_active_state.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e98daa4..7f7719fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## [Unreleased] +## v0.50.211 — 2026-04-25 + +### Changed +- **Compact sidebar timestamps** — session timestamps in the left sidebar now show short labels (`1m`, `6m`, `1h`, `1d`, `1w`) instead of verbose strings like "6 minutes ago". Keeps all existing i18n paths; bucket headers (Today, Yesterday, This week) unchanged. (`static/sessions.js`, `static/i18n.js`) [#1057 @pavolbiely] + +### Added +- **Adaptive session title refresh** — new opt-in setting (`Settings → Preferences → Adaptive title refresh`) re-generates the session title from the latest exchange every N turns (5, 10, or 20). Off by default. Runs in a daemon thread after stream end, never blocks the stream. Manual title renames are preserved (double-checked before and after LLM call). (`api/streaming.py`, `api/config.py`, `static/panels.js`, `static/i18n.js`, `static/index.html`) [#1058 @bergeouss] + +### Fixed +- **Settings picker active state** — theme, skin, and font-size picker cards in Settings → Appearance now correctly highlight the selected option. Root cause: the base CSS rule used `!important` on `border-color`, overriding the inline style set by `_syncThemePicker()` and siblings. Fix moves to an `.active` class with its own `!important` rule. (`static/style.css`, `static/boot.js`) [#1059] + ## v0.50.210 — 2026-04-25 ### Added diff --git a/ROADMAP.md b/ROADMAP.md index 1b42476d..23709130 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,7 +3,7 @@ > Goal: Full 1:1 parity with the Hermes CLI experience via a clean dark web UI. > Everything you can do from the CLI terminal, you can do from this UI. > -> Last updated: v0.50.185 (April 24, 2026) — 2107 tests collected +> Last updated: v0.50.211 (April 25, 2026) — 2276 tests collected > Tests: 2107 collected (`pytest tests/ --collect-only -q`) > Source: / diff --git a/TESTING.md b/TESTING.md index 8b32e6e3..ea140b7f 100644 --- a/TESTING.md +++ b/TESTING.md @@ -8,7 +8,7 @@ > Prerequisites: SSH tunnel is active on port 8787. Open http://localhost:8787 in browser. > Server health check: curl http://127.0.0.1:8787/health should return {"status":"ok"}. > -> Automated coverage: 2239 tests collected via `pytest tests/ --collect-only -q`. Includes onboarding coverage for bootstrap/static wizard presence, real provider config persistence (`config.yaml` + `.env`), the `/api/onboarding/*` backend, the onboarding skip/existing-config guard, and CSS regression coverage for smooth thinking/tool card disclosure animation. +> Automated coverage: 2276 tests collected via `pytest tests/ --collect-only -q`. Includes onboarding coverage for bootstrap/static wizard presence, real provider config persistence (`config.yaml` + `.env`), the `/api/onboarding/*` backend, the onboarding skip/existing-config guard, and CSS regression coverage for smooth thinking/tool card disclosure animation. > Run: `pytest tests/ -v --timeout=60` > > Local regression focus: verify that a previously closed workspace panel stays visually closed from first paint through boot completion on desktop refresh; there should be no brief open-then-close flash. diff --git a/api/config.py b/api/config.py index c56ff9e1..f0e7e8b7 100644 --- a/api/config.py +++ b/api/config.py @@ -1793,6 +1793,7 @@ _SETTINGS_DEFAULTS = { "notifications_enabled": False, # browser notification when tab is in background "show_thinking": True, # show/hide thinking/reasoning blocks in chat view "sidebar_density": "compact", # compact | detailed + "auto_title_refresh_every": "0", # adaptive title refresh: 0=off, 5/10/20=every N exchanges "password_hash": None, # PBKDF2-HMAC-SHA256 hash; None = auth disabled } _SETTINGS_LEGACY_DROP_KEYS = {"assistant_language", "bubble_layout", "default_model"} @@ -1893,6 +1894,7 @@ _SETTINGS_ALLOWED_KEYS = set(_SETTINGS_DEFAULTS.keys()) - { _SETTINGS_ENUM_VALUES = { "send_key": {"enter", "ctrl+enter"}, "sidebar_density": {"compact", "detailed"}, + "auto_title_refresh_every": {"0", "5", "10", "20"}, } _SETTINGS_BOOL_KEYS = { "onboarding_completed", @@ -1961,6 +1963,7 @@ def save_settings(settings: dict) -> dict: resolve_default_workspace(current.get("default_workspace")) ) persisted = {k: v for k, v in current.items() if k != "default_model"} + SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True) SETTINGS_FILE.write_text( json.dumps(persisted, ensure_ascii=False, indent=2), encoding="utf-8", diff --git a/api/streaming.py b/api/streaming.py index 633e08c7..a4b0e360 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -209,6 +209,58 @@ def _first_exchange_snippets(messages): return user_text[:500], asst_text[:500] +def _latest_exchange_snippets(messages): + """Return (last_user_text, last_assistant_text) snippets for title refresh. + + Walks the message list backwards to find the last user+assistant pair, + skipping empty or tool-call-only assistant messages. + """ + user_text = '' + asst_text = '' + for m in reversed(messages or []): + if not isinstance(m, dict): + continue + role = m.get('role') + if role == 'assistant' and not asst_text: + candidate = _message_text(m.get('content')) + # Skip tool-call-only preambles + if m.get('tool_calls') and (not candidate or _looks_invalid_generated_title(candidate)): + continue + if candidate: + asst_text = candidate + elif role == 'user' and not user_text: + candidate = _message_text(m.get('content')) + if candidate: + user_text = candidate + if user_text and asst_text: + break + return user_text[:500], asst_text[:500] + + +def _count_exchanges(messages): + """Count the number of user messages (rough exchange count).""" + count = 0 + for m in messages or []: + if isinstance(m, dict) and m.get('role') == 'user': + content = m.get('content', '') + if isinstance(content, list): + content = ' '.join(p.get('text', '') for p in content if isinstance(p, dict) and p.get('type') == 'text') + if str(content).strip(): + count += 1 + return count + + +def _get_title_refresh_interval() -> int: + """Read the auto_title_refresh_every setting (0 = disabled).""" + try: + from api.config import load_settings + settings = load_settings() + val = settings.get('auto_title_refresh_every', '0') + return int(val) if str(val).strip().isdigit() and int(val) > 0 else 0 + except Exception: + return 0 + + def _is_provisional_title(current_title: str, messages) -> bool: """Heuristic: title equals first-message substring placeholder.""" derived = title_from(messages, '') or '' @@ -731,6 +783,85 @@ def _run_background_title_update(session_id: str, user_text: str, assistant_text put_event('stream_end', {'session_id': session_id}) +def _run_background_title_refresh(session_id: str, user_text: str, assistant_text: str, current_title: str, put_event, agent=None): + """Refresh an existing LLM-generated title using the latest exchange text. + + Unlike _run_background_title_update, this does NOT guard on + llm_title_generated — it assumes the title was already LLM-generated + and the session has progressed enough to warrant a refresh. + It does NOT emit stream_end (the caller already did). + """ + try: + try: + s = get_session(session_id) + except KeyError: + return + # Safety: skip if user manually renamed since the check + effective = str(s.title or '').strip() + if effective != current_title: + _put_title_status(put_event, session_id, 'skipped', 'manual_title', effective) + return + if not effective or effective in ('Untitled', 'New Chat'): + return + aux_title_configured = _aux_title_configured() + if agent and not aux_title_configured: + next_title, llm_status, raw_preview = _generate_llm_session_title_for_agent(agent, user_text, assistant_text) + if not next_title and llm_status in ('llm_error', 'llm_invalid'): + next_title, llm_status, raw_preview = _generate_llm_session_title_via_aux(user_text, assistant_text, agent=agent, use_agent_model=True) + else: + next_title, llm_status, raw_preview = _generate_llm_session_title_via_aux(user_text, assistant_text) + if not next_title and agent and llm_status in ('llm_error_aux', 'llm_invalid_aux'): + next_title, llm_status, raw_preview = _generate_llm_session_title_for_agent(agent, user_text, assistant_text) + if not next_title: + _put_title_status(put_event, session_id, 'refresh_skipped', llm_status or 'empty', effective, raw_preview) + return + # Skip if the new title is essentially the same (after normalization) + normalized_current = re.sub(r'\s+', ' ', effective).strip().lower() + normalized_new = re.sub(r'\s+', ' ', next_title).strip().lower() + if normalized_current == normalized_new: + _put_title_status(put_event, session_id, 'refresh_skipped', 'same_title', effective, raw_preview) + return + with _get_session_agent_lock(session_id): + with LOCK: + s = SESSIONS.get(session_id, s) + # Re-check: user may have renamed while we were generating + if str(s.title or '').strip() != current_title: + _put_title_status(put_event, session_id, 'skipped', 'manual_title', str(s.title or '').strip()) + return + s.title = next_title + s.llm_title_generated = True + s.save(touch_updated_at=False) + effective_title = s.title + _put_title_status(put_event, session_id, 'refreshed', llm_status, effective_title, raw_preview) + put_event('title', {'session_id': session_id, 'title': effective_title}) + logger.info("Adaptive title refresh: session=%s new_title=%r", session_id, effective_title) + except Exception: + logger.debug("Background title refresh failed for session %s", session_id, exc_info=True) + + +def _maybe_schedule_title_refresh(session, put_event, agent): + """Check if the session is due for an adaptive title refresh and schedule it.""" + refresh_interval = _get_title_refresh_interval() + if refresh_interval <= 0: + return + current_title = str(session.title or '').strip() + if not current_title or current_title in ('Untitled', 'New Chat'): + return + if not getattr(session, 'llm_title_generated', False): + return + exchange_count = _count_exchanges(session.messages) + if exchange_count <= 0 or exchange_count % refresh_interval != 0: + return + last_u, last_a = _latest_exchange_snippets(session.messages) + if not last_u and not last_a: + return + threading.Thread( + target=_run_background_title_refresh, + args=(session.session_id, last_u, last_a, current_title, put_event, agent), + daemon=True, + ).start() + + def _sanitize_messages_for_api(messages): """Return a deep copy of messages with only API-safe fields. @@ -1822,6 +1953,10 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta # which may be rotated during context compression. The client captured # activeSid = original session_id so they must match for stream_end to close. put('stream_end', {'session_id': session_id}) + # Adaptive title refresh: re-generate title from latest exchange + # every N exchanges (when enabled in settings). Runs after stream_end + # so it doesn't block the stream. + _maybe_schedule_title_refresh(s, put, agent) finally: # Stop the live metering ticker _metering_stop.set() diff --git a/static/boot.js b/static/boot.js index f5fc013c..94a85aa8 100644 --- a/static/boot.js +++ b/static/boot.js @@ -710,17 +710,17 @@ function _pickSkin(name){ function _syncThemePicker(active){ document.querySelectorAll('#themePickerGrid .theme-pick-btn').forEach(btn=>{ - const sel=btn.dataset.themeVal===active; - btn.style.borderColor=sel?'var(--accent)':'var(--border2)'; - btn.style.boxShadow=sel?'0 0 0 1px var(--accent-bg-strong)':'none'; + btn.classList.toggle('active',btn.dataset.themeVal===active); + btn.style.borderColor=''; + btn.style.boxShadow=''; }); } function _syncSkinPicker(active){ document.querySelectorAll('#skinPickerGrid .skin-pick-btn').forEach(btn=>{ - const sel=btn.dataset.skinVal===active; - btn.style.borderColor=sel?'var(--accent)':'var(--border2)'; - btn.style.boxShadow=sel?'0 0 0 1px var(--accent-bg-strong)':'none'; + btn.classList.toggle('active',btn.dataset.skinVal===active); + btn.style.borderColor=''; + btn.style.boxShadow=''; }); } @@ -743,9 +743,9 @@ function _pickFontSize(size){ function _syncFontSizePicker(active){ document.querySelectorAll('#fontSizePickerGrid .font-size-pick-btn').forEach(btn=>{ - const sel=btn.dataset.fontSizeVal===(active||'default'); - btn.style.borderColor=sel?'var(--accent)':'var(--border2)'; - btn.style.boxShadow=sel?'0 0 0 1px var(--accent-bg-strong)':'none'; + btn.classList.toggle('active',btn.dataset.fontSizeVal===(active||'default')); + btn.style.borderColor=''; + btn.style.boxShadow=''; }); } diff --git a/static/i18n.js b/static/i18n.js index 44230b3a..fd8cc5df 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -308,11 +308,10 @@ const LOCALES = { new_conversation: 'New conversation', filter_conversations: 'Filter conversations...', session_time_unknown: 'Unknown', - session_time_just_now: 'just now', - session_time_minutes_ago: (n) => `${n} minute${n === 1 ? '' : 's'} ago`, - session_time_hours_ago: (n) => `${n} hour${n === 1 ? '' : 's'} ago`, - session_time_days_ago: (n) => `${n} day${n === 1 ? '' : 's'} ago`, - session_time_last_week: 'last week', + session_time_minutes_ago: (n) => `${n}m`, + session_time_hours_ago: (n) => `${n}h`, + session_time_days_ago: (n) => `${n}d`, + session_time_last_week: '1w', session_time_bucket_today: 'Today', session_time_bucket_yesterday: 'Yesterday', session_time_bucket_this_week: 'This week', @@ -340,6 +339,12 @@ const LOCALES = { 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_label_auto_title_refresh: 'Adaptive title refresh', + settings_auto_title_refresh_off: 'Off', + settings_auto_title_refresh_5: 'Every 5 exchanges', + settings_auto_title_refresh_10: 'Every 10 exchanges', + settings_auto_title_refresh_20: 'Every 20 exchanges', + settings_desc_auto_title_refresh: 'Automatically re-generates the session title based on the latest exchange, keeping it relevant as the conversation evolves. Requires an LLM title generation model to be configured.', 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.', @@ -811,38 +816,10 @@ const LOCALES = { new_conversation: 'Новая беседа', filter_conversations: 'Фильтр бесед...', session_time_unknown: 'Неизвестно', - session_time_just_now: 'только что', - session_time_minutes_ago: (n) => { - const mod10 = n % 10; - const mod100 = n % 100; - const word = mod10 === 1 && mod100 !== 11 - ? 'минута' - : (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20) - ? 'минуты' - : 'минут'); - return `${n} ${word} назад`; - }, - session_time_hours_ago: (n) => { - const mod10 = n % 10; - const mod100 = n % 100; - const word = mod10 === 1 && mod100 !== 11 - ? 'час' - : (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20) - ? 'часа' - : 'часов'); - return `${n} ${word} назад`; - }, - session_time_days_ago: (n) => { - const mod10 = n % 10; - const mod100 = n % 100; - const word = mod10 === 1 && mod100 !== 11 - ? 'день' - : (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20) - ? 'дня' - : 'дней'); - return `${n} ${word} назад`; - }, - session_time_last_week: 'на прошлой неделе', + session_time_minutes_ago: (n) => `${n}м`, + session_time_hours_ago: (n) => `${n}ч`, + session_time_days_ago: (n) => `${n}д`, + session_time_last_week: '1н', session_time_bucket_today: 'Сегодня', session_time_bucket_yesterday: 'Вчера', session_time_bucket_this_week: 'На этой неделе', @@ -869,6 +846,12 @@ const LOCALES = { settings_sidebar_density_compact: 'Компактно', settings_sidebar_density_detailed: 'Подробно', settings_desc_sidebar_density: 'Управляет тем, сколько метаданных показывается в списке сеансов на левой панели.', + settings_label_auto_title_refresh: 'Адаптивное обновление заголовка', + settings_auto_title_refresh_off: 'Выкл', + settings_auto_title_refresh_5: 'Каждые 5 обменов', + settings_auto_title_refresh_10: 'Каждые 10 обменов', + settings_auto_title_refresh_20: 'Каждые 20 обменов', + settings_desc_auto_title_refresh: 'Автоматически переформулирует заголовок сессии на основе последнего обмена, чтобы он оставался актуальным по мере развития беседы. Требует настроенную модель генерации заголовков.', 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 в фоне.', @@ -1412,11 +1395,10 @@ const LOCALES = { new_conversation: 'Nueva conversación', filter_conversations: 'Filtrar conversaciones...', session_time_unknown: 'Desconocido', - session_time_just_now: 'justo ahora', - session_time_minutes_ago: (n) => `hace ${n} minuto${n === 1 ? '' : 's'}`, - session_time_hours_ago: (n) => `hace ${n} hora${n === 1 ? '' : 's'}`, - session_time_days_ago: (n) => `hace ${n} día${n === 1 ? '' : 's'}`, - session_time_last_week: 'la semana pasada', + session_time_minutes_ago: (n) => `${n}m`, + session_time_hours_ago: (n) => `${n}h`, + session_time_days_ago: (n) => `${n}d`, + session_time_last_week: '1sem', session_time_bucket_today: 'Hoy', session_time_bucket_yesterday: 'Ayer', session_time_bucket_this_week: 'Esta semana', @@ -1444,6 +1426,12 @@ const LOCALES = { 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_label_auto_title_refresh: 'Actualización adaptativa del título', + settings_auto_title_refresh_off: 'Desactivado', + settings_auto_title_refresh_5: 'Cada 5 intercambios', + settings_auto_title_refresh_10: 'Cada 10 intercambios', + settings_auto_title_refresh_20: 'Cada 20 intercambios', + settings_desc_auto_title_refresh: 'Regenera automáticamente el título de la sesión basándose en el último intercambio, manteniéndolo relevante a medida que evoluciona la conversación. Requiere un modelo LLM de generación de títulos configurado.', 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.', @@ -1997,6 +1985,12 @@ const LOCALES = { 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_label_auto_title_refresh: 'Adaptive Titelaktualisierung', + settings_auto_title_refresh_off: 'Aus', + settings_auto_title_refresh_5: 'Alle 5 Antworten', + settings_auto_title_refresh_10: 'Alle 10 Antworten', + settings_auto_title_refresh_20: 'Alle 20 Antworten', + settings_desc_auto_title_refresh: 'Generiert den Sitzungstitel automatisch anhand des letzten Austauschs neu und hält ihn so aktuell, während sich das Gespräch entwickelt. Erfordert ein konfiguriertes LLM-Titelgenerierungsmodell.', 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.', @@ -2314,11 +2308,10 @@ const LOCALES = { new_conversation: '新建对话', filter_conversations: '筛选对话…', session_time_unknown: '未知', - session_time_just_now: '刚刚', - session_time_minutes_ago: (n) => `${n} 分钟前`, - session_time_hours_ago: (n) => `${n} 小时前`, - session_time_days_ago: (n) => `${n} 天前`, - session_time_last_week: '上周', + session_time_minutes_ago: (n) => `${n}分`, + session_time_hours_ago: (n) => `${n}小时`, + session_time_days_ago: (n) => `${n}天`, + session_time_last_week: '1周', session_time_bucket_today: '今天', session_time_bucket_yesterday: '昨天', session_time_bucket_this_week: '本周', @@ -2377,6 +2370,12 @@ const LOCALES = { settings_sidebar_density_compact: '紧凑', settings_sidebar_density_detailed: '详细', settings_desc_sidebar_density: '控制左侧会话列表展示多少元信息。', + settings_label_auto_title_refresh: '\u81ea\u9002\u5e94\u6807\u9898\u66f4\u65b0', + settings_auto_title_refresh_off: '\u5173\u95ed', + settings_auto_title_refresh_5: '\u6bcf 5 \u8f6e\u5bf9\u8bdd', + settings_auto_title_refresh_10: '\u6bcf 10 \u8f6e\u5bf9\u8bdd', + settings_auto_title_refresh_20: '\u6bcf 20 \u8f6e\u5bf9\u8bdd', + settings_desc_auto_title_refresh: '\u57fa\u4e8e\u6700\u65b0\u5bf9\u8bdd\u81ea\u52a8\u91cd\u65b0\u751f\u6210\u4f1a\u8bdd\u6807\u9898\uff0c\u4f7f\u5176\u968f\u5bf9\u8bdd\u53d1\u5c55\u4fdd\u6301\u76f8\u5173\u3002\u9700\u8981\u914d\u7f6e LLM \u6807\u9898\u751f\u6210\u6a21\u578b\u3002', 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。', @@ -2943,6 +2942,12 @@ const LOCALES = { settings_sidebar_density_compact: '精簡', settings_sidebar_density_detailed: '詳細', settings_desc_sidebar_density: '控制左側對話清單要顯示多少額外資訊。', + settings_label_auto_title_refresh: '\u81ea\u9002\u61c9\u6a19\u984c\u66f4\u65b0', + settings_auto_title_refresh_off: '\u95dc\u9589', + settings_auto_title_refresh_5: '\u6bcf 5 \u8f2a\u5c0d\u8a71', + settings_auto_title_refresh_10: '\u6bcf 10 \u8f2a\u5c0d\u8a71', + settings_auto_title_refresh_20: '\u6bcf 20 \u8f2a\u5c0d\u8a71', + settings_desc_auto_title_refresh: '\u57fa\u65bc\u6700\u65b0\u5c0d\u8a71\u81ea\u52d5\u91cd\u65b0\u751f\u6210\u6703\u8a71\u6a19\u984c\uff0c\u4f7f\u5176\u968f\u5c0d\u8a71\u767c\u5c55\u4fdd\u6301\u76f8\u95dc\u3002\u9700\u8981\u914d\u7f6e LLM \u6a19\u984c\u751f\u6210\u6a21\u578b\u3002', 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。', @@ -3151,11 +3156,10 @@ const LOCALES = { session_time_bucket_this_week: '\u672c\u9031', session_time_bucket_today: '\u4eca\u5929', session_time_bucket_yesterday: '\u6628\u5929', - session_time_days_ago: (d) => `${d} 天前`, - session_time_hours_ago: (h) => `${h} 小時前`, - session_time_just_now: '\u525b\u525b', - session_time_last_week: '\u4e0a\u9031', - session_time_minutes_ago: (m) => `${m} 分鐘前`, + session_time_days_ago: (d) => `${d}\u5929`, + session_time_hours_ago: (h) => `${h}\u5c0f\u6642`, + session_time_last_week: '1\u9031', + session_time_minutes_ago: (m) => `${m}\u5206`, session_time_unknown: '\u672a\u77e5', settings_unsaved_changes: '\u60a8\u6709\u672a\u5132\u5b58\u7684\u8b8a\u66f4\u3002', sign_out_failed: '\u767b\u51fa\u5931\u6557\uff1a', diff --git a/static/index.html b/static/index.html index d4474d6e..6c11da75 100644 --- a/static/index.html +++ b/static/index.html @@ -652,6 +652,16 @@
Controls how much metadata the session list shows in the left sidebar.
+
+ + +
Automatically re-generates the session title based on the latest exchange, keeping it relevant as the conversation evolves. Requires an LLM title generation model to be configured.
+