Commit Graph
100 Commits
Author SHA1 Message Date
Nathan Esquenazi e0a95193d8 fix: CSRF check supports reverse proxy headers (#218) (#219)
Tests pass: 20/21 QA suite (1 known skip), all browser API sanity checks green, CSRF fix verified end-to-end.
2026-04-10 01:24:18 -07:00
Nathan Esquenazi 9714c1779f Merge pull request #183 from nesquena/fix/i18n-review-fixes
fix: stray } in message HTML + JS-escape login locale strings
2026-04-08 19:07:29 -07:00
Nathan EsquenaziandClaude Opus 4.6 0126044ecb fix: stray } in message row HTML + JS-escape login locale strings
Agent review findings from PR #179:

1. static/ui.js line 542: extra } in ternary produced malformed HTML
   in message bubble div (''}} instead of ''}). Caused a literal }
   character to appear in the DOM.

2. api/routes.py: LOGIN_INVALID_PW and LOGIN_CONN_FAILED were inserted
   into JS string context without JS-string escaping. Added backslash
   escaping for ' and \ characters. Currently safe because locale values
   are hardcoded, but this prevents breakage if custom locale strings
   contain single quotes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 19:07:00 -07:00
Nathan Esquenazi 166a4c3e7b Merge pull request #179 from nesquena/feat/i18n-language-switcher
feat: pluggable i18n with English/Chinese language switcher in Settings
2026-04-08 18:59:11 -07:00
Nathan Esquenazi 1ac1e74512 fix: apply locale to DOM immediately on save — no reload needed
Add applyLocaleToDOM() which walks [data-i18n] elements and re-stamps
their textContent from t(). Called after setLocale() in saveSettings()
so the settings panel labels, checkboxes, and save button update live.
Also called on boot after /api/settings resolves so Chinese persists
without flicker on reload.

- static/i18n.js: add applyLocaleToDOM() function
- static/index.html: add data-i18n attributes to all settings panel
  static text nodes (labels, checkbox spans, save button)
- static/panels.js: call applyLocaleToDOM() + syncTopbar() after save
- static/boot.js: call applyLocaleToDOM() alongside setLocale() on boot
2026-04-08 18:58:20 -07:00
Nathan Esquenazi b979b4c443 feat: pluggable i18n with English/Chinese language switcher in Settings
Introduces a locale bundle system that makes UI language switchable at
runtime and trivially extensible to any future language.

Architecture:
- static/i18n.js: LOCALES object with 'en' and 'zh' bundles, t(key)
  helper with English fallback, setLocale()/loadLocale() for persistence
  via localStorage. Adding a new language = adding one object.
- api/config.py: 'language' setting (default 'en'), BCP-47 validation
- api/routes.py: _LOGIN_LOCALE dict for server-rendered login page;
  template placeholders substituted at request time from saved setting
- static/index.html: loads i18n.js first (before other scripts); adds
  Language dropdown to Settings panel, auto-populated from LOCALES

Wiring:
- boot.js: applies server-persisted locale at startup (after /api/settings
  fetch); speech recognition lang follows _locale._speech
- panels.js: populates Language dropdown from LOCALES on settings open;
  saves + applies locale on Save Settings
- All JS files: hardcoded user-facing strings replaced with t() calls

Coverage:
- test_sprint20.py: relaxed recognition.lang assertion to accept dynamic
  locale-driven assignment (behavior unchanged for English default)
- 499/499 tests pass

Closes #177 (incorporates Chinese translations as a proper locale bundle
rather than hardcoded strings, so English default is fully preserved)
2026-04-08 18:57:50 -07:00
Nathan Esquenazi c04caf3f5b Merge pull request #180 from nesquena/feat/notification-sound-browser
feat: notification sound and browser notifications
2026-04-08 18:56:11 -07:00
Nathan EsquenaziandClaude Opus 4.6 799cbb7eca fix: update sound/notification globals in password branch + close AudioContext
Agent review findings:
- _soundEnabled/_notificationsEnabled not updated in the password-save
  early-return branch of saveSettings() — fixed
- AudioContext never closed after oscillator finishes — added osc.onended
  callback to ctx.close() preventing resource accumulation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 18:55:57 -07:00
