- api/updates.py: add _apply_lock to prevent concurrent stash/pull/pop
- static/boot.js: set check_for_updates:false on settings fetch failure
- static/panels.js: remove dead settingsWorkspace references (element removed from HTML)
- api/routes.py + static/boot.js: add ?test_updates=1 URL param for testing banner
without being behind on git (localhost-only simulate endpoint)
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two related bugs in get_available_models():
1. cfg_base_url undefined for string model configs (#117):
cfg_base_url was defined inside 'elif isinstance(model_cfg, dict)'
but referenced unconditionally at line 506. If model config was a
plain string, NameError crashed model detection. Fix: initialize
cfg_base_url='' before the conditional.
2. Configured default_model missing from dropdown (#116):
The OpenRouter branch substituted _FALLBACK_MODELS without checking
if the user's model.default was in the list. Models like
'openrouter/free' or custom local models were invisible. Fix: after
building all groups, check if default_model is present. If not,
inject it at the top of the matching provider group.
Closes#116, closes#117
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* security: fix four audit findings -- env race, signing key, upload traversal, password hash
1. Race condition in os.environ (HIGH): Per-session _agent_lock didn't
prevent cross-session env writes from interleaving. Added global
_ENV_LOCK in streaming.py that serializes the entire env save/restore
block across all sessions.
2. Predictable signing key (MEDIUM): sha256(STATE_DIR) was deterministic.
Now generates a random 32-byte key on first startup and persists it to
STATE_DIR/.signing_key (chmod 600). Existing sessions invalidated on
first restart (acceptable for a security fix).
3. Upload path traversal (MEDIUM): Filename '..' survived the regex
sanitization (dots are allowed chars). Added explicit rejection of
dot-only names and safe_resolve_ws() check to verify the resolved
path stays within the workspace.
4. Weak password hashing (MEDIUM): Replaced bare SHA-256 with PBKDF2-
SHA256 (600k iterations per OWASP). Uses stdlib hashlib.pbkdf2_hmac,
no new dependencies. Note: existing passwords must be re-set after
this change (hash format changed).
Closes#106
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use random signing key as PBKDF2 salt (replaces predictable STATE_DIR salt)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: light theme sidebar, roles, chips, active states -- full polish
Comprehensive light theme overrides for every remaining hardcoded
dark-theme element:
Sidebar:
- Session items: warm dark text instead of faint muted gray
- Active session: blue accent (matching --blue) instead of washed-out gold
- Pin stars/headers: deep gold #996b15 instead of bright yellow #f5c542
- Session actions gradient: light bg instead of dark overlay
- Search input: dark borders, proper focus ring
Role labels:
- You: solid #2d6fa3 blue instead of faint rgba(124,185,255,0.65)
- Hermes: solid #8a6520 gold instead of faint rgba(201,168,76,0.6)
- Role icons: proper bg/border contrast for light backgrounds
Chips and interactive elements:
- Project chips: dark borders, dark hover states
- Model chip: blue accent matching theme
- New chat button: blue accent borders
- All hover states: rgba(0,0,0,.XX) instead of rgba(255,255,255,.XX)
Other surfaces:
- Composer box borders and focus ring
- Tool cards, cron items, suggestions
- File tree hover, preview badges
- Profile/workspace dropdown hovers
- Settings, nav tooltips
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: update THEMES.md with all current CSS variables
Added typography variables (--strong, --em, --code-text, --code-inline-bg,
--pre-text) to the custom theme guide. Added note about light theme
selector overrides needed for hover/border contrast.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added 5 new CSS variables to every theme block:
--strong, --em, --code-text, --code-inline-bg, --pre-text
Light theme: dark brown text, warm gray italics, saddle brown code on
subtle bg. All previously invisible text is now readable.
All themes get palette-appropriate values matching their design language
(Solarized orange, Monokai yellow, Nord green, etc).
Also fixed: remaining white borders to var(--border), light scrollbar,
code-bg contrast, settings overlay, approval card text.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: topbar, dropdowns, toast, approval card, tooltips, main area,
inputs, and hover states all used hardcoded rgba(22,33,62), #1a2535, etc.
These only looked correct on the Dark theme — all other themes showed
jarring dark-navy elements on non-navy backgrounds.
New CSS variables added to every theme block:
- --surface: dropdowns, popups, toast, approval card
- --topbar-bg: topbar background
- --main-bg: main chat area background
- --input-bg: subtle input/button backgrounds
- --hover-bg: hover state backgrounds
- --focus-ring / --focus-glow: focus border and box-shadow
Light theme now has proper light-colored surfaces, inputs, and hover
states instead of invisible white-on-white.
THEMES.md updated with all new variables documented.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Unsaved-changes guard:
- _closeSettingsPanel() intercepts all three close paths (X button, overlay
click, Escape key) and checks _settingsDirty before closing
- If dirty: shows inline 'Unsaved changes' bar with Save & Close / Discard
- Discard reverts the live theme preview to what it was when panel opened
- _markSettingsDirty() wired to all inputs via addEventListener in loadSettingsPanel()
- saveSettings() now resets dirty flag and hides the bar on successful save
Theme improvements:
- Add 'Slate' theme: warm charcoal (#2b2d30 bg), a softer/lighter dark option
that sits between Dark and the full light themes
- Rework 'Light' theme: replace pure white (#f5f5f7) with warm off-white
(#f0ede8) -- warmer, lower contrast, less harsh on most displays
- Update /theme command to include 'slate' in valid list
- Add test_settings_set_theme_slate() to test_sprint26.py
- Sprint 12 and 13 headers: add missing (COMPLETED) labels
- Sprint 23 header: corrected from 'Profile/Workspace/Model Coherence' to
'Agentic Transparency + Context Visibility' (what it actually shipped)
- Sprint 24 Track C: removed stale self-referential cleanup items that are now done
- Sprint 26 added: full plan for pluggable UI themes (light/dark/solarized/monokai/nord)
including CSS variable architecture, flicker prevention, /theme slash command,
settings picker with live preview, and test spec
- ROADMAP.md: add v0.32/v0.33 to sprint history table, add Sprint 25/26 to feature checklist
- SPRINTS.md footer: add horizon sprint line
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Three bugs found during review:
1. Class is SessionDB not HermesState -- would silently no-op on every install
2. SessionDB.__init__ takes Path not str -- would crash with AttributeError
3. _execute_write() takes a callable not SQL+params -- wrong signature.
Replaced with public set_session_title() API.
4. Each call opened a persistent SQLite connection and never closed it.
Added try/finally db.close() to prevent WAL leak under sustained load.
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
WebUI sessions were invisible to 'hermes /insights' because the WebUI
bypasses the gateway and calls AIAgent.run_conversation() directly,
never writing to state.db.
New 'Sync usage to /insights' setting (default: off) that mirrors
WebUI session metadata (tokens, cost, model, title) into state.db
after each turn. Uses absolute token counts to avoid double-counting.
Components:
- api/state_sync.py: bridge module with sync_session_start() and
sync_session_usage(). Uses ensure_session() (idempotent) and
update_token_counts(absolute=True). All wrapped in try/except.
- api/config.py: new 'sync_to_insights' boolean setting
- api/streaming.py: calls sync_session_usage() after s.save()
- api/routes.py: same for the non-streaming chat path
- Settings UI: checkbox toggle with description
Default off because:
- Writing to state.db while CLI/gateway also writes could cause
WAL lock contention on busy systems
- Some users may not want WebUI sessions in /insights stats
Closes#92
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The context indicator in the composer footer now shows real data from
the agent's context compressor instead of hardcoded estimates:
- last_prompt_tokens / context_length (e.g. '12.4k / 200k (6%)')
- Bar color: blue <50%, yellow 50-75%, red >75%
- Hover tooltip shows exact numbers + compression threshold
- Cost appended when available
Backend: streaming.py now reads context_length, threshold_tokens, and
last_prompt_tokens from agent.context_compressor after run_conversation()
and includes them in the usage dict sent with the 'done' SSE event.
This matches the CLI's context window display (the bar that shows
current context vs total window).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The agent's run_conversation() already triggers context compression
internally, but the WebUI was unaware of the side effects:
1. Session ID rotation: compression creates a new session_id inside
the agent. The WebUI kept writing to the old session file, causing
silent data loss. Fix: detect agent.session_id mismatch after
run_conversation(), rename the session file, and update in-memory
caches.
2. No user notification: compression was invisible. Fix: emit a
'compressed' SSE event when compression is detected. Frontend shows
a system message and toast.
3. No manual control: Fix: add /compact slash command that sends a
message to the agent requesting context compression. Shows in the
autocomplete dropdown.
Detection works two ways:
- agent.session_id != original session_id (ID rotation)
- agent.context_compressor.compression_count > 0 (compressor state)
Closes#90
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The delete endpoint only removed sessions from the WebUI JSON store,
silently no-oping on CLI sessions (which live in state.db). The trash
button showed 'Conversation deleted' but the session reappeared on
next refresh.
Fix: after the existing WebUI delete, also call delete_cli_session()
which removes the session + messages from state.db. Wrapped in
try/except so WebUI-only sessions still delete normally.
New delete_cli_session() in api/models.py mirrors the existing
get_cli_session_messages() pattern for state.db access.
Closes#87
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When running tests without hermes-agent, 24 tests that depend on cron,
skills, approval, or agent backend modules now skip cleanly instead of
failing with 500 errors.
Detection: conftest.py checks if the agent dir exists and if cron.jobs
and tools.skills_tool are importable. When not available, an explicit
list of 24 test names is auto-marked with pytest.mark.skip.
Result:
- Without agent: 400 passed, 24 skipped, 0 failed
- With agent: all 424 tests run normally (skip logic is a no-op)
A warning banner prints at collection time:
"hermes-agent not found — 24 agent-dependent tests will be skipped"
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- routes.py /api/git-info: get_session raises KeyError on miss, does not
return None -- wrap in try/except KeyError to correctly return 404
(PR #82, api/routes.py line 222)
- style.css ctx-bar used undefined --teal CSS variable -- replaced with
--blue which is defined in :root and fits the existing color palette
(PR #83, static/style.css)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Agent review: hardcoded 128000 is wrong for Claude (200k), Gemini (1M),
and smaller models (8k-32k). Added a lookup table keyed by model name
substring covering major families with 128k fallback. TODO comment
for fetching exact values from server.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Agent review: l[0:2].strip() produced incorrect matches for git status
--porcelain XY format. Now checks both X (index) and Y (worktree)
columns for M/A/R status codes independently.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Agent review feedback: ordered array was constructed but never iterated
(the new code uses groups[] instead). Removed the dead variable.
Added comment noting function hoisting for _renderOneSession.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>