- 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>
* 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
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>
- 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 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>
Shows a compact bar + label in the composer footer after the first
response, displaying input/output token counts, context window fill
percentage, and estimated cost. Bar turns yellow >50% and red >75%.
Updates on every response completion via the existing usage data from
the done SSE event. Hidden until first response (no usage data yet).
Inspired by PR #75 (@MartinNielsenDev).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the workspace root is a git repo, a badge in the panel header
shows the current branch name, dirty file count, and ahead/behind
status. Updates on every root directory load.
Backend:
- git_info_for_workspace() in api/workspace.py runs lightweight git
commands (rev-parse, status --porcelain, rev-list) with 3s timeout
- New GET /api/git-info endpoint returns branch, dirty count, modified,
untracked, ahead, behind
Frontend:
- _refreshGitBadge() in workspace.js fetches git info on root load
- Git badge element in panel header shows branch + status
- Badge turns gold when workspace has uncommitted changes
Inspired by PR #75 (@MartinNielsenDev).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Token events from SSE now buffer and render at most once per animation
frame via requestAnimationFrame, instead of calling renderMd() and
writing to the DOM on every single token event.
Before: ~100 tokens/sec = ~100 DOM writes/sec (causes jank on heavy output)
After: ~100 tokens/sec batched to ~60 DOM writes/sec (one per frame)
The change is a small wrapper: _scheduleRender() gates rendering behind
a rAF flag so multiple tokens arriving between frames are batched into
a single renderMd() + scrollIfPinned() call.
Inspired by PR #75 (@MartinNielsenDev).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Date group headers (Pinned, Today, Yesterday, Earlier) are now clickable
to collapse/expand their session lists. Collapsed state persists to
localStorage across page reloads.
- Refactored renderSessionListFromCache to group sessions first, then
render groups with collapsible wrappers
- Extracted _renderOneSession() helper for reuse within group bodies
- Chevron indicator rotates -90deg when collapsed
- Pinned group header keeps its gold color
Inspired by PR #75 (@MartinNielsenDev).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix five stacking/overflow bugs in static/style.css (no JS changes):
1. Profile dropdown overlaps chat messages
.topbar lacked a stacking context -- added position:relative;z-index:10
so the dropdown (z-index:200 child) always paints above .messages (z-index:0)
2. Workspace dropdown clipped by sidebar overflow:hidden
.sidebar overflow:hidden was swallowing the upward-opening ws-dropdown.
Changed to overflow:visible -- scroll is already on .session-list, not .sidebar.
Added position:relative;z-index:10;overflow:visible to .sidebar-bottom.
3. Slash-command dropdown could render behind tool cards
.composer-wrap had position:relative but no z-index.
Added z-index:10 so cmd-dropdown always sits above .messages (z-index:0).
4. Skill picker dropdown clipped inside Settings modal
.settings-panel had overflow-y:auto which clipped the absolute-positioned
skill picker. Changed to overflow:visible + display:flex;flex-direction:column,
moved overflow-y:auto to .settings-body, raised skill-picker-dropdown to z-index:1100.
5. CLI session badge blocks action buttons on hover
Added .session-item.cli-session:hover::after { display:none } so the gold
'cli' label hides on hover, making archive/delete/pin fully reachable.
6. Workspace dropdown name+path crowded on same line
.ws-opt was a plain block with inline spans. Added flex-direction:column;gap:4px
and display:block to each child so name and path stack cleanly on separate lines.
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
1. Image preview onerror fires on clearPreview (#68)
clearPreview() set previewImg.src='' which triggered the stale onerror
handler, showing 'Could not load image' on every refresh/message.
Fix: null out onerror before clearing src.
2. CLI session badge covers delete button (#69)
The ::after 'cli' label occupied the same space as the hover-revealed
.session-actions overlay, making delete unreachable.
Fix: add padding-right to .cli-session, use margin-left:auto to push
badge right, add pointer-events:none so clicks pass through.
3. Tool cards visible through profile dropdown
The .messages container had no stacking context, so tool cards could
render above the profile dropdown (z-index:200).
Fix: add position:relative;z-index:0 to .messages to establish a
stacking context that keeps all children below overlays.
Closes#68, closes#69
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a server-side boolean setting (default: false) that controls whether
CLI sessions from state.db appear in the sidebar. Off by default so the
sidebar is clean until the user explicitly opts in.
- api/config.py: add show_cli_sessions to _SETTINGS_DEFAULTS and _SETTINGS_BOOL_KEYS
- api/routes.py: gate get_cli_sessions() call on the setting at request time
- static/index.html: checkbox in settings panel with description
- static/panels.js: load/save checkbox, refresh session list on save
- static/boot.js: load on startup alongside send_key and show_token_usage
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
The backend CLI session bridge (PR #56) was complete but the frontend
never connected to it:
1. css class never applied -- el.className never included 'cli-session'
so the gold border and 'cli' badge CSS was dead code. Fixed: append
' cli-session' when s.is_cli_session is true.
2. import never triggered -- click handler always called loadSession()
directly, never POST /api/session/import_cli. Fixed: for CLI sessions,
call import_cli first (idempotent -- safe to call on every click),
then fall through to loadSession() which now finds the imported copy.
3. profile filter silently hid CLI sessions -- filter required
s.profile === S.activeProfile, but CLI sessions may have profile=null
if the SQLite DB has no profile column. Fixed: CLI sessions always
pass the filter (s.is_cli_session || s.profile === S.activeProfile).
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Resolved CHANGELOG.md and SPRINTS.md conflicts: master added v0.29
(Sprint 23: Agentic Transparency), CLI bridge becomes v0.30.
Updated all version references to v0.30.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Read CLI sessions from the agent's state.db and surface them in the
WebUI sidebar alongside local sessions, with read-only display and
import-on-click to avoid data duplication.
Key changes:
- get_cli_sessions(): reads sessions list via parameterized SQL,
wrapped in sqlite3 context manager (no connection leaks)
- get_cli_session_messages(): reads messages for a CLI session
via parameterized SQL, also context-managed
- GET /api/sessions: merges WebUI + CLI sessions with dedup
(WebUI takes priority on same session_id)
- GET /api/session: falls back to CLI store if not a WebUI session
- POST /api/session/import_cli: imports a CLI session into the
WebUI store (idempotent, no duplicates on re-import)
- Imported sessions use get_last_workspace() for the workspace field
(not a hardcoded string) and carry the active profile tag
- CSS: .cli-session with ::after 'cli' indicator (no theme changes)
Fixes review feedback:
- SQLite connections use 'with' context managers (no leaks)
- Workspace uses real path via get_last_workspace()
- Profile awareness via api.profiles.get_active_profile_name()
- Parameterized SQL queries throughout (no injection risk)
- Graceful fallback when sqlite3 or state.db is missing
Token usage display:
- Add 'show_token_usage' boolean to settings (default: false, off by default)
- Settings panel: checkbox 'Show token usage after responses'
- /usage slash command: instant toggle with toast feedback, persists to
server, updates checkbox if settings panel is open, re-renders messages
- Boot: load show_token_usage alongside send_key on startup
- ui.js: gate usage badge on window._showTokenUsage flag
Timestamps:
- streaming.py: stamp 'timestamp' on every message that lacks one at
conversation completion; old messages (no timestamp field) now get a
wall-clock time the first time they're touched by a new turn
- messages.js: stamp _ts on the last assistant message at done-event time
so the time shows immediately on the current turn before next reload
- Timestamps already render in the UI (Sprint 14): faint time on each
role header line, full opacity on hover, full date in title tooltip
Store expanded directory paths in localStorage keyed by workspace path
(key: 'hermes-webui-expanded:{workspacePath}'). On root load (loadDir('.')),
restore the saved set for the current workspace and pre-fetch dir contents
for any restored expanded directories so the tree renders fully on first
paint without requiring a second click to expand.
Saves on every expand/collapse toggle. Switching workspaces automatically
picks up that workspace's own saved state. Per-workspace (not per-session)
so the same tree state is shared across sessions using the same workspace,
which is the natural expectation.
- routes.py: reject glob wildcards (* ? [ ]) in skill name param to
prevent rglob wildcard injection when serving linked files
- panels.js: replace inline onclick+esc() with data-* attributes and
addEventListener for skill tag removal and linked-file clicks;
esc() is HTML-safe but not JS-safe -- apostrophes in names caused
JS syntax errors and _cronSelectedSkills array corruption
- ui.js: fix _fmtTokens(null/undefined) returning 'null'/'undefined'
by guarding with (!n||n<0) -> '0'; add data-role attribute to msg-row
elements so usage badge correctly targets the last assistant row
instead of the last row regardless of speaker
- tests: rename test_sprint24.py -> test_sprint23.py (wrong sprint #);
add 3 new tests: path traversal rejection, wildcard name rejection,
cron create with skills; strengthen existing tests to assert field
presence explicitly (was using .get(field, 0)==0 which never caught
a missing field)
Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)
Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name
Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS
Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files
Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display
6 new tests. Total: 421 (409 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>