Nathan Esquenazi 0d83837650 Merge pull request #182 from nesquena/fix/thinking-display-edge-cases
fix: harden thinking display streaming edge cases
2026-04-08 18:51:06 -07:00
Nathan Esquenazi 5f7564e8bb fix: harden thinking block streaming display
Hide partial <think> tag prefixes during streaming and rename the local display variable for clarity. References #181.
2026-04-08 18:14:47 +00:00
Nathan EsquenaziandClaude Opus 4.6 5e899ee8fe feat: notification sound and browser notifications on task completion
Add two new settings (both default off):
- sound_enabled: plays a short tone via Web Audio API when assistant
  finishes a response or requests approval
- notifications_enabled: shows a browser notification when a response
  completes while the tab is in the background

Uses Web Audio API (oscillator) instead of bundled MP3 file — zero
additional assets. Follows the standard 4-file settings pattern.

Also skip test_valid_skill_accepted when hermes-agent not installed
(skills endpoint returns 500 without the agent module).

Inspired by #176 (DavidSchuchert)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 09:02:02 -07:00
Nathan Esquenazi 907bb224d9 Merge pull request #178 from nesquena/fix/streaming-env-lock-deadlock
fix: resolve _ENV_LOCK deadlock that blocks chat after first message
2026-04-08 07:26:53 -07:00
Nathan EsquenaziandClaude Opus 4.6 d919b584c6 docs: v0.39.1 release notes for ENV_LOCK deadlock fix
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 07:26:41 -07:00
Nathan Esquenazi 4422a87de9 fix: resolve _ENV_LOCK deadlock that blocks chat after first message
The v0.39.0 security sprint introduced _ENV_LOCK to protect env var
mutations in the streaming path. The implementation held the lock for
the entire agent run (potentially minutes), then tried to re-acquire
it in the finally block — a guaranteed deadlock on any non-reentrant
threading.Lock().

Result: first message completes (done event fires before finally hits),
but the lock is never released. Every subsequent chat/start POST blocks
forever waiting for that lock.

Fix: narrow the lock scope to just the env mutation. Set the vars inside
the with block, then let the lock release before the agent starts. The
finally block re-acquires cleanly since it no longer re-enters an
already-held lock.

