@bitseek/hermes-webui 0.1.0-beta.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/package.json +2 -2
  2. package/vendor/agent-frontend-shell/.bitseek-source.json +2 -2
  3. package/vendor/agent-frontend-shell/CHANGELOG.md +178 -1
  4. package/vendor/agent-frontend-shell/CONTRIBUTORS.md +5 -5
  5. package/vendor/agent-frontend-shell/api/agent_health.py +134 -0
  6. package/vendor/agent-frontend-shell/api/config.py +145 -104
  7. package/vendor/agent-frontend-shell/api/gateway_chat.py +56 -12
  8. package/vendor/agent-frontend-shell/api/helpers.py +4 -2
  9. package/vendor/agent-frontend-shell/api/models.py +202 -20
  10. package/vendor/agent-frontend-shell/api/paths.py +77 -0
  11. package/vendor/agent-frontend-shell/api/plugins.py +185 -0
  12. package/vendor/agent-frontend-shell/api/profiles.py +95 -16
  13. package/vendor/agent-frontend-shell/api/routes.py +831 -30
  14. package/vendor/agent-frontend-shell/api/run_journal.py +1 -0
  15. package/vendor/agent-frontend-shell/api/state_sync.py +5 -4
  16. package/vendor/agent-frontend-shell/api/streaming.py +211 -56
  17. package/vendor/agent-frontend-shell/api/todo_state.py +122 -0
  18. package/vendor/agent-frontend-shell/api/updates.py +30 -3
  19. package/vendor/agent-frontend-shell/api/upload.py +251 -18
  20. package/vendor/agent-frontend-shell/api/workspace.py +323 -65
  21. package/vendor/agent-frontend-shell/bitseek_docs/BitSeek_Claw_Operation_Manual_EN.docx +0 -0
  22. package/vendor/agent-frontend-shell/bitseek_docs/BitSeek_Claw_Operation_Manual_ZH.docx +0 -0
  23. package/vendor/agent-frontend-shell/bitseek_docs/en/00-Installation.md +174 -0
  24. package/vendor/agent-frontend-shell/bitseek_docs/en/01-Overview.md +128 -0
  25. package/vendor/agent-frontend-shell/bitseek_docs/en/02-Page-Operations.md +461 -0
  26. package/vendor/agent-frontend-shell/bitseek_docs/en/README.md +61 -0
  27. package/vendor/agent-frontend-shell/bitseek_docs/en/images/ai-colleagues.png +0 -0
  28. package/vendor/agent-frontend-shell/bitseek_docs/en/images/chat-area.png +0 -0
  29. package/vendor/agent-frontend-shell/bitseek_docs/en/images/kanban.png +0 -0
  30. package/vendor/agent-frontend-shell/bitseek_docs/en/images/main-page.png +0 -0
  31. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-notes.png +0 -0
  32. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-overview.png +0 -0
  33. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-profile.png +0 -0
  34. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory-soul.png +0 -0
  35. package/vendor/agent-frontend-shell/bitseek_docs/en/images/memory.png +0 -0
  36. package/vendor/agent-frontend-shell/bitseek_docs/en/images/navigation-bar.png +0 -0
  37. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-appearance.png +0 -0
  38. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-conversation.png +0 -0
  39. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-overview.png +0 -0
  40. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-plugins.png +0 -0
  41. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-preferences.png +0 -0
  42. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-providers.png +0 -0
  43. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings-system.png +0 -0
  44. package/vendor/agent-frontend-shell/bitseek_docs/en/images/settings.png +0 -0
  45. package/vendor/agent-frontend-shell/bitseek_docs/en/images/sidebar.png +0 -0
  46. package/vendor/agent-frontend-shell/bitseek_docs/en/images/skills.png +0 -0
  47. package/vendor/agent-frontend-shell/bitseek_docs/en/images/tasks.png +0 -0
  48. package/vendor/agent-frontend-shell/bitseek_docs/en/images/workspace-panel.png +0 -0
  49. package/vendor/agent-frontend-shell/bitseek_docs/md_to_docx.py +351 -0
  50. package/vendor/agent-frontend-shell/bitseek_docs/zh/00-/345/256/211/350/243/205/345/220/257/345/212/250.md +174 -0
  51. package/vendor/agent-frontend-shell/bitseek_docs/zh/01-/346/225/264/344/275/223/346/246/202/350/247/210.md +128 -0
  52. package/vendor/agent-frontend-shell/bitseek_docs/zh/02-/351/241/265/351/235/242/346/223/215/344/275/234.md +463 -0
  53. package/vendor/agent-frontend-shell/bitseek_docs/zh/README.md +61 -0
  54. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/ai-colleagues.png +0 -0
  55. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/chat-area.png +0 -0
  56. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/kanban.png +0 -0
  57. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/main-page.png +0 -0
  58. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-notes.png +0 -0
  59. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-overview.png +0 -0
  60. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-profile.png +0 -0
  61. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory-soul.png +0 -0
  62. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/memory.png +0 -0
  63. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/navigation-bar.png +0 -0
  64. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-appearance.png +0 -0
  65. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-conversation.png +0 -0
  66. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-overview.png +0 -0
  67. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-plugins.png +0 -0
  68. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-preferences.png +0 -0
  69. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-providers.png +0 -0
  70. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings-system.png +0 -0
  71. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/settings.png +0 -0
  72. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/sidebar.png +0 -0
  73. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/skills.png +0 -0
  74. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/tasks.png +0 -0
  75. package/vendor/agent-frontend-shell/bitseek_docs/zh/images/workspace-panel.png +0 -0
  76. package/vendor/agent-frontend-shell/build-release.sh +62 -0
  77. package/vendor/agent-frontend-shell/ctl.sh +1 -0
  78. package/vendor/agent-frontend-shell/docker-compose.local.yml +33 -0
  79. package/vendor/agent-frontend-shell/docker-compose.yml +8 -0
  80. package/vendor/agent-frontend-shell/docker_init.bash +1 -0
  81. package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +74 -15
  82. package/vendor/agent-frontend-shell/extensions/common/index.css +6 -0
  83. package/vendor/agent-frontend-shell/extensions/manifest.json +6 -0
  84. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +60 -14
  85. package/vendor/agent-frontend-shell/readme-simple.md +103 -0
  86. package/vendor/agent-frontend-shell/requirements.txt +5 -0
  87. package/vendor/agent-frontend-shell/server.py +7 -0
  88. package/vendor/agent-frontend-shell/static/boot.js +53 -1
  89. package/vendor/agent-frontend-shell/static/commands.js +20 -10
  90. package/vendor/agent-frontend-shell/static/i18n.js +1142 -1016
  91. package/vendor/agent-frontend-shell/static/index.html +13 -3
  92. package/vendor/agent-frontend-shell/static/messages.js +48 -3
  93. package/vendor/agent-frontend-shell/static/panels.js +199 -30
  94. package/vendor/agent-frontend-shell/static/sessions.js +249 -39
  95. package/vendor/agent-frontend-shell/static/style.css +46 -2
  96. package/vendor/agent-frontend-shell/static/ui.js +323 -79
  97. package/vendor/agent-frontend-shell/static/workspace.js +185 -7
  98. package/vendor/agent-frontend-shell/README-CUSTOM.md +0 -76
  99. package/vendor/agent-frontend-shell/docker-compose.custom.yml +0 -26
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@bitseek/hermes-webui",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "description": "Npm wrapper for Bitseek Hermes WebUI runtime.",
6
6
  "bin": {
7
- "hermes-webui": "./bin/hermes-webui.mjs"
7
+ "hermes-webui": "bin/hermes-webui.mjs"
8
8
  },
