@bitseek/hermes-webui 0.1.0-beta.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 (233) hide show
  1. package/README.md +213 -0
  2. package/bin/hermes-webui.mjs +588 -0
  3. package/package.json +25 -0
  4. package/scripts/sync-vendor.mjs +74 -0
  5. package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
  6. package/templates/systemd/hermes-webui.service +13 -0
  7. package/templates/windows/hermes-webui-task.ps1 +3 -0
  8. package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
  9. package/vendor/agent-frontend-shell/.dockerignore +7 -0
  10. package/vendor/agent-frontend-shell/.env.docker.example +89 -0
  11. package/vendor/agent-frontend-shell/.env.example +34 -0
  12. package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
  13. package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
  14. package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
  15. package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
  16. package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
  17. package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
  18. package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
  19. package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
  20. package/vendor/agent-frontend-shell/AGENTS.md +80 -0
  21. package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
  22. package/vendor/agent-frontend-shell/BUGS.md +52 -0
  23. package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
  24. package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
  25. package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
  26. package/vendor/agent-frontend-shell/DESIGN.md +173 -0
  27. package/vendor/agent-frontend-shell/Dockerfile +91 -0
  28. package/vendor/agent-frontend-shell/LICENSE +21 -0
  29. package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
  30. package/vendor/agent-frontend-shell/README.md +705 -0
  31. package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
  32. package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
  33. package/vendor/agent-frontend-shell/TESTING.md +1932 -0
  34. package/vendor/agent-frontend-shell/THEMES.md +170 -0
  35. package/vendor/agent-frontend-shell/api/__init__.py +1 -0
  36. package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
  37. package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
  38. package/vendor/agent-frontend-shell/api/auth.py +592 -0
  39. package/vendor/agent-frontend-shell/api/background.py +87 -0
  40. package/vendor/agent-frontend-shell/api/clarify.py +238 -0
  41. package/vendor/agent-frontend-shell/api/commands.py +124 -0
  42. package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
  43. package/vendor/agent-frontend-shell/api/config.py +5178 -0
  44. package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
  45. package/vendor/agent-frontend-shell/api/extensions.py +253 -0
  46. package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
  47. package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
  48. package/vendor/agent-frontend-shell/api/goals.py +608 -0
  49. package/vendor/agent-frontend-shell/api/helpers.py +474 -0
  50. package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
  51. package/vendor/agent-frontend-shell/api/metering.py +194 -0
  52. package/vendor/agent-frontend-shell/api/models.py +4210 -0
  53. package/vendor/agent-frontend-shell/api/oauth.py +770 -0
  54. package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
  55. package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
  56. package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
  57. package/vendor/agent-frontend-shell/api/providers.py +2175 -0
  58. package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
  59. package/vendor/agent-frontend-shell/api/rollback.py +320 -0
  60. package/vendor/agent-frontend-shell/api/routes.py +13990 -0
  61. package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
  62. package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
  63. package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
  64. package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
  65. package/vendor/agent-frontend-shell/api/session_events.py +45 -0
  66. package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
  67. package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
  68. package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
  69. package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
  70. package/vendor/agent-frontend-shell/api/startup.py +128 -0
  71. package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
  72. package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
  73. package/vendor/agent-frontend-shell/api/system_health.py +167 -0
  74. package/vendor/agent-frontend-shell/api/terminal.py +410 -0
  75. package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
  76. package/vendor/agent-frontend-shell/api/updates.py +1261 -0
  77. package/vendor/agent-frontend-shell/api/upload.py +322 -0
  78. package/vendor/agent-frontend-shell/api/usage.py +26 -0
  79. package/vendor/agent-frontend-shell/api/workspace.py +867 -0
  80. package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
  81. package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
  82. package/vendor/agent-frontend-shell/bootstrap.py +492 -0
  83. package/vendor/agent-frontend-shell/ctl.sh +427 -0
  84. package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
  85. package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
  86. package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
  87. package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
  88. package/vendor/agent-frontend-shell/docker_init.bash +459 -0
  89. package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
  90. package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
  91. package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
  92. package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
  93. package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
  94. package/vendor/agent-frontend-shell/docs/docker.md +337 -0
  95. package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
  96. package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
  97. package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
  98. package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
  99. package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
  100. package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
  101. package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
  102. package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
  103. package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
  104. package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
  105. package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
  106. package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
  107. package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
  108. package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
  109. package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
  110. package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
  111. package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
  112. package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
  113. package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
  114. package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
  115. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
  116. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
  117. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
  118. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
  119. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
  120. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
  121. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
  122. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
  123. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
  124. package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
  125. package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
  126. package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
  127. package/vendor/agent-frontend-shell/extensions/index.js +67 -0
  128. package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
  129. package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
  130. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
  131. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
  132. package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
  133. package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
  134. package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
  135. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
  136. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
  137. package/vendor/agent-frontend-shell/install.sh +63 -0
  138. package/vendor/agent-frontend-shell/mcp_server.py +567 -0
  139. package/vendor/agent-frontend-shell/package.json +12 -0
  140. package/vendor/agent-frontend-shell/pyproject.toml +56 -0
  141. package/vendor/agent-frontend-shell/pytest.ini +3 -0
  142. package/vendor/agent-frontend-shell/requirements.txt +5 -0
  143. package/vendor/agent-frontend-shell/server.py +624 -0
  144. package/vendor/agent-frontend-shell/start.ps1 +210 -0
  145. package/vendor/agent-frontend-shell/start.sh +65 -0
  146. package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
  147. package/vendor/agent-frontend-shell/static/boot.js +1990 -0
  148. package/vendor/agent-frontend-shell/static/commands.js +1402 -0
  149. package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
  150. package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
  151. package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
  152. package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
  153. package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
  154. package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
  155. package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
  156. package/vendor/agent-frontend-shell/static/icons.js +92 -0
  157. package/vendor/agent-frontend-shell/static/index.html +1506 -0
  158. package/vendor/agent-frontend-shell/static/login.js +177 -0
  159. package/vendor/agent-frontend-shell/static/manifest.json +53 -0
  160. package/vendor/agent-frontend-shell/static/messages.js +3521 -0
  161. package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
  162. package/vendor/agent-frontend-shell/static/panels.js +7995 -0
  163. package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
  164. package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
  165. package/vendor/agent-frontend-shell/static/style.css +4774 -0
  166. package/vendor/agent-frontend-shell/static/sw.js +173 -0
  167. package/vendor/agent-frontend-shell/static/terminal.js +632 -0
  168. package/vendor/agent-frontend-shell/static/ui.js +8997 -0
  169. package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
  170. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
  171. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
  172. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  173. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  174. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  175. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  176. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  177. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  178. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  179. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  180. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  181. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  182. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  183. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  184. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  185. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
  186. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
  187. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
  188. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  189. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  190. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  191. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
  192. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
  193. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
  194. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
  195. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
  196. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
  197. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  198. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  199. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  200. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
  201. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
  202. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
  203. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  204. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  205. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  206. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  207. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  208. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  209. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  210. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  211. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  212. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
  213. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
  214. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
  215. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
  216. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
  217. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  218. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
  219. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
  220. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  221. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
  222. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
  223. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  224. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
  225. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
  226. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  227. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  228. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  229. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  230. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
  231. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
  232. package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
  233. package/vendor/agent-frontend-shell/static/workspace.js +680 -0
@@ -0,0 +1,170 @@
1
+ # Hermes Web UI — Themes
2
+
3
+ Hermes Web UI splits **appearance** into two independent pickers:
4
+
5
+ - **Theme** — the mode: `System`, `Dark`, or `Light`. Drives the background,
6
+ text, surface, and chrome colors.
7
+ - **Skin** — the accent palette: built-in skins ship as named keys. Drives only
8
+ the `--accent` family (active states, links, focus rings, primary actions).
9
+
10
+ You pick one of each and they combine, so the look adapts to your environment
11
+ without losing your favorite accent — pure CSS, no Python changes needed.
12
+
13
+ ---
14
+
15
+ ## Switching Appearance
16
+
17
+ **Settings panel:** Click the gear icon → **Appearance**. The **Theme** card
18
+ toggles Light/Dark/System; the **Skin** grid offers the built-in accent palettes.
19
+ Preview is instant — the UI updates as you click.
20
+
21
+ **Slash command:** Type `/theme <name>` in the composer. The command accepts
22
+ both theme names (`system`, `dark`, `light`) and skin names (`default`, `ares`,
23
+ `mono`, `slate`, `poseidon`, `sisyphus`, `charizard`, `sienna`,
24
+ `catppuccin`, `nous`, `geist-contrast`). It updates the matching axis and leaves the other one
25
+ alone.
26
+
27
+ **Persistence:** Both choices are stored in `localStorage` for flicker-free
28
+ loading, and saved server-side via `POST /api/settings` (under `theme` and
29
+ `skin` keys in `settings.json`).
30
+
31
+ ---
32
+
33
+ ## Built-in Themes
34
+
35
+ | Theme | Description |
36
+ |-------|-------------|
37
+ | **System** (default) | Follows the OS `prefers-color-scheme` preference and updates live. |
38
+ | **Dark** | Deep dark surfaces, low-glare for long sessions. |
39
+ | **Light** | Bright surfaces with dark text, high contrast for daylight environments. |
40
+
41
+ The theme is applied as a class on `<html>`: `.dark` is present for dark mode,
42
+ absent for light. System mode tracks the OS preference at runtime.
43
+
44
+ ---
45
+
46
+ ## Built-in Skins
47
+
48
+ | Skin | Description |
49
+ |------|-------------|
50
+ | **Default** | The original Hermes gold accent. Warm and understated. |
51
+ | **Ares** | Fiery red. High-energy and assertive. |
52
+ | **Mono** | Neutral gray. Distraction-free, for deep focus. |
53
+ | **Slate** | Slate blue-gray. Subtle and grown-up. |
54
+ | **Poseidon** | Ocean blue. Calm and focused for long sessions. |
55
+ | **Sisyphus** | Vivid purple. Distinctive without being loud. |
56
+ | **Charizard** | Warm orange. Energetic and easy on the eyes. |
57
+ | **Sienna** | Warm clay and sand earth palette. Soft and natural. |
58
+ | **Catppuccin** | Catppuccin Latte/Mocha palette with Mauve accent. |
59
+ | **Nous** | Steel-blue accent with dashed technical surfaces. |
60
+ | **Geist Contrast** (`geist-contrast`) | Geist-inspired monochrome surfaces with a restrained dark-mode `#FFF175` accent. |
61
+
62
+ Each skin defines paired light + dark variants so it reads cleanly on either
63
+ theme. The skin is applied as `data-skin="<name>"` on `<html>` (the default
64
+ skin clears the attribute).
65
+
66
+ ---
67
+
68
+ ## Creating a Custom Skin
69
+
70
+ A skin is a small CSS block that overrides the accent variables for both the
71
+ light and dark variants:
72
+
73
+ ```css
74
+ /* Light variant */
75
+ :root[data-skin="my-skin"] {
76
+ --accent: #2E7D32; /* Active states, links, primary buttons */
77
+ --accent-hover: #1B5E20; /* Hover */
78
+ --accent-bg: rgba(46,125,50,0.08); /* Soft tinted backgrounds */
79
+ --accent-bg-strong: rgba(46,125,50,0.15); /* Highlighted backgrounds */
80
+ --accent-text: #1B5E20; /* Text on accent bg */
81
+ }
82
+
83
+ /* Dark variant — usually lighter or more saturated for contrast */
84
+ :root.dark[data-skin="my-skin"] {
85
+ --accent: #66BB6A;
86
+ --accent-hover: #43A047;
87
+ --accent-bg: rgba(102,187,106,0.08);
88
+ --accent-bg-strong: rgba(102,187,106,0.15);
89
+ --accent-text: #66BB6A;
90
+ }
91
+ ```
92
+
93
+ Two ways to ship it:
94
+
95
+ 1. **In the repo (built-in):** add the block to `static/style.css`, register it
96
+ in the Settings skin picker (`static/index.html`) and in the `/theme` command
97
+ list (`static/commands.js`), then open a PR.
98
+
99
+ 2. **Self-hosted (no fork):** use the WebUI extensions surface — see
100
+ `docs/EXTENSIONS.md`. Drop your CSS in `HERMES_WEBUI_EXTENSION_DIR` and
101
+ declare it in `HERMES_WEBUI_EXTENSION_STYLESHEET_URLS`. No code changes
102
+ needed; the skin attribute can be set from your own JS.
103
+
104
+ ### Tips
105
+
106
+ - **Test both themes.** A skin that pops on Dark can be illegible on Light.
107
+ Always check `:root[data-skin]` (light) *and* `:root.dark[data-skin]` (dark).
108
+ - **Pick contrasting `--accent-text` on `--accent-bg`.** The strong variant
109
+ appears behind small labels and chips; weak contrast there reads as blur.
110
+ - **The logo gradient uses `--accent` automatically**, so it adapts to your
111
+ skin without any extra work.
112
+ - **No server changes needed.** The `skin` setting in `settings.json` accepts
113
+ any string, so your custom skin name persists without code changes once you
114
+ load the CSS.
115
+
116
+ ---
117
+
118
+ ## Creating a Custom Theme
119
+
120
+ A full custom *theme* (a different overall mood, not just an accent change) is
121
+ a larger task than a skin: it has to redefine the core palette variables
122
+ (`--bg`, `--surface`, `--text`, `--border`, `--code-bg`, and friends) for one
123
+ or both modes. The contract is defined in the top `:root` and `:root.dark`
124
+ blocks of `static/style.css` — start there.
125
+
126
+ Most of the time, a custom **skin** is what you actually want. Reach for a
127
+ custom theme only when the existing Light/Dark modes don't fit (for example,
128
+ a high-contrast accessibility theme or an OLED black variant).
129
+
130
+ ---
131
+
132
+ ## Font Size
133
+
134
+ Right under Theme/Skin in **Settings → Appearance**: `Small`, `Default`,
135
+ `Large`. Applied as `data-font-size` on `<html>` and scales the WebUI's root
136
+ font size. Persists alongside theme and skin.
137
+
138
+ ---
139
+
140
+ ## How It Works Internally
141
+
142
+ 1. **Theme:** `document.documentElement.classList.toggle('dark', isDark)` —
143
+ light mode removes the class. System mode tracks
144
+ `matchMedia('(prefers-color-scheme: dark)')`.
145
+ 2. **Skin:** `document.documentElement.dataset.skin = name` (or remove the
146
+ attribute for `default`).
147
+ 3. **Font size:** `document.documentElement.dataset.fontSize = size` (or
148
+ remove for `default`).
149
+ 4. **No flash on load:** a tiny inline `<script>` in `<head>` reads
150
+ `localStorage` before the stylesheet does, so the right look is applied
151
+ before paint.
152
+ 5. **Server sync:** preferences are saved via `POST /api/settings` and
153
+ rehydrated on boot via `GET /api/settings`.
154
+
155
+ ---
156
+
157
+ ## Contributing a Skin
158
+
159
+ Skins are the easiest extension point — pure CSS, no Python, no JS logic. To
160
+ contribute one upstream:
161
+
162
+ 1. Add your `:root[data-skin="name"]` and `:root.dark[data-skin="name"]`
163
+ blocks to `static/style.css`.
164
+ 2. Register it in the Settings skin picker in `static/index.html` and in the
165
+ skin list used by `cmdTheme()` in `static/commands.js`.
166
+ 3. Test on desktop and mobile across both Light and Dark themes.
167
+ 4. Open a PR — skins are pure CSS additions with no backend changes needed.
168
+
169
+ For a custom *theme* (overriding the base palette), prefer opening an issue
170
+ first to discuss scope, since it touches many selectors.
@@ -0,0 +1 @@
1
+ """Hermes Web UI -- API modules."""
@@ -0,0 +1,392 @@
1
+ """Hermes agent/gateway heartbeat payload helpers (#716, #1879).
2
+
3
+ The WebUI process is not always paired with a long-running Hermes gateway. Some
4
+ setups use WebUI only, while self-hosted messaging deployments run a separate
5
+ Hermes gateway daemon that records runtime metadata in the Hermes Agent home.
6
+ This module turns those existing safe runtime signals into a small UI-facing
7
+ heartbeat without shelling out or adding psutil as a hard dependency.
8
+
9
+ Cross-container note (#1879): ``gateway.status.get_running_pid()`` uses
10
+ ``fcntl.flock`` and ``os.kill(pid, 0)``, both of which require the caller to
11
+ share a PID namespace with the gateway process. In multi-container deployments
12
+ where the WebUI runs separately from ``hermes-agent`` and only a Hermes data
13
+ volume is shared, those checks always return ``None`` and the dashboard
14
+ incorrectly shows "Gateway not running". To stay accurate without forcing a
15
+ ``pid: "service:hermes-agent"`` compose workaround, we accept a recent
16
+ ``updated_at`` timestamp on ``gateway_state.json`` (combined with
17
+ ``gateway_state == "running"``) as an equivalent live-process signal. Older
18
+ gateway builds do not refresh that file periodically, so a stale
19
+ ``gateway_state == "running"`` record is treated as inconclusive rather than a
20
+ confirmed outage.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import importlib
26
+ import json
27
+ from datetime import datetime, timezone
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ _GATEWAY_PID_FILE = "gateway.pid"
32
+ _GATEWAY_RUNTIME_STATUS_FILE = "gateway_state.json"
33
+
34
+
35
+ # Two cron ticks (~60s each). Chosen to avoid false negatives during brief
36
+ # gateway restarts while still surfacing a true outage within a couple of
37
+ # minutes. Override is intentionally not exposed: keep the check deterministic
38
+ # and identical across deployments so support diagnostics are reproducible.
39
+ GATEWAY_FRESHNESS_THRESHOLD_S: float = 120.0
40
+
41
+
42
+ def _checked_at() -> str:
43
+ return datetime.now(timezone.utc).isoformat()
44
+
45
+
46
+ def _runtime_status_is_fresh(
47
+ runtime_status: dict[str, Any] | None,
48
+ *,
49
+ now: datetime | None = None,
50
+ threshold_s: float = GATEWAY_FRESHNESS_THRESHOLD_S,
51
+ ) -> bool:
52
+ """Return ``True`` when ``gateway_state.json`` looks freshly written.
53
+
54
+ "Fresh" means the gateway self-reported ``running`` and the ``updated_at``
55
+ ISO-8601 timestamp is no older than ``threshold_s`` seconds. This is the
56
+ cross-container liveness signal used when ``get_running_pid()`` returns
57
+ ``None`` purely because of PID-namespace isolation (#1879).
58
+
59
+ Any unparseable input is treated as "not fresh" — a stale or missing
60
+ timestamp must never report alive.
61
+ """
62
+ if not isinstance(runtime_status, dict):
63
+ return False
64
+ if runtime_status.get("gateway_state") != "running":
65
+ return False
66
+
67
+ raw_updated_at = runtime_status.get("updated_at")
68
+ if not isinstance(raw_updated_at, str) or not raw_updated_at:
69
+ return False
70
+
71
+ # ``datetime.fromisoformat`` accepts the exact format gateway/status.py
72
+ # writes (``datetime.now(timezone.utc).isoformat()``). We deliberately
73
+ # don't pull in dateutil — keeping this stdlib-only matches the rest of
74
+ # this module.
75
+ try:
76
+ updated_at = datetime.fromisoformat(raw_updated_at)
77
+ except (TypeError, ValueError):
78
+ return False
79
+
80
+ if updated_at.tzinfo is None:
81
+ # A naive timestamp could mean anything across containers / hosts.
82
+ # Refuse to interpret it rather than assume UTC.
83
+ return False
84
+
85
+ reference = now if now is not None else datetime.now(timezone.utc)
86
+ age_s = (reference - updated_at).total_seconds()
87
+ if age_s < 0:
88
+ # Clock skew between containers can produce small negatives. A future
89
+ # timestamp is still a "fresh" signal — the gateway clearly wrote it
90
+ # very recently — so accept it. A wildly-future timestamp (> threshold
91
+ # in the future) is rejected to avoid trusting a broken clock.
92
+ return -age_s <= threshold_s
93
+ return age_s <= threshold_s
94
+
95
+
96
+ def _runtime_status_is_stale_stopped(
97
+ runtime_status: dict[str, Any] | None,
98
+ *,
99
+ now: datetime | None = None,
100
+ threshold_s: float = GATEWAY_FRESHNESS_THRESHOLD_S,
101
+ ) -> bool:
102
+ """Return ``True`` for an old clean-stop root gateway state.
103
+
104
+ A user may run only profile-scoped gateways while a root
105
+ ``gateway_state.json`` from an older, intentionally stopped gateway remains
106
+ on disk (#1944). Treat that stale stopped file like "no root gateway
107
+ configured" so the heartbeat banner does not keep warning about a service
108
+ the user is not running. Fresh stopped state still reports down.
109
+ """
110
+ if not isinstance(runtime_status, dict):
111
+ return False
112
+ if runtime_status.get("gateway_state") != "stopped":
113
+ return False
114
+
115
+ raw_updated_at = runtime_status.get("updated_at")
116
+ if not isinstance(raw_updated_at, str) or not raw_updated_at:
117
+ return False
118
+
119
+ try:
120
+ updated_at = datetime.fromisoformat(raw_updated_at)
121
+ except (TypeError, ValueError):
122
+ return False
123
+ if updated_at.tzinfo is None:
124
+ return False
125
+
126
+ reference = now if now is not None else datetime.now(timezone.utc)
127
+ age_s = (reference - updated_at).total_seconds()
128
+ return age_s > threshold_s
129
+
130
+
131
+ def _runtime_status_is_stale_running(
132
+ runtime_status: dict[str, Any] | None,
133
+ *,
134
+ now: datetime | None = None,
135
+ threshold_s: float = GATEWAY_FRESHNESS_THRESHOLD_S,
136
+ ) -> bool:
137
+ """Return ``True`` when the gateway last self-reported running, but stale.
138
+
139
+ WebUI often runs in a separate container from the gateway. In that shape PID
140
+ checks can be impossible, and older gateway versions only update
141
+ ``gateway_state.json`` on lifecycle/platform changes. A stale ``running``
142
+ file therefore means "not enough information from WebUI" rather than
143
+ "gateway is down".
144
+ """
145
+ if not isinstance(runtime_status, dict):
146
+ return False
147
+ if runtime_status.get("gateway_state") != "running":
148
+ return False
149
+
150
+ raw_updated_at = runtime_status.get("updated_at")
151
+ if not isinstance(raw_updated_at, str) or not raw_updated_at:
152
+ return False
153
+
154
+ try:
155
+ updated_at = datetime.fromisoformat(raw_updated_at)
156
+ except (TypeError, ValueError):
157
+ return False
158
+ if updated_at.tzinfo is None:
159
+ return False
160
+
161
+ reference = now if now is not None else datetime.now(timezone.utc)
162
+ age_s = (reference - updated_at).total_seconds()
163
+ return age_s > threshold_s
164
+
165
+
166
+ def _gateway_status_module():
167
+ """Load gateway.status lazily so tests and WebUI-only installs stay isolated."""
168
+ return importlib.import_module("gateway.status")
169
+
170
+
171
+ def _gateway_root_pid_path() -> Path | None:
172
+ """Return the root Hermes gateway PID path.
173
+
174
+ Gateway runtime files are root-level singletons. A profile-scoped WebUI
175
+ process may have HERMES_HOME=<root>/profiles/<name>, but gateway.pid,
176
+ gateway.lock, and gateway_state.json still live under <root>.
177
+
178
+ When the root-level gateway.pid is absent (profile-scoped gateway
179
+ deployments write it under <root>/profiles/<name>/), fall back to the
180
+ active profile's directory so the gateway is detected correctly.
181
+ """
182
+ try:
183
+ from hermes_constants import get_default_hermes_root
184
+ root_pid = get_default_hermes_root() / _GATEWAY_PID_FILE
185
+ if root_pid.exists():
186
+ return root_pid
187
+ try:
188
+ from api.profiles import get_active_hermes_home
189
+ profile_pid = Path(get_active_hermes_home()) / _GATEWAY_PID_FILE
190
+ if profile_pid.exists():
191
+ return profile_pid
192
+ except Exception:
193
+ pass
194
+ return root_pid
195
+ except Exception:
196
+ return None
197
+
198
+
199
+ def _read_runtime_status_path(path: Path) -> dict[str, Any] | None:
200
+ try:
201
+ payload = json.loads(path.read_text(encoding="utf-8"))
202
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
203
+ return None
204
+ if isinstance(payload, dict):
205
+ return payload
206
+ return None
207
+
208
+
209
+ def _read_gateway_runtime_status(gateway_status: Any, pid_path: Path | None) -> dict[str, Any] | None:
210
+ read_runtime_status = gateway_status.read_runtime_status
211
+ if pid_path is not None:
212
+ try:
213
+ return read_runtime_status(pid_path=pid_path)
214
+ except TypeError:
215
+ try:
216
+ return read_runtime_status(pid_path)
217
+ except TypeError:
218
+ if getattr(gateway_status, "__name__", "") == "gateway.status" or hasattr(
219
+ gateway_status,
220
+ "_read_json_file",
221
+ ):
222
+ runtime_status_file = str(
223
+ getattr(gateway_status, "_RUNTIME_STATUS_FILE", _GATEWAY_RUNTIME_STATUS_FILE)
224
+ )
225
+ runtime_status = _read_runtime_status_path(pid_path.with_name(runtime_status_file))
226
+ if runtime_status is not None:
227
+ return runtime_status
228
+ return read_runtime_status()
229
+
230
+
231
+ def _gateway_running_pid(gateway_status: Any, pid_path: Path | None) -> int | None:
232
+ get_running_pid = gateway_status.get_running_pid
233
+ if pid_path is not None:
234
+ try:
235
+ return get_running_pid(pid_path=pid_path, cleanup_stale=False)
236
+ except TypeError:
237
+ try:
238
+ return get_running_pid(pid_path, cleanup_stale=False)
239
+ except TypeError:
240
+ pass
241
+ try:
242
+ return get_running_pid(cleanup_stale=False)
243
+ except TypeError:
244
+ # Older agent versions may not expose cleanup_stale. Keep compatibility.
245
+ return get_running_pid()
246
+
247
+
248
+ def _runtime_detail_subset(runtime_status: dict[str, Any] | None) -> dict[str, Any]:
249
+ """Return only non-sensitive runtime fields for the browser.
250
+
251
+ gateway.status records argv/PID metadata so the CLI can validate process
252
+ identity. The WebUI alert only needs health semantics, never raw command
253
+ lines, paths, environment, or tokens.
254
+ """
255
+ if not isinstance(runtime_status, dict):
256
+ return {}
257
+
258
+ details: dict[str, Any] = {}
259
+ gateway_state = runtime_status.get("gateway_state")
260
+ if isinstance(gateway_state, str) and gateway_state:
261
+ details["gateway_state"] = gateway_state
262
+
263
+ updated_at = runtime_status.get("updated_at")
264
+ if isinstance(updated_at, str) and updated_at:
265
+ details["updated_at"] = updated_at
266
+
267
+ try:
268
+ details["active_agents"] = max(0, int(runtime_status.get("active_agents") or 0))
269
+ except (TypeError, ValueError):
270
+ pass
271
+
272
+ platforms = runtime_status.get("platforms")
273
+ if isinstance(platforms, dict):
274
+ details["platform_count"] = len(platforms)
275
+ states: dict[str, int] = {}
276
+ for payload in platforms.values():
277
+ if not isinstance(payload, dict):
278
+ continue
279
+ state = payload.get("state")
280
+ if isinstance(state, str) and state:
281
+ states[state] = states.get(state, 0) + 1
282
+ if states:
283
+ details["platform_states"] = states
284
+
285
+ return details
286
+
287
+
288
+ def build_agent_health_payload() -> dict[str, Any]:
289
+ """Return `{alive, checked_at, details}` for the Hermes gateway/agent.
290
+
291
+ `alive` is intentionally tri-state:
292
+ * True: a gateway runtime signal says the process is alive.
293
+ * False: gateway metadata exists, but no live gateway process owns it.
294
+ * None: no gateway metadata/status is available, so this WebUI setup is
295
+ probably not configured with a separate gateway process.
296
+ """
297
+ checked_at = _checked_at()
298
+ try:
299
+ gateway_status = _gateway_status_module()
300
+ except Exception as exc:
301
+ return {
302
+ "alive": None,
303
+ "checked_at": checked_at,
304
+ "details": {
305
+ "state": "unknown",
306
+ "reason": "gateway_status_unavailable",
307
+ "error": type(exc).__name__,
308
+ },
309
+ }
310
+
311
+ gateway_pid_path = _gateway_root_pid_path()
312
+
313
+ runtime_status = None
314
+ try:
315
+ runtime_status = _read_gateway_runtime_status(gateway_status, gateway_pid_path)
316
+ except Exception:
317
+ runtime_status = None
318
+
319
+ try:
320
+ running_pid = _gateway_running_pid(gateway_status, gateway_pid_path)
321
+ except Exception:
322
+ running_pid = None
323
+
324
+ safe_details = _runtime_detail_subset(runtime_status)
325
+ if running_pid is not None:
326
+ return {
327
+ "alive": True,
328
+ "checked_at": checked_at,
329
+ "details": {
330
+ "state": "alive",
331
+ **safe_details,
332
+ },
333
+ }
334
+
335
+ # Cross-container fallback (#1879): when ``get_running_pid()`` cannot see
336
+ # the gateway because we're in a different PID namespace, a recent
337
+ # ``updated_at`` on ``gateway_state.json`` is a reliable equivalent signal
338
+ # since the gateway writes it on every tick. We only trust this fallback
339
+ # when the gateway also self-reports ``gateway_state == "running"`` so
340
+ # crash-without-cleanup scenarios still surface as "down".
341
+ if _runtime_status_is_fresh(runtime_status):
342
+ return {
343
+ "alive": True,
344
+ "checked_at": checked_at,
345
+ "details": {
346
+ "state": "alive",
347
+ "reason": "cross_container_freshness",
348
+ **safe_details,
349
+ },
350
+ }
351
+
352
+ if _runtime_status_is_stale_stopped(runtime_status):
353
+ return {
354
+ "alive": None,
355
+ "checked_at": checked_at,
356
+ "details": {
357
+ "state": "unknown",
358
+ "reason": "gateway_stale_stopped_state",
359
+ **safe_details,
360
+ },
361
+ }
362
+
363
+ if _runtime_status_is_stale_running(runtime_status):
364
+ return {
365
+ "alive": None,
366
+ "checked_at": checked_at,
367
+ "details": {
368
+ "state": "unknown",
369
+ "reason": "gateway_stale_running_state",
370
+ **safe_details,
371
+ },
372
+ }
373
+
374
+ if isinstance(runtime_status, dict):
375
+ return {
376
+ "alive": False,
377
+ "checked_at": checked_at,
378
+ "details": {
379
+ "state": "down",
380
+ "reason": "gateway_not_running",
381
+ **safe_details,
382
+ },
383
+ }
384
+
385
+ return {
386
+ "alive": None,
387
+ "checked_at": checked_at,
388
+ "details": {
389
+ "state": "unknown",
390
+ "reason": "gateway_not_configured",
391
+ },
392
+ }