@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.
- package/README.md +213 -0
- package/bin/hermes-webui.mjs +588 -0
- package/package.json +25 -0
- package/scripts/sync-vendor.mjs +74 -0
- package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
- package/templates/systemd/hermes-webui.service +13 -0
- package/templates/windows/hermes-webui-task.ps1 +3 -0
- package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
- package/vendor/agent-frontend-shell/.dockerignore +7 -0
- package/vendor/agent-frontend-shell/.env.docker.example +89 -0
- package/vendor/agent-frontend-shell/.env.example +34 -0
- package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
- package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
- package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
- package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
- package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
- package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
- package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
- package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
- package/vendor/agent-frontend-shell/AGENTS.md +80 -0
- package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
- package/vendor/agent-frontend-shell/BUGS.md +52 -0
- package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
- package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
- package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
- package/vendor/agent-frontend-shell/DESIGN.md +173 -0
- package/vendor/agent-frontend-shell/Dockerfile +91 -0
- package/vendor/agent-frontend-shell/LICENSE +21 -0
- package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
- package/vendor/agent-frontend-shell/README.md +705 -0
- package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
- package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
- package/vendor/agent-frontend-shell/TESTING.md +1932 -0
- package/vendor/agent-frontend-shell/THEMES.md +170 -0
- package/vendor/agent-frontend-shell/api/__init__.py +1 -0
- package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
- package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
- package/vendor/agent-frontend-shell/api/auth.py +592 -0
- package/vendor/agent-frontend-shell/api/background.py +87 -0
- package/vendor/agent-frontend-shell/api/clarify.py +238 -0
- package/vendor/agent-frontend-shell/api/commands.py +124 -0
- package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
- package/vendor/agent-frontend-shell/api/config.py +5178 -0
- package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
- package/vendor/agent-frontend-shell/api/extensions.py +253 -0
- package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
- package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
- package/vendor/agent-frontend-shell/api/goals.py +608 -0
- package/vendor/agent-frontend-shell/api/helpers.py +474 -0
- package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
- package/vendor/agent-frontend-shell/api/metering.py +194 -0
- package/vendor/agent-frontend-shell/api/models.py +4210 -0
- package/vendor/agent-frontend-shell/api/oauth.py +770 -0
- package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
- package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
- package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
- package/vendor/agent-frontend-shell/api/providers.py +2175 -0
- package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
- package/vendor/agent-frontend-shell/api/rollback.py +320 -0
- package/vendor/agent-frontend-shell/api/routes.py +13990 -0
- package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
- package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
- package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
- package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
- package/vendor/agent-frontend-shell/api/session_events.py +45 -0
- package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
- package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
- package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
- package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
- package/vendor/agent-frontend-shell/api/startup.py +128 -0
- package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
- package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
- package/vendor/agent-frontend-shell/api/system_health.py +167 -0
- package/vendor/agent-frontend-shell/api/terminal.py +410 -0
- package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
- package/vendor/agent-frontend-shell/api/updates.py +1261 -0
- package/vendor/agent-frontend-shell/api/upload.py +322 -0
- package/vendor/agent-frontend-shell/api/usage.py +26 -0
- package/vendor/agent-frontend-shell/api/workspace.py +867 -0
- package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
- package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
- package/vendor/agent-frontend-shell/bootstrap.py +492 -0
- package/vendor/agent-frontend-shell/ctl.sh +427 -0
- package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
- package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
- package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
- package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
- package/vendor/agent-frontend-shell/docker_init.bash +459 -0
- package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
- package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
- package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
- package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
- package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
- package/vendor/agent-frontend-shell/docs/docker.md +337 -0
- package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
- package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
- package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
- package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
- package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
- package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
- package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
- package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
- package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
- package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
- package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
- package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
- package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
- package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
- package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
- package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
- package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
- package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
- package/vendor/agent-frontend-shell/extensions/index.js +67 -0
- package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
- package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
- package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
- package/vendor/agent-frontend-shell/install.sh +63 -0
- package/vendor/agent-frontend-shell/mcp_server.py +567 -0
- package/vendor/agent-frontend-shell/package.json +12 -0
- package/vendor/agent-frontend-shell/pyproject.toml +56 -0
- package/vendor/agent-frontend-shell/pytest.ini +3 -0
- package/vendor/agent-frontend-shell/requirements.txt +5 -0
- package/vendor/agent-frontend-shell/server.py +624 -0
- package/vendor/agent-frontend-shell/start.ps1 +210 -0
- package/vendor/agent-frontend-shell/start.sh +65 -0
- package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/static/boot.js +1990 -0
- package/vendor/agent-frontend-shell/static/commands.js +1402 -0
- package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
- package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
- package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
- package/vendor/agent-frontend-shell/static/icons.js +92 -0
- package/vendor/agent-frontend-shell/static/index.html +1506 -0
- package/vendor/agent-frontend-shell/static/login.js +177 -0
- package/vendor/agent-frontend-shell/static/manifest.json +53 -0
- package/vendor/agent-frontend-shell/static/messages.js +3521 -0
- package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
- package/vendor/agent-frontend-shell/static/panels.js +7995 -0
- package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
- package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
- package/vendor/agent-frontend-shell/static/style.css +4774 -0
- package/vendor/agent-frontend-shell/static/sw.js +173 -0
- package/vendor/agent-frontend-shell/static/terminal.js +632 -0
- package/vendor/agent-frontend-shell/static/ui.js +8997 -0
- package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
- package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
- 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
|
+
}
|