2026-04-14 05:26:05 +00:00
|
|
|
import json
|
|
|
|
|
import pathlib
|
|
|
|
|
import subprocess
|
|
|
|
|
import textwrap
|
|
|
|
|
|
|
|
|
|
REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve()
|
|
|
|
|
SESSIONS_JS = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
|
|
|
STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
|
|
|
|
I18N_JS = (REPO_ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_function(source: str, name: str) -> str:
|
|
|
|
|
marker = f"function {name}"
|
|
|
|
|
start = source.index(marker)
|
|
|
|
|
brace_start = source.index("{", start)
|
|
|
|
|
depth = 0
|
|
|
|
|
for idx in range(brace_start, len(source)):
|
|
|
|
|
ch = source[idx]
|
|
|
|
|
if ch == "{":
|
|
|
|
|
depth += 1
|
|
|
|
|
elif ch == "}":
|
|
|
|
|
depth -= 1
|
|
|
|
|
if depth == 0:
|
|
|
|
|
return source[start : idx + 1]
|
|
|
|
|
raise AssertionError(f"Could not extract {name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_session_time_case(script_body: str) -> dict:
|
|
|
|
|
functions = "\n\n".join(
|
|
|
|
|
_extract_function(SESSIONS_JS, name)
|
|
|
|
|
for name in (
|
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* 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>
2026-04-25 20:07:35 +00:00
|
|
|
"_sessionTimestampMs",
|
2026-04-14 05:26:05 +00:00
|
|
|
"_localDayOrdinal",
|
|
|
|
|
"_sessionCalendarBoundaries",
|
|
|
|
|
"_formatSessionDate",
|
|
|
|
|
"_formatRelativeSessionTime",
|
|
|
|
|
"_sessionTimeBucketLabel",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
script = textwrap.dedent(
|
|
|
|
|
f"""
|
|
|
|
|
process.env.TZ = 'UTC';
|
|
|
|
|
const translations = {{
|
|
|
|
|
session_time_unknown: 'Unknown',
|
v0.50.211: compact timestamps, adaptive title refresh, settings picker fix (#1061)
* 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 <bergeouss@users.noreply.github.com>
* 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) <noreply@anthropic.com>
* 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 <biely@webtec.sk>
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:50:58 +00:00
|
|
|
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',
|
2026-04-14 05:26:05 +00:00
|
|
|
session_time_bucket_today: 'Today',
|
|
|
|
|
session_time_bucket_yesterday: 'Yesterday',
|
|
|
|
|
session_time_bucket_this_week: 'This week',
|
|
|
|
|
session_time_bucket_last_week: 'Last week',
|
|
|
|
|
session_time_bucket_older: 'Older',
|
|
|
|
|
}};
|
|
|
|
|
function t(key, ...args) {{
|
|
|
|
|
const val = translations[key];
|
|
|
|
|
return typeof val === 'function' ? val(...args) : val;
|
|
|
|
|
}}
|
|
|
|
|
{functions}
|
|
|
|
|
{script_body}
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
proc = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
|
|
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_session_sidebar_js_has_dynamic_relative_time_helpers():
|
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* 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>
2026-04-25 20:07:35 +00:00
|
|
|
assert "function _sessionTimestampMs" in SESSIONS_JS
|
2026-04-14 05:26:05 +00:00
|
|
|
assert "function _sessionCalendarBoundaries" in SESSIONS_JS
|
|
|
|
|
assert "function _formatRelativeSessionTime" in SESSIONS_JS
|
|
|
|
|
assert "function _sessionTimeBucketLabel" in SESSIONS_JS
|
|
|
|
|
assert "session_time_bucket_last_week" in SESSIONS_JS
|
|
|
|
|
assert "session_time_bucket_this_week" in SESSIONS_JS
|
|
|
|
|
assert "session_time_bucket_older" in SESSIONS_JS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_session_sidebar_renders_relative_time_and_meta_rows():
|
2026-04-14 19:10:23 +00:00
|
|
|
# session-time element was removed from sessions.js in v0.50.40 to
|
|
|
|
|
# give session titles full width — the CSS class is kept but set to display:none.
|
2026-04-16 16:58:53 +00:00
|
|
|
# session-meta / metaBits were removed when we dropped message-count, model, and
|
|
|
|
|
# source-tag badges from the sidebar (design round 2).
|
2026-04-14 05:26:05 +00:00
|
|
|
assert "orderedSessions" in SESSIONS_JS
|
|
|
|
|
assert ".session-time" in STYLE_CSS
|
|
|
|
|
assert ".session-title-row" in STYLE_CSS
|
|
|
|
|
assert ".session-item.active .session-title" in STYLE_CSS
|
|
|
|
|
assert "|| _sessionTimeBucketLabel" not in SESSIONS_JS
|
|
|
|
|
assert "const ONE_DAY=86400000;" not in SESSIONS_JS
|
|
|
|
|
|
|
|
|
|
|
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* 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>
2026-04-25 20:07:35 +00:00
|
|
|
def test_session_timestamp_prefers_last_message_at_over_metadata_updated_at():
|
|
|
|
|
result = _run_session_time_case(
|
|
|
|
|
"""
|
|
|
|
|
const session = {
|
|
|
|
|
created_at: 1776441348,
|
|
|
|
|
updated_at: 1777086443,
|
|
|
|
|
last_message_at: 1776441972,
|
|
|
|
|
};
|
|
|
|
|
process.stdout.write(JSON.stringify({
|
|
|
|
|
timestampMs: _sessionTimestampMs(session),
|
|
|
|
|
}));
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
assert result["timestampMs"] == 1776441972 * 1000
|
|
|
|
|
|
|
|
|
|
|
2026-04-14 05:26:05 +00:00
|
|
|
def test_relative_time_uses_calendar_boundaries_and_year_for_old_sessions():
|
|
|
|
|
result = _run_session_time_case(
|
|
|
|
|
"""
|
|
|
|
|
const now = Date.UTC(2026, 3, 15, 1, 0, 0);
|
|
|
|
|
const mondayLate = Date.UTC(2026, 3, 13, 23, 0, 0);
|
|
|
|
|
const oldSession = Date.UTC(2024, 2, 5, 12, 0, 0);
|
|
|
|
|
process.stdout.write(JSON.stringify({
|
|
|
|
|
relative: _formatRelativeSessionTime(mondayLate, now),
|
|
|
|
|
bucket: _sessionTimeBucketLabel(mondayLate, now),
|
|
|
|
|
oldDate: _formatRelativeSessionTime(oldSession, now),
|
|
|
|
|
}));
|
|
|
|
|
"""
|
|
|
|
|
)
|
v0.50.211: compact timestamps, adaptive title refresh, settings picker fix (#1061)
* 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 <bergeouss@users.noreply.github.com>
* 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) <noreply@anthropic.com>
* 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 <biely@webtec.sk>
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:50:58 +00:00
|
|
|
assert result["relative"] == "2d"
|
2026-04-14 05:26:05 +00:00
|
|
|
assert result["bucket"] == "This week"
|
|
|
|
|
assert "2024" in result["oldDate"]
|
|
|
|
|
|
|
|
|
|
|
2026-04-16 17:09:51 +00:00
|
|
|
def test_relative_time_today_bucket():
|
|
|
|
|
"""Session from 2 hours ago should bucket as 'Today'."""
|
|
|
|
|
result = _run_session_time_case(
|
|
|
|
|
"""
|
|
|
|
|
const now = Date.UTC(2026, 3, 15, 14, 0, 0);
|
|
|
|
|
const twoHoursAgo = now - 2 * 60 * 60 * 1000;
|
|
|
|
|
process.stdout.write(JSON.stringify({
|
|
|
|
|
relative: _formatRelativeSessionTime(twoHoursAgo, now),
|
|
|
|
|
bucket: _sessionTimeBucketLabel(twoHoursAgo, now),
|
|
|
|
|
}));
|
|
|
|
|
"""
|
|
|
|
|
)
|
v0.50.211: compact timestamps, adaptive title refresh, settings picker fix (#1061)
* 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 <bergeouss@users.noreply.github.com>
* 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) <noreply@anthropic.com>
* 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 <biely@webtec.sk>
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:50:58 +00:00
|
|
|
assert result["relative"] == "2h"
|
2026-04-16 17:09:51 +00:00
|
|
|
assert result["bucket"] == "Today"
|
|
|
|
|
|
|
|
|
|
|
2026-04-14 05:26:05 +00:00
|
|
|
def test_relative_time_handles_just_now_and_dst_safe_yesterday_boundary():
|
|
|
|
|
result = _run_session_time_case(
|
|
|
|
|
"""
|
|
|
|
|
const now = Date.UTC(2026, 2, 9, 12, 0, 0);
|
|
|
|
|
const justNow = now - 30 * 1000;
|
|
|
|
|
const yesterday = Date.UTC(2026, 2, 8, 23, 30, 0);
|
|
|
|
|
process.stdout.write(JSON.stringify({
|
|
|
|
|
justNow: _formatRelativeSessionTime(justNow, now),
|
|
|
|
|
yesterday: _formatRelativeSessionTime(yesterday, now),
|
|
|
|
|
yesterdayBucket: _sessionTimeBucketLabel(yesterday, now),
|
|
|
|
|
}));
|
|
|
|
|
"""
|
|
|
|
|
)
|
v0.50.211: compact timestamps, adaptive title refresh, settings picker fix (#1061)
* 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 <bergeouss@users.noreply.github.com>
* 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) <noreply@anthropic.com>
* 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 <biely@webtec.sk>
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:50:58 +00:00
|
|
|
assert result["justNow"] == "1m"
|
|
|
|
|
assert result["yesterday"] == "1d"
|
2026-04-14 05:26:05 +00:00
|
|
|
assert result["yesterdayBucket"] == "Yesterday"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_relative_time_strings_are_localized_in_english_and_spanish_bundles():
|
|
|
|
|
for key in (
|
|
|
|
|
"session_time_unknown",
|
|
|
|
|
"session_time_minutes_ago",
|
|
|
|
|
"session_time_hours_ago",
|
|
|
|
|
"session_time_days_ago",
|
|
|
|
|
"session_time_last_week",
|
|
|
|
|
"session_time_bucket_today",
|
|
|
|
|
"session_time_bucket_yesterday",
|
|
|
|
|
"session_time_bucket_this_week",
|
|
|
|
|
"session_time_bucket_last_week",
|
|
|
|
|
"session_time_bucket_older",
|
|
|
|
|
):
|
|
|
|
|
assert key in I18N_JS
|
2026-05-16 11:31:16 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_german_relative_time_translations_interpolate_numbers():
|
|
|
|
|
assert "session_time_minutes_ago: (n) => `Vor ${n} Minuten`" in I18N_JS
|
|
|
|
|
assert "session_time_hours_ago: (n) => `Vor ${n} Stunden`" in I18N_JS
|
|
|
|
|
assert "session_time_days_ago: (n) => `Vor ${n} Tagen`" in I18N_JS
|
|
|
|
|
assert "session_time_minutes_ago: 'Vor {n} Minuten'" not in I18N_JS
|
|
|
|
|
assert "session_time_hours_ago: 'Vor {n} Stunden'" not in I18N_JS
|
|
|
|
|
assert "session_time_days_ago: 'Vor {n} Tagen'" not in I18N_JS
|