Commit Graph

419 Commits

Author SHA1 Message Date
Nathan Esquenazi
a4136f2da5 fix(gateway): filter orphan sessions from SSE watcher (HAVING COUNT > 0) 2026-04-14 17:54:30 +00:00
Nathan Esquenazi
3c3cae89f8 fix(tests): test_sprint45 isolation + zh i18n keys + server version string
- test_sprint45.py: compute SETTINGS_FILE lazily via _get_settings_file() so it
  reads HERMES_WEBUI_TEST_STATE_DIR at call time (not at import time, when conftest
  hasn't yet set the env var). Fixes test isolation across all 1078 tests.
- test_sprint45.py: use auth cookie in teardown when clearing password post-test.
- test_sprint45.py: remove test_synced_version_strings (checks local-patch version).
- static/i18n.js: add zh missing keys: onboarding_password_will_replace,
  onboarding_password_keep_existing, onboarding_password_remains_disabled.
- server.py: revert server_version to HermesWebUI/0.50.38 (matches master).
2026-04-14 17:54:06 +00:00
SaulgoodMan-C
8b857d9efc login-module-patch: sync to v0.50.36-local.1 2026-04-14 17:54:06 +00:00
nesquena-hermes
8d1c257ea8 docs: correct test count to 1075 in TESTING.md and CHANGELOG (#447)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-14 10:17:22 -07:00
nesquena-hermes
6e303fbd93 Merge pull request #446 from nesquena/release/v0.50.38
release: v0.50.38 — mobile nav cleanup, Prism highlighting, zh-CN/zh-Hant i18n
2026-04-14 10:15:42 -07:00
Nathan Esquenazi
61ecdaded3 docs: v0.50.38 CHANGELOG, version bump, test count (1073) 2026-04-14 17:14:40 +00:00
Nathan Esquenazi
09e278461c fix(test): update test_sprint10 cron history check for i18n key refactor 2026-04-14 17:14:02 +00:00
Nathan Esquenazi
6347949463 fix(i18n): add onboarding_skip/onboarding_skipped to zh locale 2026-04-14 17:14:02 +00:00
vansour
204dc23c6b fix i18n review comments and locale test robustness 2026-04-14 17:14:01 +00:00
vansour
c4efe96725 feat(i18n): complete zh-CN hardening and locale consistency 2026-04-14 17:14:01 +00:00
Louis Wong
6a513f49b2 fix(ui): add Prism syntax highlighting with light + dark theme token colors
Closes #426:
2026-04-14 17:13:04 +00:00
Aron Prins
db392bd532 feat(ui): remove mobile bottom nav on phones
Closes #425:
2026-04-14 17:13:03 +00:00
nesquena-hermes
b394efce17 Merge pull request #445 from nesquena/pr-422-review
docs: add CONTRIBUTING.md (closes #422)
2026-04-14 09:51:21 -07:00
Aron Prins
28d226f5ce docs: add CONTRIBUTING.md
Co-authored-by: Aron Prins <pwf.aron@gmail.com>
2026-04-14 16:50:24 +00:00
nesquena-hermes
d8aa387c3c Merge pull request #439 from nesquena/release/v0.50.37
release: v0.50.37 — fix onboarding wizard for existing Hermes users
2026-04-14 09:46:08 -07:00
Nathan Esquenazi
4ad7efe8cf fix(i18n): add onboarding_skip/onboarding_skipped keys to en+es locales 2026-04-14 16:45:13 +00:00
Nathan Esquenazi
57a50591ee fix(onboarding): skip wizard if Hermes already configured
Closes #420:
2026-04-14 16:45:12 +00:00
Nathan Esquenazi
16c58e60f4 docs: v0.50.37 CHANGELOG, version bump, test count 2026-04-14 16:44:58 +00:00
nesquena-hermes
37850a4dfd fix: workspace list cleaner — all 1055 tests pass (#418)
* fix: workspace list cleaner — allow own-profile paths, remove brittle string filter

Two bugs in _clean_workspace_list() caused workspace adds to silently vanish
on the next load, making the duplicate-check test and workspace rename test fail:

1. Brittle string filter: 'if test-workspace in path or webui-mvp-test in path:
   continue' — removed. The test server's workspace IS under these paths, so any
   workspace added during testing got silently dropped on the next load_workspaces()
   call. The p.is_dir() check already handles non-existent paths.

2. Cross-profile filter too broad: 'if p is under ~/.hermes/profiles/: skip' —
   this correctly blocked cross-profile leakage but also blocked the current
   profile's own paths (e.g. ~/.hermes/profiles/webui/webui-mvp-test/...).
   Fixed: only skip if the path is under profiles/ AND under a DIFFERENT profile's
   directory. Paths under the current profile's own home are kept.

* docs: v0.50.36 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-14 00:14:25 -07:00
nesquena-hermes
415270ff03 fix: cross-platform multi-workspace trust boundary (#417)
* fix: relax workspace trust boundary to user home directory

The previous restriction required workspaces to be under DEFAULT_WORKSPACE
(/home/hermes/workspace), which blocked all profile-specific workspaces
(~/CodePath, ~/General, ~/WebUI, ~/Camanji, etc.) since each profile uses
a different directory under home.

New boundary: any directory under Path.home() is trusted.
This still blocks /etc, /tmp, /var, /root, /usr and all paths outside the
user's home, while allowing any legitimate workspace under ~/

Also updates test assertions from 'trusted workspace root' to 'outside'
since the new error message says 'outside the user home directory'.

* fix: workspace trust uses home-dir + saved-list, not single ancestor

Three-layer trust model that works cross-platform and multi-workspace:

1. BLOCKLIST: /etc, /usr, /var, /bin, /sbin, /boot, /proc, /sys, /dev, /root,
   /lib, /lib64, /opt/homebrew — always rejected, even if somehow saved
2. HOME CHECK: any path under Path.home() is trusted — covers ~/CodePath,
   ~/hermes-webui-public, ~/WebUI, ~/General, ~/Camanji simultaneously;
   Path.home() is cross-platform (Linux ~/..., macOS ~/..., Windows C:\Users\...\...)
3. SAVED LIST ESCAPE HATCH: if a path is already in the saved workspace list,
   it's trusted regardless of location — covers self-hosted deployments where
   workspaces live outside home (/data/projects, /opt/workspace, etc.)

None/empty → DEFAULT_WORKSPACE (always trusted, validated at startup)

* docs: v0.50.35 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 23:57:51 -07:00
nesquena-hermes
2a7a5ddfaf [security] fix(workspace): restrict session workspaces to trusted roots (#416)
* fix(workspace): restrict session workspaces to trusted roots

* fix: use boot-time DEFAULT_WORKSPACE instead of profile default for trusted workspace root

_profile_default_workspace() reads the agent's terminal.cwd which may differ
from the WebUI's configured workspace root. Use _BOOT_DEFAULT_WORKSPACE (which
respects HERMES_WEBUI_DEFAULT_WORKSPACE for test isolation) to stay consistent
with how new_session() seeds the initial workspace.

* docs: v0.50.34 release — version badge and CHANGELOG

---------

Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 23:44:03 -07:00
nesquena-hermes
a5abe51cc5 fix: workspace panel close button — no duplicate X on desktop, mobile X respects file preview (#414)
* fix: workspace panel close button — no duplicate X on desktop, mobile X respects file preview

Two bugs fixed in the workspace right panel:

1. Duplicate X on desktop (bug): #btnClearPreview (the X icon) was always
   visible alongside #btnCollapseWorkspacePanel (the chevron), producing two
   close controls at once. Fixed in syncWorkspacePanelUI() — on desktop, the X
   is now hidden when no file preview is open (display:none), and only shown
   when the user is viewing a file. The chevron remains as the sole close
   control in browse mode.

2. Mobile X collapses panel instead of dismissing file (bug): .mobile-close-btn
   was calling closeWorkspacePanel() directly, which collapsed the whole panel
   even when a file was open. Changed to handleWorkspaceClose(), which already
   has the correct two-step logic: clear preview first, close panel only if
   no preview is visible.

Files changed:
- static/boot.js: syncWorkspacePanelUI() hides btnClearPreview on desktop
  when hasPreview is false, guarded by !isCompact so mobile is unaffected
- static/index.html: mobile-close-btn onclick changed from
  closeWorkspacePanel() to handleWorkspaceClose()
- tests/test_sprint44.py: 10 new regression tests
- tests/test_mobile_layout.py: updated test_workspace_close_button_present()
  to accept handleWorkspaceClose() as the valid onclick target

* fix: widen test_server_delete_invalidates_index window to 1200 chars

The test extracted a 600-char window starting from the session/delete
handler to check for SESSION_INDEX_FILE. Commit 3cc5839 added session_id
character validation and path traversal guards before the unlink call,
pushing SESSION_INDEX_FILE to ~764 chars from the match — beyond the
600-char limit, causing the test to fail on CI.

Widened the window to 1200 chars, which accommodates any reasonable
amount of guard code before the SESSION_INDEX_FILE.unlink() call.

* docs: v0.50.33 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 23:25:26 -07:00
nesquena-hermes
3cc5839bf3 [security] fix(sessions): validate session_id before deleting session files (#412)
* fix(sessions): validate session_id before deleting files

* fix: remove premature session index invalidation before validation check

* docs: v0.50.32 release — version badge and CHANGELOG

---------

Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 23:10:46 -07:00
nesquena-hermes
539501ed2b fix: delegate all live model fetching to agent provider_model_ids() (#411)
* fix: delegate all live model fetching to agent's provider_model_ids()

Previously _handle_live_models() maintained its own per-provider logic:
- anthropic, google, gemini returned 'not_supported' (hardcoded exclusions)
- openai-codex had a custom branch (added in v0.50.30)
- openai/copilot had hardcoded base URLs
- other providers fell through to a generic /v1/models fetch

Now the handler delegates entirely to hermes_cli.models.provider_model_ids(),
which is the agent's authoritative resolver:
- anthropic:    live fetch via /v1/models with correct API-key or OAuth headers
- copilot:      live fetch from api.githubcopilot.com/models with Copilot headers
- openai-codex: Codex OAuth endpoint + ~/.codex/ cache fallback
- nous:         live fetch from Nous inference portal
- deepseek, kimi-coding: generic OpenAI-compat /v1/models
- opencode-zen/go: OpenCode live catalog
- openrouter:   curated static list (live returns 300+ which is overwhelming)
- google/gemini, zai, minimax: static list (non-standard or Anthropic-compat endpoints)
- any others:   graceful static fallback

Also removed the client-side skip guard in _fetchLiveModels() (ui.js) that
blocked live fetching for anthropic, google, and gemini.

The hardcoded model lists in _PROVIDER_MODELS remain as the fallback when
credentials are missing or network is unavailable — they are never shown
when live data is available.

* docs: v0.50.31 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:57:58 -07:00
nesquena-hermes
c91eaaf05f fix: route openai-codex live model fetch through agent get_codex_model_ids() (#410)
* fix: route openai-codex live model fetch through agent's get_codex_model_ids()

Previously _handle_live_models() grouped openai-codex with openai and sent a
request to https://api.openai.com/v1/models, which returns 403 because Codex
auth is OAuth-based via chatgpt.com, not a standard API key. The live fetch
silently failed and the UI showed only the hardcoded static list.

Now: openai-codex has a dedicated early-exit branch that calls
hermes_cli.codex_models.get_codex_model_ids() — the same path the agent CLI
uses. It resolves models in order: live Codex API (if OAuth token available) >
~/.codex/ local cache > DEFAULT_CODEX_MODELS. This means:

- If the user has a valid Codex OAuth session, the UI gets the exact model list
  their subscription provides (e.g. gpt-5.2, gpt-5.3-codex-spark that aren't
  in the hardcoded list)
- If the OAuth session is expired, falls back to local ~/.codex/ cache
- Always has DEFAULT_CODEX_MODELS as final fallback

Also: improved label generation for Codex model IDs (GPT-5.4 Mini vs GPT 5 4 Mini).
Added 1 structural regression test.

* docs: v0.50.30 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:49:04 -07:00
nesquena-hermes
d3fea34c41 fix: correct tool call card rendering on session load after context compaction (#408)
* fix: correct tool call card rendering on session load

Two bugs caused duplicate/incorrect tool call cards when loading
sessions (especially after context compaction):

1. loadSession() sanitized messages (B9 filter) but did NOT update
   the session-level tool_calls array's assistant_msg_idx references.
   Since compact() returns only sanitized messages and recomputes
   tool_calls with indices into the compacted array, the original
   assistant_msg_idx values became stale/misaligned.

2. loadSession() then assigned the broken session-level tool_calls
   directly to S.toolCalls. This prevented renderMessages()'s fallback
   path (which derives tool_calls from per-message tool_calls using
   correct sanitized-array indices) from ever running.

Fix:
- Keep full sanitization loop with index remapping for session-level
  tool_calls (in case they're needed by other code paths).
- Instead of assigning broken session-level tool_calls to S.toolCalls,
  set S.toolCalls=[] so renderMessages() uses the fallback derivation
  from per-message tool_calls, which already have correct indices.

* test: add 8 regression tests for issue #401 tool call index remapping

* docs: v0.50.29 release — version badge and CHANGELOG

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:41:31 -07:00
nesquena-hermes
a2258139f2 fix: expand openai-codex model catalog to match DEFAULT_CODEX_MODELS (#407)
* fix: expand openai-codex model catalog to match agent DEFAULT_CODEX_MODELS

The _PROVIDER_MODELS["openai-codex"] catalog only listed codex-mini-latest,
so the model dropdown for profiles using openai-codex provider (e.g. CodePath)
showed only that one entry — even when the profile's saved default_model was
gpt-5.4 or another standard Codex model.

Updated to match DEFAULT_CODEX_MODELS from hermes_cli/codex_models.py:
- gpt-5.4
- gpt-5.4-mini
- gpt-5.3-codex
- gpt-5.2-codex
- gpt-5.1-codex-max
- gpt-5.1-codex-mini
- codex-mini-latest (kept, relabeled as 'Codex Mini (latest)')

Also adds 2 regression tests: catalog includes gpt-5.4, display name correct.

* docs: v0.50.28 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:35:27 -07:00
nesquena-hermes
1345ccccee feat: relative time labels in session sidebar (#406)
* feat: add relative time to session sidebar

(cherry picked from commit 272be9787fdff75d3da2dbc73175820477a3390e)

* fix: address session sidebar relative-time review feedback

* docs: v0.50.27 release — version badge and CHANGELOG

---------

Co-authored-by: Jordan SkyLF <jordan@skylinkfiber.net>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:26:05 -07:00
nesquena-hermes
4de4ed9a15 fix(sessions): redact sensitive titles in session list and search responses (#405)
* fix(sessions): redact titles in list and search responses

* docs: v0.50.26 release — version badge and CHANGELOG

---------

Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:20:21 -07:00
nesquena-hermes
04ed0ff43d v0.50.25: mobile scroll, import timestamps, profile security, mic fallback (#404)
* fix: restore mobile chat scrolling and drawer close (#397)

- static/style.css: add min-height:0 to .layout and .main (flex shrink chain fix for mobile scroll)
- static/style.css: add -webkit-overflow-scrolling:touch, touch-action:pan-y, overscroll-behavior-y:contain to .messages
- static/boot.js: call closeMobileSidebar() on new-conversation button onclick and Ctrl+K shortcut
- tests/test_mobile_layout.py: 41 new lines covering all three CSS fixes and both JS call sites

Original PR by @Jordan-SkyLF

* fix: preserve imported session timestamps (#395)

- api/models.py: add touch_updated_at: bool = True param to Session.save(); import_cli_session() accepts created_at/updated_at kwargs and saves with touch_updated_at=False
- api/routes.py: extract created_at/updated_at from get_cli_sessions() metadata and forward to import_cli_session(); use touch_updated_at=False on post-import save
- tests/test_gateway_sync.py: +53 lines — integration test verifying imported session keeps original timestamp and sorts correctly vs newer sessions; also fix: add WebUI session file cleanup in finally block

Original PR by @Jordan-SkyLF

* fix(profiles): block path traversal in profile switch and delete flows (#399)

Master was vulnerable: switch_profile and delete_profile_api joined user-supplied profile
names directly into filesystem paths with no validation. An attacker could send
'../../etc/passwd' as a profile name to traverse outside the profiles directory.

- api/profiles.py: add _resolve_named_profile_home(name) — validates name with
  ^[a-z0-9][a-z0-9_-]{0,63}$ regex then enforces path containment via
  candidate.resolve().relative_to(profiles_root); use in switch_profile()
- api/profiles.py: add _validate_profile_name() call to delete_profile_api() entry
- api/routes.py: add _validate_profile_name() call at HTTP handler level for
  both /api/profile/switch and /api/profile/delete (fail-fast at API boundary)
- tests/test_profile_path_security.py: 3 tests — traversal rejected, valid name passes

Cherry-picked commit aae7a30 from @Hinotoi-agent (PR was 62 commits behind master)

* feat: add desktop microphone transcription fallback (#396)

Mic button now works in browsers that support getUserMedia/MediaRecorder but
lack SpeechRecognition (e.g. Firefox desktop, some Chromium builds).

- static/boot.js: detect _canRecordAudio (navigator.mediaDevices + getUserMedia + MediaRecorder);
  keep mic button enabled when either SpeechRecognition or MediaRecorder is available;
  MediaRecorder fallback records audio, sends blob to /api/transcribe, inserts transcript
  into the composer; _stopMic() handles all three states (recognition, mediaRecorder, neither)
- api/upload.py: add transcribe_audio() helper — saves uploaded blob to temp file, calls
  transcription_tools.transcribe_audio(), always cleans up temp file
- api/routes.py: add /api/transcribe POST handler — CSRF protected, auth-gated, 20MB limit,
  returns {text:...} or {error:...}
- api/helpers.py: change Permissions-Policy microphone=() to microphone=(self) (required to
  allow getUserMedia in the same origin)
- tests/test_voice_transcribe_endpoint.py: 87 new lines — 3 tests with mocked transcription
- tests/test_sprint19.py: +1 regression guard (microphone=(self) in Permissions-Policy)
- tests/test_sprint20.py: 3 updated tests for new fallback-capability checks

Original PR by @Jordan-SkyLF

* docs: v0.50.25 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 22:11:45 -07:00
nesquena-hermes
2beebaa6a2 feat: opt-in chat bubble layout (closes #336) (#403)
* feat(ui): opt-in chat bubble layout

Closes #336.

Adds a settings toggle that right-aligns user messages and left-aligns
assistant replies. Off by default - the current full-width layout is
friendlier to code blocks and tool output, so bubbles are strictly
opt-in per the maintainer note on the issue.

Wiring follows the existing token-usage / cli-sessions pattern:

- api/config.py: new bubble_layout bool in _SETTINGS_DEFAULTS and
  _SETTINGS_BOOL_KEYS, validated + persisted like the rest.
- static/style.css: .bubble-layout gated selectors using :has() to
  tag msg-rows by .msg-role.user / .msg-role.assistant without any JS
  changes to message creation. User rows get align-self: flex-end,
  max-width: 75%, and a row-reverse header; assistant rows flex-start.
  A 700px media query widens the max to 92% on narrow screens.
- static/index.html: new checkbox with i18n keys next to the existing
  token-usage toggle.
- static/panels.js: loads the setting into the checkbox, saves it
  back, and toggles body.bubble-layout immediately on save.
- static/boot.js: applies the class on initial load so refreshed
  tabs honor the persisted setting without a flash.
- static/i18n.js: English label + description.

Test suite errors are environmental (test server fails to start on
port 8788 on main as well).

* i18n(es): add Spanish translations for bubble_layout setting

* fix+test: boot.js bubble-layout reset on failure; add 22 tests for issue #336

* docs: v0.50.24 release — version badge and CHANGELOG

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 21:42:01 -07:00
nesquena-hermes
0f8fec7ccd docs: v0.50.23 release — version badge and CHANGELOG (#393)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:46:51 -07:00
nesquena-hermes
12a60faaee fix: add OpenCode Zen and Go provider support (closes #362) (#392)
* Add OpenCode Zen and OpenCode Go provider support

The webui model dropdown had no knowledge of these providers.
When hermes_cli detected them as authenticated, they fell through
to the unknown-provider fallback showing wrong models.

Changes:
- Add opencode-zen and opencode-go to _PROVIDER_DISPLAY
- Add model lists for both to _PROVIDER_MODELS
- Add OPENCODE_ZEN_API_KEY and OPENCODE_GO_API_KEY to env-var fallback detection
- Fix custom:* provider IDs (e.g. custom:my-server) displaying raw ID instead of "Custom"

* Add tests for OpenCode provider registration and detection

---------

Co-authored-by: David Case <david.case@shruggr.cloud>
2026-04-13 18:46:11 -07:00
nesquena-hermes
2acee7fc34 fix: onboarding unblocked for reverse proxy / SSH tunnel deployments (fixes #390) (#391)
- Read X-Forwarded-For and X-Real-IP before falling back to raw socket IP
- Add HERMES_WEBUI_ONBOARDING_OPEN=1 env var escape hatch for remote servers
- Error message now includes the env var hint
- 18 new tests (TestOnboardingIPLogic + TestOnboardingSetupEndpoint)

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 17:52:07 -07:00
nesquena-hermes
acc14f2f0b docs: update ROADMAP, SPRINTS, TESTING to v0.50.21 (961 tests)
ROADMAP.md:
- Header: v0.49.1/700 → v0.50.21/961
- Sprint history table: 12 new rows covering v0.40 → v0.50.21 (500+ commits)
- Architecture block: updated line counts and module list

SPRINTS.md:
- Header state: v0.36/433 → v0.50.21/961
- 'Where we are now' section updated with parity status
- Historical planning content preserved as reference

TESTING.md:
- Version reference: v0.36.2 → v0.50.21
- Test count: 700 → 961 (two places)

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 17:43:16 -07:00
nesquena-hermes
9948fcf1db docs: fix CHANGELOG ordering + README architecture counts
- CHANGELOG: reorder v0.50.19/v0.50.20/v0.50.21 to correct newest-first
  (v0.50.19 was mistakenly at the top above v0.50.21 and v0.50.20)
- README: fix architecture block test count 51 files/802 functions → 61 files/961
- README: update line counts to actual wc -l values:
  routes.py ~2250, streaming.py ~660, ui.js ~1740,
  messages.js ~655, sessions.js ~800

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 17:34:22 -07:00
nesquena-hermes
6a1dda4082 docs: add remaining contributors — Argonaut790, indigokarasu, zenc-cp (complete to 33)
- @Argonaut790 (#239): HTML entity decode fix + Traditional Chinese locale
  (fix shipped in v0.46.0; zh-Hant locale added same PR)
- @indigokarasu (#213): CSS-only visual redesign proposal — design token system
  + icon rail + 7 themes (influenced v0.50.0 design language)
- @zenc-cp (#133): Anti-hallucination guard for ReAct loop — streaming token
  buffer + post-run scrub pattern

README now has 33 contributors covering full project history.

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 16:52:39 -07:00
nesquena-hermes
56944cc0ab docs: update contributors, test count, line counts (v0.50.21)
- Add 21 new contributor entries covering v0.50.x era and all external
  contributions that were incorporated via review branches
- Fix test count: 802 → 961
- Fix line counts for routes.py, streaming.py, ui.js, messages.js, sessions.js
  (all grew significantly from live reasoning, reload recovery, CSRF fixes etc.)
- New major tier: Jordan-SkyLF (live streaming + session recovery)
- New feature tier: gabogabucho, bergeouss, ccqqlo, betamod, TaraTheStar,
  thadreber-web, deboste
- New bug/security tier: Hinotoi-agent, lawrencel1ng, lx3133584, DelightRun,
  shaoxianbilly, huangzt, kcclaw001, mbac, andrewy-wizard, mmartial,
  vCillusion, carlytwozero, mangodxd

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 16:47:28 -07:00
nesquena-hermes
7f69155904 docs: v0.50.21 release — version badge
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 16:26:48 -07:00
nesquena-hermes
54181d1a07 fix: durable inflight reload snapshots via localStorage (#367)
* fix: persist durable inflight reload snapshots

* fix: remove duplicate loadInflightState stub, update CHANGELOG test count

The stub added in the previous review branch is superseded by the author's
real localStorage-backed implementation in the cherry-picked commit 36051c0.
Remove the duplicate. Update CHANGELOG to 961 tests and document the durable
inflight state feature.

---------

Co-authored-by: Jordan SkyLF <jordan@skylinkfiber.net>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 16:25:31 -07:00
nesquena-hermes
9542639a90 fix: live reasoning, tool progress, in-flight session recovery (#367)
* fix: preserve live session output across chat switches

(cherry picked from commit 401e3b643d25e8dad8c06883b478b3c3073f07a5)

* fix: preserve todo state after session reload

(cherry picked from commit 7ee093ba19978af23b79148df2f2347e2f1e5bde)

* fix: preserve live assistant anchor across rerenders

* fix: stream live reasoning and tool progress

* fix: recover inflight session state after reload

* fix: add loadInflightState stub + CHANGELOG v0.50.21

- static/ui.js: add loadInflightState() function (currently returns null —
  the typeof guard in sessions.js means reload recovery works via the
  else-path attachLiveStream call; this stub satisfies the guard cleanly
  and documents the extension point for future localStorage-backed state)
- CHANGELOG.md: v0.50.21 entry; 960 tests (up from 949)

---------

Co-authored-by: Jordan SkyLF <jordan@skylinkfiber.net>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 16:18:15 -07:00
nesquena-hermes
bcdd7ed3f3 docs: v0.50.20 release — version badge
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 15:53:52 -07:00
nesquena-hermes
7a80e73eb2 fix: silent agent errors, stale model list, live model fetching (#377)
* fix: silent errors, stale models, live model fetching (#373, #374, #375)

- api/streaming.py: detect empty agent response (_assistant_added check),
  emit apperror(type='no_response' or 'auth_mismatch') instead of silent done
- api/streaming.py: add _token_sent flag so guard works for streaming agents
- static/messages.js: done handler belt-and-suspenders guard for zero replies
- static/messages.js: apperror handler labels 'no_response' type distinctly

- api/config.py: remove gpt-4o and o3 from _FALLBACK_MODELS and
  _PROVIDER_MODELS['openai'] (superseded by gpt-5.4-mini and o4-mini)

- api/routes.py: new /api/models/live?provider= endpoint, fetches /v1/models
  from provider API with B310 scheme check + SSRF guard
- static/ui.js: _fetchLiveModels() background fetch after static list loads,
  appends new models to dropdown, caches per session, skips unsupported providers

Other:
- tests/test_issues_373_374_375.py: 25 new structural tests
- tests/test_regressions.py: extend done-handler window 1500->2500 chars
- CHANGELOG.md: v0.50.19 entry; 947 tests (up from 922)

* fix: SSRF hostname bypass + auth detection operator precedence

1. routes.py: SSRF guard used substring matching (any(k in hostname))
   which allows bypass via hostnames like evil-ollama.attacker.com.
   Changed to exact hostname matching against a fixed set of known
   local hostnames (localhost, 127.0.0.1, 0.0.0.0, ::1).

2. streaming.py: _is_auth detection had a Python operator precedence
   bug on the ternary expression. The line:
     'AuthenticationError' in type(...).__name__ if _last_err else False
   parsed as the ternary absorbing the rest of the or-chain when
   _last_err was falsy. Fixed to: (_last_err and 'AuthenticationError' in ...)

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

* docs: fix v0.50.20 CHANGELOG version number and test count (949 tests)

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:52:35 -07:00
nesquena-hermes
78de40e015 docs: v0.50.19 release — version badge
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 15:44:19 -07:00
nesquena-hermes
00eb13b316 fix: unicode filenames in Content-Disposition headers (#378)
* Fix unicode filenames in file download headers

* docs: v0.50.19 CHANGELOG entry for unicode filename fix (PR #378)

* docs: fix test count in v0.50.19 CHANGELOG (924 not 926)

---------

Co-authored-by: shaoxianbilly <40623436+shaoxianbilly@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 15:43:01 -07:00
nesquena-hermes
a71047bbc3 docs: v0.50.18 release — version badge
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 14:38:21 -07:00
nesquena-hermes
68426124c5 fix: recover from invalid default workspace paths (#366)
* fix: recover from bad default workspace paths

(cherry picked from commit 789d7537a325d1c7d3aa03c387918dddd2d0897d)

* fix: recover from invalid default workspace paths — 7 tests, CHANGELOG (#366)

- tests/test_default_workspace_fallback.py: 5 additional tests (dedup,
  RuntimeError, env var priority, mkdir on missing dir, unwritable path)
- CHANGELOG.md: v0.50.18 entry; 922 tests (up from 915)

---------

Co-authored-by: Jordan SkyLF <jordan@skylinkfiber.net>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 14:28:24 -07:00
nesquena-hermes
4c8042ea00 docs: v0.50.17 release — version badge
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 12:38:00 -07:00
nesquena-hermes
a6484f69a8 fix: Docker uv pre-install at build time + workspace permissions (#365)
* fix: pre-install uv in Docker image + fix workspace dir permissions (#357)

Two fixes for Docker startup reliability:

1. Install uv at build time in the Dockerfile so the container works
   without internet access at runtime. The init script now skips the
   download when uv is already on PATH.

2. Use sudo mkdir/chown for the workspace directory, matching the
   pattern used for /app. Docker auto-creates bind-mount directories
   as root, leaving them unwritable by the hermeswebui user.

Fixes #357

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

* fix: Docker uv pre-install as root to /usr/local/bin + tests + CHANGELOG

Dockerfile: install uv as root with UV_INSTALL_DIR=/usr/local/bin so it
lands in /usr/local/bin (system PATH) rather than /home/hermeswebuitoo/.local/bin
which the hermeswebui runtime user can't see.

tests/test_issue357.py: 15 structural tests covering Dockerfile uv build-time
install (system-wide, as root, before COPY), init script skip-if-present
logic, and workspace sudo mkdir/chown.

CHANGELOG.md: v0.50.17 entry; 915 tests (up from 900)

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:36:11 -07:00
nesquena-hermes
f13f753de8 docs: v0.50.16 release — version badge
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 12:24:35 -07:00