diff --git a/CHANGELOG.md b/CHANGELOG.md
index 48e58a6e..1a68ebc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,26 @@
### Fixed
+- **PR #2210** by @Jordan-SkyLF — MCP Tools list in Settings → System no longer renders an unbounded inventory that makes the settings panel scroll-trapping. Added a toolbar (result-count summary, page-size 5/10/20/50/all, search input), bounded scroll area with consistent height, paginated rendering, and focused regression coverage for the large-inventory case. Existing WebUI-only/runtime-only contract preserved (no MCP server probing, no agent-side changes). Visual before/after evidence shipped under `docs/pr-media/2210/`.
+
+- **PR #2213** by @franksong2702 (fixes #2152) — Literal ``/`` discussions in normal assistant prose are no longer stripped from saved messages and re-renders. The old server cleanup and stored-message render regexes stripped the first closed thinking-looking block anywhere in the content. PR aligns saved/static paths with the existing streaming rule: provider reasoning wrappers (`...`, MiniMax `<|channel>thought...`, Gemma 4 `<|turn|>thinking...`) are stripped only when they lead the response (i.e. the wrapper is the first non-whitespace content).
+
+- **PR #2149** by @starship-s — `/api/session` loads no longer pay the cost of full external CLI session discovery when opening an ordinary WebUI-native chat. Caches CLI/external session scans briefly, skips CLI metadata lookup for ordinary WebUI-native session loads, and reuses a single in-memory ID snapshot during session-index pruning. Messaging, read-only, external-agent, and CLI-marked sidecars still take the CLI metadata path; CLI-only sessions still use the existing fallback. Stage-351 maintainer fix renamed the existing `_needs_cli_session_metadata()` gate to the broader `_session_requires_cli_metadata_lookup()` from this PR — strictly more inclusive (now also covers `read_only=True` sidecars, `session_source` markers, and source_tag/raw_source/platform metadata so legacy-imported sidecars still get the slow path when they need it).
+
+### Added
+
+- **PR #2207** by @Jordan-SkyLF (fixes #1579) — Update banner now shows target-aware "What's new?" links: WebUI updates link to the WebUI comparison, Agent updates link to the Agent comparison. Agent-only and WebUI-only update states no longer show a misleading cross-target comparison action. Opt-in settings toggle enables human-readable LLM-generated update summaries for each target's diff; users can still open the original diff from the summary. Cached/generated-summary button states persist across refreshes. Extended update-banner regression coverage for the diff-link and summary flows. Visual evidence: `docs/images/update-banner-whats-new-{before,after}.png` + summary on/off variants.
+
+- **PR #2206** by @vcavichini — Cron list now shows a 🤖 emoji badge for jobs running in agent mode (`no_agent=false`). Cron detail view shows the configured provider/model next to the Mode badge, falling back to "default" when neither is explicitly set for agent-mode crons. UI-only change.
+
+- **PR #2212** by @dobby-d-elf — Tunes the Activity sweep animation introduced in PR #2203 (stage-350) — softer color stops, less aggressive contrast, smoother fade. CSS-only follow-up.
+
+## [v0.51.57] — 2026-05-13 — Release AG (stage-350 — 7-PR medium-risk batch — auth trilogy + cancel-status with conflict resolution + Ollama label guard + provider precedence + Activity animation + Opus dedup tightening)
+
+### Fixed
+
+- **Issue #2152** — Literal discussions of reasoning tags such as `` and `` no longer disappear from saved or re-rendered assistant messages. WebUI now treats `...`, MiniMax `<|channel>thought...`, and Gemma 4 `<|turn|>thinking...` blocks as hidden reasoning metadata only when the wrapper is the first non-whitespace content in the response; provider wrappers with leading whitespace still strip as before.
+
- **PR #2191** by @lucasrc (auth refactor 1/3) — Thread-safe login rate limiter (new `_LOGIN_ATTEMPTS_LOCK`) + PBKDF2 key separation (new `_pbkdf2_key()` reading `.pbkdf2_key` separately from `_signing_key()` reading `.signing_key` — previously both shared `.signing_key`, a key-reuse anti-pattern across HMAC and PBKDF2 primitives) + transparent migration in `verify_password()` that re-salts legacy hashes with the new key on next successful login. 241-line regression suite covering the lock + migration paths. Split from earlier #2167 per maintainer review request.
- **PR #2192** by @lucasrc (auth refactor 2/3, depends on #2191) — Invalidate password-hash cache when password changes via the Settings panel. The PR #2191 cache lives for the process lifetime, but `save_settings({'_set_password': ...})` could mutate `settings.json.password_hash` without telling the auth module — leaving the cache stale and verifying against the old password until restart. Now `save_settings()` calls `_invalidate_password_hash_cache()` on both `_set_password` and `_clear_password` paths. 52-line regression suite + `verify_password()` simplified to rely on the new hook instead of doing the invalidation itself.
@@ -36,6 +56,8 @@
### Fixed
+- MCP Tools in Settings → System now uses a bounded scroll region with 5-item default pages, a per-page selector up to 40 tools, and a visible result summary, so large MCP tool inventories no longer make the settings panel balloon indefinitely.
+
- **PR #2201** by @MrFant — Multi-turn conversations with thinking-mode providers (MiMo/Xiaomi, DeepSeek, Kimi/Moonshot) no longer 400 with `Param Incorrect: reasoning_content must be passed back`. WebUI's `_sanitize_messages_for_api()` strips fields not in `_API_SAFE_MSG_KEYS` before sending conversation history to the LLM; `reasoning_content` was missing from the whitelist, so when history was replayed on the second turn, the assistant message with `tool_calls` arrived without `reasoning_content` and providers enforcing thinking-mode echo-back rejected it. One-line fix: adds `'reasoning_content'` to `_API_SAFE_MSG_KEYS`. CLI was unaffected because `run_agent.py` has its own `_copy_reasoning_content_for_api()` that doesn't go through this filter.
- **PR #2198** by @Michaelyklam — Fork-from-here keep-count was off-by-one (or larger) for truncated sessions where the visible-message index didn't match the absolute transcript index. JS now sends `_oldestIdx + msgIdx` (the absolute message index in the full transcript) as `keep_count` instead of the visible-window-relative index — captured *before* `_ensureAllMessagesLoaded()` resets `_oldestIdx`, so the index remains stable. Backend `source_messages[:keep_count]` then forks from the correct point even when the user has only loaded a tail window. When the full transcript is loaded (`_oldestIdx==0`), behavior is unchanged. 186-line regression suite in `tests/test_issue2184_fork_from_here_absolute_index.py` explicitly pins `keep_count: absoluteKeepCount` (and forbids the old `keep_count: msgIdx` form).
diff --git a/api/config.py b/api/config.py
index a6f7daaf..223df061 100644
--- a/api/config.py
+++ b/api/config.py
@@ -3909,6 +3909,7 @@ _SETTINGS_DEFAULTS = {
"show_cli_sessions": False, # merge CLI sessions from state.db into the sidebar
"sync_to_insights": False, # mirror WebUI token usage to state.db for /insights
"check_for_updates": True, # check if webui/agent repos are behind upstream
+ "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
@@ -4037,6 +4038,7 @@ _SETTINGS_BOOL_KEYS = {
"show_cli_sessions",
"sync_to_insights",
"check_for_updates",
+ "whats_new_summary_enabled",
"sound_enabled",
"notifications_enabled",
"show_thinking",
diff --git a/api/models.py b/api/models.py
index b15d5531..589cc38a 100644
--- a/api/models.py
+++ b/api/models.py
@@ -1,5 +1,6 @@
"""Hermes Web UI -- Session model and in-memory session store."""
import collections
+import copy
import datetime
import hashlib
import json
@@ -22,6 +23,9 @@ from api.agent_sessions import read_importable_agent_session_rows, read_session_
logger = logging.getLogger(__name__)
CLI_VISIBLE_SESSION_LIMIT = 20
+_CLI_SESSIONS_CACHE_TTL_SECONDS = 5.0
+_CLI_SESSIONS_CACHE_LOCK = threading.Lock()
+_CLI_SESSIONS_CACHE = {}
# ---------------------------------------------------------------------------
# Stale temp-file cleanup
@@ -383,6 +387,7 @@ class Session:
self.raw_source = kwargs.get('raw_source')
self.session_source = kwargs.get('session_source')
self.source_label = kwargs.get('source_label')
+ self.read_only = bool(kwargs.get('read_only', False))
self.enabled_toolsets = enabled_toolsets # List[str] or None — per-session toolset override
self.composer_draft = composer_draft if isinstance(composer_draft, dict) else {}
self._metadata_message_count = None
@@ -426,7 +431,7 @@ class Session:
'gateway_routing', 'gateway_routing_history', 'llm_title_generated',
'parent_session_id',
'worktree_path', 'worktree_branch', 'worktree_repo_root', 'worktree_created_at',
- 'is_cli_session', 'source_tag', 'raw_source', 'session_source', 'source_label',
+ 'is_cli_session', 'source_tag', 'raw_source', 'session_source', 'source_label', 'read_only',
'enabled_toolsets', 'composer_draft',
]
meta = {k: getattr(self, k, None) for k in METADATA_FIELDS}
@@ -610,6 +615,7 @@ class Session:
'raw_source': self.raw_source,
'session_source': self.session_source,
'source_label': self.source_label,
+ 'read_only': self.read_only,
'enabled_toolsets': self.enabled_toolsets,
'composer_draft': self.composer_draft if isinstance(self.composer_draft, dict) else {},
'is_streaming': _is_streaming_session(
@@ -1016,9 +1022,11 @@ def all_sessions(diag=None):
_diag_stage(diag, "all_sessions.read_index")
index = json.loads(SESSION_INDEX_FILE.read_text(encoding='utf-8'))
_diag_stage(diag, "all_sessions.prune_index")
+ with LOCK:
+ in_memory_ids = set(SESSIONS.keys())
index = [
s for s in index
- if _index_entry_exists(s.get('session_id'))
+ if _index_entry_exists(s.get('session_id'), in_memory_ids=in_memory_ids)
]
backfilled = []
for i, s in enumerate(index):
@@ -1536,20 +1544,51 @@ def get_claude_code_session_messages(sid, projects_dir: Path | str | None = None
return []
-def get_cli_sessions() -> list:
- """Read CLI sessions from the agent's SQLite store and return them as
- dicts in a format the WebUI sidebar can render alongside local sessions.
+def clear_cli_sessions_cache() -> None:
+ with _CLI_SESSIONS_CACHE_LOCK:
+ _CLI_SESSIONS_CACHE.clear()
- Returns empty list if the SQLite DB is missing or any error occurs -- the
- bridge is purely additive and never crashes the WebUI.
- """
- import os
- cli_sessions = []
+
+def _copy_cli_sessions(sessions: list) -> list:
+ return copy.deepcopy(sessions)
+
+
+def _cli_sessions_cache_ttl_seconds() -> float:
try:
- cli_sessions.extend(get_claude_code_sessions())
- except Exception:
- logger.debug("Claude Code session scan failed", exc_info=True)
+ return max(0.0, float(_CLI_SESSIONS_CACHE_TTL_SECONDS))
+ except (TypeError, ValueError):
+ return 5.0
+
+def _path_cache_key(path) -> str | None:
+ if path is None:
+ return None
+ try:
+ return str(Path(path).expanduser().resolve(strict=False))
+ except Exception:
+ return str(path)
+
+
+def _path_stat_cache_key(path):
+ if path is None:
+ return None
+ try:
+ st = Path(path).stat()
+ return (st.st_mtime_ns, st.st_size)
+ except OSError:
+ return None
+
+
+def _sqlite_file_stat_cache_key(db_path: Path):
+ """Return a cheap invalidation key for a SQLite DB and WAL sidecars."""
+ return (
+ _path_stat_cache_key(db_path),
+ _path_stat_cache_key(Path(f"{db_path}-wal")),
+ _path_stat_cache_key(Path(f"{db_path}-shm")),
+ )
+
+
+def _resolve_cli_sessions_context():
# Use the active WebUI profile's HERMES_HOME to find state.db.
# The active profile is determined by what the user has selected in the UI
# (stored in the server's runtime config). This means:
@@ -1564,17 +1603,35 @@ def get_cli_sessions() -> list:
except Exception:
hermes_home = Path(os.getenv('HERMES_HOME', str(HOME / '.hermes'))).expanduser().resolve()
- db_path = hermes_home / 'state.db'
- if not db_path.exists():
- return cli_sessions
-
- # Try to resolve the active CLI profile so imported sessions integrate
- # with the WebUI profile filter (available since Sprint 22).
try:
from api.profiles import get_active_profile_name
- _cli_profile = get_active_profile_name()
- except ImportError:
- _cli_profile = None # older agent -- fall back to no profile
+ cli_profile = get_active_profile_name()
+ except Exception:
+ cli_profile = None
+
+ db_path = hermes_home / 'state.db'
+ projects_dir = _default_claude_code_projects_dir()
+ cache_key = (
+ str(hermes_home),
+ str(cli_profile or ''),
+ str(db_path),
+ _sqlite_file_stat_cache_key(db_path),
+ _path_cache_key(projects_dir),
+ _path_stat_cache_key(projects_dir),
+ _path_stat_cache_key(SESSION_INDEX_FILE),
+ )
+ return hermes_home, db_path, cli_profile, cache_key
+
+
+def _load_cli_sessions_uncached(hermes_home: Path, db_path: Path, _cli_profile) -> list:
+ cli_sessions = []
+ try:
+ cli_sessions.extend(get_claude_code_sessions())
+ except Exception:
+ logger.debug("Claude Code session scan failed", exc_info=True)
+
+ if not db_path.exists():
+ return cli_sessions
# Memoize the cron project ID for this scan so we don't pay a lock-acquire +
# disk-read of projects.json per cron session in the loop below.
@@ -1585,96 +1642,129 @@ def get_cli_sessions() -> list:
_cron_pid_cache[0] = ensure_cron_project()
return _cron_pid_cache[0]
- try:
- for row in read_importable_agent_session_rows(
- db_path,
- limit=CLI_VISIBLE_SESSION_LIMIT,
- log=logger,
- exclude_sources=None,
- ):
- sid = row['id']
- raw_ts = row['last_activity'] or row['started_at']
- # Prefer the CLI session's own profile from the DB; fall back to
- # the active CLI profile so sidebar filtering works either way.
- profile = _cli_profile # CLI DB has no profile column; use active profile
+ for row in read_importable_agent_session_rows(
+ db_path,
+ limit=CLI_VISIBLE_SESSION_LIMIT,
+ log=logger,
+ exclude_sources=None,
+ ):
+ sid = row['id']
+ raw_ts = row['last_activity'] or row['started_at']
+ # Prefer the CLI session's own profile from the DB; fall back to
+ # the active CLI profile so sidebar filtering works either way.
+ profile = _cli_profile # CLI DB has no profile column; use active profile
- _source = row['source'] or 'cli'
- _title = row['title']
- if not _title and _source == 'cron' and sid.startswith('cron_'):
- # Extract job_id from session ID (cron_{job_id}_{timestamp})
- # and look up the human-friendly job name from jobs.json
- parts = sid.split('_')
- if len(parts) >= 3:
- _job_id = parts[1]
- try:
- _jobs_path = hermes_home / 'cron' / 'jobs.json'
- if _jobs_path.exists():
- import json as _json
- _jobs_data = _json.loads(_jobs_path.read_text())
- for _j in _jobs_data.get('jobs', []):
- if _j.get('id') == _job_id:
- _title = _j.get('name') or _title
- break
- except Exception:
- pass # degrade gracefully
- # If a WebUI JSON file exists for this session (e.g. previously
- # imported or renamed in the sidebar), prefer its title over the
- # state.db title. This fixes rename-not-persisting for CLI sessions
- # after compression chain extension (#1486).
+ _source = row['source'] or 'cli'
+ _title = row['title']
+ if not _title and _source == 'cron' and sid.startswith('cron_'):
+ # Extract job_id from session ID (cron_{job_id}_{timestamp})
+ # and look up the human-friendly job name from jobs.json
+ parts = sid.split('_')
+ if len(parts) >= 3:
+ _job_id = parts[1]
+ try:
+ _jobs_path = hermes_home / 'cron' / 'jobs.json'
+ if _jobs_path.exists():
+ import json as _json
+ _jobs_data = _json.loads(_jobs_path.read_text())
+ for _j in _jobs_data.get('jobs', []):
+ if _j.get('id') == _job_id:
+ _title = _j.get('name') or _title
+ break
+ except Exception:
+ pass # degrade gracefully
+ # If a WebUI JSON file exists for this session (e.g. previously
+ # imported or renamed in the sidebar), prefer its title over the
+ # state.db title. This fixes rename-not-persisting for CLI sessions
+ # after compression chain extension (#1486).
+ try:
+ _webui_meta = Session.load_metadata_only(sid)
+ if _webui_meta and getattr(_webui_meta, 'title', None):
+ _title = _webui_meta.title
+ except Exception:
+ pass
+ _display_title = _title or f'{_source.title()} Session'
+ cli_sessions.append({
+ 'session_id': sid,
+ 'title': _display_title,
+ 'workspace': str(get_last_workspace()),
+ 'model': row['model'] or None,
+ 'message_count': row['message_count'] or row['actual_message_count'] or 0,
+ 'created_at': row['started_at'],
+ 'updated_at': raw_ts,
+ 'pinned': False,
+ 'archived': False,
+ 'project_id': _cron_pid() if is_cron_session(sid, _source) else None,
+ 'profile': profile,
+ 'source_tag': _source,
+ 'raw_source': row.get('raw_source'),
+ 'user_id': row.get('user_id'),
+ 'chat_id': row.get('chat_id') or row.get('origin_chat_id'),
+ 'chat_type': row.get('chat_type'),
+ 'thread_id': row.get('thread_id'),
+ 'session_key': row.get('session_key'),
+ 'platform': row.get('platform'),
+ 'session_source': row.get('session_source'),
+ 'source_label': row.get('source_label'),
+ 'parent_session_id': row.get('parent_session_id'),
+ 'parent_title': row.get('parent_title'),
+ 'parent_source': row.get('parent_source'),
+ 'relationship_type': row.get('relationship_type'),
+ '_parent_lineage_root_id': row.get('_parent_lineage_root_id'),
+ 'end_reason': row.get('end_reason'),
+ 'actual_message_count': row.get('actual_message_count'),
+ 'user_message_count': row.get('actual_user_message_count'),
+ '_lineage_root_id': row.get('_lineage_root_id'),
+ '_lineage_tip_id': row.get('_lineage_tip_id'),
+ '_compression_segment_count': row.get('_compression_segment_count'),
+ 'is_cli_session': True,
+ })
+
+ return cli_sessions
+
+
+def get_cli_sessions() -> list:
+ """Read CLI sessions from the agent's SQLite store and return them as
+ dicts in a format the WebUI sidebar can render alongside local sessions.
+
+ Returns empty list if the SQLite DB is missing or any error occurs -- the
+ bridge is purely additive and never crashes the WebUI.
+ """
+ hermes_home, db_path, cli_profile, cache_key = _resolve_cli_sessions_context()
+ ttl = _cli_sessions_cache_ttl_seconds()
+ now = time.monotonic()
+
+ if ttl > 0:
+ with _CLI_SESSIONS_CACHE_LOCK:
+ cached = _CLI_SESSIONS_CACHE.get(cache_key)
+ if cached:
+ expires_at, cached_sessions = cached
+ if expires_at > now:
+ return _copy_cli_sessions(cached_sessions)
+ _CLI_SESSIONS_CACHE.pop(cache_key, None)
try:
- _webui_meta = Session.load_metadata_only(sid)
- if _webui_meta and getattr(_webui_meta, 'title', None):
- _title = _webui_meta.title
- except Exception:
- pass
- _display_title = _title or f'{_source.title()} Session'
- cli_sessions.append({
- 'session_id': sid,
- 'title': _display_title,
- 'workspace': str(get_last_workspace()),
- 'model': row['model'] or None,
- 'message_count': row['message_count'] or row['actual_message_count'] or 0,
- 'created_at': row['started_at'],
- 'updated_at': raw_ts,
- 'pinned': False,
- 'archived': False,
- 'project_id': _cron_pid() if is_cron_session(sid, _source) else None,
- 'profile': profile,
- 'source_tag': _source,
- 'raw_source': row.get('raw_source'),
- 'user_id': row.get('user_id'),
- 'chat_id': row.get('chat_id') or row.get('origin_chat_id'),
- 'chat_type': row.get('chat_type'),
- 'thread_id': row.get('thread_id'),
- 'session_key': row.get('session_key'),
- 'platform': row.get('platform'),
- 'session_source': row.get('session_source'),
- 'source_label': row.get('source_label'),
- 'parent_session_id': row.get('parent_session_id'),
- 'parent_title': row.get('parent_title'),
- 'parent_source': row.get('parent_source'),
- 'relationship_type': row.get('relationship_type'),
- '_parent_lineage_root_id': row.get('_parent_lineage_root_id'),
- 'end_reason': row.get('end_reason'),
- 'actual_message_count': row.get('actual_message_count'),
- 'user_message_count': row.get('actual_user_message_count'),
- '_lineage_root_id': row.get('_lineage_root_id'),
- '_lineage_tip_id': row.get('_lineage_tip_id'),
- '_compression_segment_count': row.get('_compression_segment_count'),
- 'is_cli_session': True,
- })
+ sessions = _load_cli_sessions_uncached(hermes_home, db_path, cli_profile)
+ except Exception as _cli_err:
+ logger.warning(
+ "get_cli_sessions() failed — check state.db schema or path (%s): %s",
+ db_path, _cli_err,
+ )
+ return []
+ _CLI_SESSIONS_CACHE[cache_key] = (
+ time.monotonic() + ttl,
+ _copy_cli_sessions(sessions),
+ )
+ return _copy_cli_sessions(sessions)
+
+ try:
+ return _load_cli_sessions_uncached(hermes_home, db_path, cli_profile)
except Exception as _cli_err:
- # DB schema changed, locked, or corrupted -- log warning so admins can diagnose.
- # Still degrade gracefully (don't crash the WebUI).
- import logging as _logging
- _logging.getLogger(__name__).warning(
+ logger.warning(
"get_cli_sessions() failed — check state.db schema or path (%s): %s",
db_path, _cli_err,
)
return []
- return cli_sessions
-
def _json_loads_if_string(value):
if not isinstance(value, str):
diff --git a/api/routes.py b/api/routes.py
index 765857ca..3d3c5a15 100644
--- a/api/routes.py
+++ b/api/routes.py
@@ -1525,18 +1525,6 @@ def _lookup_cli_session_metadata(session_id: str) -> dict:
return {}
-def _needs_cli_session_metadata(session) -> bool:
- """Return true when /api/session should pay for Agent/CLI metadata lookup."""
- if not session:
- return False
- is_cli = (
- bool(session.get("is_cli_session"))
- if isinstance(session, dict)
- else bool(getattr(session, "is_cli_session", False))
- )
- return is_cli or _is_messaging_session_record(session)
-
-
def _messaging_session_identity(session: dict, raw_source: str) -> str:
metadata = _lookup_gateway_session_identity(session.get("session_id"))
session_key = _safe_first(
@@ -1684,6 +1672,43 @@ def _messages_include_tool_metadata(messages) -> bool:
return False
+def _session_requires_cli_metadata_lookup(session) -> bool:
+ """Return True when a sidecar/session row still needs CLI metadata.
+
+ Legacy imported sidecars may predate the ``read_only`` field and therefore
+ load with ``read_only=False``. They still persist ``is_cli_session`` and/or
+ source metadata from import time, so those markers intentionally keep them
+ on the CLI lookup path while ordinary WebUI-native sessions take the fast
+ path.
+
+ Supersedes the simpler is-cli-or-messaging gate from PR #1822 — the new
+ gate is strictly more inclusive (also covers ``read_only=True`` sidecars,
+ ``session_source`` markers, and source_tag/raw_source/platform metadata)
+ so all sessions that previously took the slow path still do, plus a few
+ more legacy shapes.
+ """
+ if not session:
+ return False
+
+ def _field(name):
+ return session.get(name) if isinstance(session, dict) else getattr(session, name, None)
+
+ if _is_messaging_session_record(session):
+ return True
+ if bool(_field("is_cli_session")) or bool(_field("read_only")):
+ return True
+ session_source = _normalize_messaging_source(_safe_first(_field("session_source")))
+ if session_source in {"messaging", "external_agent", "external-agent"}:
+ return True
+ return bool(_safe_first(
+ _field("source_tag"),
+ _field("raw_source"),
+ _field("source"),
+ _field("source_label"),
+ _field("platform"),
+ ))
+
+
def _is_messaging_session_id(sid: str) -> bool:
"""Detect messaging-backed sessions from WebUI metadata or Agent rows."""
try:
@@ -3252,7 +3277,7 @@ def handle_get(handler, parsed) -> bool:
_t1 = _time.monotonic()
s = get_session(sid, metadata_only=(not load_messages))
_clear_stale_stream_state(s)
- cli_meta = _lookup_cli_session_metadata(sid) if _needs_cli_session_metadata(s) else {}
+ cli_meta = _lookup_cli_session_metadata(sid) if _session_requires_cli_metadata_lookup(s) else {}
is_messaging_session = _is_messaging_session_record(s) or _is_messaging_session_record(cli_meta)
cli_messages = []
if is_messaging_session:
@@ -3718,6 +3743,8 @@ def handle_get(handler, parsed) -> bool:
"current_sha": "abc1234",
"latest_sha": "def5678",
"branch": "master",
+ "repo_url": "https://github.com/nesquena/hermes-webui",
+ "compare_url": "https://github.com/nesquena/hermes-webui/compare/abc1234...def5678",
},
"agent": {
"name": "agent",
@@ -3725,6 +3752,8 @@ def handle_get(handler, parsed) -> bool:
"current_sha": "aaa0001",
"latest_sha": "bbb0002",
"branch": "master",
+ "repo_url": "https://github.com/NousResearch/hermes-agent",
+ "compare_url": "https://github.com/NousResearch/hermes-agent/compare/aaa0001...bbb0002",
},
"checked_at": 0,
},
@@ -5272,6 +5301,63 @@ def handle_post(handler, parsed) -> bool:
return j(handler, apply_force_update(target))
+ if parsed.path == "/api/updates/summary":
+ from api.updates import summarize_update_payload
+
+ updates = body.get("updates") if isinstance(body, dict) else {}
+ target = body.get("target") if isinstance(body, dict) else None
+
+ def _llm_update_summary(system_prompt: str, user_prompt: str) -> str:
+ from run_agent import AIAgent
+ from api.config import (
+ get_effective_default_model,
+ resolve_model_provider,
+ resolve_custom_provider_connection,
+ )
+
+ _model, _provider, _base_url = resolve_model_provider(get_effective_default_model())
+ _api_key = None
+ try:
+ from api.oauth import resolve_runtime_provider_with_anthropic_env_lock
+ from hermes_cli.runtime_provider import resolve_runtime_provider
+
+ _rt = resolve_runtime_provider_with_anthropic_env_lock(
+ resolve_runtime_provider,
+ requested=_provider,
+ )
+ _api_key = _rt.get("api_key")
+ if not _provider:
+ _provider = _rt.get("provider")
+ if not _base_url:
+ _base_url = _rt.get("base_url")
+ except Exception as _e:
+ logger.debug("update summary runtime provider resolution failed: %s", _e)
+ if isinstance(_provider, str) and _provider.startswith("custom:"):
+ _cp_key, _cp_base = resolve_custom_provider_connection(_provider)
+ if not _api_key and _cp_key:
+ _api_key = _cp_key
+ if not _base_url and _cp_base:
+ _base_url = _cp_base
+ agent = AIAgent(
+ model=_model,
+ provider=_provider,
+ base_url=_base_url,
+ api_key=_api_key,
+ platform="webui",
+ quiet_mode=True,
+ enabled_toolsets=[],
+ session_id=f"updates-summary-{uuid.uuid4().hex[:8]}",
+ )
+ result = agent.run_conversation(
+ user_message=user_prompt,
+ system_message=system_prompt,
+ conversation_history=[],
+ task_id=f"updates-summary-{uuid.uuid4().hex[:8]}",
+ )
+ return str(result.get("final_response") or "").strip()
+
+ return j(handler, summarize_update_payload(updates, llm_callback=_llm_update_summary, target=target))
+
# ── CLI session import (POST) ──
if parsed.path == "/api/session/import_cli":
return _handle_session_import_cli(handler, body)
diff --git a/api/streaming.py b/api/streaming.py
index 91dbc738..bf473d90 100644
--- a/api/streaming.py
+++ b/api/streaming.py
@@ -689,9 +689,12 @@ def _strip_thinking_markup(text: str) -> str:
if not text:
return ''
s = str(text)
- s = re.sub(r'.*?', ' ', s, flags=re.IGNORECASE | re.DOTALL)
- s = re.sub(r'<\|channel\|>thought.*?', ' ', s, flags=re.IGNORECASE | re.DOTALL)
- s = re.sub(r'<\|turn\|>thinking\n.*?', ' ', s, flags=re.IGNORECASE | re.DOTALL) # Gemma 4
+ # Treat provider thinking wrappers as metadata only when they lead the
+ # response. Literal discussion of these tags later in normal prose should
+ # stay visible (#2152).
+ s = re.sub(r'^\s*.*?\s*', ' ', s, flags=re.IGNORECASE | re.DOTALL)
+ s = re.sub(r'^\s*<\|channel\|?>thought\n?.*?\s*', ' ', s, flags=re.IGNORECASE | re.DOTALL)
+ s = re.sub(r'^\s*<\|turn\|>thinking\n.*?\s*', ' ', s, flags=re.IGNORECASE | re.DOTALL) # Gemma 4
s = re.sub(r'^\s*(the|ther)\s+user\s+is\s+asking.*$', ' ', s, flags=re.IGNORECASE | re.MULTILINE)
# Strip plain-text thinking preambles from models that don't use tags (e.g. Qwen3).
# These appear as the very first sentence of the assistant response and are not useful as titles.
diff --git a/api/updates.py b/api/updates.py
index e3e025c2..0bbca829 100644
--- a/api/updates.py
+++ b/api/updates.py
@@ -8,10 +8,14 @@ at most twice per hour regardless of client count.
Skips repos that are not git checkouts (e.g. Docker baked images where
.git does not exist).
"""
+import hashlib
+import json
+import re
import subprocess
import threading
import time
from pathlib import Path
+from urllib.parse import urlparse
from api.config import REPO_ROOT, STREAMS, STREAMS_LOCK
@@ -22,6 +26,7 @@ except ImportError:
_AGENT_DIR = None
_update_cache = {'webui': None, 'agent': None, 'checked_at': 0}
+_summary_cache = {}
_cache_lock = threading.Lock()
_check_in_progress = False
_apply_lock = threading.Lock() # prevents concurrent stash/pull/pop on same repo
@@ -169,6 +174,16 @@ def _normalize_remote_url(remote_url):
return remote_url.rstrip('/')
+def _build_compare_url(repo_url, current_sha, latest_sha):
+ """Return a safe browser compare URL, or None when any piece is missing."""
+ if not (repo_url and current_sha and latest_sha):
+ return None
+ parsed = urlparse(repo_url)
+ if parsed.scheme not in ('http', 'https') or not parsed.netloc:
+ return None
+ return f"{repo_url}/compare/{current_sha}...{latest_sha}"
+
+
def _split_remote_ref(ref):
"""Split 'origin/branch-name' into ('origin', 'branch-name').
@@ -262,6 +277,7 @@ def _check_repo(path, name):
'latest_sha': latest,
'branch': compare_ref,
'repo_url': remote_url,
+ 'compare_url': _build_compare_url(remote_url, current, latest),
}
@@ -289,6 +305,234 @@ def check_for_updates(force=False):
_check_in_progress = False
+def _repo_path_for_update_target(target: str):
+ if target == 'webui':
+ return REPO_ROOT
+ if target == 'agent':
+ return _AGENT_DIR
+ return None
+
+
+def _commit_subjects_for_update(info: dict, *, limit: int = 24) -> list[str]:
+ """Return commit subjects for an update range, if the local git refs exist."""
+ if not isinstance(info, dict):
+ return []
+ target = info.get('name')
+ if target not in ('webui', 'agent'):
+ target = 'webui' if info.get('repo_url', '').endswith('hermes-webui') else target
+ path = _repo_path_for_update_target(target)
+ if path is None or not (Path(path) / '.git').exists():
+ return []
+ current = str(info.get('current_sha') or '').strip()
+ latest = str(info.get('latest_sha') or '').strip()
+ if not (current and latest):
+ return []
+ out, ok = _run_git(['log', '--format=%s', f'{current}..{latest}', f'-n{limit}'], path, timeout=5)
+ if not ok or not out:
+ return []
+ return [line.strip() for line in out.splitlines() if line.strip()]
+
+
+def _summary_cache_key(updates: dict, details: list[dict]) -> str:
+ """Stable key for the exact update range being summarized."""
+ payload = []
+ for item in details:
+ payload.append({
+ 'name': item.get('name'),
+ 'behind': item.get('behind'),
+ 'current_sha': item.get('current_sha'),
+ 'latest_sha': item.get('latest_sha'),
+ 'compare_url': item.get('compare_url'),
+ })
+ blob = json.dumps(payload, sort_keys=True, separators=(',', ':'))
+ return hashlib.sha256(blob.encode('utf-8')).hexdigest()
+
+
+def _clean_summary_bullet(line: str) -> str:
+ line = re.sub(r'^\s*(?:[-*•]+|\d+[.)])\s*', '', str(line or '')).strip()
+ line = re.sub(r'\s+', ' ', line)
+ if not line:
+ return ''
+ if line[-1] not in '.!?':
+ line += '.'
+ return line[:240]
+
+
+def _summary_bullets_from_text(text: str, *, fallback_items: list[str]) -> list[str]:
+ raw = str(text or '').strip()
+ candidates = []
+ for line in raw.splitlines():
+ cleaned = _clean_summary_bullet(line)
+ if cleaned:
+ candidates.append(cleaned)
+ if len(candidates) <= 1 and raw:
+ candidates = [_clean_summary_bullet(part) for part in re.split(r'(?<=[.!?])\s+', raw)]
+ candidates = [item for item in candidates if item]
+ if not candidates:
+ candidates = [_clean_summary_bullet(item) for item in fallback_items]
+ seen = set()
+ bullets = []
+ for item in candidates:
+ key = item.lower()
+ if item and key not in seen:
+ bullets.append(item)
+ seen.add(key)
+ if len(bullets) >= 5:
+ break
+ return bullets or ['Updates are available.']
+
+
+def _fallback_update_bullets(details: list[dict]) -> list[str]:
+ bullets = []
+ for item in details:
+ label = item.get('label') or item.get('name') or 'Hermes'
+ behind = item.get('behind') or 0
+ commits = item.get('commits') or []
+ if commits:
+ highlights = '; '.join(commits[:3])
+ bullets.append(f"{label} has {behind} update(s), including: {highlights}.")
+ else:
+ bullets.append(f"{label} has {behind} update(s) available.")
+ return bullets or ['Updates are available.']
+
+
+def _worth_knowing_bullets(details: list[dict]) -> list[str]:
+ targets = [
+ f"{item.get('label') or item.get('name') or 'Hermes'} ({item.get('behind') or 0} update{'s' if (item.get('behind') or 0) != 1 else ''})"
+ for item in details
+ if item.get('behind')
+ ]
+ if len(targets) > 1:
+ return ['This summary combines updates from ' + ' and '.join(targets) + '.']
+ if targets:
+ return ['This summary covers ' + targets[0] + '.']
+ return ['No update details were available to summarize.']
+
+
+def _format_update_summary_sections(summary_text: str, details: list[dict]) -> tuple[list[dict], str]:
+ bullets = _summary_bullets_from_text(summary_text, fallback_items=_fallback_update_bullets(details))
+ if len(bullets) > 1:
+ notice_items = bullets[:3]
+ worth_items = bullets[3:] or bullets[1:]
+ else:
+ notice_items = bullets
+ worth_items = []
+ worth_items = worth_items[:2] or _worth_knowing_bullets(details)
+ sections = [
+ {
+ 'title': "What you'll notice",
+ 'items': notice_items,
+ },
+ {
+ 'title': 'Worth knowing',
+ 'items': worth_items,
+ },
+ ]
+ lines = []
+ for section in sections:
+ lines.append(section['title'])
+ lines.extend(f"- {item}" for item in section['items'])
+ lines.append('')
+ return sections, '\n'.join(lines).strip()
+
+
+def _fallback_update_summary(updates: dict, details: list[dict]) -> str:
+ _sections, summary = _format_update_summary_sections('', details)
+ return summary
+
+
+def _update_summary_prompt(details: list[dict]) -> tuple[str, str]:
+ system = (
+ "You write human-readable release summaries for Hermes users. "
+ "Focus on what the user will notice in the product. Keep it simple, specific, and short. "
+ "avoid technical jargon, implementation details, SHA names, branch names, and file paths unless necessary. "
+ "Return only bullets. Do not include headings, markdown tables, intro paragraphs, or closing notes."
+ )
+ user_lines = [
+ "Summarize these available updates as 3-5 concise bullets.",
+ "Use everyday language and explain visible behavior changes, not code mechanics.",
+ "Return only bullets; the WebUI will add the fixed section headings separately.",
+ "",
+ ]
+ for item in details:
+ user_lines.append(f"{item['label']}: {item['behind']} commit(s) behind")
+ commits = item.get('commits') or []
+ if commits:
+ user_lines.extend(f"- {subject}" for subject in commits)
+ else:
+ user_lines.append("- No local commit subjects available; summarize only the update count.")
+ user_lines.append("")
+ return system, '\n'.join(user_lines)
+
+
+def summarize_update_payload(updates: dict, llm_callback=None, *, target: str | None = None, use_cache: bool = True) -> dict:
+ """Build a human-readable What's New summary and keep regular diff comparison links.
+
+ ``llm_callback`` receives ``(system_prompt, user_prompt)`` and returns text.
+ The caller may wire that to AIAgent; this module keeps a deterministic
+ fallback so the banner remains useful when no LLM provider is configured.
+ Summaries are cached per exact update range so refreshes do not generate
+ slightly different wording for the same available updates.
+ """
+ if not isinstance(updates, dict):
+ updates = {}
+ requested_target = target if target in ('webui', 'agent') else None
+ details = []
+ for key, label in (('webui', 'WebUI'), ('agent', 'Agent')):
+ if requested_target and key != requested_target:
+ continue
+ info = updates.get(key)
+ if not isinstance(info, dict) or int(info.get('behind') or 0) <= 0:
+ continue
+ item = {
+ 'name': key,
+ 'label': label,
+ 'behind': int(info.get('behind') or 0),
+ 'current_sha': info.get('current_sha'),
+ 'latest_sha': info.get('latest_sha'),
+ 'compare_url': info.get('compare_url'),
+ 'commits': _commit_subjects_for_update({'name': key, **info}),
+ }
+ details.append(item)
+ cache_key = _summary_cache_key(updates, details)
+ if use_cache:
+ with _cache_lock:
+ cached = _summary_cache.get(cache_key)
+ if cached:
+ result = dict(cached)
+ result['cached'] = True
+ return result
+
+ generated_by = 'fallback'
+ candidate = ''
+ if details and callable(llm_callback):
+ system, prompt = _update_summary_prompt(details)
+ try:
+ candidate = (llm_callback(system, prompt) or '').strip()
+ if candidate:
+ generated_by = 'llm'
+ except Exception:
+ candidate = ''
+ sections, summary = _format_update_summary_sections(candidate, details)
+ result = {
+ 'ok': True,
+ 'summary': summary,
+ 'summary_sections': sections,
+ 'generated_by': generated_by,
+ 'cached': False,
+ 'cache_key': cache_key,
+ 'target': requested_target,
+ 'targets': details,
+ }
+ if use_cache:
+ with _cache_lock:
+ _summary_cache[cache_key] = dict(result)
+ return result
+
+
+# ── Self-update application ───────────────────────────────────────────────────
+
+
def _schedule_restart(delay: float = 2.0) -> None:
"""Re-exec this process after *delay* seconds.
diff --git a/docs/images/update-banner-whats-new-after-summary-off.png b/docs/images/update-banner-whats-new-after-summary-off.png
new file mode 100644
index 00000000..fbc1a861
Binary files /dev/null and b/docs/images/update-banner-whats-new-after-summary-off.png differ
diff --git a/docs/images/update-banner-whats-new-after-summary-on.png b/docs/images/update-banner-whats-new-after-summary-on.png
new file mode 100644
index 00000000..7ff4a81c
Binary files /dev/null and b/docs/images/update-banner-whats-new-after-summary-on.png differ
diff --git a/docs/images/update-banner-whats-new-after.png b/docs/images/update-banner-whats-new-after.png
new file mode 100644
index 00000000..3fbed52d
Binary files /dev/null and b/docs/images/update-banner-whats-new-after.png differ
diff --git a/docs/images/update-banner-whats-new-before.png b/docs/images/update-banner-whats-new-before.png
new file mode 100644
index 00000000..e5af9b39
Binary files /dev/null and b/docs/images/update-banner-whats-new-before.png differ
diff --git a/docs/pr-media/2210/after-mcp-tools-paginated.png b/docs/pr-media/2210/after-mcp-tools-paginated.png
new file mode 100644
index 00000000..e2793510
Binary files /dev/null and b/docs/pr-media/2210/after-mcp-tools-paginated.png differ
diff --git a/docs/pr-media/2210/before-mcp-tools-unbounded.png b/docs/pr-media/2210/before-mcp-tools-unbounded.png
new file mode 100644
index 00000000..9123a563
Binary files /dev/null and b/docs/pr-media/2210/before-mcp-tools-unbounded.png differ
diff --git a/static/boot.js b/static/boot.js
index e08ad6e9..27dfa638 100644
--- a/static/boot.js
+++ b/static/boot.js
@@ -1379,6 +1379,10 @@ function applyBotName(){
window._showCliSessions=!!s.show_cli_sessions;
window._soundEnabled=!!s.sound_enabled;
window._notificationsEnabled=!!s.notifications_enabled;
+ // Persist default workspace so the blank new-chat page can show it
+ // and workspace actions (New file/folder) work before the first session (#804).
+ if(s.default_workspace) S._profileDefaultWorkspace=s.default_workspace;
+ window._whatsNewSummaryEnabled=!!s.whats_new_summary_enabled;
window._showThinking=s.show_thinking!==false;
window._simplifiedToolCalling=s.simplified_tool_calling!==false;
window._sidebarDensity=(s.sidebar_density==='detailed'?'detailed':'compact');
@@ -1386,9 +1390,6 @@ function applyBotName(){
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
- // and workspace actions (New file/folder) work before the first session (#804).
- if(s.default_workspace) S._profileDefaultWorkspace=s.default_workspace;
window._sessionJumpButtonsEnabled=!!s.session_jump_buttons;
const appearance=_normalizeAppearance(s.theme,s.skin);
localStorage.setItem('hermes-theme',appearance.theme);
@@ -1415,6 +1416,7 @@ function applyBotName(){
window._showCliSessions=false;
window._soundEnabled=false;
window._notificationsEnabled=false;
+ window._whatsNewSummaryEnabled=false;
window._showThinking=true;
window._simplifiedToolCalling=true;
window._sessionJumpButtonsEnabled=false;
diff --git a/static/i18n.js b/static/i18n.js
index b0a28ade..fb0b0944 100644
--- a/static/i18n.js
+++ b/static/i18n.js
@@ -92,6 +92,20 @@ const LOCALES = {
mcp_tools_load_failed: 'Failed to load MCP tools.',
mcp_tools_schema_empty: 'No schema parameters.',
mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.',
+ mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
+ mcp_tools_summary_none: 'No MCP tools to show.',
+ mcp_tools_summary_matching: (query) => ` matching “${query}”`,
+ mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
+ mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
+ mcp_tools_page_size_prefix: 'Show',
+ mcp_tools_page_size_suffix: 'per page',
+ mcp_tools_per_page_aria: 'MCP tools per page',
+ mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
+ mcp_tools_pagination_label: 'MCP tools pagination',
+ mcp_tools_previous_page: '‹ Prev',
+ mcp_tools_previous_page_aria: 'Previous MCP tools page',
+ mcp_tools_next_page: 'Next ›',
+ mcp_tools_next_page_aria: 'Next MCP tools page',
// PDF preview (#480)
pdf_loading: 'Loading PDF {0}…',
pdf_too_large: 'PDF too large for inline preview',
@@ -513,6 +527,7 @@ const LOCALES = {
settings_label_external_sessions: 'Show non-WebUI sessions',
settings_label_sync_insights: 'Sync to insights',
settings_label_check_updates: 'Check for updates',
+ settings_label_whats_new_summary: "Summarize What's New with AI",
settings_label_bot_name: 'Assistant Name',
settings_label_password: 'Access Password',
settings_saved: 'Settings saved',
@@ -744,6 +759,7 @@ const LOCALES = {
settings_desc_external_sessions: 'Show conversations from CLI, Telegram, Discord, Slack, and other channels in the session list. Click to import and continue.',
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.',
+ settings_desc_whats_new_summary: "Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.",
settings_desc_bot_name: 'Display name for the assistant throughout the UI. Defaults to Hermes.',
settings_desc_password: 'Enter a new password to set or change it. Leave blank to keep current setting.',
password_placeholder: 'Enter new password…',
@@ -1217,6 +1233,20 @@ const LOCALES = {
mcp_tools_load_failed: 'Caricamento strumenti MCP fallito.',
mcp_tools_schema_empty: 'Nessun parametro schema.',
mcp_tools_runtime_note: "L'inventario strumenti usa solo dati runtime MCP già noti; la WebUI non avvia né interroga i server.",
+ mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
+ mcp_tools_summary_none: 'No MCP tools to show.',
+ mcp_tools_summary_matching: (query) => ` matching “${query}”`,
+ mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
+ mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
+ mcp_tools_page_size_prefix: 'Show',
+ mcp_tools_page_size_suffix: 'per page',
+ mcp_tools_per_page_aria: 'MCP tools per page',
+ mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
+ mcp_tools_pagination_label: 'MCP tools pagination',
+ mcp_tools_previous_page: '‹ Prev',
+ mcp_tools_previous_page_aria: 'Previous MCP tools page',
+ mcp_tools_next_page: 'Next ›',
+ mcp_tools_next_page_aria: 'Next MCP tools page',
// PDF preview (#480)
pdf_loading: 'Caricamento PDF {0}…',
pdf_too_large: "PDF troppo grande per l'anteprima inline",
@@ -1638,6 +1668,7 @@ const LOCALES = {
settings_label_external_sessions: 'Mostra sessioni non-WebUI',
settings_label_sync_insights: 'Sincronizza con insights',
settings_label_check_updates: 'Verifica aggiornamenti',
+ settings_label_whats_new_summary: "Summarize What's New with AI",
settings_label_bot_name: 'Nome Assistente',
settings_label_password: 'Password di Accesso',
settings_saved: 'Impostazioni salvate',
@@ -1861,6 +1892,7 @@ const LOCALES = {
settings_desc_external_sessions: 'Mostra conversazioni da CLI, Telegram, Discord, Slack e altri canali nella lista sessioni. Clicca per importare e continuare.',
settings_desc_sync_insights: 'Rispecchia l\'uso token WebUI su state.db così hermes /insights include i dati delle sessioni browser. Disattivato per impostazione predefinita.',
settings_desc_check_updates: 'Mostra un banner quando sono disponibili versioni più recenti della WebUI o dell\'Agente. Esegue un git fetch in background periodicamente.',
+ settings_desc_whats_new_summary: "Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.",
settings_desc_bot_name: 'Nome visualizzato per l\'assistente in tutta l\'interfaccia. Predefinito: Hermes.',
settings_desc_password: 'Inserisci una nuova password per impostarla o cambiarla. Lascia vuoto per mantenere l\'impostazione attuale.',
password_placeholder: 'Inserisci nuova password…',
@@ -2334,6 +2366,20 @@ const LOCALES = {
mcp_tools_load_failed: 'MCP ツールの読み込みに失敗しました。',
mcp_tools_schema_empty: 'スキーマパラメータはありません。',
mcp_tools_runtime_note: 'ツール一覧は既知の MCP ランタイム情報のみを使用します。WebUI はサーバーの起動や探索を行いません。',
+ mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
+ mcp_tools_summary_none: 'No MCP tools to show.',
+ mcp_tools_summary_matching: (query) => ` matching “${query}”`,
+ mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
+ mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
+ mcp_tools_page_size_prefix: 'Show',
+ mcp_tools_page_size_suffix: 'per page',
+ mcp_tools_per_page_aria: 'MCP tools per page',
+ mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
+ mcp_tools_pagination_label: 'MCP tools pagination',
+ mcp_tools_previous_page: '‹ Prev',
+ mcp_tools_previous_page_aria: 'Previous MCP tools page',
+ mcp_tools_next_page: 'Next ›',
+ mcp_tools_next_page_aria: 'Next MCP tools page',
// PDF preview (#480)
pdf_loading: 'PDF {0} を読み込み中…',
pdf_too_large: 'PDF が大きすぎてインラインプレビューできません',
@@ -2755,6 +2801,7 @@ const LOCALES = {
settings_label_external_sessions: '非WebUIセッションを表示',
settings_label_sync_insights: 'インサイトに同期',
settings_label_check_updates: 'アップデートを確認',
+ settings_label_whats_new_summary: "Summarize What's New with AI",
settings_label_bot_name: 'アシスタント名',
settings_label_password: 'アクセスパスワード',
settings_saved: '設定を保存しました',
@@ -2983,6 +3030,7 @@ const LOCALES = {
settings_desc_external_sessions: 'CLI、Telegram、Discord、Slack その他のチャネルからの会話をセッション一覧に表示します。クリックでインポートして続行できます。',
settings_desc_sync_insights: 'WebUI のトークン使用量を state.db にミラーし、hermes /insights にブラウザセッションのデータを含めます。デフォルトはオフ。',
settings_desc_check_updates: 'WebUI または Agent の新しいバージョンが利用可能な時にバナーを表示します。バックグラウンドで定期的に git fetch を実行します。',
+ settings_desc_whats_new_summary: "Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.",
settings_desc_bot_name: 'UI 全体で表示されるアシスタントの名前。デフォルトは Hermes。',
settings_desc_password: '新しいパスワードを入力すると設定または変更します。空欄なら現在の設定を維持。',
password_placeholder: '新しいパスワードを入力…',
@@ -3453,6 +3501,20 @@ const LOCALES = {
mcp_tools_load_failed: 'Failed to load MCP tools.',
mcp_tools_schema_empty: 'No schema parameters.',
mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.',
+ mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
+ mcp_tools_summary_none: 'No MCP tools to show.',
+ mcp_tools_summary_matching: (query) => ` matching “${query}”`,
+ mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
+ mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
+ mcp_tools_page_size_prefix: 'Show',
+ mcp_tools_page_size_suffix: 'per page',
+ mcp_tools_per_page_aria: 'MCP tools per page',
+ mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
+ mcp_tools_pagination_label: 'MCP tools pagination',
+ mcp_tools_previous_page: '‹ Prev',
+ mcp_tools_previous_page_aria: 'Previous MCP tools page',
+ mcp_tools_next_page: 'Next ›',
+ mcp_tools_next_page_aria: 'Next MCP tools page',
thinking: 'Думаю',
expand_all: 'Развернуть всё',
collapse_all: 'Свернуть всё',
@@ -3697,6 +3759,7 @@ const LOCALES = {
settings_label_external_sessions: 'Показывать внешние сеансы',
settings_label_sync_insights: 'Синхронизировать с Insights',
settings_label_check_updates: 'Проверять обновления',
+ settings_label_whats_new_summary: "Summarize What's New with AI",
settings_label_bot_name: 'Имя помощника',
settings_label_password: 'Пароль доступа',
settings_saved: 'Настройки сохранены',
@@ -3874,6 +3937,7 @@ const LOCALES = {
settings_desc_external_sessions: 'Показать беседы из CLI, Telegram, Discord, Slack и других каналов в списке сеансов. Нажмите для импорта и продолжения.',
settings_desc_sync_insights: 'Синхронизирует использование токенов WebUI в state.db, чтобы Hermes /insights включал данные браузерных сеансов. Выключено по умолчанию.',
settings_desc_check_updates: 'Показывает баннер, когда доступны более новые версии WebUI или Agent. Периодически выполняет git fetch в фоне.',
+ settings_desc_whats_new_summary: "Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.",
settings_desc_bot_name: 'Отображаемое имя помощника во всём интерфейсе. По умолчанию Hermes.',
settings_desc_password: 'Введите новый пароль, чтобы задать или изменить его. Оставьте пустым, чтобы сохранить текущую настройку.',
password_placeholder: 'Введите новый пароль…',
@@ -4504,6 +4568,20 @@ const LOCALES = {
mcp_tools_load_failed: 'Failed to load MCP tools.',
mcp_tools_schema_empty: 'No schema parameters.',
mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.',
+ mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
+ mcp_tools_summary_none: 'No MCP tools to show.',
+ mcp_tools_summary_matching: (query) => ` matching “${query}”`,
+ mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
+ mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
+ mcp_tools_page_size_prefix: 'Show',
+ mcp_tools_page_size_suffix: 'per page',
+ mcp_tools_per_page_aria: 'MCP tools per page',
+ mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
+ mcp_tools_pagination_label: 'MCP tools pagination',
+ mcp_tools_previous_page: '‹ Prev',
+ mcp_tools_previous_page_aria: 'Previous MCP tools page',
+ mcp_tools_next_page: 'Next ›',
+ mcp_tools_next_page_aria: 'Next MCP tools page',
thinking: 'Pensando',
expand_all: 'Expandir todo',
collapse_all: 'Contraer todo',
@@ -4743,6 +4821,7 @@ const LOCALES = {
settings_label_external_sessions: 'Mostrar sesiones externas',
settings_label_sync_insights: 'Sincronizar con insights',
settings_label_check_updates: 'Buscar actualizaciones',
+ settings_label_whats_new_summary: "Summarize What's New with AI",
settings_label_bot_name: 'Nombre del asistente',
settings_label_password: 'Contraseña de acceso',
settings_saved: 'Configuración guardada',
@@ -4931,6 +5010,7 @@ const LOCALES = {
settings_desc_external_sessions: 'Mostrar conversaciones de CLI, Telegram, Discord, Slack y otros canales en la lista de sesiones. Haz clic para importar y continuar.',
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.',
+ settings_desc_whats_new_summary: "Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.",
settings_desc_bot_name: 'Nombre visible del asistente en toda la UI. Por defecto es Hermes.',
settings_desc_password: 'Introduce una nueva contraseña para establecerla o cambiarla. Déjalo en blanco para mantener la configuración actual.',
password_placeholder: 'Introduce una contraseña nueva…',
@@ -5558,6 +5638,20 @@ const LOCALES = {
mcp_tools_load_failed: 'Failed to load MCP tools.',
mcp_tools_schema_empty: 'No schema parameters.',
mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.',
+ mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
+ mcp_tools_summary_none: 'No MCP tools to show.',
+ mcp_tools_summary_matching: (query) => ` matching “${query}”`,
+ mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
+ mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
+ mcp_tools_page_size_prefix: 'Show',
+ mcp_tools_page_size_suffix: 'per page',
+ mcp_tools_per_page_aria: 'MCP tools per page',
+ mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
+ mcp_tools_pagination_label: 'MCP tools pagination',
+ mcp_tools_previous_page: '‹ Prev',
+ mcp_tools_previous_page_aria: 'Previous MCP tools page',
+ mcp_tools_next_page: 'Next ›',
+ mcp_tools_next_page_aria: 'Next MCP tools page',
thinking: 'Nachdenken',
expand_all: 'Alle ausklappen',
collapse_all: 'Alle einklappen',
@@ -5782,6 +5876,7 @@ const LOCALES = {
settings_label_external_sessions: 'Externe Sitzungen anzeigen',
settings_label_sync_insights: 'Mit Insights synchronisieren',
settings_label_check_updates: 'Nach Updates suchen',
+ settings_label_whats_new_summary: "Summarize What's New with AI",
settings_label_bot_name: 'Assistenten-Name',
settings_label_password: 'Zugangspasswort',
settings_saved: 'Einstellungen gespeichert',
@@ -5960,6 +6055,7 @@ const LOCALES = {
settings_desc_external_sessions: 'Zeigt Gespräche von CLI, Telegram, Discord, Slack und anderen Kanälen in der Sitzungsliste an. Klicken zum Importieren und Fortsetzen.',
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.',
+ settings_desc_whats_new_summary: "Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.",
settings_desc_bot_name: 'Anzeigename für den Assistenten in der UI. Standardmäßig Hermes.',
settings_desc_password: 'Geben Sie ein neues Passwort ein, um es zu setzen oder zu ändern. Leer lassen, um die aktuelle Einstellung beizubehalten.',
password_placeholder: 'Neues Passwort eingeben…',
@@ -6616,6 +6712,20 @@ const LOCALES = {
mcp_tools_load_failed: '加载 MCP 工具失败。',
mcp_tools_schema_empty: '无参数。',
mcp_tools_runtime_note: '工具清单仅使用已知的活跃 MCP 运行时数据;WebUI 不会启动或探测服务器。',
+ mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
+ mcp_tools_summary_none: 'No MCP tools to show.',
+ mcp_tools_summary_matching: (query) => ` matching “${query}”`,
+ mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
+ mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
+ mcp_tools_page_size_prefix: 'Show',
+ mcp_tools_page_size_suffix: 'per page',
+ mcp_tools_per_page_aria: 'MCP tools per page',
+ mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
+ mcp_tools_pagination_label: 'MCP tools pagination',
+ mcp_tools_previous_page: '‹ Prev',
+ mcp_tools_previous_page_aria: 'Previous MCP tools page',
+ mcp_tools_next_page: 'Next ›',
+ mcp_tools_next_page_aria: 'Next MCP tools page',
thinking: '思考过程',
expand_all: '全部展开',
collapse_all: '全部折叠',
@@ -6862,6 +6972,7 @@ const LOCALES = {
settings_label_external_sessions: '显示外部会话',
settings_label_sync_insights: '同步到 insights',
settings_label_check_updates: '检查更新',
+ settings_label_whats_new_summary: "Summarize What's New with AI",
settings_label_bot_name: '助手名称',
settings_label_password: '访问密码',
settings_saved: '设置已保存',
@@ -7077,6 +7188,7 @@ const LOCALES = {
settings_desc_external_sessions: '在会话列表中显示来自 CLI、Telegram、Discord、Slack 等渠道的对话。点击可导入并继续对话。',
settings_desc_sync_insights: '将 WebUI token 使用情况同步到 state.db,使 hermes /insights 包含浏览器会话数据。默认关闭。',
settings_desc_check_updates: '当有更新的 WebUI 或助手版本时显示横幅。会在后台定期执行 git fetch。',
+ settings_desc_whats_new_summary: "Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.",
settings_desc_bot_name: '助手在 UI 中的显示名称。默认为 Hermes。',
settings_desc_password: '输入新密码以设置或更改。留空保持当前设置。',
// onboarding
@@ -7662,6 +7774,20 @@ const LOCALES = {
mcp_tools_load_failed: 'Failed to load MCP tools.',
mcp_tools_schema_empty: 'No schema parameters.',
mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.',
+ mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
+ mcp_tools_summary_none: 'No MCP tools to show.',
+ mcp_tools_summary_matching: (query) => ` matching “${query}”`,
+ mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
+ mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
+ mcp_tools_page_size_prefix: 'Show',
+ mcp_tools_page_size_suffix: 'per page',
+ mcp_tools_per_page_aria: 'MCP tools per page',
+ mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
+ mcp_tools_pagination_label: 'MCP tools pagination',
+ mcp_tools_previous_page: '‹ Prev',
+ mcp_tools_previous_page_aria: 'Previous MCP tools page',
+ mcp_tools_next_page: 'Next ›',
+ mcp_tools_next_page_aria: 'Next MCP tools page',
thinking: '\u601d\u8003\u904e\u7a0b',
expand_all: '\u5168\u90e8\u5c55\u958b',
collapse_all: '\u5168\u90e8\u6298\u758a',
@@ -7927,6 +8053,7 @@ const LOCALES = {
settings_label_external_sessions: '顯示外部會話',
settings_label_sync_insights: '\u540c\u6b65\u5230 insights',
settings_label_check_updates: '\u6aa2\u67e5\u66f4\u65b0',
+ settings_label_whats_new_summary: "Summarize What's New with AI",
settings_label_bot_name: '\u52a9\u624b\u540d\u7a31',
settings_label_password: '\u8a2a\u554f\u5bc6\u78bc',
settings_saved: '\u8a2d\u5b9a\u5df2\u5132\u5b58',
@@ -8102,6 +8229,7 @@ const LOCALES = {
settings_desc_external_sessions: '在會話列表中顯示來自 CLI、Telegram、Discord、Slack 等管道的對話。點擊可導入並繼續對話。',
settings_desc_sync_insights: '將 WebUI token 使用情況同步到 state.db,使 hermes /insights 包含瀏覽器會話數據。預設未啟用。',
settings_desc_check_updates: '當有更新的 WebUI 或助手版本時顯示標記。將在後台正常執行 Git-Fetch。',
+ settings_desc_whats_new_summary: "Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.",
settings_desc_bot_name: '助手在 UI 中的顯示名稱。預設未更改。',
settings_desc_password: '\u8a2d\u5b9a WebUI \u767b\u5165\u5bc6\u78bc\u3002\u5047\u5982\u5df2\u8a2d\u7f6e\uff0c\u6bcf\u6b21\u52a0\u8f09\u90fd\u9700\u8981\u767b\u5165\u3002',
onboarding_password_will_enable: '\u5c07\u6703\u555f\u7528',
@@ -9143,6 +9271,7 @@ const LOCALES = {
settings_label_external_sessions: 'Mostrar sessões externas',
settings_label_sync_insights: 'Sincronizar para insights',
settings_label_check_updates: 'Verificar atualizações',
+ settings_label_whats_new_summary: "Summarize What's New with AI",
settings_label_bot_name: 'Nome do Assistente',
settings_label_password: 'Senha de Acesso',
settings_saved: 'Configurações salvas',
@@ -9324,6 +9453,7 @@ const LOCALES = {
settings_desc_external_sessions: 'Mostrar conversas de CLI, Telegram, Discord, Slack e outros canais na lista de sessões. Clique para importar e continuar.',
settings_desc_sync_insights: 'Espelha uso de tokens para state.db.',
settings_desc_check_updates: 'Mostrar banner quando versões mais novas estiverem disponíveis.',
+ settings_desc_whats_new_summary: "Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.",
settings_desc_bot_name: 'Nome de exibição do assistente. Padrão: Hermes.',
settings_desc_password: 'Digite nova senha para definir ou trocar. Deixe em branco para manter.',
password_placeholder: 'Digite nova senha…',
@@ -9764,6 +9894,20 @@ const LOCALES = {
mcp_tools_load_failed: 'Failed to load MCP tools.',
mcp_tools_schema_empty: 'No schema parameters.',
mcp_tools_runtime_note: 'Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.',
+ mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
+ mcp_tools_summary_none: 'No MCP tools to show.',
+ mcp_tools_summary_matching: (query) => ` matching “${query}”`,
+ mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
+ mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
+ mcp_tools_page_size_prefix: 'Show',
+ mcp_tools_page_size_suffix: 'per page',
+ mcp_tools_per_page_aria: 'MCP tools per page',
+ mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
+ mcp_tools_pagination_label: 'MCP tools pagination',
+ mcp_tools_previous_page: '‹ Prev',
+ mcp_tools_previous_page_aria: 'Previous MCP tools page',
+ mcp_tools_next_page: 'Next ›',
+ mcp_tools_next_page_aria: 'Next MCP tools page',
thinking: '생각 중',
expand_all: '모두 펼치기',
collapse_all: '모두 접기',
@@ -10164,6 +10308,7 @@ const LOCALES = {
settings_label_external_sessions: '외부 세션 표시',
settings_label_sync_insights: 'Insights에 동기화',
settings_label_check_updates: '업데이트 확인',
+ settings_label_whats_new_summary: "Summarize What's New with AI",
settings_label_bot_name: 'Assistant 이름',
settings_label_password: '접근 비밀번호',
settings_saved: '설정 저장됨',
@@ -10344,6 +10489,7 @@ const LOCALES = {
settings_desc_external_sessions: 'CLI, Telegram, Discord, Slack 및 기타 채널의 대화를 세션 목록에 표시합니다. 클릭하여 가져오고 계속하세요.',
settings_desc_sync_insights: 'WebUI 토큰 사용량을 state.db에 반영하여 hermes /insights에 브라우저 세션 데이터가 포함되도록 합니다. 기본값은 꺼짐입니다.',
settings_desc_check_updates: 'WebUI 또는 Agent의 새 버전이 있으면 배너를 표시합니다. 백그라운드에서 주기적으로 git fetch를 실행합니다.',
+ settings_desc_whats_new_summary: "Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.",
settings_desc_bot_name: 'UI 전체에 표시되는 Assistant 이름입니다. 기본값은 Hermes입니다.',
settings_desc_password: '새 비밀번호를 설정하거나 변경하려면 입력하세요. 현재 설정을 유지하려면 비워 두세요.',
password_placeholder: '새 비밀번호 입력…',
@@ -10878,6 +11024,20 @@ const LOCALES = {
mcp_tools_load_failed: 'Échec du chargement des outils MCP.',
mcp_tools_schema_empty: 'Aucun paramètre de schéma.',
mcp_tools_runtime_note: 'L\'inventaire des outils utilise uniquement les données d\'exécution MCP actives déjà connues ; le WebUI ne démarre pas et ne sonde pas les serveurs.',
+ mcp_tools_summary_no_matches: (query,total) => `No MCP tools matching “${query}” (${total} total MCP tools).`,
+ mcp_tools_summary_none: 'No MCP tools to show.',
+ mcp_tools_summary_matching: (query) => ` matching “${query}”`,
+ mcp_tools_summary_total_note: (total) => ` (${total} total MCP tools)`,
+ mcp_tools_summary_showing: (start,end,filtered,searchNote,totalNote,page,pages) => `Showing ${start}-${end} of ${filtered} MCP tools${searchNote}${totalNote}. Page ${page} of ${pages}.`,
+ mcp_tools_page_size_prefix: 'Show',
+ mcp_tools_page_size_suffix: 'per page',
+ mcp_tools_per_page_aria: 'MCP tools per page',
+ mcp_tools_inactive_configured_servers: (servers) => `Configured but inactive in this WebUI runtime: ${servers}.`,
+ mcp_tools_pagination_label: 'MCP tools pagination',
+ mcp_tools_previous_page: '‹ Prev',
+ mcp_tools_previous_page_aria: 'Previous MCP tools page',
+ mcp_tools_next_page: 'Next ›',
+ mcp_tools_next_page_aria: 'Next MCP tools page',
pdf_loading: 'Chargement du PDF {0}…',
pdf_too_large: 'PDF trop volumineux pour un aperçu en ligne',
pdf_no_pages: 'Le PDF n\'a pas de pages',
@@ -11201,6 +11361,7 @@ const LOCALES = {
settings_label_external_sessions: 'Afficher les sessions non-WebUI',
settings_label_sync_insights: 'Synchroniser avec les insights',
settings_label_check_updates: 'Vérifier les mises à jour',
+ settings_label_whats_new_summary: "Summarize What's New with AI",
settings_label_bot_name: 'Nom de l\'assistant',
settings_label_password: 'Mot de passe d\'accès',
settings_saved: 'Paramètres enregistrés',
@@ -11391,6 +11552,7 @@ const LOCALES = {
settings_desc_external_sessions: 'Affichez les conversations de CLI, Telegram, Discord, Slack et d\'autres chaînes dans la liste des sessions. Cliquez pour importer et continuer.',
settings_desc_sync_insights: 'Met en miroir l\'utilisation du jeton WebUI dans state.db afin que Hermes /insights inclut les données de session du navigateur. Désactivé par défaut.',
settings_desc_check_updates: 'Afficher une bannière lorsque des versions plus récentes de WebUI ou de l\'agent sont disponibles. Exécute périodiquement une récupération git en arrière-plan.',
+ settings_desc_whats_new_summary: "Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.",
settings_desc_bot_name: 'Nom d’affichage de l’assistant dans l’interface utilisateur. Par défaut, Hermès.',
settings_desc_password: 'Saisissez un nouveau mot de passe pour le définir ou le modifier. Laissez vide pour conserver le paramètre actuel.',
password_placeholder: 'Entrez le nouveau mot de passe…',
diff --git a/static/index.html b/static/index.html
index 8a530d16..424e9c44 100644
--- a/static/index.html
+++ b/static/index.html
@@ -342,7 +342,11 @@
Show a banner when newer versions of the WebUI or Agent are available. Runs a background git fetch periodically.
+
+
+
Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.
+
Display name for the assistant throughout the UI. Defaults to Hermes.
@@ -1138,7 +1149,9 @@
Search known tools across active MCP servers.
-
+
+
+
Tool inventory only uses already-known active MCP runtime data; the WebUI does not start or probe servers.