9
9
  "engines": {
10
10
  "node": ">=20 <25"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "repo": "adv-org/agent-frontend-shell",
3
3
  "branch": "dev-zkp",
4
- "commit": "36860bfd509554ac8e1a6b56c0dba36ba6692758",
5
- "synced_at": "2026-06-02T04:40:48.989Z"
4
+ "commit": "243559540b8e4aa38233f3252634c56c7ef856e0",
5
+ "synced_at": "2026-06-03T02:49:34.188Z"
6
6
  }
@@ -3,11 +3,187 @@
3
3
 
4
4
  ## [Unreleased]
5
5
 
6
+ ## [v0.51.222] — 2026-06-02 — Release GP (stage-p4 — backend bugfix batch: title language drift + orphaned CLI sidecar prune + pin-quota lineage)
7
+
8
+ ### Fixed
9
+ - Auto-generated session titles no longer persist in the wrong language. The title-language guard previously only rejected English titles for *German* conversation starts, so an English chat whose LLM-generated title came back in Chinese, Russian, or another script sailed through and was saved. `_title_language_mismatch` now also does a language-agnostic cross-script check: when the conversation start has a clear dominant writing script and the generated title introduces a substantial amount of a different script (CJK / Cyrillic / Arabic / etc.), the title is rejected and generation falls back to the deterministic topic title. The threshold tolerates a borrowed technical term (a CJK title with one English word still trips; an English title with a single foreign place-name does not), and the legacy German→English heuristic is preserved (#3293).
10
+ - WebUI sidebar now reconciles orphaned imported-CLI sessions. When a CLI/agent session is opened in the WebUI it gets a WebUI-owned sidecar so it can render and reopen; previously, if the user then deleted that session from the CLI / local Hermes storage, nothing pruned the sidecar and the stale row lingered in the sidebar indefinitely (there is no WebUI delete affordance for CLI rows). Orphaned sidecars whose backing session no longer exists are now pruned on reconciliation (#3238).
11
+ - Pin quota is now counted by visible session lineage rather than raw session rows, so continuation siblings in the same sidebar-visible lineage no longer each consume a separate pin slot. Previously a pinned session that had been compressed/continued into multiple rows could exhaust the pin limit with what the user sees as a single pinned conversation. The limit check now collapses each lineage to its visible root before counting against `pinned_sessions_limit` (#3288, @andrewkangkr).
12
+
13
+ ## [v0.51.221] — 2026-06-02 — Release GO (stage-p3e — block all workspace symlink escapes [security])
14
+
15
+ ### Security
16
+ - The workspace file API now blocks **all** symlink escapes from the selected workspace, not just symlinks pointing at system directories. Previously a symlink placed inside a workspace could resolve to an arbitrary external host path (e.g. `~/.ssh`, `~/.hermes/auth.json`) and be read through `/api/list` / `read_file_content` — and since that API is reachable by LLM agent tool calls, an imported or crafted workspace could expose credentials. `safe_resolve_ws` now requires the resolved path stay under the workspace root, `list_dir` hides escaping symlinks (they could never be opened anyway), and `read_file_content` rejects them. Symlinks that resolve back under the workspace still work normally. The directory-list, file-read, file-upload, and archive-extraction paths are additionally hardened against a symlink-swap **TOCTOU** race: each path is opened component-by-component from the workspace root with `O_NOFOLLOW` (an anchored `openat` walk on Linux/macOS, with a plain-open fallback on platforms without `dir_fd` support such as Windows, where creating symlinks needs admin anyway), so a symlink raced into any component after the containment check cannot redirect the read/list/write outside the workspace. Note: an intentional in-workspace symlink pointing to an external directory is no longer followed (#3398, @Hinotoi-agent).
17
+
18
+ ## [v0.51.220] — 2026-06-02 — Release GN (stage-p3c — fix aux title generation with @provider: model ids)
19
+
20
+ ### Fixed
21
+ - Manual session-title regeneration and background auxiliary title generation no longer fail with `422` / `llm_error_aux` when `auxiliary.title_generation.model` in `config.yaml` is set using the WebUI model-picker's `@provider:model` format (e.g. `@gemini:gemini-3.1-flash-lite`). The `@provider:` prefix is now normalized away via the canonical helper before the id reaches the provider API (#3430, @pamnard).
22
+
23
+ ## [v0.51.219] — 2026-06-02 — Release GM (stage-p3b — extend URI-scheme model-ID fix to backend normalization + matching)
24
+
25
+ ### Fixed
26
+ - Extended the #3429 URI-scheme fix beyond the visible model chip (fixed in v0.51.218) to the model-identity normalization and matching paths: `api/config.py` `_norm_model_id` / `_get_label_for_model` and `static/ui.js` `_normalizeConfiguredModelKey` no longer strip the first `/`-segment of a `scheme://` id (e.g. `gpt://${FOLDER}/model/latest`), where the slashes are path separators rather than a provider prefix. This prevents the #3360-class identity collision/mislabel for URI-shaped model IDs in dropdown matching, badge assignment, and configured-entry dedup. Backend/front-end parity is covered by tests (#3436, @b3nw).
27
+
28
+ ## [v0.51.218] — 2026-06-02 — Release GL (stage-p3a — fix getModelLabel mangling URI-scheme model IDs)
29
+
30
+ ### Fixed
31
+ - The composer model chip no longer shows env-var path junk for model IDs that use a URI scheme (e.g. Yandex `gpt://${FOLDER}/deepseek-v4-flash/latest`). A regression from #3366 (v0.51.210): `getModelLabel()` stripped the first `/`-segment, which for a `scheme://` id landed inside the `://` and left `/${FOLDER}/…`. The label now detects a URI scheme, drops scheme + authority, and takes the last meaningful path segment (skipping `${…}` placeholders and bare version tails like `latest`); non-URI multi-slash IDs keep their #3360 behavior (#3429).
32
+
33
+ ## [v0.51.217] — 2026-06-02 — Release GK (stage-p2f — decode and complete zh-Hant locale strings)
34
+
35
+ ### Changed
36
+ - Decoded the `zh-Hant` (Traditional Chinese) locale block from `\u`-escaped sequences to literal Chinese text and backfilled missing keys so `zh-Hant` now has full coverage of the English key set. Makes future locale review readable and prevents newer UI keys from falling back to English for Traditional Chinese users. Locale-only — no runtime behavior change (#3414, @PeterDaveHello).
37
+
38
+ ### Fixed
39
+ - Added the missing `provider_mismatch_warning` string to the French (`fr`) locale. It was absent entirely; the gap was masked by a stale duplicate of the same key in the `zh-Hant` block that #3414 removed, so all locales now carry the key.
40
+
41
+ ## [v0.51.216] — 2026-06-02 — Release GJ (stage-p2e — fix consecutive-user-turn rejection on strict chat templates)
42
+
43
+ ### Fixed
44
+ - WebUI session/delivery context (connected platforms, home channels, scheduled-task delivery hints) is now injected into the ephemeral **system prompt** instead of being appended as a prefill `user` message. The old prefill produced two consecutive `user` turns (session context + the actual message), which models with strict chat templates (Mistral, Gemma via llama.cpp) reject with a Jinja 500. The same context is preserved — just delivered in a role-alternation-safe place (#3324, @aether-agent, closes #3276).
45
+
46
+ ## [v0.51.215] — 2026-06-02 — Release GI (stage-p2d — deduplicate legacy messages in append-only merge)
47
+
48
+ ### Fixed
49
+ - `merge_session_messages_append_only` now deduplicates true duplicate legacy messages (same role, content, AND exact timestamp) that could accumulate in state, while preserving legitimately-repeated identical turns whose timestamps differ even slightly. This avoids both the stale-duplicate buildup and the data-loss class where collapsing same-second distinct turns would drop real messages (#3393, @thanhtoantnt, closes #3346).
50
+
51
+ ## [v0.51.214] — 2026-06-02 — Release GH (stage-p2c — preserve loaded transcript width on same-session external refresh)
52
+
53
+ ### Fixed
54
+ - A same-session external refresh (e.g. a background poll triggering a force-reload of the conversation you're reading) no longer collapses a long transcript back to the default 30-message tail window and jumps the viewport to a different slice. The already-loaded transcript width and scroll position are now captured before the in-memory transcript is cleared and preserved across the authoritative reload (#3326, @viraatdas, closes #3239).
55
+
56
+ ## [v0.51.213] — 2026-06-02 — Release GG (stage-p2b — keep gateway context visible in chat transcripts)
57
+
58
+ ### Fixed
59
+ - Gateway-backed chat now backfills model-context turns into the visible transcript before saving the latest reply, while keeping hidden `[context compaction]` markers out of the visible transcript. Previously a context-compacted gateway session could collapse the sidebar/header message count to a two-message conversation (and drop older visible turns) while the assistant was responding to hidden prior context. Older visible turns are preserved and compaction markers stay hidden from `saved.messages` (#3300, @AJV20).
60
+
61
+ ## [v0.51.212] — 2026-06-02 — Release GF (stage-batch2 — i18n regenerate-title strings + self-restart argv + todos cold-load)
62
+
63
+ ### Fixed
64
+ - Localized the five `session_title_regenerate*` session-menu strings (the "Regenerate title" action, its description, and the regenerating/regenerated/failed states) that shipped as English text in every non-English locale. Translated across it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr, and tr, matching each locale's existing terminology; `zh`/`zh-Hant` keep the `\u`-escaped style of those blocks (#3396, @vanshaj-pahwa, closes #3364).
65
+ - Self-update re-exec now distinguishes source checkouts from frozen/packaged builds: a frozen binary (`sys.frozen`) re-execs with `sys.argv` as-is, while source checkouts keep the `[sys.executable] + sys.argv` CPython idiom. Previously the frozen path re-inserted the binary as `argv[1]`, turning re-exec into a no-op that left the WebUI stuck "offline" after every self-update (#3395, @PatrickNoFilter).
66
+ - The Todos panel now hydrates correctly on a cold session load (page refresh) even when the latest todo tool result is outside the truncated display window: `/api/session` derives a compact `todo_state` sidecar from the full settled transcript, and an explicit empty todo list is honored as the current state instead of falling through to an older non-empty write. A malformed historical tool message can never break session loading (#3373, @v2psv).
67
+
68
+ ## [v0.51.211] — 2026-06-02 — Release GE (stage-batch1 — reasoning heuristics + /model shortest-match + Copilot env-token filter)
69
+
70
+ ### Fixed
71
+ - Generalized reasoning-effort capability checks in `_candidate_supports_reasoning` to target whole model families (GPT-5+, Claude 4/3.7, Qwen-3, Kimi, Minimax, Mimo, GLM, Step, and DeepSeek) instead of anchoring on hardcoded version numbers or vendor formats. This prevents the thinking-level configuration selector from being hidden on custom providers, new model releases, or when names carry suffixes like `-free` or `:free` (common on integrations such as Kilo Code or OpenCode Zen). The GPT heuristic is now version-anchored (5+) to avoid falsely enabling reasoning_effort for gpt-4o/4.1/3.5 on aggregator providers (#3379, @b3nw, closes #3377).
72
+ - The `/model` slash command no longer selects a longer model variant when a shorter name is a prefix of it (e.g. `/model mimo-v2.5` selecting `mimo-v2.5-pro`). The fuzzy fallback now prefers an exact id/label match and otherwise the shortest matching option, applied to both the main and bare-name (`provider/...`) fallbacks (#3394, @vanshaj-pahwa, closes #3368).
73
+ - `GITHUB_TOKEN` and `GH_TOKEN` environment variables are now filtered from the Copilot credential pool alongside the seeded `gh`-CLI token, so a classic PAT (`ghp_*`) auto-detected from the environment no longer makes Copilot appear in the model picker when the Copilot API can't use it. User-specific `COPILOT_GITHUB_TOKEN` is still respected (#3382, @happy5318).
74
+
75
+ ## [v0.51.210] — 2026-06-02 — Release GD (stage-batch1 — model-picker multi-slash fix + extensionless preview highlighting)
76
+
77
+ ### Fixed
78
+ - Model picker no longer snaps to the wrong model when multiple multi-slash model IDs from the same proxy provider share the same base name. Exact-match priority in `_findModelInDropdown` and first-segment-only stripping in `_normalizeConfiguredModelKey` / `_norm_model_id` prevent collisions in selection, badge assignment, and configured-entry dedup (#3360, @b3nw).
79
+ - Workspace file previews now syntax-highlight common code/config filenames without useful extensions, including `Dockerfile`, `Dockerfile.*`, `Makefile`, `GNUmakefile`, `CMakeLists.txt`, `.gitignore`, and `.dockerignore` (#3365, @AJV20).
80
+
81
+ ## [v0.51.209] — 2026-06-02 — Release GC (WebUI dashboard plugin system with iframe isolation)
82
+
83
+ ### Added
84
+ - WebUI dashboard plugins: plugins that ship a UI under `~/.hermes/plugins/<name>/dashboard/` (with a `manifest.json`) now appear as opt-in cards in Settings → Plugins (default off). Once enabled, an **Open** button renders the plugin page inside a sandboxed iframe (`sandbox="allow-scripts allow-forms allow-popups"` — no `allow-same-origin`, so plugin JS/CSS/modals stay fully isolated from the parent app). New `/plugins/` (shared assets) and `/dashboard-plugins/<name>/` (per-plugin assets) static routes serve only built `dist/`/`static/` files with path-traversal, dotfile, and extension-allowlist protection (plugin source/config such as `plugin_api.py`/`manifest.json`/`.env` is never served), and both the page and asset routes are gated server-side on the enable state + an HTTP `sandbox` CSP + `nosniff`. Plugin `name` and `tab.path` are validated at load. Display-only — no plugin backend/subprocess execution (#2622, @pix0127).
85
+
86
+ ## [v0.51.208] — 2026-06-02 — Release GB (workspace upload hardening hotfix)
87
+
88
+ ### Fixed
89
+ - Hardened the workspace file-upload surface (#3104 follow-up): (1) a negative `Content-Length` no longer bypasses the size cap and triggers an unbounded `rfile.read(-1)` — the length is now validated `[0, MAX_UPLOAD_BYTES]` centrally in `parse_multipart` for every upload handler; (2) `.tar`, `.tbz2`, and `.txz` archives now auto-extract (the upload handler's archive-suffix set was narrower than `extract_archive`'s, so those silently landed as raw files); (3) a rejected archive (zip-slip / zip-bomb / corrupt / too-many-members) now surfaces an error toast in the workspace panel instead of a misleading "Uploaded" success; (4) an in-workspace symlink subpath can no longer make the upload target `mkdir`/write outside the workspace root. Regression tests added.
90
+
91
+ ## [v0.51.207] — 2026-06-02 — Release GA (Edge TTS as an alternative speech engine)
92
+
93
+ ### Added
94
+ - Added an optional server-side **Edge TTS** speech engine (Microsoft neural voices) selectable in Settings → Preferences → TTS Engine, alongside the existing browser speech synthesis. The voice list switches to the Edge neural voices when selected. A new `POST /api/tts` endpoint streams the audio, gated by the same-origin CSRF check + session auth, a per-client rate limit, a 5000-character cap, and a voice allowlist. `edge-tts` is an optional dependency — the endpoint returns a clear install hint (503) when it isn't present, so existing installs are unaffected (#2931, @liuqiangweb-svg).
95
+
96
+ ## [v0.51.206] — 2026-06-02 — Release FZ (workspace file upload + drag-and-drop with archive extraction)
97
+
98
+ ### Added
99
+ - Workspace file panel: an **Upload** button and drag-and-drop that POST to a new `/api/workspace/upload` endpoint. Files land in the session workspace (resolved via the trusted-workspace guard), are de-duplicated with `-1`/`-2` suffixes, and archives (`.zip`/`.tar.*`) are auto-extracted into the target subdirectory with zip-bomb (size-cap + member-count-cap) and zip-slip (path-containment) protections. The extraction size cap is tunable via `HERMES_WEBUI_MAX_EXTRACTED_MB` (defaults to 10× the upload cap). Extraction errors are surfaced to the frontend instead of being silently swallowed, and the archive is removed on failure (#3104, @antoniocarlos97ss).
100
+
101
+ ## [v0.51.205] — 2026-06-01 — Release FY (stage-hi1 — workspace syntax highlighting + generated-image cards + manual title regeneration)
102
+
103
+ ### Added
104
+ - Workspace file previews now render with syntax highlighting via Prism.js (already loaded for chat code blocks), covering common languages (Python, JS/TS, CSS, JSON, SQL, shell, and more) and degrading gracefully to plain text for unknown/plain files and when offline. The preview code surface uses a single uniform background across light and dark themes (#3337, @mysoul12138).
105
+ - Generated local image artifacts now render as a clean inline image (with click-to-zoom lightbox) plus a hover/focus-revealed **Download** action overlaid on the image, served through authenticated `/api/media` URLs — matching the common AI-chat pattern of letting the image be the hero rather than wrapping it in a permanent card (#3220, @AJV20).
106
+ - The session action menu can regenerate conversation titles on demand from the saved transcript, updating the sidebar without touching conversation chronology and syncing the new title through to state.db when Insights sync is enabled. The menu was also streamlined to a compact icon + label layout (descriptions move to hover tooltips). Closes #3106 (#3223, @AJV20).
107
+
108
+ ## [v0.51.204] — 2026-06-01 — Release FX (stage-batch17 — project/session operations honor the session's own profile)
109
+
110
+ ### Fixed
111
+ - Project and session operations (project create/rename/recolor/delete/unassign, session move, and the profile chip label) now key on the session's own profile (`S.session.profile`) instead of the global active profile, so switching between sessions from different profiles no longer causes silent 404s, misleading chip labels, or project-picker entries from the wrong profile. The project picker also filters to the session's profile and surfaces an error toast on failure instead of a silent no-op (#3331, @PINKIIILQWQ).
112
+
113
+ ## [v0.51.203] — 2026-06-01 — Release FW (stage-batch15 — sticky manual unpin for streaming chat scroll)
114
+
115
+ ### Changed
116
+ - Streaming chat scroll now uses a sticky manual-unpin model: once you scroll up to read earlier content during a streaming response, the view stays put and no longer auto-follows the live tail until you scroll back to the bottom (near-bottom hysteresis on downward motion) or click the scroll-to-bottom control. Tool cards, token updates, and layout growth no longer re-pin the viewport after a reading pause. This replaces the #3250 upward-intent timeout and supersedes the v0.51.199 proximity-re-pin (#3330), matching the streaming-scroll behavior of ChatGPT/Claude/Codex. Fresh streams reset the follow state on attach (#3343, @pamnard).
117
+
118
+ ## [v0.51.202] — 2026-06-01 — Release FV (stage-batch14 — filter interrupted-recovery control text from visible transcript)
119
+
120
+ ### Fixed
121
+ - Interrupted SSE-recovery control text (the synthetic `stale_interrupted_event` run-journal payload) is now kept out of the visible chat transcript instead of being replayed as a message: it's marked `recovery_control` on the backend and filtered across the `msgContent()` render path, the SSE settle/error handlers, and final transcript filtering, so platform-only control state no longer leaks into the conversation (#3321, @franksong2702).
122
+
123
+ ## [v0.51.201] — 2026-06-01 — Release FU (stage-batch13 — colored diff lines in tool-card snippets)
124
+
125
+ ### Added
126
+ - Tool-card result snippets that contain a unified diff now render with the same green/red/cyan diff coloring already used for diffs in chat messages (reusing the existing `.diff-block` styles), with an expand/collapse toggle that preserves the coloring. Non-diff snippets are unchanged (#3336, @mysoul12138).
127
+
128
+ ## [v0.51.200] — 2026-06-01 — Release FT (stage-batch12 — remote-gateway health probe + ephemeral-turn-field preservation)
129
+
130
+ ### Fixed
131
+ - The Tasks/Cron panel no longer shows a spurious "Gateway not configured" banner in multi-container Docker deployments where the WebUI image doesn't ship the `gateway` Python package: agent-health now probes the remote gateway via `HERMES_API_URL` before falling back to the local `gateway.status` import. Closes #3281 (#3312, @Sanjays2402).
132
+ - Force-reloading the active session (`loadSession(sid, {forceReload:true})`) no longer drops ephemeral turn fields (`_turnUsage`, `_turnDuration`, `_turnTps`, `_gatewayRouting`, `_statusCard`): the ephemeral-field carry-forward now reads the prior `S.messages` before it's reset, so the token-usage badge and status cards survive an external refresh. Closes #3306 (#3313, @Sanjays2402).
133
+
134
+ ## [v0.51.199] — 2026-06-01 — Release FS (stage-batch11 — pinned-scroll recovery + inline-math currency false-positive)
135
+
136
+ ### Fixed
137
+ - Pinned chat now recovers its scroll position after a DOM rebuild: `_setMessageScrollToBottom` retries on the next layout frame, and `scrollIfPinned` re-pins when the pane has drifted more than 500px from the bottom, so a message-list rebuild no longer leaves a pinned conversation stranded mid-scroll. Closes #3319 (#3330, @jianongHe).
138
+ - The `$...$` inline-math renderer no longer treats currency like `$1,000 xuống ~$95` as math: the opening `$` followed by a digit is now rejected (aligning with smd's `se()` guard), so dollar amounts render as plain text. Digit-leading inline math (e.g. `$2x = 4$`) should now use the LaTeX-style `\(2x = 4\)` or display `$$2x = 4$$` delimiters (#3311, @toanalien).
139
+
140
+ ## [v0.51.198] — 2026-06-01 — Release FR (stage-batch10 — custom-provider reasoning model-id normalize + profile skill counts + run-adapter RFC slice)
141
+
142
+ ### Fixed
143
+ - Reasoning-effort detection for named `custom:*` providers now normalizes non-slash model ids before applying its fallback family heuristics, so separator variants such as `deepseek.v3.2`, `deepseek_v4_flash`, and vendor-namespaced ids like `vendor.deepseek.v3.2` resolve the same way as `deepseek-v4-flash`. The keyword fallback is now token-aware rather than substring-based, preserving names like `model-thinking-preview` without falsely enabling reasoning for unrelated prefixes such as `thinkinghub.llama-3.1-70b` (#3327, @Carry00).
144
+ - Profile cards now show enabled vs compatible skill counts (computed with an 8s TTL cache that clears on profile switch) instead of a single ambiguous count. Closes #3339 (#3341, @b3nw).
145
+
146
+ ### Changed
147
+ - The #1925 runtime-adapter RFC now marks the configured runner-client boundary as shipped in v0.51.188 (#3073 / #3274) and defines the next Slice 4g gate for a supervised local runner process harness: real runner-owned `AIAgent` execution, restart/reattach proof, bounded runner health diagnostics, and no new WebUI runtime-surrogate globals (#3334, @Michaelyklam).
148
+
149
+ ## [v0.51.197] — 2026-06-01 — Release FQ (stage-batch9 — stop agent replaying edited/undone messages)
150
+
151
+ ### Fixed
152
+ - Editing or undoing a message no longer lets the agent replay the original pre-edit content from `state.db`: the truncation-watermark filter now also skips replaced/stale rows whose timestamp sorts *below* the watermark, and `POST /api/session/truncate` truncates `context_messages` in sync with `messages` so the agent's context matches the visible transcript after Edit/Regenerate. The earlier `_clamp_context_to_watermark()` approach (which turned the watermark into a permanent ceiling that dropped every new turn) is removed. Closes #2914 (#3102, @AlexeyDsov).
153
+
154
+ ## [v0.51.196] — 2026-06-01 — Release FP (stage-batch8 — file-manager external sessions + artifacts tool metadata + edge-toggle icon + type hints)
155
+
156
+ ### Fixed
157
+ - File manager (folder download, raw file fetch, and related handlers) now falls back to a `state.db` lookup for sessions created by Telegram/CLI rather than the WebUI, resolving them against the active WebUI workspace instead of returning a 404. Closes #3280 (#3314, @Sanjays2402).
158
+ - Artifacts tab now detects files from structured `tool_calls` (OpenAI format) and `tool_use` content blocks (Anthropic format) on messages, not just text-mined diff fences, so artifacts surface even when `S.toolCalls` is cleared after a reload; display paths are trimmed of the workspace prefix (#3329, @mysoul12138).
159
+ - Workspace panel edge-toggle chevron now points left (toward the panel it reveals) instead of right (#3318, @xz-dev).
160
+
161
+ ### Internal
162
+ - `api/state_sync.py` now uses `Optional[T]` annotations for parameters defaulting to `None` instead of the implicit `T = None` form (#3323, @kuishou68).
163
+
164
+ ## [v0.51.195] — 2026-06-01 — Release FO (stage-batch7 — hide attachment path markers in chat UI)
165
+
166
+ ### Fixed
167
+ - Uploaded image attachment path context (`[Attached files: …]`) remains available to the agent in the stored message, but the chat transcript, sidebar display title, and server-derived provisional titles no longer show the raw path suffix to the user (#3296, @AJV20).
168
+
169
+ ## [v0.51.194] — 2026-06-01 — Release FN (stage-batch6 — profiles config-import-cycle fix)
170
+
171
+ ### Fixed
172
+ - Profile startup now shares platform-default Hermes home resolution through a small `api/paths.py` helper instead of importing the full `api.config` module from `api.profiles`, so importing profiles before config no longer hits a latent circular-load that silently skipped active-profile initialization. Closes #3283 (#3303, @AJV20).
173
+
174
+ ## [v0.51.193] — 2026-06-01 — Release FM (stage-batch5 — ctl dotenv opt-out + workspace inline-open + gateway reply polish)
175
+
176
+ ### Fixed
177
+ - `ctl.sh` now honors `HERMES_WEBUI_NO_DOTENV=1`, letting tests and scripted launches opt out of repo-local `.env` loading so host-specific `HERMES_WEBUI_STATE_DIR` values do not make the ctl test suite flaky. Closes #3246 (#3304, @AJV20).
178
+ - Workspace **Open in browser** now opens HTML files inline (with the same `inline=1` + CSP sandbox isolation as the file preview) instead of forcing a download, and uses `noopener` for the new tab (#3305, @xz-dev).
179
+ - Gateway-backed chat now carries the same WebUI final-answer polish guidance as the in-process chat paths, so terse scratchpad fragments such as "Need script" are not encouraged as visible assistant replies (#3301, @AJV20).
180
+
6
181
  ## [v0.51.192] — 2026-05-31 — Release FL (stage-batch4 — per-model context_length default-only guard)
7
182
 
8
183
  ### Fixed
9
184
  - A global `model.context_length` cap (set in config for the default model, e.g. 232000) no longer silently shrinks **non-default** models' real context windows. The cap is now applied only when the session model equals `model.default`; other models (e.g. a 1M-context variant) keep their real metadata window. The guard is applied consistently across the session context-length resolver (`api/routes.py`), the per-turn persistence path, and the live SSE usage payload, and the auto-compress `threshold_tokens` is rescaled to the real cap so the context-window indicator and compression trigger reflect the actual window. The live-usage perf path caches the resolved per-model window once per stream (it runs ~10×/sec during streaming) so non-default-model streams don't take a config/metadata lookup on every metering tick. Backend-only; default-model sessions are unaffected. Closes #3256 (#3263, @allenliang2022).
10
185
 
186
+
11
187
  ## [v0.51.191] — 2026-05-31 — Release FK (stage-batch3 — skills-detail markdown styling + launchd duplicate-start guard)
12
188
 
13
189
  ### Fixed
@@ -47,6 +223,7 @@
47
223
  - Agent self-update no longer advertises or applies unreachable release tags when the checkout tracks `main` past an older tag but the newest published tag lives on a divergent side branch (for example `v2026.5.29` → `v2026.5.29.2`). The update checker and apply path now fall through to the configured upstream branch when `git pull --ff-only <latest-tag>` cannot fast-forward, matching the existing #2653/#3140 release-vs-branch routing (#3257, @pamnard).
48
224
  - Added regression coverage pinning `_run_git()`'s UTF-8 decoding (`encoding='utf-8'`, `errors='replace'`) and its defensive `None`-stdout guard, so version detection cannot crash on non-UTF-8 Windows console output (#3254, @zapabob).
49
225
 
226
+
50
227
  ## [v0.51.185] — 2026-05-31 — Release FE (stage-batchE — clarify-card bug-fix batch: identical-prompt dedup + autofill guard + GBK startup crash)
51
228
 
52
229
  ### Fixed
@@ -7292,4 +7469,4 @@ Critical regressions introduced during the server.py split, caught by users and
7292
7469
  - **SSE loop did not break on `cancel` event** -- connection hung after cancel
7293
7470
  - **Regression test file added** (`tests/test_regressions.py`): 10 tests, one per introduced bug. These form a permanent regression gate so each class of error can never silently return.
7294
7471
 
7295
- ---
7472
+ ---
@@ -4,9 +4,9 @@ Hermes WebUI is a community project. **194 people** have shipped code that lande
4
4
 
5
5
  A contributor's PR count is the number of distinct PRs they get credit for: PRs they authored that merged directly, PRs they authored that were closed-but-absorbed into a release commit (batch merges, salvage rewrites, cherry-picked-and-attributed work), and PRs where they were explicitly attributed in `CHANGELOG.md`. All count the same.
6
6
 
7
- **Total contributors tracked:** 194
8
- **Total PR credits:** 843
9
- **Last refreshed:** v0.51.192, 2026-05-31
7
+ **Total contributors tracked:** 197
8
+ **Total PR credits:** 846
9
+ **Last refreshed:** v0.51.217, 2026-06-02 (attribution backfill — 3 absorbed-PR contributors that lacked CONTRIBUTORS.md entries; see PR description)
10
10
 
11
11
  Generated by `scripts/regen_contributors.py` in the maintainer workspace, which unions three sources: the GitHub merged-PR list, `CHANGELOG.md` attribution lines, and `Co-authored-by:` trailers on commits that landed on master (the canonical signal for a CLOSED PR whose commits were cherry-picked in and attributed). If your name is missing or wrong, open a PR against `CONTRIBUTORS.md` — we cross-check against the changelog on each release.
12
12
 
@@ -67,11 +67,11 @@ Generated by `scripts/regen_contributors.py` in the maintainer workspace, which
67
67
 
68
68
  [@aliceisjustplaying](https://github.com/aliceisjustplaying), [@allenliang2022](https://github.com/allenliang2022), [@andrewkangkr](https://github.com/andrewkangkr), [@Carry00](https://github.com/Carry00), [@eleboucher](https://github.com/eleboucher), [@insecurejezza](https://github.com/insecurejezza), [@junjunjunbong](https://github.com/junjunjunbong), [@linuxid10t](https://github.com/linuxid10t), [@michael-dg](https://github.com/michael-dg), [@mmartial](https://github.com/mmartial), [@MrFant](https://github.com/MrFant), [@plerohellec](https://github.com/plerohellec), [@renatomott](https://github.com/renatomott), [@swftwolfzyq](https://github.com/swftwolfzyq), [@vcavichini](https://github.com/vcavichini), [@xz-dev](https://github.com/xz-dev), [@zichen0116](https://github.com/zichen0116).
69
69
 
70
- ## Single-PR contributors (136)
70
+ ## Single-PR contributors (139)
71
71
 
72
72
  Each of these folks landed exactly one PR that shipped — a bug fix, a locale, a security hardening, a doc improvement, an infrastructure tweak. Every one moved the project forward.
73
73
 
74
- [@29n](https://github.com/29n), [@86cloudyun-afk](https://github.com/86cloudyun-afk), [@AdoneyGalvan](https://github.com/AdoneyGalvan), [@amlyczz](https://github.com/amlyczz), [@andrewy-wizard](https://github.com/andrewy-wizard), [@Argonaut790](https://github.com/Argonaut790), [@arshkumarsingh](https://github.com/arshkumarsingh), [@ashbuildslife](https://github.com/ashbuildslife), [@Asunfly](https://github.com/Asunfly), [@ayushere](https://github.com/ayushere), [@bengdan](https://github.com/bengdan), [@betamod](https://github.com/betamod), [@bjb2](https://github.com/bjb2), [@Bobby9228](https://github.com/Bobby9228), [@bschmidy10](https://github.com/bschmidy10), [@carlytwozero](https://github.com/carlytwozero), [@Charanis](https://github.com/Charanis), [@ChaseFlorell](https://github.com/ChaseFlorell), [@chwps](https://github.com/chwps), [@colin-chang](https://github.com/colin-chang), [@cyberdyne187](https://github.com/cyberdyne187), [@darkopetrovic](https://github.com/darkopetrovic), [@davidsben](https://github.com/davidsben), [@DavidSchuchert](https://github.com/DavidSchuchert), [@DelightRun](https://github.com/DelightRun), [@dev-rehaann](https://github.com/dev-rehaann), [@dotBeeps](https://github.com/dotBeeps), [@DrMaks22](https://github.com/DrMaks22), [@eba8](https://github.com/eba8), [@emanon312](https://github.com/emanon312), [@eov128](https://github.com/eov128), [@Fail-Safe](https://github.com/Fail-Safe), [@FrancescoFarinola](https://github.com/FrancescoFarinola), [@gabogabucho](https://github.com/gabogabucho), [@galvani](https://github.com/galvani), [@gavinssr](https://github.com/gavinssr), [@GeoffBao](https://github.com/GeoffBao), [@georgebdavis](https://github.com/georgebdavis), [@GiggleSamurai](https://github.com/GiggleSamurai), [@hacker1e7](https://github.com/hacker1e7), [@hacker2005](https://github.com/hacker2005), [@halmisen](https://github.com/halmisen), [@hermes-gimmethebeans](https://github.com/hermes-gimmethebeans), [@hi-friday](https://github.com/hi-friday), [@hualong1009](https://github.com/hualong1009), [@huangzt](https://github.com/huangzt), [@indigokarasu](https://github.com/indigokarasu), [@intellectronica](https://github.com/intellectronica), [@jeffscottward](https://github.com/jeffscottward), [@Jellypowered](https://github.com/Jellypowered), [@jimdawdy-hub](https://github.com/jimdawdy-hub), [@JinYue-GitHub](https://github.com/JinYue-GitHub), [@joaompfp](https://github.com/joaompfp), [@jundev0001](https://github.com/jundev0001), [@KayZz69](https://github.com/KayZz69), [@kcclaw001](https://github.com/kcclaw001), [@kevin-ho](https://github.com/kevin-ho), [@koshikai](https://github.com/koshikai), [@kowenhaoai](https://github.com/kowenhaoai), [@lawrencel1ng](https://github.com/lawrencel1ng), [@leap233](https://github.com/leap233), [@legeantbleu](https://github.com/legeantbleu), [@likawa3b](https://github.com/likawa3b), [@lost9999](https://github.com/lost9999), [@lucky-yonug](https://github.com/lucky-yonug), [@lx3133584](https://github.com/lx3133584), [@MacLeodMike](https://github.com/MacLeodMike), [@malulian](https://github.com/malulian), [@mangodxd](https://github.com/mangodxd), [@mariosam95](https://github.com/mariosam95), [@MatzAgent](https://github.com/MatzAgent), [@mbac](https://github.com/mbac), [@migueltavares](https://github.com/migueltavares), [@MinhoJJang](https://github.com/MinhoJJang), [@mittyok](https://github.com/mittyok), [@mslovy](https://github.com/mslovy), [@mvanhorn](https://github.com/mvanhorn), [@nanookclaw](https://github.com/nanookclaw), [@ng-technology-llc](https://github.com/ng-technology-llc), [@nickgiulioni1](https://github.com/nickgiulioni1), [@octo-patch](https://github.com/octo-patch), [@OneFat3](https://github.com/OneFat3), [@PINKIIILQWQ](https://github.com/PINKIIILQWQ), [@rhelmer](https://github.com/rhelmer), [@rickchew](https://github.com/rickchew), [@RobertoVillegas](https://github.com/RobertoVillegas), [@ruxme](https://github.com/ruxme), [@ryan-remeo](https://github.com/ryan-remeo), [@ryansombraio](https://github.com/ryansombraio), [@s905060](https://github.com/s905060), [@Saik0s](https://github.com/Saik0s), [@samuelgudi](https://github.com/samuelgudi), [@SaulgoodMan-C](https://github.com/SaulgoodMan-C), [@sbe27](https://github.com/sbe27), [@shaoxianbilly](https://github.com/shaoxianbilly), [@sheng-di](https://github.com/sheng-di), [@shruggr](https://github.com/shruggr), [@sixianli](https://github.com/sixianli), [@skspade](https://github.com/skspade), [@smurmann](https://github.com/smurmann), [@snuffxxx](https://github.com/snuffxxx), [@someaka](https://github.com/someaka), [@spektro33](https://github.com/spektro33), [@Stampede](https://github.com/Stampede), [@stocky789](https://github.com/stocky789), [@suinia](https://github.com/suinia), [@sunilkumarvalmiki](https://github.com/sunilkumarvalmiki), [@sunnysktsang](https://github.com/sunnysktsang), [@TaraTheStar](https://github.com/TaraTheStar), [@tgaalman](https://github.com/tgaalman), [@thadreber-web](https://github.com/thadreber-web), [@the-own-lab](https://github.com/the-own-lab), [@theh4v0c](https://github.com/theh4v0c), [@theseussss](https://github.com/theseussss), [@tiansiyuan](https://github.com/tiansiyuan), [@tomaioo](https://github.com/tomaioo), [@trucuit](https://github.com/trucuit), [@ts2111](https://github.com/ts2111), [@v2psv](https://github.com/v2psv), [@vansour](https://github.com/vansour), [@vCillusion](https://github.com/vCillusion), [@vikarag](https://github.com/vikarag), [@waldmanz](https://github.com/waldmanz), [@wali-reheman](https://github.com/wali-reheman), [@watzon](https://github.com/watzon), [@weidzhou](https://github.com/weidzhou), [@weiwei83](https://github.com/weiwei83), [@wind-chant](https://github.com/wind-chant), [@wirtsi](https://github.com/wirtsi), [@woaijiadanoo](https://github.com/woaijiadanoo), [@xingyue52077](https://github.com/xingyue52077), [@xolom](https://github.com/xolom), [@yunyunyunyun-yun](https://github.com/yunyunyunyun-yun), [@yzp12138](https://github.com/yzp12138), [@zapabob](https://github.com/zapabob), [@zenc-cp](https://github.com/zenc-cp).
74
+ [@29n](https://github.com/29n), [@86cloudyun-afk](https://github.com/86cloudyun-afk), [@AdoneyGalvan](https://github.com/AdoneyGalvan), [@amlyczz](https://github.com/amlyczz), [@andrewy-wizard](https://github.com/andrewy-wizard), [@antoniocarlos97ss](https://github.com/antoniocarlos97ss), [@Argonaut790](https://github.com/Argonaut790), [@arshkumarsingh](https://github.com/arshkumarsingh), [@ashbuildslife](https://github.com/ashbuildslife), [@Asunfly](https://github.com/Asunfly), [@ayushere](https://github.com/ayushere), [@bengdan](https://github.com/bengdan), [@betamod](https://github.com/betamod), [@bjb2](https://github.com/bjb2), [@Bobby9228](https://github.com/Bobby9228), [@bschmidy10](https://github.com/bschmidy10), [@carlytwozero](https://github.com/carlytwozero), [@Charanis](https://github.com/Charanis), [@ChaseFlorell](https://github.com/ChaseFlorell), [@chwps](https://github.com/chwps), [@colin-chang](https://github.com/colin-chang), [@cyberdyne187](https://github.com/cyberdyne187), [@darkopetrovic](https://github.com/darkopetrovic), [@davidsben](https://github.com/davidsben), [@DavidSchuchert](https://github.com/DavidSchuchert), [@DelightRun](https://github.com/DelightRun), [@dev-rehaann](https://github.com/dev-rehaann), [@dotBeeps](https://github.com/dotBeeps), [@DrMaks22](https://github.com/DrMaks22), [@eba8](https://github.com/eba8), [@emanon312](https://github.com/emanon312), [@eov128](https://github.com/eov128), [@Fail-Safe](https://github.com/Fail-Safe), [@FrancescoFarinola](https://github.com/FrancescoFarinola), [@gabogabucho](https://github.com/gabogabucho), [@galvani](https://github.com/galvani), [@gavinssr](https://github.com/gavinssr), [@GeoffBao](https://github.com/GeoffBao), [@georgebdavis](https://github.com/georgebdavis), [@GiggleSamurai](https://github.com/GiggleSamurai), [@hacker1e7](https://github.com/hacker1e7), [@hacker2005](https://github.com/hacker2005), [@halmisen](https://github.com/halmisen), [@hermes-gimmethebeans](https://github.com/hermes-gimmethebeans), [@hi-friday](https://github.com/hi-friday), [@hualong1009](https://github.com/hualong1009), [@huangzt](https://github.com/huangzt), [@indigokarasu](https://github.com/indigokarasu), [@intellectronica](https://github.com/intellectronica), [@jeffscottward](https://github.com/jeffscottward), [@Jellypowered](https://github.com/Jellypowered), [@jimdawdy-hub](https://github.com/jimdawdy-hub), [@JinYue-GitHub](https://github.com/JinYue-GitHub), [@joaompfp](https://github.com/joaompfp), [@jundev0001](https://github.com/jundev0001), [@KayZz69](https://github.com/KayZz69), [@kcclaw001](https://github.com/kcclaw001), [@kevin-ho](https://github.com/kevin-ho), [@koshikai](https://github.com/koshikai), [@kowenhaoai](https://github.com/kowenhaoai), [@lawrencel1ng](https://github.com/lawrencel1ng), [@leap233](https://github.com/leap233), [@legeantbleu](https://github.com/legeantbleu), [@likawa3b](https://github.com/likawa3b), [@liuqiangweb-svg](https://github.com/liuqiangweb-svg), [@lost9999](https://github.com/lost9999), [@lucky-yonug](https://github.com/lucky-yonug), [@lx3133584](https://github.com/lx3133584), [@MacLeodMike](https://github.com/MacLeodMike), [@malulian](https://github.com/malulian), [@mangodxd](https://github.com/mangodxd), [@mariosam95](https://github.com/mariosam95), [@MatzAgent](https://github.com/MatzAgent), [@mbac](https://github.com/mbac), [@migueltavares](https://github.com/migueltavares), [@MinhoJJang](https://github.com/MinhoJJang), [@mittyok](https://github.com/mittyok), [@mslovy](https://github.com/mslovy), [@mvanhorn](https://github.com/mvanhorn), [@nanookclaw](https://github.com/nanookclaw), [@ng-technology-llc](https://github.com/ng-technology-llc), [@nickgiulioni1](https://github.com/nickgiulioni1), [@octo-patch](https://github.com/octo-patch), [@OneFat3](https://github.com/OneFat3), [@PINKIIILQWQ](https://github.com/PINKIIILQWQ), [@pix0127](https://github.com/pix0127), [@rhelmer](https://github.com/rhelmer), [@rickchew](https://github.com/rickchew), [@RobertoVillegas](https://github.com/RobertoVillegas), [@ruxme](https://github.com/ruxme), [@ryan-remeo](https://github.com/ryan-remeo), [@ryansombraio](https://github.com/ryansombraio), [@s905060](https://github.com/s905060), [@Saik0s](https://github.com/Saik0s), [@samuelgudi](https://github.com/samuelgudi), [@SaulgoodMan-C](https://github.com/SaulgoodMan-C), [@sbe27](https://github.com/sbe27), [@shaoxianbilly](https://github.com/shaoxianbilly), [@sheng-di](https://github.com/sheng-di), [@shruggr](https://github.com/shruggr), [@sixianli](https://github.com/sixianli), [@skspade](https://github.com/skspade), [@smurmann](https://github.com/smurmann), [@snuffxxx](https://github.com/snuffxxx), [@someaka](https://github.com/someaka), [@spektro33](https://github.com/spektro33), [@Stampede](https://github.com/Stampede), [@stocky789](https://github.com/stocky789), [@suinia](https://github.com/suinia), [@sunilkumarvalmiki](https://github.com/sunilkumarvalmiki), [@sunnysktsang](https://github.com/sunnysktsang), [@TaraTheStar](https://github.com/TaraTheStar), [@tgaalman](https://github.com/tgaalman), [@thadreber-web](https://github.com/thadreber-web), [@the-own-lab](https://github.com/the-own-lab), [@theh4v0c](https://github.com/theh4v0c), [@theseussss](https://github.com/theseussss), [@tiansiyuan](https://github.com/tiansiyuan), [@tomaioo](https://github.com/tomaioo), [@trucuit](https://github.com/trucuit), [@ts2111](https://github.com/ts2111), [@v2psv](https://github.com/v2psv), [@vansour](https://github.com/vansour), [@vCillusion](https://github.com/vCillusion), [@vikarag](https://github.com/vikarag), [@waldmanz](https://github.com/waldmanz), [@wali-reheman](https://github.com/wali-reheman), [@watzon](https://github.com/watzon), [@weidzhou](https://github.com/weidzhou), [@weiwei83](https://github.com/weiwei83), [@wind-chant](https://github.com/wind-chant), [@wirtsi](https://github.com/wirtsi), [@woaijiadanoo](https://github.com/woaijiadanoo), [@xingyue52077](https://github.com/xingyue52077), [@xolom](https://github.com/xolom), [@yunyunyunyun-yun](https://github.com/yunyunyunyun-yun), [@yzp12138](https://github.com/yzp12138), [@zapabob](https://github.com/zapabob), [@zenc-cp](https://github.com/zenc-cp).
75
75
 
76
76
  ---
77
77
 
@@ -24,9 +24,14 @@ from __future__ import annotations
24
24
 
25
25
  import importlib
26
26
  import json
27
+ import os
28
+ import threading
29
+ import time
27
30
  from datetime import datetime, timezone
28
31
  from pathlib import Path
29
32
  from typing import Any
33
+ from urllib import error as urllib_error
34
+ from urllib import request as urllib_request
30
35
 
31
36
  _GATEWAY_PID_FILE = "gateway.pid"
32
37
  _GATEWAY_RUNTIME_STATUS_FILE = "gateway_state.json"
@@ -285,6 +290,125 @@ def _runtime_detail_subset(runtime_status: dict[str, Any] | None) -> dict[str, A
285
290
  return details
286
291
 
287
292
 
293
+ # Remote-gateway probe (#3281)
294
+ # ------------------------------------------------------------------
295
+ # In multi-container Docker deployments the WebUI container does not ship the
296
+ # ``gateway`` Python package. The lazy ``importlib.import_module("gateway.status")``
297
+ # therefore raises ``ModuleNotFoundError`` and the payload falls through to
298
+ # ``gateway_not_configured`` even though ``HERMES_API_URL`` points at a perfectly
299
+ # reachable remote gateway. The Tasks/Cron banner then shows a spurious amber
300
+ # "Gateway not configured" warning.
301
+ #
302
+ # When ``HERMES_API_URL`` is set we treat that as an explicit declaration that
303
+ # the gateway lives elsewhere, and probe it over HTTP before touching any local
304
+ # filesystem / module signal. The probe result is cached briefly so a dashboard
305
+ # rerender that fans out to multiple panels does not hammer the gateway.
306
+
307
+ _REMOTE_PROBE_TIMEOUT_S: float = 2.0
308
+ _REMOTE_PROBE_CACHE_TTL_S: float = 5.0
309
+ _REMOTE_PROBE_PATHS: tuple[str, ...] = ("/health", "/status", "/api/gateway/status")
310
+
311
+ _remote_probe_lock = threading.Lock()
312
+ _remote_probe_cache: dict[str, Any] = {"url": None, "expires_at": 0.0, "result": None}
313
+
314
+
315
+ def _remote_gateway_base_url() -> str | None:
316
+ raw = os.environ.get("HERMES_API_URL")
317
+ if not isinstance(raw, str):
318
+ return None
319
+ url = raw.strip()
320
+ if not url:
321
+ return None
322
+ return url.rstrip("/")
323
+
324
+
325
+ def _http_probe(url: str, timeout_s: float) -> tuple[bool, int | None, str | None]:
326
+ """GET ``url`` and return (ok, status_code, error_name).
327
+
328
+ ``ok`` is True only for a 2xx response. 5xx and network errors are not OK.
329
+ 4xx is also treated as "responded" (the gateway is up, just answering 404
330
+ on this particular path) so the caller can move on to the next path.
331
+ """
332
+ req = urllib_request.Request(url, method="GET")
333
+ try:
334
+ with urllib_request.urlopen(req, timeout=timeout_s) as resp: # noqa: S310 - trusted env var URL
335
+ status = getattr(resp, "status", None) or resp.getcode()
336
+ return (200 <= int(status) < 300, int(status), None)
337
+ except urllib_error.HTTPError as exc:
338
+ return (False, int(exc.code), "HTTPError")
339
+ except Exception as exc: # urllib_error.URLError, socket.timeout, ssl, etc.
340
+ return (False, None, type(exc).__name__)
341
+
342
+
343
+ def _probe_remote_gateway(base_url: str, *, now: float | None = None) -> dict[str, Any]:
344
+ """Return an agent-health payload dict for a remote gateway base URL.
345
+
346
+ Result is cached for ``_REMOTE_PROBE_CACHE_TTL_S`` seconds per base_url.
347
+ """
348
+ current = time.monotonic() if now is None else now
349
+ with _remote_probe_lock:
350
+ if (
351
+ _remote_probe_cache.get("url") == base_url
352
+ and _remote_probe_cache.get("expires_at", 0.0) > current
353
+ and _remote_probe_cache.get("result") is not None
354
+ ):
355
+ cached = _remote_probe_cache["result"]
356
+ # Refresh checked_at so the UI shows a current timestamp without
357
+ # actually re-hitting the gateway.
358
+ return {**cached, "checked_at": _checked_at()}
359
+
360
+ last_status: int | None = None
361
+ last_error: str | None = None
362
+ for path in _REMOTE_PROBE_PATHS:
363
+ ok, status, err = _http_probe(base_url + path, _REMOTE_PROBE_TIMEOUT_S)
364
+ if ok:
365
+ payload = {
366
+ "alive": True,
367
+ "checked_at": _checked_at(),
368
+ "details": {
369
+ "state": "alive",
370
+ "reason": "remote_gateway",
371
+ "endpoint": base_url + path,
372
+ "status_code": status,
373
+ },
374
+ }
375
+ break
376
+ # Remember the most informative failure signal we saw.
377
+ if status is not None:
378
+ last_status = status
379
+ if err is not None:
380
+ last_error = err
381
+ else:
382
+ details: dict[str, Any] = {
383
+ "state": "down",
384
+ "reason": "remote_gateway_unreachable",
385
+ "endpoint": base_url,
386
+ }
387
+ if last_status is not None:
388
+ details["status_code"] = last_status
389
+ if last_error is not None:
390
+ details["error"] = last_error
391
+ payload = {
392
+ "alive": False,
393
+ "checked_at": _checked_at(),
394
+ "details": details,
395
+ }
396
+
397
+ with _remote_probe_lock:
398
+ _remote_probe_cache["url"] = base_url
399
+ _remote_probe_cache["expires_at"] = current + _REMOTE_PROBE_CACHE_TTL_S
400
+ _remote_probe_cache["result"] = payload
401
+ return payload
402
+
403
+
404
+ def _reset_remote_probe_cache_for_tests() -> None:
405
+ """Test hook: clear the in-process remote-probe cache."""
406
+ with _remote_probe_lock:
407
+ _remote_probe_cache["url"] = None
408
+ _remote_probe_cache["expires_at"] = 0.0
409
+ _remote_probe_cache["result"] = None
410
+
411
+
288
412
  def build_agent_health_payload() -> dict[str, Any]:
289
413
  """Return `{alive, checked_at, details}` for the Hermes gateway/agent.
290
414
 
@@ -295,6 +419,16 @@ def build_agent_health_payload() -> dict[str, Any]:
295
419
  probably not configured with a separate gateway process.
296
420
  """
297
421
  checked_at = _checked_at()
422
+
423
+ # Multi-container deployments (#3281): when HERMES_API_URL is set the
424
+ # gateway lives in another container/host. Probe it over HTTP before
425
+ # touching local module/pid/state-file signals, otherwise a missing
426
+ # ``gateway`` Python package in this image masquerades as
427
+ # "gateway_not_configured" and produces a spurious banner.
428
+ remote_base = _remote_gateway_base_url()
429
+ if remote_base is not None:
430
+ return _probe_remote_gateway(remote_base)
431
+
298
432
  try:
299
433
  gateway_status = _gateway_status_module()
300
434
  except Exception as exc: