v0.50.210: gpt-5.5, cron titles, agent cache, bfcache fix, onboarding fix, mermaid CSP, PWA auth (#1056)
* feat(models): add gpt-5.5 to openai, openai-codex, copilot catalogs
Adds GPT-5.5 and GPT-5.5 Mini entries to the static _PROVIDER_MODELS
catalog so they appear in the model picker for the openai, openai-codex,
and copilot providers.
Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent
* fix(models): add gpt-5.5-mini to copilot provider catalog
* fix(renderer): suppress Mermaid Google Fonts CSP violation via fontFamily inherit (#1044)
Mermaid's built-in 'dark' and 'default' themes inject an @import for
fonts.googleapis.com/Manrope into every generated SVG. The CSP style-src
only allows cdn.jsdelivr.net, so this request is blocked on every diagram
render, filling the console with CSP errors.
Fix: pass fontFamily:'inherit' (and fontSize:'14px') in the themeVariables
block of mermaid.initialize() in renderMermaidBlocks(). This suppresses
Mermaid's external font import and uses the page's existing font stack.
Avoids adding fonts.googleapis.com to the CSP — no new external dependency,
no font FOUT, consistent with the rest of the UI typography.
3 regression tests added in tests/test_1044_mermaid_csp_font.py.
2215/2215 tests passing.
* fix(onboarding): non-standard provider/path cluster (#1029)
* fix(bfcache): restore full layout on tab/session restore — rail, topbar, panels (#1045)
The pageshow handler added for #822 only cleared the session search filter
and re-rendered the session list. This left the rest of the layout chrome
(topbar, rail icons, workspace panel, resize handles, gateway SSE) in the
stale bfcache DOM state, causing a broken layout (oversized search icon,
uninitialized rail) that required a hard refresh to fix.
Fix: extend the pageshow handler to re-run the full set of layout sync calls
that the boot IIFE runs on a fresh page load:
syncTopbar() — restores model chip, title, topbar state
syncWorkspacePanelState() — restores workspace panel open/closed
_initResizePanels() — reattaches panel resize drag listeners
startGatewaySSE() — reconnects the gateway SSE watcher
(bfcache-persisted connections are dead)
All four calls are typeof-guarded for safe degradation if a helper is not
yet defined. The existing #822 fixes (sessionSearch clear +
renderSessionListFromCache) are preserved unchanged.
loadSession() is intentionally NOT re-called — it would cause message
flicker; the sync calls above are sufficient to restore visual state.
7 regression tests added in tests/test_1045_bfcache_layout_restore.py.
2219/2219 tests passing.
* fix(bfcache): also close open dropdowns on bfcache restore (#1045)
Additional symptom noted in issue #1045: bfcache freezes the DOM including
any open dropdown/popover state. The thinking-level selector (and other
composer dropdowns) left open when navigating away would appear open without
user interaction on tab restore.
Extend the pageshow handler to call all four named close functions before
the layout sync:
closeModelDropdown() — composer model selector
closeReasoningDropdown() — thinking/reasoning effort selector
closeWsDropdown() — workspace chip dropdown
closeProfileDropdown() — profile switcher dropdown
All calls are typeof-guarded, matching the style of the layout sync calls
already in the handler.
2 new tests (9 total in test_1045_bfcache_layout_restore.py):
- pageshow closes all four named dropdowns
- dropdown closes appear before layout sync calls (clean state first)
2221/2221 tests passing.
* fix(bfcache): remove _initResizePanels() — bfcache preserves listeners
* fix(bfcache): remove _initResizePanels from pageshow — bfcache preserves listeners; update test
* fix(sessions): use cron job name as session title when available (#1032)
* fix(test): add id column to messages table in cron title test fixture
* fix(merge): inject cron title lookup into read_importable loop, remove stale sqlite3 block
* fix(pwa): redirect to /login client-side on 401 — fixes iOS PWA auth expiry trap (#1038)
When an auth session expires, the server returns a 302→/login for page
requests. In a normal browser this works fine, but in an iOS PWA running
in standalone mode the redirect navigates out of the PWA shell into Safari,
leaving the app permanently stuck on 'Authentication required' with no
recovery path.
Fix: intercept 401 responses client-side before surfacing any error.
- workspace.js api(): check res.status===401 first; call
window.location.href='/login' and return immediately (no throw)
- ui.js: add _redirectIfUnauth() helper; wire into all direct fetch()
calls that bypass api() — api/models, api/models/live, api/upload
All fetch paths that could receive a 401 now redirect cleanly within
the PWA frame rather than opening Safari.
6 regression tests added in tests/test_1038_pwa_auth_redirect.py.
2175/2175 tests passing.
* fix(pwa): preserve current URL in ?next= param on 401 redirect
* fix(test): update 401-redirect assertion to accept ?next= URL format
* feat(pwa): add _safeNextPath() to login.js so ?next= param is honored after re-login
Addresses reviewer suggestion: the ?next= URL set on 401 redirect was ignored by
the login success handler (always redirected to ./). _safeNextPath() validates and
returns the ?next= param with open-redirect guards: rejects non-path-absolute inputs,
// protocol-relative URLs, backslash variants, and control characters.
4 new regression tests added.
* Implement session agent cache for AIAgent reuse
Added session agent cache to reuse AIAgent across messages.
* Implement agent caching for session management
* Implement session agent eviction on session deletion
Added session agent eviction to prevent turn count leakage in recycled sessions.
* docs: v0.50.210 release notes — 7 PRs, 2239 tests (+27)
* docs(changelog): drop stale [Unreleased] entries duplicated by v0.50.210
Three entries in the [Unreleased] section are duplicates of items now
listed under v0.50.210:
- Mermaid CSP font fix (#1044) → v0.50.210 / Mermaid Google Fonts CSP
- bfcache layout restore (#1045) → v0.50.210 / bfcache layout and dropdown restore
- iOS PWA auth redirect (#1038) → v0.50.210 / Login redirects back to original URL
The original drafts landed in [Unreleased] when individual PRs (#1047,
#1048, #1043) were approved; the v0.50.210 release-notes commit then
added the same items under the version section without removing the
[Unreleased] copies. Drop the duplicates so users reading the CHANGELOG
don't see the same fix listed twice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent
Co-authored-by: Pix (Hermes) <aliceisjustplaying@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: qxxaa <mrhanoi@outlook.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:47:44 +00:00
|
|
|
"""Tests for cron session title fallback in get_cli_sessions().
|
|
|
|
|
|
|
|
|
|
When a CLI session originates from cron and has no title in state.db, the
|
|
|
|
|
WebUI sidebar should display the human-friendly job name from cron/jobs.json
|
|
|
|
|
instead of a generic "Cron Session" label.
|
|
|
|
|
|
|
|
|
|
Session ID format produced by hermes-agent: cron_<job_id>_<YYYYMMDD>_<HHMMSS>
|
|
|
|
|
"""
|
|
|
|
|
import json
|
|
|
|
|
import sqlite3
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
import api.models as models
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_state_db(path, sessions):
|
|
|
|
|
"""Create a state.db with the schema get_cli_sessions() expects.
|
|
|
|
|
|
|
|
|
|
`sessions` is a list of (id, title, source) tuples.
|
|
|
|
|
"""
|
|
|
|
|
conn = sqlite3.connect(str(path))
|
|
|
|
|
conn.execute("""
|
|
|
|
|
CREATE TABLE sessions (
|
|
|
|
|
id TEXT PRIMARY KEY,
|
|
|
|
|
title TEXT,
|
|
|
|
|
model TEXT,
|
|
|
|
|
message_count INTEGER,
|
|
|
|
|
started_at REAL,
|
|
|
|
|
source TEXT
|
|
|
|
|
)
|
|
|
|
|
""")
|
|
|
|
|
conn.execute("""
|
|
|
|
|
CREATE TABLE messages (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
session_id TEXT,
|
|
|
|
|
timestamp REAL
|
|
|
|
|
)
|
|
|
|
|
""")
|
|
|
|
|
for sid, title, source in sessions:
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT INTO sessions (id, title, model, message_count, started_at, source) "
|
|
|
|
|
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
|
|
|
(sid, title, "gpt-x", 1, 1700000000.0, source),
|
|
|
|
|
)
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT INTO messages (session_id, timestamp) VALUES (?, ?)",
|
|
|
|
|
(sid, 1700000001.0),
|
|
|
|
|
)
|
|
|
|
|
conn.commit()
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _write_jobs_json(hermes_home, jobs):
|
|
|
|
|
"""Write cron/jobs.json with the given jobs list."""
|
|
|
|
|
cron_dir = hermes_home / "cron"
|
|
|
|
|
cron_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
(cron_dir / "jobs.json").write_text(
|
|
|
|
|
json.dumps({"jobs": jobs}), encoding="utf-8"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def fake_hermes_home(tmp_path, monkeypatch):
|
|
|
|
|
"""Point get_cli_sessions() at a temporary HERMES_HOME and disable
|
|
|
|
|
profile lookups so the test runs hermetically."""
|
|
|
|
|
home = tmp_path / "hermes"
|
|
|
|
|
home.mkdir()
|
|
|
|
|
|
|
|
|
|
# Both profile helpers are imported lazily inside get_cli_sessions(),
|
|
|
|
|
# so patching the api.profiles module reaches them.
|
|
|
|
|
import api.profiles as profiles
|
|
|
|
|
monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: home)
|
|
|
|
|
monkeypatch.setattr(profiles, "get_active_profile_name", lambda: None)
|
|
|
|
|
|
|
|
|
|
return home
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cron_session_uses_job_name_when_title_missing(fake_hermes_home):
|
|
|
|
|
"""A cron session with no title should display the friendly job name."""
|
|
|
|
|
_write_jobs_json(fake_hermes_home, [
|
|
|
|
|
{"id": "cd65df6fc1a8", "name": "wiki-auto-ingest"},
|
|
|
|
|
])
|
|
|
|
|
_make_state_db(fake_hermes_home / "state.db", [
|
|
|
|
|
("cron_cd65df6fc1a8_20260417_191049", None, "cron"),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
sessions = models.get_cli_sessions()
|
|
|
|
|
|
|
|
|
|
assert len(sessions) == 1
|
|
|
|
|
assert sessions[0]["title"] == "wiki-auto-ingest"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cron_session_falls_back_when_jobs_json_missing(fake_hermes_home):
|
|
|
|
|
"""No jobs.json should not crash; title falls back to 'Cron Session'."""
|
|
|
|
|
_make_state_db(fake_hermes_home / "state.db", [
|
|
|
|
|
("cron_abc123_20260417_191049", None, "cron"),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
sessions = models.get_cli_sessions()
|
|
|
|
|
|
|
|
|
|
assert sessions[0]["title"] == "Cron Session"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cron_session_falls_back_when_job_id_not_in_jobs_json(fake_hermes_home):
|
|
|
|
|
"""Stale session whose job has been deleted falls back gracefully."""
|
|
|
|
|
_write_jobs_json(fake_hermes_home, [
|
|
|
|
|
{"id": "different_job", "name": "Some Other Job"},
|
|
|
|
|
])
|
|
|
|
|
_make_state_db(fake_hermes_home / "state.db", [
|
|
|
|
|
("cron_orphan_20260417_191049", None, "cron"),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
sessions = models.get_cli_sessions()
|
|
|
|
|
|
|
|
|
|
assert sessions[0]["title"] == "Cron Session"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_explicit_title_is_preserved(fake_hermes_home):
|
|
|
|
|
"""If state.db already has a title, the cron job lookup should not
|
|
|
|
|
override it."""
|
|
|
|
|
_write_jobs_json(fake_hermes_home, [
|
|
|
|
|
{"id": "cd65df6fc1a8", "name": "wiki-auto-ingest"},
|
|
|
|
|
])
|
|
|
|
|
_make_state_db(fake_hermes_home / "state.db", [
|
|
|
|
|
("cron_cd65df6fc1a8_20260417_191049", "User-edited title", "cron"),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
sessions = models.get_cli_sessions()
|
|
|
|
|
|
|
|
|
|
assert sessions[0]["title"] == "User-edited title"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_non_cron_sessions_unaffected(fake_hermes_home):
|
|
|
|
|
"""The cron-name lookup must not run for cli-source sessions, so the
|
|
|
|
|
generic 'Cli Session' fallback still applies when title is empty."""
|
|
|
|
|
_write_jobs_json(fake_hermes_home, [
|
|
|
|
|
{"id": "cd65df6fc1a8", "name": "wiki-auto-ingest"},
|
|
|
|
|
])
|
|
|
|
|
# A 'cli' session whose ID coincidentally starts with 'cron_' must not
|
|
|
|
|
# pick up the job name — the source check guards against this.
|
|
|
|
|
_make_state_db(fake_hermes_home / "state.db", [
|
|
|
|
|
("cron_cd65df6fc1a8_xx", None, "cli"),
|
|
|
|
|
])
|
2026-05-04 02:10:24 +00:00
|
|
|
# PR #1587 hides one-off default-titled CLI rows. Keep this fixture visible
|
|
|
|
|
# so the test remains focused on the cron-name guard rather than sidebar
|
|
|
|
|
# filtering.
|
|
|
|
|
conn = sqlite3.connect(str(fake_hermes_home / "state.db"))
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT INTO messages (session_id, timestamp) VALUES (?, ?)",
|
|
|
|
|
("cron_cd65df6fc1a8_xx", 1700000002.0),
|
|
|
|
|
)
|
|
|
|
|
conn.commit()
|
|
|
|
|
conn.close()
|
v0.50.210: gpt-5.5, cron titles, agent cache, bfcache fix, onboarding fix, mermaid CSP, PWA auth (#1056)
* feat(models): add gpt-5.5 to openai, openai-codex, copilot catalogs
Adds GPT-5.5 and GPT-5.5 Mini entries to the static _PROVIDER_MODELS
catalog so they appear in the model picker for the openai, openai-codex,
and copilot providers.
Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent
* fix(models): add gpt-5.5-mini to copilot provider catalog
* fix(renderer): suppress Mermaid Google Fonts CSP violation via fontFamily inherit (#1044)
Mermaid's built-in 'dark' and 'default' themes inject an @import for
fonts.googleapis.com/Manrope into every generated SVG. The CSP style-src
only allows cdn.jsdelivr.net, so this request is blocked on every diagram
render, filling the console with CSP errors.
Fix: pass fontFamily:'inherit' (and fontSize:'14px') in the themeVariables
block of mermaid.initialize() in renderMermaidBlocks(). This suppresses
Mermaid's external font import and uses the page's existing font stack.
Avoids adding fonts.googleapis.com to the CSP — no new external dependency,
no font FOUT, consistent with the rest of the UI typography.
3 regression tests added in tests/test_1044_mermaid_csp_font.py.
2215/2215 tests passing.
* fix(onboarding): non-standard provider/path cluster (#1029)
* fix(bfcache): restore full layout on tab/session restore — rail, topbar, panels (#1045)
The pageshow handler added for #822 only cleared the session search filter
and re-rendered the session list. This left the rest of the layout chrome
(topbar, rail icons, workspace panel, resize handles, gateway SSE) in the
stale bfcache DOM state, causing a broken layout (oversized search icon,
uninitialized rail) that required a hard refresh to fix.
Fix: extend the pageshow handler to re-run the full set of layout sync calls
that the boot IIFE runs on a fresh page load:
syncTopbar() — restores model chip, title, topbar state
syncWorkspacePanelState() — restores workspace panel open/closed
_initResizePanels() — reattaches panel resize drag listeners
startGatewaySSE() — reconnects the gateway SSE watcher
(bfcache-persisted connections are dead)
All four calls are typeof-guarded for safe degradation if a helper is not
yet defined. The existing #822 fixes (sessionSearch clear +
renderSessionListFromCache) are preserved unchanged.
loadSession() is intentionally NOT re-called — it would cause message
flicker; the sync calls above are sufficient to restore visual state.
7 regression tests added in tests/test_1045_bfcache_layout_restore.py.
2219/2219 tests passing.
* fix(bfcache): also close open dropdowns on bfcache restore (#1045)
Additional symptom noted in issue #1045: bfcache freezes the DOM including
any open dropdown/popover state. The thinking-level selector (and other
composer dropdowns) left open when navigating away would appear open without
user interaction on tab restore.
Extend the pageshow handler to call all four named close functions before
the layout sync:
closeModelDropdown() — composer model selector
closeReasoningDropdown() — thinking/reasoning effort selector
closeWsDropdown() — workspace chip dropdown
closeProfileDropdown() — profile switcher dropdown
All calls are typeof-guarded, matching the style of the layout sync calls
already in the handler.
2 new tests (9 total in test_1045_bfcache_layout_restore.py):
- pageshow closes all four named dropdowns
- dropdown closes appear before layout sync calls (clean state first)
2221/2221 tests passing.
* fix(bfcache): remove _initResizePanels() — bfcache preserves listeners
* fix(bfcache): remove _initResizePanels from pageshow — bfcache preserves listeners; update test
* fix(sessions): use cron job name as session title when available (#1032)
* fix(test): add id column to messages table in cron title test fixture
* fix(merge): inject cron title lookup into read_importable loop, remove stale sqlite3 block
* fix(pwa): redirect to /login client-side on 401 — fixes iOS PWA auth expiry trap (#1038)
When an auth session expires, the server returns a 302→/login for page
requests. In a normal browser this works fine, but in an iOS PWA running
in standalone mode the redirect navigates out of the PWA shell into Safari,
leaving the app permanently stuck on 'Authentication required' with no
recovery path.
Fix: intercept 401 responses client-side before surfacing any error.
- workspace.js api(): check res.status===401 first; call
window.location.href='/login' and return immediately (no throw)
- ui.js: add _redirectIfUnauth() helper; wire into all direct fetch()
calls that bypass api() — api/models, api/models/live, api/upload
All fetch paths that could receive a 401 now redirect cleanly within
the PWA frame rather than opening Safari.
6 regression tests added in tests/test_1038_pwa_auth_redirect.py.
2175/2175 tests passing.
* fix(pwa): preserve current URL in ?next= param on 401 redirect
* fix(test): update 401-redirect assertion to accept ?next= URL format
* feat(pwa): add _safeNextPath() to login.js so ?next= param is honored after re-login
Addresses reviewer suggestion: the ?next= URL set on 401 redirect was ignored by
the login success handler (always redirected to ./). _safeNextPath() validates and
returns the ?next= param with open-redirect guards: rejects non-path-absolute inputs,
// protocol-relative URLs, backslash variants, and control characters.
4 new regression tests added.
* Implement session agent cache for AIAgent reuse
Added session agent cache to reuse AIAgent across messages.
* Implement agent caching for session management
* Implement session agent eviction on session deletion
Added session agent eviction to prevent turn count leakage in recycled sessions.
* docs: v0.50.210 release notes — 7 PRs, 2239 tests (+27)
* docs(changelog): drop stale [Unreleased] entries duplicated by v0.50.210
Three entries in the [Unreleased] section are duplicates of items now
listed under v0.50.210:
- Mermaid CSP font fix (#1044) → v0.50.210 / Mermaid Google Fonts CSP
- bfcache layout restore (#1045) → v0.50.210 / bfcache layout and dropdown restore
- iOS PWA auth redirect (#1038) → v0.50.210 / Login redirects back to original URL
The original drafts landed in [Unreleased] when individual PRs (#1047,
#1048, #1043) were approved; the v0.50.210 release-notes commit then
added the same items under the version section without removing the
[Unreleased] copies. Drop the duplicates so users reading the CHANGELOG
don't see the same fix listed twice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: Pix (PiClaw, claude-opus-4-7) via Hermes Agent
Co-authored-by: Pix (Hermes) <aliceisjustplaying@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: qxxaa <mrhanoi@outlook.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:47:44 +00:00
|
|
|
|
|
|
|
|
sessions = models.get_cli_sessions()
|
|
|
|
|
|
|
|
|
|
assert sessions[0]["title"] == "Cli Session"
|