mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-23 10:50:14 +00:00
ad8e10304c
* fix: remove orphaned i18n keys from top-level LOCALES object Three Traditional Chinese translation keys (cmd_status, memory_saved, profile_delete_title) were placed outside any locale block between the en and ru blocks in static/i18n.js. They became top-level properties of the LOCALES object, causing them to appear as invalid language options in the Settings > Preferences dropdown. The correct translations already exist in the zh-Hant locale block. Fixes #1008 * fix: block stale SSE events from polluting new session's DOM - appendThinking(): guard with !S.session||!S.activeStreamId to drop events from a previous session's SSE stream during a session switch - appendLiveToolCard(): same guard for consistency - finalizeThinkingCard(): scroll thinking-card-body to top when scroll is pinned, so completed response is immediately visible - appendThinking(): auto-scroll thinking card body to bottom while streaming if user is watching (scroll pinned) * Fix empty agent sessions in sidebar * fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status Fixes #995 — three sub-issues in the Cron Jobs UI: 1. Dual play icons ambiguous: Resume button now shows a distinct play+bar icon (play triangle + vertical line) instead of the identical triangle used by Run now. 2. Toast notification overlapping header buttons: Added position:relative; z-index:10 to .main-view-header so it stacks above the fixed toast (z-index:100 within its layer). 3. No running status after trigger: After triggering a job, the status badge immediately shows 'running…' with a CSS spinner animation, and polls the cron list every 3s (up to 30s) to refresh when the job completes. - Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant) - Added .detail-badge.running CSS class with spinner animation - New functions: _setCronDetailStatus(), _startCronRunningPoll() * fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback - _clearCronDetail() now clears _cronRunningPoll interval on navigation - Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker) - When poll ends (30s max), detail re-renders with actual status as fallback * feat: create folder and add space directly from UI (#782) - After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog - Add Create folder if it doesnt exist checkbox in the New Space form - Backend: support create flag in /api/workspaces/add to mkdir before validation - i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales * fix: validate workspace path before mkdir to prevent orphan directories Review feedback (critical): the previous code called mkdir() before validate_workspace_to_add(), which meant a rejected path (e.g. system dir) would leave an orphan directory on disk. New flow: 1. Resolve path and check against blocked system roots BEFORE any mutation 2. mkdir() only if path passes the blocklist check 3. Full validation (exists, is_dir) after mkdir Also imports _workspace_blocked_roots for the pre-mutation blocklist check. * fix(#1014): classify model-not-found errors with helpful message - Add model_not_found error type to streaming.py exception classifier - Detect 404, 'not found', 'does not exist', 'invalid model' patterns - Strip HTML tags from provider error messages (nginx 404 pages, etc.) - Add model_not_found branch to apperror handler in messages.js - Add i18n key model_not_found_label in all 6 locales - 15 tests covering detection, sanitization, frontend, and i18n * feat(ui): add live TPS stat to header Adds a TPS (Tokens Per Second) chip to the right of the header title bar that updates live while AI output is streaming. Metering (api/metering.py) - Tracks per-session output + reasoning tokens via GlobalMeter singleton - Per-session TPS = total_tokens / elapsed_time - Global TPS = average of active sessions' TPS values - HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling window (only recorded when > 0, so idle periods are excluded) - Thread-safe with a single lock Metering events emitted from streaming.py - Throttled at 100ms from token/reasoning/tool callbacks so the display updates rapidly during fast token streams - 1Hz ticker as fallback for slow streams (exits when no active sessions) - Final stats emitted on stream end Routes (api/routes.py) - Removed POST /api/metering/interval endpoint (dynamic interval via focus/blur was replaced with simple always-1s-when-active approach) UI (static/messages.js, index.html, style.css) - TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low' - Default: '0.0 t/s . 0.0 high' when idle - Display updates on every metering SSE event (throttled to 100ms) * feat: session restore speed + title gen reasoning hardening (#1025, #1026) PR #1025 (@franksong2702): Speed up large session restore paths - GET /api/session?messages=0 now parses only metadata before the messages array - Metadata-only loads no longer populate the full-session LRU cache - Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup - Hard reload no longer waits for populateModelDropdown() before restoring session PR #1026 (@franksong2702): Harden auto title generation for reasoning models - Raises title-gen completion budget to 512 tokens (reasoning-safe) - Retries once with 1024 tokens on empty content / finish_reason:length - Applies retry to both auxiliary and active-agent fallback routes - Preserves underlying failure reason in title_status on local fallback Co-authored-by: Frank Song <franksong2702@gmail.com> * feat: session attention indicators in right slot + last_message_at timestamps (#1024) PR #1024 (@franksong2702): Polish session attention indicators - Streaming spinners and unread dots now reuse the right-side actions slot - Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps - Date group carets point down when expanded, right when collapsed - Pinned group no longer repeats pinned-star icon per row - Running indicators appear immediately after send (local busy state while /api/sessions catches up) - Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message) so metadata-only saves don't make old sessions appear under Today Co-authored-by: Frank Song <franksong2702@gmail.com> * docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36) --------- Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: Josh <josh@fyul.link> Co-authored-by: Frank Song <franksong2702@gmail.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
79 lines
3.9 KiB
Python
79 lines
3.9 KiB
Python
"""
|
|
Tests for #634: CLI sessions not visible when setting is enabled.
|
|
|
|
Root cause: get_cli_sessions() swallowed all errors silently (bare except → return []).
|
|
Users with older hermes-agent versions (missing 'source' column in state.db) got
|
|
an empty list with no log output, making diagnosis impossible.
|
|
|
|
Fixes:
|
|
1. Schema introspection: check for 'source' column before querying, log a warning
|
|
if missing and return early.
|
|
2. Exception path: log a warning instead of silently returning [].
|
|
"""
|
|
import pathlib
|
|
import re
|
|
|
|
MODELS_PY = pathlib.Path(__file__).parent.parent / 'api' / 'models.py'
|
|
AGENT_SESSIONS_PY = pathlib.Path(__file__).parent.parent / 'api' / 'agent_sessions.py'
|
|
src = MODELS_PY.read_text(encoding='utf-8')
|
|
agent_src = AGENT_SESSIONS_PY.read_text(encoding='utf-8')
|
|
combined_src = src + "\n" + agent_src
|
|
|
|
|
|
class TestCliSessionsErrorSurface:
|
|
"""get_cli_sessions() must log warnings instead of silently returning []."""
|
|
|
|
def test_schema_introspection_present(self):
|
|
"""The function must check for the 'source' column before querying."""
|
|
assert "PRAGMA table_info(sessions)" in combined_src
|
|
|
|
def test_missing_source_column_logs_warning(self):
|
|
"""If 'source' column is absent, a warning is logged."""
|
|
# The warning message must mention the missing column and how to fix it
|
|
assert "no 'source' column" in combined_src or "has no 'source' column" in combined_src
|
|
|
|
def test_missing_source_column_suggests_upgrade(self):
|
|
"""Warning message must suggest upgrading hermes-agent."""
|
|
assert "Upgrade hermes-agent" in combined_src or "upgrade hermes-agent" in combined_src.lower()
|
|
|
|
def test_exception_path_logs_warning(self):
|
|
"""The except clause must call logger.warning, not silently pass."""
|
|
# Find the exception handler in get_cli_sessions
|
|
func_start = src.find("def get_cli_sessions()")
|
|
func_end = src.find("\ndef ", func_start + 1)
|
|
func_body = src[func_start:func_end] if func_end != -1 else src[func_start:]
|
|
assert "warning(" in func_body, \
|
|
"get_cli_sessions() exception handler must call logging.warning()"
|
|
|
|
def test_exception_path_includes_db_path(self):
|
|
"""The warning must include the db_path for diagnosability."""
|
|
func_start = src.find("def get_cli_sessions()")
|
|
func_end = src.find("\ndef ", func_start + 1)
|
|
func_body = src[func_start:func_end] if func_end != -1 else src[func_start:]
|
|
# db_path should appear in the warning call
|
|
warning_pos = func_body.find("warning(")
|
|
warning_block = func_body[warning_pos:warning_pos + 300]
|
|
assert "db_path" in warning_block, \
|
|
"Warning must include db_path so admins can find the problematic database"
|
|
|
|
def test_still_returns_empty_on_error(self):
|
|
"""Function must still return [] after logging (graceful degradation)."""
|
|
# After the warning, it should return cli_sessions (the empty list) not raise
|
|
func_start = src.find("def get_cli_sessions()")
|
|
func_end = src.find("\ndef ", func_start + 1)
|
|
func_body = src[func_start:func_end] if func_end != -1 else src[func_start:]
|
|
# Must have a 'return' after the warning call
|
|
warning_pos = func_body.find("_cli_err:")
|
|
after_warning = func_body[warning_pos:warning_pos + 400]
|
|
assert "return" in after_warning, \
|
|
"Function must return after the warning (not raise)"
|
|
|
|
def test_source_column_check_before_sql_query(self):
|
|
"""Schema check must happen before the main SQL SELECT."""
|
|
pragma_pos = agent_src.find("PRAGMA table_info(sessions)")
|
|
select_pos = agent_src.find("SELECT s.id, s.title, s.model")
|
|
assert pragma_pos != -1, "PRAGMA check not found"
|
|
assert select_pos != -1, "SELECT query not found"
|
|
assert pragma_pos < select_pos, \
|
|
"Schema introspection must run before the main SQL query"
|