No logic change — only the critical section boundary moves.
2026-04-08 14:22:39 +00:00
Nathan Esquenazi ac969e4bd6 Merge pull request #169 from tgaalman/fix/hermes-reasoning-display
Fix: show top-level reasoning field in thinking card
2026-04-07 12:01:23 -07:00
Nathan Esquenazi a9b2a2f22f Merge pull request #168 from kevin-ho/oled-theme
feat: add OLED theme for true black displays
2026-04-07 11:12:05 -07:00
Nathan EsquenaziandClaude Opus 4.6 8aa1c9684d fix: sync message_count to state.db for /insights (#163) (#164)
* fix: sync message_count to state.db for /insights (#163)

sync_session_usage() didn't write message_count to state.db, so
/insights showed 0 messages for all WebUI sessions even with
sync_to_insights enabled.

Added message_count parameter to sync_session_usage() and pass
len(s.messages) from both the streaming and non-streaming chat paths.

Fixes #163

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use callable pattern for _execute_write in sync_session_usage

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:56:27 -07:00
Nathan EsquenaziandClaude Opus 4.6 d89639dbb3 fix: tool call cards persist across page reload (#140) (#149)
Tool cards disappeared on page refresh because assistant messages with
only tool_use content (no text) were filtered out of the visible
messages list. Since tool cards anchor to DOM rows via data-msg-idx,
removing the anchor row meant cards had nothing to attach to.

Fix: keep assistant messages in the render list if they contain
tool_use blocks, even when they have no text content. The row renders
with the role label but empty body, providing an anchor point for the
tool card insertion pass.

Fixes #140

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:10:33 -07:00
Nathan EsquenaziandClaude Opus 4.6 2442fca5e5 fix: personalities from config.yaml + ephemeral_system_prompt (#139) (#148)
The previous implementation read SOUL.md files from a filesystem directory.
The Hermes agent uses config.yaml agent.personalities section with string
or dict format (system_prompt, tone, style), resolved via
_resolve_personality_prompt() and passed to AIAgent via
ephemeral_system_prompt.

Changes:
- /api/personalities: reads from config.yaml agent.personalities, not
  filesystem SOUL.md directories. Calls reload_config() to pick up
  config changes without restart.
- /api/personality/set: resolves prompt from config.yaml using the same
  logic as hermes-agent cli.py (string or dict with system_prompt/tone/style)
- streaming.py: passes personality via agent.ephemeral_system_prompt
  (agent's own mechanism) instead of prepending to system_message
- Removed unused 're' import from streaming.py
- Updated tests to match config-based approach

Fixes #139

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:10:30 -07:00
Nathan EsquenaziandClaude Opus 4.6 442b0d872a fix: multi-provider model routing via @provider: hint (#138) (#146)
The previous fix (#142) prefixed non-default provider models with
'provider/model' which then hit the cross-provider guard and routed
to OpenRouter — worse than before for users without an OpenRouter key.

New approach: non-default provider models use '@provider:model' format
(e.g. @minimax:MiniMax-M2.7). resolve_model_provider() parses this
hint and returns (bare_model, provider, None). streaming.py and
routes.py then pass the resolved provider to
resolve_runtime_provider(requested=provider) which gets the correct
per-provider API key and base_url from hermes-agent.

This uses the agent's own credential resolution instead of reinventing
routing logic in the webui.

Fixes #138

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:10:26 -07:00
Nathan Esquenazi 3bba645364 Merge pull request #145 from jeffscottward/fix/claude-haiku-model-id
fix: correct Claude Haiku model ID from 3-5 to 4-5
2026-04-06 13:27:31 -07:00
Nathan EsquenaziandClaude Opus 4.6 58eb6e7fd5 feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration

Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.

Backend:
- GET /api/personalities: lists available personalities from the active
  profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
  and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
  the system_message when run_conversation is called

Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality

Session model: new 'personality' field (backward-compatible, defaults to None)

Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: path traversal in personality name + case sensitivity

Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.

Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /personality command — hardened, compact() fix, tests

Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
  personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
  resolve() check — prevents reading SOUL.md from symlink targets
  outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
  so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
  prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.

Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:16:37 -07:00
Nathan EsquenaziandClaude Opus 4.6 76cdfb69e0 fix: prefix non-default provider model IDs for correct routing (#142)
* fix: prefix non-default provider model IDs for correct routing

When multiple providers are configured, models from non-default providers
(e.g. MiniMax when Anthropic is default) were sent as bare names without
provider context. resolve_model_provider() couldn't determine the target
provider and routed them to the default provider's API, which failed.

Fix: get_available_models() now prefixes model IDs with the provider name
(e.g. minimax/MiniMax-M2.7) for providers that are NOT the active config
provider. The default provider's models keep bare names for direct API
routing. This matches the existing pattern for OpenRouter models.

Added 2 tests to test_model_resolver.py for cross-provider routing.

Closes #138
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: model prefix — null-guard, case normalization, mutation safety, tests

Four fixes on top of original PR:
- active_provider=None guard: without a confirmed provider all models were
  being prefixed. Only prefix when active_provider is set.
- Case normalisation: compare pid against active_provider.lower() so
  config.yaml entries like 'Anthropic' match pid 'anthropic'.
- Mutation safety: default branch used raw reference to _PROVIDER_MODELS[pid];
  the default_model injector later calls list.insert() on that reference,
  permanently mutating the shared constant. Both branches now use a copy.
- Already-prefixed model IDs pass through as-is (no double-prefix).

Added 3 tests for get_available_models() prefix behaviour:
- Non-default provider models are prefixed
- Active provider's own entries remain bare
- No double-prefix when active_provider is absent

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:05:44 -07:00
Nathan EsquenaziandClaude Opus 4.6 4622b64ca9 fix: tool call cards persist correctly after page reload (#141)
* fix: tool call cards persist correctly after page reload

Root cause: the insertion logic looked for the NEXT assistant row to
insert BEFORE, but when the triggering assistant message contained only
tool_use blocks (no text), it was filtered from the DOM by msgContent()
and no anchor row existed. Cards were silently dropped.

Fix: find the row AT the assistant_msg_idx first (exact match), fall
back to the nearest preceding visible row, then insert AFTER it (not
before the next). This handles both text+tool and tool-only assistant
messages correctly.

Closes #140
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: tool card ordering — role-filter fallback anchor, preserve order for same-anchor groups

Two bugs in the initial PR:
- Fallback 'nearest row' had no role filter, could anchor to a user message
  row causing cards to appear after a user bubble. Fixed: check role==='assistant'
  in the fallback loop.
- Back-to-back tool-card groups sharing a filtered anchor were inserted in
  reverse order because each group re-read anchorRow.nextSibling from the live
  DOM. Fixed: track the last inserted node per anchor via anchorInsertAfter Map,
  so each subsequent group appends after the previous one.

Also restores the 'Collect card elements before they get moved to DOM' comment
explaining why Array.from() is used on frag before DOM insertion.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:02:25 -07:00
Nathan Esquenazi 173261c428 Merge pull request #136 from nesquena/docs/changelog-v0.36.3
docs: update CHANGELOG with v0.36.3 configurable bot name
2026-04-06 08:14:59 -07:00
Nathan EsquenaziandClaude Opus 4.6 863dc4e938 docs: update CHANGELOG with v0.36.3 configurable bot name
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:14:40 -07:00
Nathan Esquenazi 4407c3097b Merge pull request #135 from nesquena/fix/pr-131-bot-name
fix: harden bot_name feature from PR #131 — crash guard, XSS, tests
2026-04-06 08:13:49 -07:00
Nathan Esquenazi 71dd691ed0 fix: harden bot_name — crash guard, XSS escape, sanitization, tests
- Move `import html` to module top (was inside function body)
- Fix IndexError crash in /login when bot_name is empty string;
  use `or 'Hermes'` fallback instead of .get() default which
  doesn't guard against stored empty string
- Add server-side sanitization in POST /api/settings: strip + default
  empty/whitespace bot_name to 'Hermes' before persisting
- Escape _bn initial char in ui.js innerHTML (esc() consistency)
- Add maxlength=64 to #settingsBotName input field
- Add tests/test_sprint27.py: 9 tests covering API round-trip,
  empty/whitespace defaults, login page rendering, and XSS escaping
2026-04-06 15:06:16 +00:00
Nathan EsquenaziandClaude Opus 4.6 e829fa50d5 fix: OpenRouter models stripped of prefix, causing 404 (#116)
When config has provider=openrouter and model=openrouter/free,
resolve_model_provider() stripped the 'openrouter/' prefix because
prefix == config_provider. This sent 'free' to OpenRouter's API,
which returned 404 (model not found).

OpenRouter always needs the full provider/model path (e.g.
openrouter/free, anthropic/claude-sonnet-4.6). The prefix-stripping
logic is only correct for direct-API providers.

Fix: skip prefix stripping entirely when config_provider is 'openrouter'.
Return the full model_id with provider='openrouter'.

Added 7 unit tests for resolve_model_provider() covering:
- openrouter/free keeps full path (the bug)
- openrouter cross-provider models keep full path
- direct API providers still strip prefix correctly
- cross-provider routing to openrouter
- bare model names use config provider
- empty model returns defaults

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 13:58:37 -07:00
Nathan EsquenaziandClaude Opus 4.6 48ba2e79e2 fix: Enter key reliably submits login form (#124)
The login form used 'return doLogin(event)' in onsubmit, but doLogin is
async so it returns a Promise (truthy), which some browsers interpret as
'proceed with native form submit'. Changed to 'doLogin(event);return false'
and added an explicit onkeydown Enter handler on the password input as
belt-and-suspenders.

Closes #124

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 12:45:57 -07:00
Nathan Esquenazi 52e3fb70e0 Merge pull request #123 from nesquena/docs/update-parity-assessment
docs: update SPRINTS parity assessment from v0.21 to v0.36
2026-04-05 10:10:49 -07:00
Nathan EsquenaziandClaude Opus 4.6 c1bbdf9aeb docs: update SPRINTS parity assessment from v0.21 to v0.36
CLI parity: ~90% -> ~95% (added profiles, CLI bridge, compaction,
self-update, git detection, slash commands, rAF throttle)

Claude parity: ~70% -> ~85% (added themes, voice, mobile responsive,
collapsible groups, context indicator, token display, Docker, subagent
delegation cards)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 10:10:33 -07:00
Nathan Esquenazi af76622c97 Merge pull request #121 from nesquena/feat/self-update-checker
feat: self-update checker with one-click update
2026-04-05 09:27:39 -07:00
Nathan EsquenaziandClaude Opus 4.6 27706367b7 docs: v0.36 release notes, version bump for self-update checker
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:27:27 -07:00
Nathan Esquenazi beb56b1a8b fix: apply_update concurrency lock, boot.js settings-fail guard, dead workspace code, test_updates URL param
- 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)
2026-04-05 16:20:12 +00:00
Nathan EsquenaziandClaude Opus 4.6 8d1b7a1e01 feat: self-update checker with one-click update for WebUI + Agent
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>
2026-04-05 09:11:44 -07:00
Nathan EsquenaziandClaude Opus 4.6 b327103885 fix: model dropdown missing custom/configured models (#116, #117)
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>
2026-04-05 08:29:40 -07:00
Nathan Esquenazi 2f01afd557 Merge pull request #115 from mangodxd/cleanup/fix-py-issues-2a3ea49e
chore: add missing type hints (10 files)
2026-04-04 23:55:21 -07:00
Nathan EsquenaziandClaude Opus 4.6 74fcd2e0ab fix: correct 9 inaccurate type hints
- get_password_hash() -> str | None (not bool, returns hash or None)
- parse_cookie() -> str | None (not None, returns cookie value)
- Session.__init__ session_id: str (not int, uuid hex)
- Session.__init__ project_id: str (not int)
- Session.__init__ **kwargs (remove incorrect dict annotation)
- Session.load() remove -> None (returns Session | None)
- import_cli_session session_id: str (not int)
- sync_session_start session_id: str (not int)
- sync_session_usage session_id: str (not int)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:54:58 -07:00
Nathan Esquenazi 3d063b08a9 rebuild: clean public history after AGENTS.md rewrite removal 2026-04-05 06:25:24 +00:00
Nathan EsquenaziandClaude Opus 4.6 39066bc614 security: fix env race, signing key, upload traversal, password hash (#106)
* 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>
2026-04-04 22:25:08 -07:00
Nathan EsquenaziandClaude Opus 4.6 2f58724863 fix: light theme final polish -- sidebar, roles, chips, all interactive elements
* 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>
2026-04-04 22:11:42 -07:00
Nathan EsquenaziandClaude Opus 4.6 5762aaafba fix: theme-aware text colors -- light mode readable, all themes polished
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>
2026-04-04 21:59:12 -07:00
Nathan EsquenaziandClaude Opus 4.6 2eddef3275 fix: replace 30+ hardcoded dark-navy colors with theme CSS variables
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>
2026-04-04 21:43:53 -07:00
Nathan Esquenazi 7bcd6623e9 Merge pull request #98 from nesquena/sprint-26-themes
Sprint 26: Pluggable UI themes — dark, light, solarized, monokai, nord
2026-04-04 21:13:10 -07:00
Nathan EsquenaziandClaude Opus 4.6 82a942a2b1 docs: v0.34 release — themes CHANGELOG, README, add light to picker
- CHANGELOG: v0.34 Sprint 26 entry (6 themes, /theme command, settings UX)
- README: themes section, updated slash commands, THEMES.md in docs list
- THEMES.md: added Slate to theme table, matches actual CSS/dropdown
- commands.js: added 'light' to /theme valid list, updated description
- index.html: added Light option to theme dropdown, version v0.34
- SPRINTS/CHANGELOG footers updated to v0.34 / 433 tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:13:01 -07:00
Nathan Esquenazi 805fa296c8 fix: cut light theme from picker, shorten Save button label 2026-04-05 04:06:02 +00:00
Nathan Esquenazi b8b063f325 fix: settings panel taller -- show Save button without scrolling 2026-04-05 04:01:56 +00:00
Nathan Esquenazi 882fc947e5 fix: settings unsaved-changes guard, add Slate theme, improve Light theme
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
2026-04-05 04:00:24 +00:00
Nathan EsquenaziandClaude Opus 4.6 96137750a4 feat: Sprint 26 — pluggable UI themes (dark, light, solarized, monokai, nord)
Five built-in themes with instant switching, persistent preference,
and zero-flicker loading. Custom themes are pure CSS additions.

Theme system:
- CSS variable overrides via :root[data-theme="name"] blocks
- Flicker prevention: inline <script> reads localStorage before
  stylesheet parses, preventing dark-flash on light-mode users
- Server-side persistence via settings.json (theme field)
- Boot.js syncs server preference to DOM + localStorage

Built-in themes:
- Dark (default): deep navy/indigo, muted blue accents
- Light: clean white/gray, high contrast, scrollbar overrides
- Solarized Dark: teal background, warm accents
- Monokai: warm dark, green/pink accents
- Nord: arctic blue-gray, calm and minimal

UI integration:
- Settings panel: theme dropdown with instant live preview
- /theme slash command: /theme dark|light|solarized|monokai|nord
- No enum constraint on theme setting — custom themes just work

Documentation:
- THEMES.md: how to switch themes, create custom themes, contribute

8 new tests. All 408 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 20:48:05 -07:00
Nathan EsquenaziandClaude Opus 4.6 bb595afde9 feat: opt-in state.db sync for /insights visibility (#92)
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>
2026-04-04 20:07:05 -07:00
Nathan Esquenazi e0d52f39dc Merge pull request #91 from nesquena/feat/auto-compaction-handling
feat: handle auto-compaction side effects + /compact command (#90)
2026-04-04 19:00:15 -07:00
Nathan EsquenaziandClaude Opus 4.6 4a6769ec08 docs: v0.32 release notes, version bump for auto-compaction handling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 19:00:02 -07:00
Nathan EsquenaziandClaude Opus 4.6 2797e5189b feat: context window usage indicator with real agent data
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>
2026-04-04 18:50:17 -07:00
Nathan EsquenaziandClaude Opus 4.6 429a0ea228 feat: handle auto-compaction side effects + /compact command
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>
2026-04-04 18:46:34 -07:00
Nathan EsquenaziandClaude Opus 4.6 181641db6b fix: allow deleting CLI sessions from sidebar (#87)
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>
2026-04-04 17:33:55 -07:00
Nathan EsquenaziandClaude Opus 4.6 fdc7d281a3 fix: auto-skip agent-dependent tests when hermes-agent not installed (#86)
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>
2026-04-04 14:51:15 -07:00
Nathan Esquenazi 5a17df2573 Merge pull request #85 from nesquena/docs/v0.31-release-notes
docs: v0.31 — update all markdown for Sprint 24 features
2026-04-04 14:30:21 -07:00
Nathan EsquenaziandClaude Opus 4.6 1e6746c66b docs: v0.31 — update all markdown for Sprint 24 features
README: added rAF-throttled streaming, context usage indicator, git
detection badge, collapsible date groups. Updated architecture line
counts to current values.

ROADMAP: v0.29 -> v0.31, marked streaming perf, git detection,
collapsible groups, and context indicator as done (Sprint 24).

SPRINTS: v0.30.1 -> v0.31 in header and footer.

CHANGELOG: footer updated to v0.31.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 14:29:57 -07:00
Nathan Esquenazi fffdc34fdb Merge pull request #83 from nesquena/feat/context-usage-indicator
feat: context usage indicator in composer footer
2026-04-04 14:26:23 -07:00
Nathan EsquenaziandClaude Opus 4.6 c1db709ef3 fix: model-aware context window estimation instead of hardcoded 128k
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>
2026-04-04 14:26:13 -07:00
Nathan Esquenazi 4b55f08961 Merge pull request #82 from nesquena/feat/workspace-git-detection
feat: workspace git detection with branch/status badge
2026-04-04 14:25:17 -07:00
Nathan EsquenaziandClaude Opus 4.6 e184eb5ff5 fix: correct modified/untracked counting in git status parser
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>
2026-04-04 14:25:07 -07:00
Nathan Esquenazi b60c4fd498 Merge pull request #80 from nesquena/feat/collapsible-date-groups
feat: collapsible date groups in session sidebar
2026-04-04 14:24:13 -07:00
Nathan EsquenaziandClaude Opus 4.6 a2243f4c4f fix: remove unused ordered variable, add hoisting note
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>
2026-04-04 14:24:04 -07:00
Nathan Esquenazi ac5929918c Merge pull request #81 from nesquena/feat/raf-streaming-throttle
perf: rAF-throttled token streaming for smoother rendering
2026-04-04 14:22:55 -07:00
Nathan Esquenazi 9ceb3773f8 Merge pull request #79 from nesquena/docs/roadmap-community-features-pr75
docs: add community feature ideas from PR #75 to roadmap
2026-04-04 14:22:42 -07:00
Nathan EsquenaziandClaude Opus 4.6 516062bd41 feat: context usage indicator in composer footer
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>
2026-04-04 14:11:28 -07:00
Nathan EsquenaziandClaude Opus 4.6 d8e6079a2c feat: workspace git detection with branch/status badge
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>
2026-04-04 14:08:25 -07:00
Nathan EsquenaziandClaude Opus 4.6 c0769c50a2 perf: rAF-throttled token streaming for smoother rendering
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>
2026-04-04 14:05:51 -07:00
Nathan EsquenaziandClaude Opus 4.6 42590fceb3 feat: collapsible date groups in session sidebar
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>
2026-04-04 14:05:00 -07:00
Nathan EsquenaziandClaude Opus 4.6 84b6dde078 docs: add community feature ideas from PR #75 to roadmap
Extracted genuinely new feature ideas from @MartinNielsenDev's PR #75
and added them to the Advanced/Future section of the roadmap:

- Subagent session tree (sidebar hierarchy with expand/collapse)
- Specialized tool card renderers (diff, terminal, todo views)
- Streaming performance (rAF-throttled token rendering)
- Git integration modal (branch/status/log in workspace)
- Collapsible date groups in session list
- LLM-generated session titles
- Workspace git detection (branch/dirty status)
- Clarify dialog (blocking agent questions)
- Gateway approval polling
- Unified session storage (SessionDB shared with CLI)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:59:18 -07:00
Nathan Esquenazi e2d24f57ac Merge pull request #78 from carlytwozero/fix/pass-api-key-to-aiagent
fix: pass api_key to AIAgent for non-Anthropic /anthropic providers
2026-04-04 13:05:29 -07:00
Nathan Esquenazi 6c54eda462 Merge pull request #76 from vCillusion/fix/agent-dir-pip-shadow
fix: resolve pip packages from site-packages instead of agent dir
2026-04-04 12:01:55 -07:00
Nathan Esquenazi dac58c8162 Merge pull request #70 from nesquena/fix/ui-polish-cli-badge-image-error-zindex
fix: UI polish -- image error, CLI badge overlap, dropdown z-index
2026-04-04 09:18:01 -07:00
Nathan EsquenaziandClaude Opus 4.6 6a4b20f3f2 fix: three UI glitches -- image error, CLI badge overlap, dropdown z-index
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>
2026-04-04 09:09:31 -07:00
Nathan EsquenaziandClaude Opus 4.6 90b5ad8d99 fix: strip webui metadata from messages before sending to LLM API (#67)
The webui stores display-only fields on messages (attachments, timestamp,
_ts) for UI rendering. These leaked into the conversation_history passed
to AIAgent.run_conversation(). Most providers ignore unknown fields, but
Z.AI/GLM tries to deserialize 'attachments' as its native ChatAttachments
type, causing HTTP 400 on every subsequent message after an image upload.

Fix: _sanitize_messages_for_api() creates a clean copy with only
API-standard keys (role, content, tool_calls, tool_call_id, name,
refusal) before passing to run_conversation(). Applied to both the
streaming path (streaming.py) and non-streaming path (routes.py).

Closes #66

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 22:13:12 -07:00
Nathan Esquenazi c1320b4712 Merge pull request #62 from nesquena/docs/v0.30.1-release
docs: v0.30.1 release — CLI bridge fixes + README update
2026-04-03 21:12:15 -07:00
Nathan Esquenazi d3b693524f docs: v0.30.1 release — CLI bridge fixes, README update
CHANGELOG: add v0.30.1 entry covering PRs #57-#61 (CLI session bridge
fixes: sidebar rendering, profile-aware state.db path, silent SQL error,
show/hide toggle in Settings.

README: add CLI session bridge, token/cost display, subagent cards,
/usage command, skills linked files, show CLI sessions toggle.

Version label: v0.30 -> v0.30.1 in index.html, SPRINTS, CHANGELOG footer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EOF
)
2026-04-03 21:11:52 -07:00
Nathan Esquenazi 4f10080501 Merge pull request #56 from thadreber-web/feat/cli-session-bridge
feat: CLI session bridge - read CLI sessions from agent SQLite store
2026-04-03 20:42:21 -07:00
Nathan EsquenaziandClaude Opus 4.6 f8ea02c14d merge: resolve conflicts with master (v0.29), bump to v0.30
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>
2026-04-03 20:42:11 -07:00
Nathan EsquenaziandClaude Opus 4.6 122fe955b6 docs: v0.29 release notes for CLI session bridge, version bump
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:39:27 -07:00
Nathan EsquenaziandClaude Opus 4.6 017d7f1eca fix: add missing HOME import to models.py (NameError crash)
get_cli_sessions() and get_cli_session_messages() reference HOME but
it was not imported from api.config. This caused /api/sessions to 500
on every request, breaking the entire session list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:37:34 -07:00
Nathan Esquenazi be951a4d1d Merge pull request #54 from nesquena/fix/stricter-docker-regex
fix(ci): stricter semver regex for Docker tags
2026-04-03 19:33:14 -07:00
Nathan EsquenaziandClaude Opus 4.6 bba9a236c3 fix(ci): use stricter semver regex for Docker tag extraction
Use v(\d+\.\d+(?:\.\d+)?) instead of v(.*) to only match real
version numbers (v0.29, v0.29.1), not arbitrary strings.
Keep latest unconditional since all tag pushes are releases.

Based on review of PR #52 approach vs #53.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:32:50 -07:00
Nathan Esquenazi 846565484b Merge pull request #53 from nesquena/fix/release-docker-tags
fix(ci): Docker tag extraction for 2-part versions (v0.29)
2026-04-03 19:28:18 -07:00
Nathan EsquenaziandClaude Opus 4.6 1a579ef9cf fix(ci): use match pattern for Docker tags to support 2-part versions
The semver pattern in docker/metadata-action requires 3-part versions
(e.g. v0.29.0). Our tags use 2-part (v0.29), causing the metadata
step to produce empty tags, which made build-push-action fail.

Fix: use type=match with regex to extract the version string directly,
plus type=raw for the latest tag unconditionally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:27:41 -07:00
Nathan Esquenazi 1605d65226 Merge pull request #51 from nesquena/sprint-23-agentic-transparency
Sprint 23: Agentic transparency — token costs, subagent cards, skill picker, linked files
2026-04-03 19:16:42 -07:00
Nathan EsquenaziandClaude Opus 4.6 3d4d7f2b53 fix: test hardening for sprint 23 — handle missing cron/skills modules in test env
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:16:17 -07:00
Nathan Esquenazi 2fb2ddeaaa feat: token usage toggle (setting + /usage command) + timestamp fixes
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
2026-04-03 19:11:36 -07:00
Nathan Esquenazi b1d687ba22 feat: persist workspace tree expanded state across refreshes
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.
2026-04-03 19:11:36 -07:00
Nathan Esquenazi c1dcd73502 fix: security, correctness, and test hardening from review
- 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)
2026-04-03 19:11:36 -07:00
Nathan EsquenaziandClaude Opus 4.6 df06c1cdca feat: Sprint 23 — agentic transparency + polish
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>
2026-04-03 18:33:49 -07:00
Nathan Esquenazi 2c0f6e80b6 Merge pull request #49 from nesquena/docs/update-all-markdown-v0.28
docs: update all markdown to v0.28.1 state
2026-04-03 14:20:29 -07:00
Nathan EsquenaziandClaude Opus 4.6 4a4af209ad docs: update all markdown to v0.28.1 state
- README: add GHCR pre-built images to Docker section, update line counts
  and test count (426 tests, 22 files), add CI/CD to architecture tree
- ROADMAP: update header to v0.28.1/426 tests, mark all user-requested
  features as shipped, collapse completed Waves 3-7 into summary table,
  update architecture line counts, add CI/CD row
- CHANGELOG: add v0.28.1 entry for CI pipeline + multi-arch Docker builds,
  update footer version
- SPRINTS: update header and footer to v0.28.1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 14:18:50 -07:00
Nathan Esquenazi 279690e4c1 Merge pull request #48 from nesquena/fix/ci-action-versions
fix(ci): use standard version tags for GitHub Actions
2026-04-03 14:10:39 -07:00
Nathan EsquenaziandClaude Opus 4.6 2766314e81 fix(ci): use standard version tags for GitHub Actions
The SHA-pinned versions from the security hardening commit referenced
non-existent commit hashes, causing the workflow to fail with 'unable
to resolve action'. Switch to standard major version tags (v4, v3, v2,
v6, v5) which are the recommended approach for GitHub-maintained and
well-known actions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 14:09:36 -07:00
Nathan Esquenazi 4a3b9571f1 fix(ci): pin all GitHub Actions to full commit SHAs for supply chain security
Pinned all 7 third-party actions from mutable version tags to immutable
commit SHAs. Mutable tags (e.g. @v4) can be force-pushed by the action
author (or a compromised account) to inject malicious code into the workflow,
which runs with write access to the repo and GHCR registry.

Also moved 'permissions' from workflow level to job level (best practice:
scope permissions as narrowly as possible).

Pin mapping:
  actions/checkout@v4               -> @11bd71901bbe...  (v4.2.2)
  softprops/action-gh-release@v2    -> @c062e08bd532...  (v2.2.1)
  docker/setup-qemu-action@v3       -> @49b3bc8e6bdd...  (v3.2.0)
  docker/setup-buildx-action@v3     -> @c47758b77c97...  (v3.7.1)
  docker/login-action@v3            -> @9780b0c442fb...  (v3.3.0)
  docker/metadata-action@v5         -> @369eb591f429...  (v5.6.1)
  docker/build-push-action@v6       -> @ca877d9245fe...  (v6.10.0)
2026-04-03 21:02:08 +00:00
Nathan Esquenazi c488031fe3 Merge pull request #45 from nesquena/fix/profile-creation-docker-fallback
fix: profile creation fallback for Docker (#44)
2026-04-03 14:01:04 -07:00
Nathan EsquenaziandClaude Opus 4.6 94b080fa1e docs: v0.27 release notes, version bump for profile creation fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 14:00:46 -07:00