Commit Graph

14 Commits

Author SHA1 Message Date
Rose
3bdf430413 merge: upgrade to upstream v0.50.95 + keep custom additions
Upstream v0.50.95 features merged (Russian localization, slash commands,
mic toggle fix, gateway sync fix, KaTeX/Prism.js, etc.)

Custom additions preserved:
- Tier-2 agent switching commands in commands.js
- MC panel in index.html + MC CSS
- _resolve_cli_toolsets() in config.py
- Custom routes.py, server.py, boot.js, i18n.js, messages.js, workspace.js

Files with conflict resolution (took upstream, custom code in other files):
- CHANGELOG.md, config.py, commands.js, index.html, panels.js, style.css, ui.js
2026-04-19 10:06:28 +02:00
nesquena-hermes
54e83fb8b6 feat: support subpath mount via reverse proxy — v0.50.67 (PR #588 by @vcavichini)
Squash-merges feature from PR #588 by @vcavichini. Dynamic <base href> injection + api() helper slash-stripping enables deploying hermes-webui behind a reverse proxy at any subpath without configuration. Also fixes pre-existing bug: api/upload was using location.origin instead of location.href (closes #596). Co-authored-by: vcavichini <vcavichini@users.noreply.github.com>
2026-04-16 11:20:08 -07:00
nesquena-hermes
d0e08fee88 feat: KaTeX math rendering for LaTeX in chat + workspace previews (#352)
* feat: KaTeX math rendering for $..$ and $$..$$ in chat and previews (fixes #347)

- Stash math delimiters before markdown pipeline, restore as .katex-block/.katex-inline elements
- KaTeX JS lazy-loaded from CDN on first math block (mirrors mermaid pattern)
- KaTeX CSS loaded eagerly in <head> to prevent layout shift
- SRI hashes on both CDN tags
- throwOnError:false — bad LaTeX degrades to code span
- Supports $$, $, \\(...\\), \\[...\\] delimiters
- 18 new tests, 831/831 passing

* fix: remove invalid \' escape sequences in math stash lines

Lines 311, 314, 316, 317 had \' (backslash-quote) instead of plain '
in the arrow function bodies. This is a JS syntax error — node --check
fails with 'Invalid or unexpected token'. Likely caused by a
serialization artifact during code generation.

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

* fix: swap stash order (fence before math) to protect code spans; add renderKatexBlocks to workspace preview

- static/ui.js: fence_stash now runs BEFORE math_stash so dollar signs
  inside backtick code spans are not extracted as math. Previously
  `$x$` would render as KaTeX inside a <code> tag instead of
  showing the literal string $x$.
- static/workspace.js: add requestAnimationFrame(renderKatexBlocks)
  after markdown preview renders so math works in workspace file
  previews, not only in chat messages.

* feat: KaTeX math rendering + stash order fix + workspace wiring (#352)

- tests/test_issue347.py: 11 new tests (29 total) covering fence-before-math
  ordering, workspace.js renderKatexBlocks call, stash token distinctness,
  false-positive prevention, safe-tags boundary check
- CHANGELOG.md: v0.50.15 entry; 870 tests total (up from 841)

* fix: use literal null byte (\x00M) in math stash token — matches restore regex

The original PR's second commit (fix: remove invalid \' escapes) accidentally
doubled the backslash in the math stash tokens: '\\x00M' is a 5-char string
(backslash + x + 0 + 0 + M) but the restore regex /\x00M/ expects a null byte.
Result: $...$ in messages produced visible \x00M0\x00 tokens instead of
KaTeX spans.

Changed all 4 math stash return statements to use '\x00M' (single backslash =
null byte, same convention as fence_stash's '\x00F').

Also updates test_stash_tokens_distinct to check for the correct pattern.

* fix: add null-byte token test; update CHANGELOG to v0.50.15 with fixes

- tests/test_issue347.py: add test_math_stash_token_uses_single_backslash_null_byte
  to catch the \\x00M double-backslash regression; 30 tests total (up from 29)
- CHANGELOG.md: v0.50.15 entry documents all fixes including the token bug
  and workspace preview wiring; 871 tests total

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:40:15 -07:00
nesquena-hermes
28a0f0bef9 fix+feat: session title guard + breadcrumb nav + wider panel + responsive msgs (closes #300, #292)
PR #301 changes:
- api/streaming.py: guard title_from() with s.title == 'Untitled' check
- api/routes.py: same guard in sync/non-streaming path

PR #302 changes (cleaned — restores accidentally-removed features):
- static/boot.js: PANEL_MAX 500 -> 1200
- static/boot.js: clearPreview() calls renderBreadcrumb() to restore dir view
- static/style.css: responsive .messages-inner breakpoints (1400px/1800px)
- static/workspace.js: renderFileBreadcrumb() function with clickable segments
- static/workspace.js: openFile() calls renderFileBreadcrumb(path)

12 new tests in tests/test_sprint35.py

Note: PR #302 branch contained several accidental regressions (removed app-dialog
system, onboarding CSS, _checkProviderMismatch, closeMobileFiles, etc.) that were
not part of its stated scope. This clean branch applies only the three intended
features on top of current master.

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 10:51:48 -07:00
nesquena-hermes
b86ace6ce3 v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)

When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.

Fixes #230

* test: mobile layout regression suite — 14 tests for every QA run (#254)

Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.

* feat: /skills slash command lists and filters available Hermes skills (#257)

Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.

Fixes #248

* feat: shared app dialogs replace native confirm()/prompt() calls (#251)

Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.

Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.

Extracted from PR #242.

* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)

Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.

Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.

Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.

Fixes #247 Fixes #246

* feat: session ⋯ action dropdown replaces per-row buttons (#252)

Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.

5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.

Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.

Extracted from PR #242.

* docs: v0.47.0 release notes, bump version, update test counts (645)

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 12:19:12 -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
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 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
c778c1eb0c fix: profile switch fails with 'does not exist' when server starts on non-default profile
Root cause: _DEFAULT_HERMES_HOME was evaluated at module import time from
os.getenv('HERMES_HOME'). HERMES_HOME is a MUTABLE env var -- init_profile_state()
at server startup calls _set_hermes_home() which writes to os.environ['HERMES_HOME'].
If the sticky active_profile file pointed to e.g. 'webui', HERMES_HOME was set to
~/.hermes/profiles/webui BEFORE api/profiles.py imported. So _DEFAULT_HERMES_HOME
resolved to ~/.hermes/profiles/webui. Then switch_profile('webui') computed:
  home = ~/.hermes/profiles/webui / 'profiles' / 'webui'
       = ~/.hermes/profiles/webui/profiles/webui  -- doesn't exist -> 404 ValueError

Fix: replace the one-liner assignment with _resolve_base_hermes_home() which:
  1. Checks HERMES_BASE_HOME env var (explicit override)
  2. Checks HERMES_HOME -- but if it looks like a profiles/ subdir (parent.name ==
     'profiles'), walks up two levels to the actual base
  3. Falls back to Path.home() / '.hermes'

This means the server can start with HERMES_HOME pointing to any profile and
_DEFAULT_HERMES_HOME will still correctly point to ~/.hermes.

Also fix: api() helper in workspace.js was throwing new Error(await res.text())
which surfaced raw JSON to the UI: 'Switch failed: {"error":"Profile X does not exist."}'
Now parses the JSON and extracts j.error so the toast shows clean human-readable text.

Regression tests added in test_sprint23.py:
- test_profile_switch_base_home_not_subdir: static analysis verifying the resolver
- test_api_helper_returns_clean_error_message: verifies api() parses JSON errors
- test_profile_switch_resolve_base_home_logic: verifies the profiles/ subdir detection

Tests: 426 passed, 0 failed.
2026-04-03 19:29:24 +00:00
Nathan Esquenazi
d0aef93372 fix: apply review fixes, update version to v0.20, add Sprint 18 changelog
- Fix stale tree cache: clear _dirCache and _expandedDirs on root nav
- Fix clearPreview: prompt before discarding unsaved preview edits
- Update UI version label from v0.17.1 to v0.20
- Add Sprint 18 entry to CHANGELOG.md
- Update SPRINTS.md current state to v0.20

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 05:02:30 -07:00
Nathan Esquenazi
67324cc3bc feat: Sprint 18 — file preview auto-close, thinking display, workspace tree
- File preview auto-close: clearPreview() extracted as named function
  and called from loadDir(). Navigating directories (breadcrumbs, up
  button, folder clicks) now automatically closes the right panel
  file preview instead of leaving stale content visible.

- Thinking/reasoning display: assistant messages with structured content
  arrays containing type=thinking or type=reasoning blocks now render
  as collapsible gold-themed cards above the response text. Collapsed
  by default, click header to expand. Works with Claude extended thinking
  and o3 reasoning tokens when preserved in the message array.

- Workspace tree view (Issue #22): directories expand/collapse in-place
  with toggle arrows. Single-click toggles, double-click navigates
  (breadcrumb view). Subdirectory contents fetched lazily and cached.
  Indentation shows nesting depth. Empty directories show "(empty)".
  S._expandedDirs tracks open state, S._dirCache caches fetched entries.

Tests: 295 passed, 23 pre-existing failures, 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 04:33:24 -07:00
Nathan Esquenazi
0f2bd537f1 feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory

Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally

Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json

5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 04:13:38 -07:00
deboste
96547f68a3 fix(frontend): use URL origin for fetch/EventSource to support reverse proxy auth
When Hermes WebUI runs behind a reverse proxy with HTTP basic auth
(e.g. Caddy basic_auth), browsers embed credentials in the page URL.
The Fetch API and EventSource reject requests constructed from URLs
that include credentials (per Fetch spec, all modern browsers).

Fix: construct all fetch() and EventSource URLs via
new URL(path, location.origin) which strips credentials from the
base URL. Add credentials:"include" to ensure auth headers are
forwarded on each request.
2026-03-31 15:00:38 +00:00
Nathan Esquenazi
a4e2174c29 Hermes WebUI v0.1.0 — initial public release 2026-03-30 20:40:19 -07:00