@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,1658 @@
|
|
|
1
|
+
# Hermes Web UI: Developer and Architecture Guide
|
|
2
|
+
|
|
3
|
+
> This document is the canonical reference for anyone (human or agent) working on the
|
|
4
|
+
> Hermes Web UI. It covers the exact current state of the code, every design decision and
|
|
5
|
+
> quirk discovered during development, and a phased architecture improvement roadmap that
|
|
6
|
+
> runs in parallel with the feature roadmap in ROADMAP.md.
|
|
7
|
+
>
|
|
8
|
+
> Keep this document updated as architecture changes are made.
|
|
9
|
+
|
|
10
|
+
> Current shipped build: `v0.51.192` (May 31, 2026).
|
|
11
|
+
> Automated coverage: ~7,150 tests via `pytest tests/ --collect-only -q`. CI runs on
|
|
12
|
+
> Python 3.11, 3.12, and 3.13 (3 parallel shards each) against every PR, plus a ruff
|
|
13
|
+
> lint gate, a headless browser smoke test, and a Docker smoke test.
|
|
14
|
+
>
|
|
15
|
+
> Notable architecture state: the bootstrap and first-run onboarding flow own setup discovery; the default WebUI state directory is `~/.hermes/webui`; `ctl.sh` provides a daemon wrapper for homelab installs; chat streaming is still WebUI-owned SSE with stream-ownership guards, cancellation, async manual compression, and turn-journal audit plumbing; provider/model discovery is profile-aware with live-model cache invalidation and custom-provider scoping. (Version/test-count numbers above are a periodic snapshot — the authoritative source is the latest git tag and `pytest --collect-only`.)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. Overview and Purpose
|
|
20
|
+
|
|
21
|
+
The Hermes Web UI is a lightweight web application that gives you a browser-based
|
|
22
|
+
interface to the Hermes agent that is functionally equivalent to the CLI. It is modeled on
|
|
23
|
+
the Claude-style interface: a sidebar for session management, a central chat area,
|
|
24
|
+
and a demand-driven right panel used for workspace browsing and preview surfaces.
|
|
25
|
+
The right panel is closed by default on desktop and opens only when it is actively
|
|
26
|
+
being used for browsing or previewing content.
|
|
27
|
+
|
|
28
|
+
To prevent a visible first-paint mismatch on refresh, `static/index.html` preloads the
|
|
29
|
+
saved workspace panel state into `document.documentElement.dataset.workspacePanel`
|
|
30
|
+
before the main stylesheet loads. Desktop CSS honors that preload marker immediately,
|
|
31
|
+
and `static/boot.js` keeps the dataset synchronized with the runtime panel state machine.
|
|
32
|
+
|
|
33
|
+
The design philosophy is deliberately minimal. There is no build step, no bundler, no
|
|
34
|
+
frontend framework. The Python server is split into a routing shell (server.py) and
|
|
35
|
+
business logic modules (api/). The frontend is seven vanilla JS modules loaded from static/.
|
|
36
|
+
This makes the code easy to modify from a terminal or by an agent.
|
|
37
|
+
|
|
38
|
+
Hermes-level chrome is intentionally consolidated: the sidebar has no dedicated brand header.
|
|
39
|
+
Instead, the footer exposes a single "Hermes WebUI" launch button that opens one tabbed
|
|
40
|
+
control-center modal for global preferences, conversation import/export, and clear-conversation
|
|
41
|
+
actions. The topbar remains focused on conversation context and the workspace/files toggle.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. File Inventory
|
|
46
|
+
|
|
47
|
+
<repo>/
|
|
48
|
+
server.py Thin routing shell + HTTP Handler + auth middleware.
|
|
49
|
+
Delegates all route handling to api/routes.py.
|
|
50
|
+
bootstrap.py One-shot launcher: optional agent install, deps, health wait, browser open.
|
|
51
|
+
start.sh Thin wrapper around bootstrap.py for shell-based startup.
|
|
52
|
+
ctl.sh Daemon lifecycle wrapper (start/stop/restart/status/logs) for homelab installs.
|
|
53
|
+
pyproject.toml Tooling config (ruff lint gate). NOT a packaged distribution.
|
|
54
|
+
Dockerfile python:3.12-slim container image
|
|
55
|
+
docker-compose.yml Compose config with named volume and optional auth
|
|
56
|
+
.dockerignore Excludes .git, tests/, .env* from Docker builds
|
|
57
|
+
api/
|
|
58
|
+
__init__.py Package marker
|
|
59
|
+
auth.py Optional password authentication, signed cookies, passkeys/WebAuthn
|
|
60
|
+
config.py Discovery, globals, model detection, reloadable config
|
|
61
|
+
helpers.py HTTP helpers: j(), bad(), require(), safe_resolve(), security headers
|
|
62
|
+
models.py Session model + CRUD, per-session profile tracking, CLI/state.db bridge
|
|
63
|
+
profiles.py Profile state management, hermes_cli wrapper
|
|
64
|
+
onboarding.py First-run onboarding status, real provider config writes, OAuth linking, readiness detection
|
|
65
|
+
routes.py All GET + POST route handlers (if/elif dispatch, no decorators)
|
|
66
|
+
startup.py Startup helpers: auto_install_agent_deps()
|
|
67
|
+
state_sync.py /insights sync — message_count to the agent's state.db
|
|
68
|
+
streaming.py SSE engine, run_agent, cancel, compression, HERMES_HOME save/restore
|
|
69
|
+
updates.py Self-update check and release notes
|
|
70
|
+
upload.py Multipart parser, file upload handler
|
|
71
|
+
workspace.py File ops: list_dir, read_file_content, git detection, workspace helpers
|
|
72
|
+
static/
|
|
73
|
+
index.html HTML template
|
|
74
|
+
style.css All CSS incl. mobile responsive, themes + skins, KaTeX
|
|
75
|
+
ui.js DOM helpers, renderMd, tool cards, context indicator, file tree
|
|
76
|
+
workspace.js File preview, file ops, git badge, central api() fetch wrapper
|
|
77
|
+
sessions.js Session CRUD, list rendering, collapsible groups, search, SSE sync
|
|
78
|
+
messages.js send(), SSE event handlers, approval/clarify, transcript, recovery
|
|
79
|
+
panels.js Cron, skills, memory, profiles, todo, settings (Control Center)
|
|
80
|
+
commands.js Slash command registry, parser, autocomplete dropdown
|
|
81
|
+
boot.js Event wiring, mobile nav, voice input, theme/skin boot, bfcache handler
|
|
82
|
+
onboarding.js First-run wizard overlay, provider setup flow
|
|
83
|
+
i18n.js Localization catalog (en, es, de, zh, zh-Hant, ru, …)
|
|
84
|
+
login.js Login page + open-redirect guard
|
|
85
|
+
icons.js Lucide icon path registry
|
|
86
|
+
sw.js Service worker: offline shell cache, version-pinned assets
|
|
87
|
+
tests/
|
|
88
|
+
conftest.py Isolated test server/state fixtures
|
|
89
|
+
~700 test files ~7,150 tests collected via pytest (run `pytest --collect-only -q` for exact)
|
|
90
|
+
test_regressions.py Permanent regression gate
|
|
91
|
+
CONTRIBUTING.md Contributor workflow and PR expectations.
|
|
92
|
+
ROADMAP.md Feature and product roadmap document.
|
|
93
|
+
SPRINTS.md Forward sprint plan with CLI + Claude parity targets.
|
|
94
|
+
ARCHITECTURE.md THIS FILE.
|
|
95
|
+
TESTING.md Manual browser test plan and automated coverage reference.
|
|
96
|
+
CHANGELOG.md Release notes per version.
|
|
97
|
+
CONTRIBUTORS.md Community credit roll (regenerated via the maintainer workspace script).
|
|
98
|
+
requirements.txt Python dependencies.
|
|
99
|
+
.env.example Sample environment variable overrides.
|
|
100
|
+
|
|
101
|
+
> Per-file line counts intentionally omitted — they drift every release. Use
|
|
102
|
+
> `git ls-files | xargs wc -l` (or your editor) for current sizes; the role of
|
|
103
|
+
> each file above is the durable part.
|
|
104
|
+
|
|
105
|
+
State directory (runtime data, separate from source):
|
|
106
|
+
|
|
107
|
+
~/.hermes/webui/
|
|
108
|
+
sessions/ One JSON file per session: {session_id}.json
|
|
109
|
+
workspaces.json Registered workspaces list
|
|
110
|
+
last_workspace.txt Last-used workspace path
|
|
111
|
+
settings.json User settings (default model, workspace, send key, password hash)
|
|
112
|
+
projects.json Session project groups (name, color, id)
|
|
113
|
+
|
|
114
|
+
Log file:
|
|
115
|
+
|
|
116
|
+
~/.hermes/webui/bootstrap-8787.log start.sh/bootstrap background server log
|
|
117
|
+
~/.hermes/webui.log ctl.sh daemon log
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 3. Runtime Environment
|
|
122
|
+
|
|
123
|
+
- Python interpreter: <agent-dir>/venv/bin/python
|
|
124
|
+
- The venv has all Hermes agent dependencies (run_agent, tools/*, cron/*)
|
|
125
|
+
- Server binds to 127.0.0.1:8787 (localhost only, not public internet)
|
|
126
|
+
- Access from Mac: SSH tunnel: ssh -N -L 8787:127.0.0.1:8787 <user>@<your-server>
|
|
127
|
+
- The server imports Hermes modules via sys.path.insert(0, parent_dir)
|
|
128
|
+
|
|
129
|
+
Environment variables controlling behavior:
|
|
130
|
+
|
|
131
|
+
HERMES_WEBUI_HOST Bind address (default: 127.0.0.1)
|
|
132
|
+
HERMES_WEBUI_PORT Port (default: 8787)
|
|
133
|
+
HERMES_WEBUI_DEFAULT_WORKSPACE Default workspace path for new sessions
|
|
134
|
+
HERMES_WEBUI_STATE_DIR Where sessions/ folder lives
|
|
135
|
+
HERMES_CONFIG_PATH Path to ~/.hermes/config.yaml
|
|
136
|
+
HERMES_WEBUI_DEFAULT_MODEL Optional model override; unset means provider default
|
|
137
|
+
HERMES_WEBUI_PASSWORD Optional: enable password auth (off by default)
|
|
138
|
+
HERMES_WEBUI_SKIP_ONBOARDING Optional: bypass the first-run onboarding wizard
|
|
139
|
+
HERMES_PREFILL_MESSAGES_FILE Optional JSON message list for browser-turn prefill context
|
|
140
|
+
HERMES_WEBUI_PREFILL_MESSAGES_SCRIPT Optional command that prints JSON messages or plain-text user prefill context
|
|
141
|
+
HERMES_WEBUI_PREFILL_MESSAGES_SCRIPT_TIMEOUT Optional script timeout in seconds (default 5, max 30)
|
|
142
|
+
HERMES_WEBUI_PREFILL_CONTEXT_MAX_CHARS Optional parsed prefill budget in characters (default 12000, 0 disables)
|
|
143
|
+
HERMES_HOME Base directory for Hermes state (~/.hermes by default)
|
|
144
|
+
|
|
145
|
+
Test isolation environment variables (set by conftest.py):
|
|
146
|
+
|
|
147
|
+
HERMES_WEBUI_TEST_PORT=... Optional pinned test port
|
|
148
|
+
HERMES_WEBUI_TEST_STATE_DIR=~/.hermes/webui-test-* Optional pinned test state
|
|
149
|
+
HERMES_WEBUI_DEFAULT_WORKSPACE=.../test-workspace Isolated test workspace
|
|
150
|
+
|
|
151
|
+
Tests NEVER talk to the production server (port 8787).
|
|
152
|
+
The test state dir is wiped before each test session and deleted after.
|
|
153
|
+
See: <repo>/tests/conftest.py
|
|
154
|
+
|
|
155
|
+
Per-request environment variables (set by chat handler, restored after):
|
|
156
|
+
|
|
157
|
+
TERMINAL_CWD Set to session.workspace before running agent.
|
|
158
|
+
The terminal tool reads this to default cwd.
|
|
159
|
+
HERMES_EXEC_ASK Set to "1" to enable approval gate for dangerous commands.
|
|
160
|
+
HERMES_SESSION_KEY Set to session_id. The approval tool keys pending entries
|
|
161
|
+
by this value, enabling per-session approval state.
|
|
162
|
+
HERMES_HOME Set to the active profile's directory before running agent.
|
|
163
|
+
Saved and restored around each agent run.
|
|
164
|
+
|
|
165
|
+
WARNING: These env vars are process-global. Two concurrent chat requests will clobber
|
|
166
|
+
each other. This is safe only for single-user, single-concurrent-request use.
|
|
167
|
+
See Architecture Phase B for the fix.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 4. Server Architecture: Current State
|
|
172
|
+
|
|
173
|
+
### 4.1 HTTP Server Layer
|
|
174
|
+
|
|
175
|
+
Python stdlib ThreadingHTTPServer (from http.server). Each HTTP request runs in its own
|
|
176
|
+
thread. The Handler class subclasses BaseHTTPRequestHandler with two methods:
|
|
177
|
+
|
|
178
|
+
do_GET Routes: /, /health, /api/session, /api/sessions, /api/list,
|
|
179
|
+
/api/chat/stream, /api/file, /api/approval/pending,
|
|
180
|
+
/api/session/worktree/status
|
|
181
|
+
do_POST Routes: /api/upload, /api/session/new, /api/session/update,
|
|
182
|
+
/api/session/delete, /api/chat/start, /api/chat,
|
|
183
|
+
/api/approval/respond, /api/session/worktree/remove
|
|
184
|
+
|
|
185
|
+
Routing is a flat if/elif chain inside each method. No routing framework.
|
|
186
|
+
|
|
187
|
+
Helper functions used by all handlers:
|
|
188
|
+
|
|
189
|
+
j(handler, payload, status=200) Sends JSON response with correct headers
|
|
190
|
+
t(handler, payload, status=200, ct) Sends plain text or HTML response
|
|
191
|
+
read_body(handler) Reads and JSON-parses the POST body
|
|
192
|
+
|
|
193
|
+
CRITICAL ORDERING RULE in do_POST:
|
|
194
|
+
The /api/upload check MUST appear BEFORE calling read_body(). read_body() calls
|
|
195
|
+
handler.rfile.read() which consumes the HTTP body stream. The upload handler also
|
|
196
|
+
needs rfile (to read the multipart payload). If read_body() runs first on a multipart
|
|
197
|
+
request, the upload handler receives an empty body and the upload silently fails.
|
|
198
|
+
|
|
199
|
+
### 4.2 Session Model
|
|
200
|
+
|
|
201
|
+
Session is a plain Python class (not a dataclass, not SQLAlchemy):
|
|
202
|
+
|
|
203
|
+
Fields:
|
|
204
|
+
session_id hex string, 12 chars (uuid4().hex[:12])
|
|
205
|
+
title string, auto-set from first user message
|
|
206
|
+
workspace absolute path string, resolved at creation
|
|
207
|
+
model model ID string (e.g. "anthropic/claude-sonnet-4.6")
|
|
208
|
+
messages list of OpenAI-format message dicts
|
|
209
|
+
created_at float Unix timestamp
|
|
210
|
+
updated_at float Unix timestamp, updated on every save()
|
|
211
|
+
pinned bool, default False (Sprint 12)
|
|
212
|
+
archived bool, default False (Sprint 14)
|
|
213
|
+
project_id string or null, FK to projects.json (Sprint 15)
|
|
214
|
+
tool_calls list of tool call dicts (Sprint 10)
|
|
215
|
+
|
|
216
|
+
Key methods:
|
|
217
|
+
path (property) Returns SESSION_DIR/{session_id}.json
|
|
218
|
+
save() Writes __dict__ as pretty JSON to path, updates updated_at
|
|
219
|
+
load(cls, sid) Class method: reads JSON from disk, returns Session or None
|
|
220
|
+
compact() Returns metadata-only dict (no messages) for the session list
|
|
221
|
+
|
|
222
|
+
In-memory cache:
|
|
223
|
+
SESSIONS = {} dict: session_id -> Session object
|
|
224
|
+
LOCK = threading.Lock() defined but NOT currently used around SESSIONS access
|
|
225
|
+
|
|
226
|
+
get_session(sid): checks SESSIONS cache, loads from disk on miss, raises KeyError
|
|
227
|
+
new_session(workspace, model): creates Session, caches in SESSIONS, saves, returns
|
|
228
|
+
all_sessions(): scans SESSION_DIR/*.json + SESSIONS, deduplicates, sorts by updated_at,
|
|
229
|
+
returns list of compact() dicts
|
|
230
|
+
|
|
231
|
+
all_sessions() does a full directory scan on every call.
|
|
232
|
+
With 10 sessions: negligible. With 1000+: will be slow.
|
|
233
|
+
See Architecture Phase C for the index file fix.
|
|
234
|
+
|
|
235
|
+
title_from(): takes messages list, finds first user message, returns first 64 chars.
|
|
236
|
+
Called after run_conversation() completes to set the session title retroactively.
|
|
237
|
+
|
|
238
|
+
### 4.3 SSE Streaming Engine
|
|
239
|
+
|
|
240
|
+
This is the most architecturally interesting part. Two endpoints cooperate:
|
|
241
|
+
|
|
242
|
+
POST /api/chat/start Receives the user message. Creates a queue.Queue, stores it
|
|
243
|
+
in STREAMS[stream_id], spawns a daemon thread running
|
|
244
|
+
_run_agent_streaming(), returns {stream_id} immediately.
|
|
245
|
+
|
|
246
|
+
GET /api/chat/stream Long-lived SSE connection. Reads from STREAMS[stream_id]
|
|
247
|
+
and forwards events to the browser until 'done' or 'error'.
|
|
248
|
+
|
|
249
|
+
Queue registry:
|
|
250
|
+
|
|
251
|
+
STREAMS = {} dict: stream_id -> queue.Queue
|
|
252
|
+
STREAMS_LOCK = threading.Lock()
|
|
253
|
+
|
|
254
|
+
SSE event types and their data shapes:
|
|
255
|
+
|
|
256
|
+
token {"text": "..."} LLM token delta
|
|
257
|
+
tool {"name": "...", "preview": "..."} Tool invocation started
|
|
258
|
+
approval {"command": "...", "description": "...", "pattern_keys": [...]}
|
|
259
|
+
done {"session": {compact_fields + messages}} Agent finished successfully
|
|
260
|
+
error {"message": "...", "trace": "..."} Agent threw exception
|
|
261
|
+
|
|
262
|
+
The SSE handler loop:
|
|
263
|
+
- Blocks on queue.get(timeout=30)
|
|
264
|
+
- On timeout (no events in 30s): sends a heartbeat comment (": heartbeat
|
|
265
|
+
|
|
266
|
+
")
|
|
267
|
+
to keep the connection alive through proxies and firewalls
|
|
268
|
+
- On 'done' or 'error' event: breaks the loop and returns
|
|
269
|
+
- Catches BrokenPipeError and ConnectionResetError silently (browser disconnected)
|
|
270
|
+
|
|
271
|
+
Stream cleanup: _run_agent_streaming() pops its stream_id from STREAMS in a finally
|
|
272
|
+
block. If the browser disconnects mid-stream, the daemon thread runs to completion and
|
|
273
|
+
then cleans up. The queue fills and the put_nowait() calls fail silently (queue.Full
|
|
274
|
+
is caught).
|
|
275
|
+
|
|
276
|
+
Fallback sync endpoint: POST /api/chat still exists and holds the connection open until
|
|
277
|
+
the agent finishes. The frontend never uses it but it can be useful for debugging.
|
|
278
|
+
|
|
279
|
+
### 4.4 Agent Invocation (_run_agent_streaming)
|
|
280
|
+
|
|
281
|
+
def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id):
|
|
282
|
+
|
|
283
|
+
1. Fetches session from SESSIONS (not from disk -- session was just updated by /api/chat/start)
|
|
284
|
+
2. Sets TERMINAL_CWD, HERMES_EXEC_ASK, HERMES_SESSION_KEY env vars
|
|
285
|
+
3. Creates AIAgent with:
|
|
286
|
+
- model=model, platform='cli', quiet_mode=True
|
|
287
|
+
- enabled_toolsets=CLI_TOOLSETS (from config.yaml or hardcoded default)
|
|
288
|
+
- session_id=session_id
|
|
289
|
+
- stream_delta_callback=on_token (fires per token)
|
|
290
|
+
- tool_progress_callback=on_tool (fires per tool invocation)
|
|
291
|
+
4. Calls agent.run_conversation(user_message=msg_text, conversation_history=s.messages,
|
|
292
|
+
task_id=session_id)
|
|
293
|
+
NOTE: keyword is task_id NOT session_id (common mistake, documented in skill)
|
|
294
|
+
5. On return: updates s.messages, calls title_from(), saves session
|
|
295
|
+
6. Puts ('done', {session: ...}) into queue
|
|
296
|
+
7. Finally block: restores env vars, pops stream_id from STREAMS
|
|
297
|
+
|
|
298
|
+
on_token callback:
|
|
299
|
+
if text is None: return # end-of-stream sentinel from AIAgent
|
|
300
|
+
put('token', {'text': text})
|
|
301
|
+
|
|
302
|
+
on_tool callback:
|
|
303
|
+
put('tool', {'name': name, 'preview': preview})
|
|
304
|
+
# Also immediately surface any pending approval:
|
|
305
|
+
if has_pending(session_id):
|
|
306
|
+
with _lock: p = dict(_pending.get(session_id, {}))
|
|
307
|
+
if p: put('approval', p)
|
|
308
|
+
|
|
309
|
+
The approval surface-on-tool logic means approvals appear immediately after the tool
|
|
310
|
+
fires (within the same SSE stream), without waiting for the next poll cycle.
|
|
311
|
+
|
|
312
|
+
### 4.5 Approval System Integration
|
|
313
|
+
|
|
314
|
+
The approval system uses the existing Hermes gateway module at tools/approval.py.
|
|
315
|
+
All state lives in module-level variables in that file:
|
|
316
|
+
|
|
317
|
+
_pending = {} dict: session_key -> pending_entry_dict
|
|
318
|
+
_lock = Lock() protects _pending
|
|
319
|
+
_permanent_approved set of permanently approved pattern keys
|
|
320
|
+
|
|
321
|
+
Because server.py imports tools.approval at module load time and everything runs in the
|
|
322
|
+
same process, this state IS shared between HTTP threads and agent daemon threads.
|
|
323
|
+
|
|
324
|
+
Important: this only works because Python imports are cached (sys.modules). The same
|
|
325
|
+
module object is used everywhere. If the approval module were ever imported in a subprocess
|
|
326
|
+
or via importlib.reload(), this would break.
|
|
327
|
+
|
|
328
|
+
GET /api/approval/pending:
|
|
329
|
+
- Peeks at _pending[sid] without removing it
|
|
330
|
+
- Returns {pending: entry} or {pending: null}
|
|
331
|
+
- Called by the browser every 1500ms while S.busy is true (polling fallback)
|
|
332
|
+
|
|
333
|
+
POST /api/approval/respond:
|
|
334
|
+
- Pops _pending[sid] (removes it)
|
|
335
|
+
- For choice "once" or "session": calls approve_session(sid, pattern_key) for each key
|
|
336
|
+
- For choice "always": calls approve_session + approve_permanent + save_permanent_allowlist
|
|
337
|
+
- For choice "deny": just pops, does nothing (agent gets denied result)
|
|
338
|
+
- Returns {ok: true, choice: choice}
|
|
339
|
+
|
|
340
|
+
### 4.6 File Upload Parser
|
|
341
|
+
|
|
342
|
+
parse_multipart(rfile, content_type, content_length):
|
|
343
|
+
- Reads all content_length bytes from rfile into memory (up to MAX_UPLOAD_BYTES, default 20MB, env-overridable via HERMES_WEBUI_MAX_UPLOAD_MB)
|
|
344
|
+
- Extracts boundary from Content-Type header
|
|
345
|
+
- Splits raw bytes on b'--' + boundary
|
|
346
|
+
- For each part: parses MIME headers via email.parser.HeaderParser
|
|
347
|
+
- Returns (fields, files) where fields is {name: value} and files is {name: (filename, bytes)}
|
|
348
|
+
|
|
349
|
+
handle_upload(handler):
|
|
350
|
+
- Calls parse_multipart()
|
|
351
|
+
- Validates: file field present, filename present, session exists
|
|
352
|
+
- Sanitizes filename: replaces non-word chars with _, truncates to 200 chars
|
|
353
|
+
- Writes bytes to session.workspace / safe_name
|
|
354
|
+
- Returns {filename, path, size}
|
|
355
|
+
|
|
356
|
+
Why not cgi.FieldStorage:
|
|
357
|
+
- Deprecated in Python 3.11+
|
|
358
|
+
- Broken for binary files (silently corrupts or throws)
|
|
359
|
+
- The manual parser handles all file types correctly
|
|
360
|
+
|
|
361
|
+
### 4.7 File System Operations
|
|
362
|
+
|
|
363
|
+
safe_resolve(root, requested):
|
|
364
|
+
- Resolves requested path relative to root
|
|
365
|
+
- Calls .relative_to(root) to assert the result is inside root
|
|
366
|
+
- Raises ValueError on path traversal (../../etc/passwd)
|
|
367
|
+
|
|
368
|
+
list_dir(workspace, rel='.'):
|
|
369
|
+
- Calls safe_resolve, then iterdir()
|
|
370
|
+
- Sorts: directories first, then files, case-insensitive alpha within each group
|
|
371
|
+
- Returns up to 200 entries with {name, path, type, size}
|
|
372
|
+
|
|
373
|
+
read_file_content(workspace, rel):
|
|
374
|
+
- Calls safe_resolve
|
|
375
|
+
- Enforces MAX_FILE_BYTES = 200KB size limit
|
|
376
|
+
- Reads as UTF-8 with errors='replace' (binary files show replacement chars)
|
|
377
|
+
- Returns {path, content, size, lines}
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## 5. Frontend Architecture: Current State
|
|
382
|
+
|
|
383
|
+
### 5.1 Structure
|
|
384
|
+
|
|
385
|
+
The frontend is served from static/ as separate files: one HTML template, one CSS file,
|
|
386
|
+
and multiple JavaScript modules. External dependencies include Prism.js (syntax
|
|
387
|
+
highlighting), Mermaid.js (diagrams), xterm.js, and KaTeX assets loaded with the
|
|
388
|
+
current static template's integrity/CSP assumptions.
|
|
389
|
+
|
|
390
|
+
Core JS modules loaded by the app include:
|
|
391
|
+
1. ui.js (~7216 lines) DOM helpers, renderMd, tool card rendering, global state
|
|
392
|
+
2. workspace.js (~369 lines) File tree, preview, file operations
|
|
393
|
+
3. sessions.js (~3517 lines) Session CRUD, list rendering, search, SVG icons, dropdown actions, project picker
|
|
394
|
+
4. messages.js (~2301 lines) send(), SSE event handlers, approval, transcript
|
|
395
|
+
5. panels.js (~6480 lines) Cron, skills, memory, workspace, profiles, todo, settings
|
|
396
|
+
6. commands.js (~1302 lines) Slash command registry, parser, autocomplete dropdown
|
|
397
|
+
7. boot.js (~1607 lines) Event wiring + boot IIFE
|
|
398
|
+
|
|
399
|
+
sessions.js defines an `ICONS` constant at module level with hardcoded SVG strings for all
|
|
400
|
+
session action buttons (pin, unpin, folder, archive, unarchive, duplicate, trash). All icons
|
|
401
|
+
inherit `currentColor` for consistent theming.
|
|
402
|
+
|
|
403
|
+
Three-panel layout (in static/index.html):
|
|
404
|
+
|
|
405
|
+
<aside class="sidebar"> Left panel: session list, nav tabs, sidebar-footer Hermes WebUI trigger
|
|
406
|
+
<main class="main"> Center: topbar, messages area, approval card, composer
|
|
407
|
+
<aside class="rightpanel"> Right panel: workspace file tree and file preview
|
|
408
|
+
|
|
409
|
+
Composer footer layout (current):
|
|
410
|
+
|
|
411
|
+
left cluster attach button, mic button, per-conversation model selector
|
|
412
|
+
right cluster compact circular context-usage badge, send button
|
|
413
|
+
|
|
414
|
+
The model selector is still the authoritative control for new-session creation
|
|
415
|
+
and session updates; it was moved out of the sidebar so model choice feels scoped
|
|
416
|
+
to the active conversation rather than a global app setting.
|
|
417
|
+
|
|
418
|
+
### 5.2 Global State
|
|
419
|
+
|
|
420
|
+
const S = {
|
|
421
|
+
session: null, // current Session compact dict (includes model, workspace, title)
|
|
422
|
+
messages: [], // full messages array for current session
|
|
423
|
+
entries: [], // current directory listing
|
|
424
|
+
busy: false, // true while agent is running (disables Send button)
|
|
425
|
+
pendingFiles: [] // File objects queued for upload with next message
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const INFLIGHT = {}
|
|
429
|
+
// keyed by session_id while a request is in-flight for that session
|
|
430
|
+
// value: {messages: [...snapshot...], uploaded: [...filenames...]}
|
|
431
|
+
// Purpose: if user switches sessions while a request is pending,
|
|
432
|
+
// switching back shows the in-progress state instead of the saved state
|
|
433
|
+
|
|
434
|
+
### 5.3 Key Functions Reference
|
|
435
|
+
|
|
436
|
+
Session management:
|
|
437
|
+
newSession() POST /api/session/new, update S.session, save to localStorage
|
|
438
|
+
loadSession(sid) GET /api/session?session_id=X, check INFLIGHT first, update S
|
|
439
|
+
deleteSession(sid) POST /api/session/delete, handle active/inactive cases correctly
|
|
440
|
+
renderSessionList() GET /api/sessions, rebuild #sessionList DOM
|
|
441
|
+
|
|
442
|
+
Chat:
|
|
443
|
+
send() Main action: upload files, POST /api/chat/start, open EventSource
|
|
444
|
+
uploadPendingFiles() Upload each file in S.pendingFiles, return filenames array
|
|
445
|
+
appendThinking() Adds three-dot animation to message list
|
|
446
|
+
removeThinking() Removes thinking dots (called on first token or on error)
|
|
447
|
+
|
|
448
|
+
Rendering:
|
|
449
|
+
renderMessages() Full rebuild of #msgInner from S.messages
|
|
450
|
+
renderMd(raw) Homegrown markdown renderer (see 5.4 for known gaps)
|
|
451
|
+
syncTopbar() Updates topbar title, meta, model chip, workspace chip
|
|
452
|
+
renderTray() Updates attach tray showing pending files
|
|
453
|
+
|
|
454
|
+
Approval:
|
|
455
|
+
showApprovalCard(p) Shows the approval card with command/description text
|
|
456
|
+
hideApprovalCard() Hides approval card, clears text
|
|
457
|
+
respondApproval(ch) POST /api/approval/respond, hide card
|
|
458
|
+
startApprovalPolling setInterval 1500ms GET /api/approval/pending
|
|
459
|
+
stopApprovalPolling clearInterval
|
|
460
|
+
|
|
461
|
+
UI helpers:
|
|
462
|
+
setStatus(t) Fallback helper: shows a toast for non-chat status/error messages
|
|
463
|
+
setComposerStatus(t) Updates the inline composer status label for turn-scoped states
|
|
464
|
+
setBusy(v) Sets S.busy, disables/enables Send button, clears status on false
|
|
465
|
+
showToast(msg, ms) Bottom-center fade toast (default 2800ms)
|
|
466
|
+
showConfirmDialog(o) Shared in-app confirmation modal, resolves true/false
|
|
467
|
+
showPromptDialog(o) Shared in-app input modal, resolves string/null
|
|
468
|
+
autoResize() Auto-resize #msg textarea up to 200px
|
|
469
|
+
|
|
470
|
+
Dialog policy:
|
|
471
|
+
Native browser confirm()/prompt() are not used in the Web UI.
|
|
472
|
+
Destructive actions use showConfirmDialog(...), then a toast on success.
|
|
473
|
+
Lightweight naming flows (new file/folder/project) use showPromptDialog(...).
|
|
474
|
+
|
|
475
|
+
Files:
|
|
476
|
+
loadDir(path) GET /api/list, rebuild #fileTree
|
|
477
|
+
openFile(path) GET /api/file, show in #previewArea
|
|
478
|
+
|
|
479
|
+
Transcript:
|
|
480
|
+
transcript() Builds markdown string from S.messages for download
|
|
481
|
+
|
|
482
|
+
Boot IIFE:
|
|
483
|
+
localStorage key 'hermes-webui-session' stores last session_id
|
|
484
|
+
On load: try to loadSession(saved), fall back to empty state if missing or fails
|
|
485
|
+
NEVER auto-creates a session on boot
|
|
486
|
+
|
|
487
|
+
### 5.4 Markdown Renderer (renderMd)
|
|
488
|
+
|
|
489
|
+
A hand-rolled regex chain with HTML safety. Processes in this order:
|
|
490
|
+
|
|
491
|
+
Pre-pass (v0.18.1):
|
|
492
|
+
0a. Stash fenced code blocks and backtick spans (fence_stash array)
|
|
493
|
+
0b. Convert safe HTML tags to markdown equivalents:
|
|
494
|
+
<strong>/<b> -> **text**, <em>/<i> -> *text*, <code> -> `text`, <br> -> newline
|
|
495
|
+
0c. Restore stashed code blocks
|
|
496
|
+
|
|
497
|
+
Pipeline:
|
|
498
|
+
1. Mermaid blocks (```mermaid ... ```) -> <div class="mermaid-block">
|
|
499
|
+
2. Code blocks (``` lang ... ```) -> <pre><code> with language header
|
|
500
|
+
3. Inline code (`...`) -> <code>
|
|
501
|
+
4. Bold+italic (***..***) -> <strong><em>
|
|
502
|
+
5. Bold (**...**) -> <strong>
|
|
503
|
+
6. Italic (*...*) -> <em>
|
|
504
|
+
7. Headings (# ## ###) -> <h1> <h2> <h3> (uses inlineMd() for content)
|
|
505
|
+
8. Horizontal rules (---+) -> <hr>
|
|
506
|
+
9. Blockquotes (> ...) -> <blockquote> (uses inlineMd() for content)
|
|
507
|
+
10. Unordered lists (- or * or + at line start) -> <ul><li> (uses inlineMd())
|
|
508
|
+
11. Ordered lists (N. at line start) -> <ol><li> (uses inlineMd())
|
|
509
|
+
12. Links ([text](https://...)) -> <a href target=_blank>
|
|
510
|
+
13. Tables (| col | col |) -> <table>
|
|
511
|
+
14. Safety net: escape any HTML tag not in SAFE_TAGS allowlist via esc()
|
|
512
|
+
15. Paragraph wrapping: remaining double-newline-separated blocks -> <p>
|
|
513
|
+
|
|
514
|
+
inlineMd() helper (v0.18.1):
|
|
515
|
+
Processes inline bold/italic/code/links within list items, blockquotes,
|
|
516
|
+
and headings. Escapes unknown tags via SAFE_INLINE allowlist. Replaces
|
|
517
|
+
the old direct esc() calls which would double-escape pre-pass output.
|
|
518
|
+
|
|
519
|
+
SAFE_TAGS allowlist:
|
|
520
|
+
strong, em, code, pre, h1-6, ul, ol, li, table, thead, tbody, tr, th,
|
|
521
|
+
td, hr, blockquote, p, br, a, div. Everything else is escaped.
|
|
522
|
+
|
|
523
|
+
Known gaps:
|
|
524
|
+
- Nested lists: single regex pass, multi-level indentation not handled
|
|
525
|
+
- Mixed bold+link in same line: may produce garbled output
|
|
526
|
+
|
|
527
|
+
### 5.5 Model Label Resolution (Fixed in Sprint 1, reused by composer selector)
|
|
528
|
+
|
|
529
|
+
B3 was resolved in Sprint 1. Current code uses a MODEL_LABELS dict:
|
|
530
|
+
|
|
531
|
+
const MODEL_LABELS = {
|
|
532
|
+
'openai/gpt-5.4-mini': 'GPT-5.4 Mini', 'openai/gpt-4o': 'GPT-4o',
|
|
533
|
+
'openai/o3': 'o3', 'openai/o4-mini': 'o4-mini',
|
|
534
|
+
'anthropic/claude-sonnet-4.6': 'Sonnet 4.6', 'anthropic/claude-sonnet-4-5': 'Sonnet 4.5',
|
|
535
|
+
'anthropic/claude-haiku-3-5': 'Haiku 3.5', 'google/gemini-2.5-pro': 'Gemini 2.5 Pro',
|
|
536
|
+
'deepseek/deepseek-chat-v3-0324': 'DeepSeek V3', 'meta-llama/llama-4-scout': 'Llama 4 Scout',
|
|
537
|
+
};
|
|
538
|
+
getModelLabel(m) => MODEL_LABELS[m] || (m.split('/').pop() || 'Unknown');
|
|
539
|
+
|
|
540
|
+
Fallback: any unlisted model shows its short ID (after the last /) rather than a wrong label.
|
|
541
|
+
To add a new model: add an entry to MODEL_LABELS and add an <option> to the composer footer <select>.
|
|
542
|
+
|
|
543
|
+
### 5.6 Session Delete Rules (from skill)
|
|
544
|
+
|
|
545
|
+
These rules are critical. GPT-5.4-mini has repeatedly re-introduced broken versions.
|
|
546
|
+
|
|
547
|
+
1. deleteSession() NEVER calls newSession(). Deleting does not create.
|
|
548
|
+
2. If deleted session was active AND other sessions exist: load sessions[0] (most recent).
|
|
549
|
+
3. If deleted session was active AND no sessions remain: show empty state.
|
|
550
|
+
4. If deleted session was not active: just re-render the list.
|
|
551
|
+
5. Always show toast("Conversation deleted") after any delete.
|
|
552
|
+
|
|
553
|
+
### 5.7 Send() Session Guard
|
|
554
|
+
|
|
555
|
+
Before any async operations in send():
|
|
556
|
+
const activeSid = S.session.session_id;
|
|
557
|
+
|
|
558
|
+
After the agent completes:
|
|
559
|
+
if (S.session && S.session.session_id === activeSid) {
|
|
560
|
+
// apply result, re-render
|
|
561
|
+
setBusy(false);
|
|
562
|
+
} else {
|
|
563
|
+
// user switched sessions mid-flight
|
|
564
|
+
// only refresh sidebar, do NOT call setBusy(false) on the new session
|
|
565
|
+
await renderSessionList();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
This prevents a session switch mid-flight from either clobbering the new session's state
|
|
569
|
+
or unlocking the Send button on the wrong session.
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## 6. Data Flow: Full Chat Round Trip
|
|
574
|
+
|
|
575
|
+
Step-by-step trace of what happens when you type a message and press Send:
|
|
576
|
+
|
|
577
|
+
1. User types, presses Enter. send() is called.
|
|
578
|
+
2. Guard: return if (!text && !pendingFiles) || S.busy
|
|
579
|
+
3. If S.session is null: await newSession(), await renderSessionList()
|
|
580
|
+
4. Capture activeSid = S.session.session_id (before any awaits)
|
|
581
|
+
5. uploadPendingFiles(): POST each file in S.pendingFiles to /api/upload
|
|
582
|
+
- Shows upload progress bar
|
|
583
|
+
- Clears S.pendingFiles on completion
|
|
584
|
+
- Returns array of uploaded filenames
|
|
585
|
+
6. Build msgText from text + file note
|
|
586
|
+
7. Build userMsg {role:'user', content: displayText, attachments?: filenames}
|
|
587
|
+
8. Push userMsg to S.messages, call renderMessages(), appendThinking()
|
|
588
|
+
9. setBusy(true), setStatus('Hermes is thinking...')
|
|
589
|
+
10. INFLIGHT[activeSid] = {messages: [...S.messages], uploaded}
|
|
590
|
+
11. startApprovalPolling(activeSid)
|
|
591
|
+
12. POST /api/chat/start {session_id, message, model, workspace}
|
|
592
|
+
Server: saves session, creates queue.Queue, starts daemon thread, returns {stream_id}
|
|
593
|
+
13. Browser opens EventSource('/api/chat/stream?stream_id=X')
|
|
594
|
+
14. In the SSE loop:
|
|
595
|
+
- 'token': assistantText += d.text, ensureAssistantRow(), render markdown
|
|
596
|
+
- 'tool': setStatus('tool name...')
|
|
597
|
+
- 'approval': showApprovalCard(d)
|
|
598
|
+
- 'done': sync S from d.session, renderMessages(), loadDir, renderSessionList,
|
|
599
|
+
setBusy(false), delete INFLIGHT[activeSid]
|
|
600
|
+
- 'error': show error message, setBusy(false)
|
|
601
|
+
- es.onerror: handle network drops (show error, setBusy(false))
|
|
602
|
+
15. If approval needed: user clicks a button, respondApproval() fires
|
|
603
|
+
POST /api/approval/respond -> server pops _pending, calls approve_*
|
|
604
|
+
Agent retries the command (now is_approved() returns True) and continues
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## 7. Dependency Map
|
|
609
|
+
|
|
610
|
+
server.py imports from api/ modules (config, helpers, models, workspace, upload, streaming).
|
|
611
|
+
The api/ modules in turn import Hermes internals:
|
|
612
|
+
|
|
613
|
+
api/streaming.py imports:
|
|
614
|
+
run_agent.AIAgent Main agent class. Wraps LLM + tool execution.
|
|
615
|
+
api/config.py imports:
|
|
616
|
+
yaml Config loading.
|
|
617
|
+
server.py imports:
|
|
618
|
+
tools.approval.* Module-level approval state (with graceful fallback).
|
|
619
|
+
Standard library across all modules: json, os, re, sys, threading, time, traceback,
|
|
620
|
+
uuid, http.server, pathlib, urllib.parse, email.parser, queue, collections
|
|
621
|
+
|
|
622
|
+
AIAgent constructor parameters used:
|
|
623
|
+
|
|
624
|
+
model= OpenRouter model ID string
|
|
625
|
+
platform='cli' Sets the platform context for tool selection
|
|
626
|
+
quiet_mode=True Suppresses agent's own stdout output
|
|
627
|
+
enabled_toolsets= List of toolset names from config.yaml
|
|
628
|
+
session_id= Used for tool state keying (memory, todos, etc.)
|
|
629
|
+
stream_delta_callback= Called per token delta (or None as sentinel)
|
|
630
|
+
tool_progress_callback= Called per tool invocation (name, preview, args)
|
|
631
|
+
|
|
632
|
+
AIAgent.run_conversation() parameters:
|
|
633
|
+
|
|
634
|
+
user_message= The human turn text
|
|
635
|
+
conversation_history= Prior messages list (OpenAI format)
|
|
636
|
+
task_id= Session ID (NOTE: NOT session_id=, it is task_id=)
|
|
637
|
+
|
|
638
|
+
Return value:
|
|
639
|
+
|
|
640
|
+
{
|
|
641
|
+
'messages': [...], Full conversation including new turns
|
|
642
|
+
'final_response': '...', Last assistant text response
|
|
643
|
+
'completed': True/False, Whether the conversation completed normally
|
|
644
|
+
...other fields
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## 8. Configuration Loading
|
|
650
|
+
|
|
651
|
+
On startup, server.py reads ~/.hermes/config.yaml:
|
|
652
|
+
|
|
653
|
+
cfg = yaml.safe_load(CONFIG_PATH.read_text())
|
|
654
|
+
CLI_TOOLSETS = cfg.get('platform_toolsets', {}).get('cli', [...default...])
|
|
655
|
+
|
|
656
|
+
Default toolset list (hardcoded fallback):
|
|
657
|
+
browser, clarify, code_execution, cronjob, delegation, file,
|
|
658
|
+
image_gen, memory, session_search, skills, terminal, todo, tts, vision, web
|
|
659
|
+
|
|
660
|
+
The web UI always runs with the full CLI toolset. There is no per-session toolset
|
|
661
|
+
restriction from the UI yet (see ROADMAP.md Wave 4 for the plan).
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## 9. Known Bugs and Technical Debt Summary
|
|
666
|
+
|
|
667
|
+
| ID | Severity | Description | Status | Fix |
|
|
668
|
+
|-----|----------|------------------------------------------------------|------------------|------------------|
|
|
669
|
+
| B1 | Critical | Approval wiring untested; pattern_keys not shown | FIXED Sprint 1 | Card shows keys; inject_test endpoint added for verification |
|
|
670
|
+
| B2 | High | File input no accept attribute | FIXED Sprint 1 | accept= added with image/*, text/*, pdf, code extensions |
|
|
671
|
+
| B3 | High | Model chip label hardcodes sonnet substring check | FIXED Sprint 1 | MODEL_LABELS map; fallback to short model ID |
|
|
672
|
+
| B4 | High | Reload mid-stream: stream_id lost, no reconnect | FIXED Sprint 1 | stream/status endpoint; reconnect banner via localStorage |
|
|
673
|
+
| B5 | High | INFLIGHT in-memory only, lost on reload | FIXED Sprint 1 | markInflight/clearInflight in localStorage |
|
|
674
|
+
| B6 | Medium | New sessions always use DEFAULT_WORKSPACE | FIXED Sprint 3 | newSession() passes S.session.workspace to /api/session/new |
|
|
675
|
+
| B7 | Medium | Sidebar title overflow: missing min-width:0 | FIXED Sprint 1 | min-width:0 on .session-item |
|
|
676
|
+
| B8 | Medium | renderMd missing tables, nested lists | PARTIAL Sprint 4 | Tables Sprint 2; nested lists improved Sprint 4; full fix still Phase E |
|
|
677
|
+
| B9 | Medium | Empty assistant messages can render | FIXED Sprint 1 | loadSession() filters empty-text assistant messages |
|
|
678
|
+
| B10 | Low | Thinking dots stay during tool-running | FIXED Sprint 3 | removeThinking() on first tool event; compact 'Running X...' row shown |
|
|
679
|
+
| B11 | Low | GET /api/session no-ID silently creates session | FIXED Sprint 1 | Returns 400 with error message |
|
|
680
|
+
| B12 | Low | Preview panel display:none to flex layout jump | FIXED Sprint 4 | visibility/opacity transition replaces display:none toggle |
|
|
681
|
+
| B13 | Low | No CORS headers | Open | Phase H |
|
|
682
|
+
| B14 | Low | No keyboard shortcut for new chat | FIXED Sprint 3 | Cmd/Ctrl+K triggers newSession() from anywhere |
|
|
683
|
+
| TD1 | Critical | Env vars are process-global (concurrent request bug) | PARTIAL Sprint 5 | Thread-local _set_thread_env() added. Per-session lock from Sprint 4. Process-level env still written as fallback. Full fix needs terminal tool to read thread-local. |
|
|
684
|
+
| TD2 | High | SESSIONS cache: no eviction, locking missing | FIXED Sprint 5 | OrderedDict + LRU cap 100 + move_to_end on access. LOCK from Sprint 1. Complete. |
|
|
685
|
+
| TD3 | High | No test coverage | PARTIAL Sprint 1 | 19 HTTP integration tests added; unit tests pending Phase A split |
|
|
686
|
+
| TD4 | Medium | All code in one file (HTML/CSS/JS/Python mingled) | FIXED Sprint 5 | JS extracted to static/app.js in Sprint 5 (Sprint 9: app.js deleted, replaced by 6 modules). Phase A complete. |
|
|
687
|
+
| TD5 | Medium | No request validation (KeyError -> 500 + traceback) | FIXED Sprint 4 | All endpoints hardened: /api/list, /api/file, /api/crons/* all return clean 400/404 |
|
|
688
|
+
| TD6 | Low | all_sessions() full directory scan every call | FIXED Sprint 5 | Session index file (_index.json) built on every save. all_sessions() reads index O(1). Phase C partial. |
|
|
689
|
+
| TD7 | Low | No structured logging | FIXED Sprint 1 | log_request() override emits JSON per request |
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## 10. Architecture Improvement Roadmap
|
|
694
|
+
|
|
695
|
+
These phases run in parallel with the feature roadmap. Each phase targets software
|
|
696
|
+
quality: testability, resilience, maintainability, and modularity.
|
|
697
|
+
|
|
698
|
+
### Phase A: File Separation -- COMPLETE
|
|
699
|
+
|
|
700
|
+
Split server.py into a proper package. Completed across Sprints 4-10.
|
|
701
|
+
|
|
702
|
+
Current structure:
|
|
703
|
+
|
|
704
|
+
<repo>/
|
|
705
|
+
server.py Entry point + HTTP Handler dispatch (~446 lines)
|
|
706
|
+
api/
|
|
707
|
+
__init__.py
|
|
708
|
+
routes.py All GET + POST route handlers (~9772 lines)
|
|
709
|
+
config.py Configuration, constants, global state, model discovery (~4139 lines)
|
|
710
|
+
helpers.py HTTP helpers: j(), bad(), require(), safe_resolve() (~302 lines)
|
|
711
|
+
models.py Session model + CRUD (~1927 lines)
|
|
712
|
+
workspace.py File ops, workspace management (~810 lines)
|
|
713
|
+
upload.py Multipart parser, file upload handler (~284 lines)
|
|
714
|
+
streaming.py SSE engine, run_agent, cancel support (~4420 lines)
|
|
715
|
+
static/
|
|
716
|
+
index.html HTML document (served from disk)
|
|
717
|
+
style.css All CSS (~3767 lines)
|
|
718
|
+
ui.js, workspace.js, sessions.js, messages.js, panels.js, commands.js, boot.js
|
|
719
|
+
tests/
|
|
720
|
+
conftest.py Isolated test server/state fixtures
|
|
721
|
+
488 test files 5303 tests collected
|
|
722
|
+
test_regressions.py Permanent regression gate
|
|
723
|
+
|
|
724
|
+
Route extraction to api/routes.py completed in Sprint 11. server.py remains a
|
|
725
|
+
thin shell relative to the rest of the app: Handler class with headers,
|
|
726
|
+
structured logging, dispatch to routes, TLS wrapping, and main().
|
|
727
|
+
|
|
728
|
+
### Phase B: Thread-Safe Request Context (Priority: Critical, Effort: Medium)
|
|
729
|
+
|
|
730
|
+
Replace process-global env vars with thread-local or explicit parameter passing.
|
|
731
|
+
|
|
732
|
+
Root cause: TERMINAL_CWD, HERMES_EXEC_ASK, HERMES_SESSION_KEY are set via os.environ
|
|
733
|
+
in _run_agent_streaming(). Two concurrent sessions clobber each other.
|
|
734
|
+
|
|
735
|
+
Fix options (in order of preference):
|
|
736
|
+
|
|
737
|
+
Option 1 (best): Check if AIAgent constructor accepts a context dict. Pass workspace,
|
|
738
|
+
exec_ask, and session_key directly. Zero env var usage in server code.
|
|
739
|
+
|
|
740
|
+
Option 2: Use threading.local():
|
|
741
|
+
_ctx = threading.local()
|
|
742
|
+
# In _run_agent_streaming:
|
|
743
|
+
_ctx.workspace = str(workspace)
|
|
744
|
+
_ctx.session_key = session_id
|
|
745
|
+
# In tools that read env vars: check _ctx first, fall back to os.environ
|
|
746
|
+
|
|
747
|
+
Option 3 (interim, safe for single-user): Wrap the env var block in a per-session lock:
|
|
748
|
+
SESSION_AGENT_LOCKS = {} # session_id -> Lock
|
|
749
|
+
# Only one agent run per session at a time
|
|
750
|
+
with SESSION_AGENT_LOCKS.setdefault(session_id, threading.Lock()):
|
|
751
|
+
os.environ[...] = ...
|
|
752
|
+
result = agent.run_conversation(...)
|
|
753
|
+
|
|
754
|
+
Phase B also includes: review all other os.environ reads/writes in the codebase for
|
|
755
|
+
similar thread-safety issues.
|
|
756
|
+
|
|
757
|
+
### Phase C: Session Store Improvements -- COMPLETE
|
|
758
|
+
|
|
759
|
+
All three problems fixed in Sprint 5:
|
|
760
|
+
|
|
761
|
+
1. SESSIONS cache: OrderedDict with LRU cap of 100, oldest evicted automatically.
|
|
762
|
+
2. LOCK: all SESSIONS dict reads/writes wrapped with LOCK (from Sprint 1).
|
|
763
|
+
3. Session index: `sessions/_index.json` maintained on every save/delete.
|
|
764
|
+
`all_sessions()` reads the index file (O(1)) instead of scanning all JSONs.
|
|
765
|
+
|
|
766
|
+
### Phase D: Input Validation and Error Handling -- COMPLETE
|
|
767
|
+
|
|
768
|
+
Completed in Sprint 4-6:
|
|
769
|
+
|
|
770
|
+
1. `require()` and `bad()` helpers in `api/helpers.py` for parameter validation.
|
|
771
|
+
2. All endpoints return clean 400/404 responses instead of tracebacks.
|
|
772
|
+
3. Structured JSON request logging via `log_request()` override (Sprint 1).
|
|
773
|
+
|
|
774
|
+
### Phase E: Frontend Modularization -- COMPLETE
|
|
775
|
+
|
|
776
|
+
Completed across Sprints 5, 6, and 9:
|
|
777
|
+
|
|
778
|
+
1. HTML extracted to `static/index.html` (Sprint 6).
|
|
779
|
+
2. CSS extracted to `static/style.css` (Sprint 4).
|
|
780
|
+
3. `app.js` deleted Sprint 9, replaced by 6 focused modules:
|
|
781
|
+
`ui.js`, `workspace.js`, `sessions.js`, `messages.js`, `panels.js`, `boot.js`.
|
|
782
|
+
Loaded as standard `<script>` tags (not ES modules) in dependency order.
|
|
783
|
+
4. Prism.js added for syntax highlighting (Sprint 8) via CDN, deferred load.
|
|
784
|
+
|
|
785
|
+
Remaining: renderMd() is still a hand-rolled regex chain. Tables partially supported.
|
|
786
|
+
Replacing with marked.js + DOMPurify is a future improvement (not blocking).
|
|
787
|
+
|
|
788
|
+
### Phase F: API Design Cleanup (Priority: Low, Effort: Medium)
|
|
789
|
+
|
|
790
|
+
1. Version prefix: add /api/v1/ to all new endpoints.
|
|
791
|
+
Keep /api/* as aliases for backward compatibility.
|
|
792
|
+
|
|
793
|
+
2. Standard response envelope:
|
|
794
|
+
Success: {"ok": true, "data": {...}}
|
|
795
|
+
Error: {"ok": false, "error": "message", "code": "ERROR_CODE"}
|
|
796
|
+
|
|
797
|
+
3. Session list pagination:
|
|
798
|
+
GET /api/v1/sessions?limit=30&offset=0
|
|
799
|
+
Response: {"ok": true, "data": {"sessions": [...], "total": N, "has_more": false}}
|
|
800
|
+
|
|
801
|
+
4. Consistent naming: use snake_case for all JSON keys.
|
|
802
|
+
|
|
803
|
+
### Phase G: Observability -- MOSTLY COMPLETE
|
|
804
|
+
|
|
805
|
+
1. Structured JSON logging: COMPLETE (Sprint 1). Per-request JSON is printed to the active launcher log (`~/.hermes/webui/bootstrap-8787.log` for `start.sh`, `~/.hermes/webui.log` for `ctl.sh`).
|
|
806
|
+
2. Enhanced /health: COMPLETE (Sprint 7). Returns `active_streams`, `uptime_seconds`.
|
|
807
|
+
3. GET /api/debug/stats: NOT YET IMPLEMENTED. Low priority.
|
|
808
|
+
|
|
809
|
+
### Phase H: Authentication (Priority: Low, Effort: Medium)
|
|
810
|
+
|
|
811
|
+
Optional password gate for non-SSH-tunnel deployments.
|
|
812
|
+
|
|
813
|
+
1. HERMES_WEBUI_PASSWORD env var enables auth
|
|
814
|
+
2. Login page: minimal dark form, POST /api/auth/login
|
|
815
|
+
3. Server sets HttpOnly + SameSite=Strict cookie on successful login
|
|
816
|
+
4. All API endpoints check cookie if HERMES_WEBUI_PASSWORD is set
|
|
817
|
+
5. Cookie validity: 30 days from last activity
|
|
818
|
+
|
|
819
|
+
### Phase I: Test Infrastructure -- COMPLETE
|
|
820
|
+
|
|
821
|
+
5303 tests across 488 test files + regression gates. The pytest fixture derives
|
|
822
|
+
an isolated port and state directory from the repo path unless
|
|
823
|
+
`HERMES_WEBUI_TEST_PORT` / `HERMES_WEBUI_TEST_STATE_DIR` pin them explicitly.
|
|
824
|
+
Production data never touched.
|
|
825
|
+
|
|
826
|
+
Fixtures in `conftest.py`: auto-cleanup, profile/config isolation, cron
|
|
827
|
+
isolation, workspace reset, and test-server lifecycle.
|
|
828
|
+
|
|
829
|
+
### Phase J: Performance (Priority: Low, Effort: High)
|
|
830
|
+
|
|
831
|
+
For scale beyond single-user casual use.
|
|
832
|
+
|
|
833
|
+
1. Session index (Phase C prerequisite): O(1) session list loads
|
|
834
|
+
2. Message pagination: /api/session returns last 50 messages, paginate older ones
|
|
835
|
+
3. Frontend virtual scroll: IntersectionObserver for both message list and session list
|
|
836
|
+
4. Stream cleanup background thread: evict STREAMS entries older than 5 minutes
|
|
837
|
+
5. File tree lazy loading: expand-on-click fetches subdirectory contents
|
|
838
|
+
|
|
839
|
+
---
|
|
840
|
+
|
|
841
|
+
## 11. How To Add a New API Endpoint
|
|
842
|
+
|
|
843
|
+
Follow this exact pattern. Review existing handlers in do_GET/do_POST for reference.
|
|
844
|
+
|
|
845
|
+
### Backend (server.py -> future: api/handlers.py)
|
|
846
|
+
|
|
847
|
+
GET endpoint:
|
|
848
|
+
|
|
849
|
+
# Inside do_GET, before the 404 fallback line:
|
|
850
|
+
if parsed.path == '/api/your/endpoint':
|
|
851
|
+
qs = parse_qs(parsed.query)
|
|
852
|
+
param = qs.get('param', [''])[0]
|
|
853
|
+
if not param:
|
|
854
|
+
return j(self, {'error': 'param is required'}, status=400)
|
|
855
|
+
# do work
|
|
856
|
+
return j(self, {'result': value})
|
|
857
|
+
|
|
858
|
+
POST endpoint (AFTER /api/upload check, body already parsed):
|
|
859
|
+
|
|
860
|
+
if parsed.path == '/api/your/endpoint':
|
|
861
|
+
value = body.get('field', '')
|
|
862
|
+
if not value:
|
|
863
|
+
return j(self, {'error': 'field is required'}, status=400)
|
|
864
|
+
# do work
|
|
865
|
+
return j(self, {'ok': True, 'data': result})
|
|
866
|
+
|
|
867
|
+
Endpoint requiring a valid session:
|
|
868
|
+
|
|
869
|
+
sid = body.get('session_id', '')
|
|
870
|
+
try:
|
|
871
|
+
s = get_session(sid)
|
|
872
|
+
except KeyError:
|
|
873
|
+
return j(self, {'error': 'Session not found'}, status=404)
|
|
874
|
+
|
|
875
|
+
Endpoint that calls Hermes Python modules:
|
|
876
|
+
|
|
877
|
+
# Example: calling cron.jobs
|
|
878
|
+
import sys
|
|
879
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
880
|
+
from cron.jobs import list_jobs
|
|
881
|
+
jobs = list_jobs(include_disabled=True)
|
|
882
|
+
return j(self, {'jobs': jobs})
|
|
883
|
+
|
|
884
|
+
### Frontend (6 static JS modules: ui.js, workspace.js, sessions.js, messages.js, panels.js, boot.js)
|
|
885
|
+
|
|
886
|
+
Simple GET fetch:
|
|
887
|
+
|
|
888
|
+
const data = await api('/api/your/endpoint?param=' + encodeURIComponent(value));
|
|
889
|
+
// data is parsed JSON response, throws on error
|
|
890
|
+
|
|
891
|
+
POST:
|
|
892
|
+
|
|
893
|
+
const data = await api('/api/your/endpoint', {
|
|
894
|
+
method: 'POST',
|
|
895
|
+
body: JSON.stringify({field: value})
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
The api() helper:
|
|
899
|
+
|
|
900
|
+
async function api(path, opts={}) {
|
|
901
|
+
const r = await fetch(path, {headers:{'Content-Type':'application/json'},...opts});
|
|
902
|
+
const d = await r.json();
|
|
903
|
+
if (!r.ok) throw new Error(d.error || r.statusText);
|
|
904
|
+
return d;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
## 12. Common Debugging Commands
|
|
910
|
+
|
|
911
|
+
# Server health and session count
|
|
912
|
+
curl -s http://127.0.0.1:8787/health | python3 -m json.tool
|
|
913
|
+
|
|
914
|
+
# Tail the server log live
|
|
915
|
+
tail -f ~/.hermes/webui/bootstrap-8787.log
|
|
916
|
+
tail -f ~/.hermes/webui.log # when launched through ctl.sh
|
|
917
|
+
|
|
918
|
+
# List all sessions (metadata only)
|
|
919
|
+
curl -s http://127.0.0.1:8787/api/sessions | python3 -m json.tool
|
|
920
|
+
|
|
921
|
+
# Inspect a full session with messages
|
|
922
|
+
SID=your_session_id_here
|
|
923
|
+
curl -s "http://127.0.0.1:8787/api/session?session_id=$SID" | python3 -m json.tool
|
|
924
|
+
|
|
925
|
+
# Kill and restart server cleanly
|
|
926
|
+
pkill -f "python.*server.py"
|
|
927
|
+
<repo>/start.sh
|
|
928
|
+
|
|
929
|
+
# Check if server process is running
|
|
930
|
+
ps aux | grep "server.py"
|
|
931
|
+
|
|
932
|
+
# Inspect session files on disk
|
|
933
|
+
ls -lt ~/.hermes/webui/sessions/
|
|
934
|
+
cat ~/.hermes/webui/sessions/SESSION_ID.json | python3 -m json.tool
|
|
935
|
+
|
|
936
|
+
# Count messages in a session
|
|
937
|
+
python3 -c "import json; d=json.load(open('sessions/SID.json')); print(len(d['messages']))"
|
|
938
|
+
|
|
939
|
+
# Check approval module state
|
|
940
|
+
cd <agent-dir>
|
|
941
|
+
venv/bin/python -c "from tools.approval import _pending; print(_pending)"
|
|
942
|
+
|
|
943
|
+
# Check active SSE streams (requires server access)
|
|
944
|
+
curl -s http://127.0.0.1:8787/health # streams not exposed yet, add in Phase G
|
|
945
|
+
|
|
946
|
+
# Find all sessions with messages (not Untitled empty)
|
|
947
|
+
ls ~/.hermes/webui/sessions/ | xargs -I{} python3 -c "
|
|
948
|
+
import json, sys
|
|
949
|
+
d = json.load(open('~/.hermes/webui/sessions/{}'))
|
|
950
|
+
if d['messages']: print('{}', d['title'][:50])
|
|
951
|
+
" 2>/dev/null
|
|
952
|
+
|
|
953
|
+
---
|
|
954
|
+
|
|
955
|
+
## 13. Architecture Decision Records
|
|
956
|
+
|
|
957
|
+
### ADR-001: Single-File Server
|
|
958
|
+
Decision: All code in server.py
|
|
959
|
+
Rationale: No build step, easy agent modification, zero deployment complexity.
|
|
960
|
+
Trade-off: Maintenance burden grows with file size.
|
|
961
|
+
Resolution: Phase A splits the file.
|
|
962
|
+
|
|
963
|
+
### ADR-002: HTML as Python Raw String
|
|
964
|
+
Decision: Frontend embedded in server.py as r"""..."""
|
|
965
|
+
Rationale: Simplest way to serve frontend without static file server or build system.
|
|
966
|
+
Trade-off: No editor syntax highlighting, complex patching, base64 gymnastics for large edits.
|
|
967
|
+
Resolution: Phase A moves to static/index.html served from disk.
|
|
968
|
+
|
|
969
|
+
### ADR-003: ThreadingHTTPServer
|
|
970
|
+
Decision: Python stdlib, synchronous threads, not asyncio.
|
|
971
|
+
Rationale: No dependencies, synchronous agent calls fit naturally in threads.
|
|
972
|
+
Trade-off: Memory scales linearly with concurrent users. Thread pool is unbounded.
|
|
973
|
+
Resolution: Acceptable for single-user. Phase J adds concurrency limits if needed.
|
|
974
|
+
|
|
975
|
+
### ADR-004: SSE over WebSockets
|
|
976
|
+
Decision: Server-Sent Events for streaming.
|
|
977
|
+
Rationale: Simpler than WebSockets, unidirectional, no upgrade handshake, EventSource is
|
|
978
|
+
standard browser API.
|
|
979
|
+
Trade-off: Server-to-client only. Approval events use SSE from agent thread + polling fallback.
|
|
980
|
+
Resolution: No plan to switch. SSE is sufficient.
|
|
981
|
+
|
|
982
|
+
### ADR-005: Module-Level Approval State
|
|
983
|
+
Decision: tools/approval.py uses module-level _pending dict shared across all threads.
|
|
984
|
+
Rationale: The approval system was pre-existing; sharing state via same Python process works.
|
|
985
|
+
Trade-off: Breaks if ever moved to multi-process (gunicorn workers) or subprocess.
|
|
986
|
+
Resolution: Document the constraint. Move to SQLite if scaling is ever needed.
|
|
987
|
+
|
|
988
|
+
### ADR-006: No Authentication
|
|
989
|
+
Decision: No auth initially.
|
|
990
|
+
Rationale: Localhost-only via SSH tunnel. Auth adds complexity without security benefit
|
|
991
|
+
when the transport layer (SSH) is already authenticated.
|
|
992
|
+
Trade-off: Anyone on the VPS with localhost access can use the server.
|
|
993
|
+
Resolution: Phase H adds optional password gate for direct-access deployments.
|
|
994
|
+
|
|
995
|
+
### ADR-007: Approval State via Environment Variables
|
|
996
|
+
Decision: HERMES_EXEC_ASK and HERMES_SESSION_KEY passed via os.environ.
|
|
997
|
+
Rationale: tools/approval.py and terminal_tool.py already read these env vars.
|
|
998
|
+
Trade-off: Process-global. Two concurrent chat requests clobber each other.
|
|
999
|
+
Resolution: Phase B replaces with thread-local or explicit parameter passing.
|
|
1000
|
+
|
|
1001
|
+
---
|
|
1002
|
+
|
|
1003
|
+
## 14. Version History
|
|
1004
|
+
|
|
1005
|
+
v0.1 Initial MVP: single-file server, sync /api/chat, no streaming
|
|
1006
|
+
v0.2 SSE streaming via /api/chat/start + /api/chat/stream
|
|
1007
|
+
v0.2 INFLIGHT session guard, session delete rules, toast UI
|
|
1008
|
+
v0.2 Binary file upload fixed (replaced cgi.FieldStorage with parse_multipart)
|
|
1009
|
+
v0.2 Approval card UI wired to tools/approval.py
|
|
1010
|
+
v0.2 Approval SSE event (immediate surface on tool invocation)
|
|
1011
|
+
v0.3 Sprint 1 (March 30, 2026):
|
|
1012
|
+
Bug fixes: B1 B2 B3 B4/B5 B7 B9 B11 all resolved
|
|
1013
|
+
Architecture: LOCK on SESSIONS, section headers, structured JSON logging
|
|
1014
|
+
Tests: 19/19 HTTP integration tests passing
|
|
1015
|
+
Features: 10-model dropdown with provider groups, reconnect banner,
|
|
1016
|
+
GET /api/chat/stream/status, GET /api/approval/inject_test
|
|
1017
|
+
v0.4 Sprint 2 (March 30, 2026):
|
|
1018
|
+
Features: image preview via /api/file/raw, rendered markdown in right panel,
|
|
1019
|
+
table support in renderMd(), smart file icons, type badge in path bar
|
|
1020
|
+
Tests: 8 new tests, 27/27 total passing
|
|
1021
|
+
v0.5 [Planned] Wave 1 features: cron viewer, skills viewer, memory viewer
|
|
1022
|
+
v0.5 Sprint 3 (March 30, 2026):
|
|
1023
|
+
Features: sidebar nav tabs (Chat/Tasks/Skills/Memory), cron viewer,
|
|
1024
|
+
skills viewer (search + SKILL.md preview), memory viewer
|
|
1025
|
+
Bug fixes: B6, B10, B14
|
|
1026
|
+
Arch: Phase D partial (require()/bad() validation helpers)
|
|
1027
|
+
New endpoints: /api/crons, /api/crons/output, /api/crons/run, /api/crons/pause,
|
|
1028
|
+
/api/crons/resume, /api/skills, /api/skills/content, /api/memory
|
|
1029
|
+
Tests: 21 new tests, 48/48 total
|
|
1030
|
+
v0.6 Sprint 4 (March 30, 2026):
|
|
1031
|
+
Relocation: source moved to <repo>/, symlink back
|
|
1032
|
+
Phase A partial: CSS extracted to static/style.css, served from disk
|
|
1033
|
+
Phase B partial: per-session agent lock (SESSION_AGENT_LOCKS)
|
|
1034
|
+
Features: session rename (inline), session search, file delete, file create
|
|
1035
|
+
Bug fixes: B12, B8 improved, TD5 completed
|
|
1036
|
+
New endpoints: /api/session/rename, /api/sessions/search, /api/file/delete, /api/file/create, GET /static/*
|
|
1037
|
+
Tests: 20 new tests, 68/68 total
|
|
1038
|
+
v0.7 Sprint 5 (March 30, 2026):
|
|
1039
|
+
Arch: Phase A complete (JS -> static/app.js), TD2 LRU cache, TD1 thread-local, Phase C index
|
|
1040
|
+
Features: workspace management panel + topbar quick-switch, copy message, inline file editor
|
|
1041
|
+
New endpoints: /api/workspaces, /api/workspaces/add, /api/workspaces/remove, /api/workspaces/rename, /api/file/save
|
|
1042
|
+
New state files: workspaces.json, last_workspace.txt, sessions/_index.json
|
|
1043
|
+
Tests: 18 new tests, 86/86 total
|
|
1044
|
+
v0.8 Sprint 6 (March 31, 2026):
|
|
1045
|
+
Phase E complete: HTML to static/index.html (server.py now 903 lines, pure Python)
|
|
1046
|
+
Phase D complete: all endpoints validated
|
|
1047
|
+
Features: resizable panels (localStorage), cron create from UI, session JSON export
|
|
1048
|
+
Bug fix: Escape from file editor now cancels edits
|
|
1049
|
+
New endpoints: POST /api/crons/create, GET /api/session/export
|
|
1050
|
+
Tests: 16 new, 106/106 total
|
|
1051
|
+
v0.10 Sprint 8 (March 31, 2026):
|
|
1052
|
+
Features: edit+regenerate messages, regenerate last response, clear conversation,
|
|
1053
|
+
Prism.js syntax highlighting, message queue (MSG_QUEUE + drain on idle),
|
|
1054
|
+
INFLIGHT-first loadSession (message persists on switch-away/back)
|
|
1055
|
+
Bug fixes: A1 (reconnect banner false positive), A2 (session list scroll clip)
|
|
1056
|
+
New endpoints: POST /api/session/clear, POST /api/session/truncate
|
|
1057
|
+
Tests: 14 new, 139/139 total
|
|
1058
|
+
JS: MSG_QUEUE global, updateQueueBadge(), setBusy drain logic, send() queues when busy,
|
|
1059
|
+
loadSession checks INFLIGHT before server fetch
|
|
1060
|
+
v0.12.2 Concurrency sweeps (March 31, 2026):
|
|
1061
|
+
R10-R15: approval cross-session, activity bar per-session, live card
|
|
1062
|
+
restore on switch-back, settled cards after done, model source,
|
|
1063
|
+
newSession card clear. 190/190 tests.
|
|
1064
|
+
v0.12 Sprint 10 (March 31, 2026):
|
|
1065
|
+
Arch: server.py split into api/ modules (config, helpers, models, workspace, upload, streaming)
|
|
1066
|
+
Features: background task cancel, cron run history, tool card UX polish
|
|
1067
|
+
Post-sprint fixes: SSE cancel event breaks loop, Cancel button always hidden on setBusy(false),
|
|
1068
|
+
S.activeStreamId initialized, tool-card show-more uses data attributes, version label v0.12,
|
|
1069
|
+
Session.__init__ **kwargs forward-compat, test cron isolation via HERMES_HOME,
|
|
1070
|
+
last_workspace reset in conftest between tests, tool cards grouped by assistant turn
|
|
1071
|
+
Tests: 18 new, 167/167 total
|
|
1072
|
+
Regressions fixed: uuid, AIAgent, has_pending, SSE cancel loop, Session.__init__ tool_calls
|
|
1073
|
+
test_regressions.py: 10 tests -- one per introduced bug, permanent regression gate
|
|
1074
|
+
Total after fixes: 177/177
|
|
1075
|
+
v0.11 Sprint 9 (March 31, 2026):
|
|
1076
|
+
Arch: app.js deleted; replaced by ui.js, workspace.js, sessions.js, messages.js, panels.js, boot.js
|
|
1077
|
+
Features: tool call cards (inline collapsible, live + history), attachment persistence,
|
|
1078
|
+
todo list panel (parses tool results from session history)
|
|
1079
|
+
Tests: 10 new, 149/149 total
|
|
1080
|
+
v0.9 Sprint 7 (March 31, 2026):
|
|
1081
|
+
Features: cron edit+delete, skill create/edit/delete, memory write, session content search
|
|
1082
|
+
Arch: Phase G partial (active_streams+uptime in /health), git init
|
|
1083
|
+
Bug fixes: A1 (activity bar min-height), A2 (model chip sync), A3 (cron output overflow)
|
|
1084
|
+
New endpoints: /api/crons/update, /api/crons/delete, /api/skills/save, /api/skills/delete,
|
|
1085
|
+
/api/memory/write, /api/sessions/search (extended)
|
|
1086
|
+
Tests: 19 new, 125/125 total
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
---
|
|
1090
|
+
|
|
1091
|
+
## 15. Sprint Log
|
|
1092
|
+
|
|
1093
|
+
This section records what was actually built and changed in each sprint. It is the
|
|
1094
|
+
permanent history of the codebase. Update it at the end of every sprint.
|
|
1095
|
+
|
|
1096
|
+
### Sprint 1 (March 30, 2026): Bug Fixes, Arch Foundations, First Tests
|
|
1097
|
+
|
|
1098
|
+
**Tracks:** Bug fixes (7), Architecture (3), Tests (1)
|
|
1099
|
+
**Test result:** 19/19 passing
|
|
1100
|
+
**Backup:** server.py.sprint1.bak
|
|
1101
|
+
|
|
1102
|
+
#### Bug Fixes Applied
|
|
1103
|
+
|
|
1104
|
+
| ID | Description | Change |
|
|
1105
|
+
|-----|--------------------------------------|------------------------------------------------------------------------|
|
|
1106
|
+
| B3 | Model chip label wrong for new models | Replaced substring check with MODEL_LABELS dict; 10 models supported |
|
|
1107
|
+
| B7 | Sidebar title overflow | Added min-width:0 to .session-item |
|
|
1108
|
+
| B11 | /api/session GET creates session silently | Returns 400 with error message when session_id is missing |
|
|
1109
|
+
| B2 | File input no accept attribute | Added accept= with image/*, text/*, pdf, json, common code extensions |
|
|
1110
|
+
| B9 | Empty assistant messages render | loadSession() filters out empty-text assistant messages before render |
|
|
1111
|
+
| B1 | Approval card missing pattern context | showApprovalCard() now appends pattern_keys to description text |
|
|
1112
|
+
| B4/B5 | Reload mid-stream loses context | markInflight/clearInflight in localStorage; checkInflightOnBoot() shows gold reconnect banner; GET /api/chat/stream/status endpoint added |
|
|
1113
|
+
|
|
1114
|
+
Model dropdown also expanded from 2 options to 10, grouped by provider in <optgroup>.
|
|
1115
|
+
|
|
1116
|
+
#### Architecture Improvements Applied
|
|
1117
|
+
|
|
1118
|
+
| Item | Description | Change |
|
|
1119
|
+
|--------|--------------------------------------|---------------------------------------------------------------------------|
|
|
1120
|
+
| Arch-1 | Section headers | 8 clear # === SECTION === banners dividing server.py into logical zones |
|
|
1121
|
+
| Arch-2 | LOCK around SESSIONS dict | get_session, new_session, delete now hold LOCK; eliminates race condition |
|
|
1122
|
+
| Arch-3 | Structured request logging | log_request() override emits JSON per request to /tmp/webui-mvp.log |
|
|
1123
|
+
|
|
1124
|
+
Request log format:
|
|
1125
|
+
{"ts": "2026-03-30T17:30:08Z", "method": "GET", "path": "/health", "status": 200, "ms": 0.1}
|
|
1126
|
+
|
|
1127
|
+
#### Test Suite Added
|
|
1128
|
+
|
|
1129
|
+
File: webui-mvp/tests/test_sprint1.py (19 tests)
|
|
1130
|
+
File: webui-mvp/tests/__init__.py
|
|
1131
|
+
|
|
1132
|
+
Test categories:
|
|
1133
|
+
Health check (1)
|
|
1134
|
+
Session CRUD: create, load, update, delete, sort, B11 footgun (6)
|
|
1135
|
+
Multipart parser unit tests: text file, binary/PNG (2)
|
|
1136
|
+
HTTP upload: success, too large, no file, bad session (4)
|
|
1137
|
+
Approval API: pending/none, inject+deny, inject+session-approve (3)
|
|
1138
|
+
Stream status endpoint (1)
|
|
1139
|
+
File browser: list dir, path traversal block (2)
|
|
1140
|
+
|
|
1141
|
+
Run tests:
|
|
1142
|
+
cd <agent-dir>
|
|
1143
|
+
venv/bin/python -m pytest webui-mvp/tests/test_sprint1.py -v
|
|
1144
|
+
|
|
1145
|
+
#### Section 5.5 Update (B3 resolved)
|
|
1146
|
+
|
|
1147
|
+
The model chip label bug is now fixed. The MODEL_LABELS object in syncTopbar():
|
|
1148
|
+
|
|
1149
|
+
const MODEL_LABELS = {
|
|
1150
|
+
'openai/gpt-5.4-mini': 'GPT-5.4 Mini',
|
|
1151
|
+
'openai/gpt-4o': 'GPT-4o',
|
|
1152
|
+
'openai/o3': 'o3',
|
|
1153
|
+
'openai/o4-mini': 'o4-mini',
|
|
1154
|
+
'anthropic/claude-sonnet-4.6': 'Sonnet 4.6',
|
|
1155
|
+
'anthropic/claude-sonnet-4-5': 'Sonnet 4.5',
|
|
1156
|
+
'anthropic/claude-haiku-3-5': 'Haiku 3.5',
|
|
1157
|
+
'google/gemini-2.5-pro': 'Gemini 2.5 Pro',
|
|
1158
|
+
'deepseek/deepseek-chat-v3-0324': 'DeepSeek V3',
|
|
1159
|
+
'meta-llama/llama-4-scout': 'Llama 4 Scout',
|
|
1160
|
+
};
|
|
1161
|
+
getModelLabel(m) => MODEL_LABELS[m] || (m.split('/').pop() || 'Unknown');
|
|
1162
|
+
|
|
1163
|
+
Fallback: splits on '/' and uses the last segment, so any unlisted model shows its
|
|
1164
|
+
short identifier rather than a wrong hardcoded label.
|
|
1165
|
+
|
|
1166
|
+
#### Version History Update
|
|
1167
|
+
|
|
1168
|
+
v0.3 Sprint 1: B3/B7/B11/B2/B9/B1/B4/B5 bug fixes
|
|
1169
|
+
v0.3 Sprint 1: Model dropdown expanded to 10 models in provider groups
|
|
1170
|
+
v0.3 Sprint 1: LOCK added around SESSIONS dict (thread safety)
|
|
1171
|
+
v0.3 Sprint 1: Section headers added throughout server.py
|
|
1172
|
+
v0.3 Sprint 1: Structured JSON request logging via log_request() override
|
|
1173
|
+
v0.3 Sprint 1: GET /api/chat/stream/status endpoint
|
|
1174
|
+
v0.3 Sprint 1: Reconnect banner (markInflight/clearInflight/checkInflightOnBoot)
|
|
1175
|
+
v0.3 Sprint 1: GET /api/approval/inject_test endpoint (test-only)
|
|
1176
|
+
v0.3 Sprint 1: First pytest suite, 19 tests, all passing
|
|
1177
|
+
|
|
1178
|
+
---
|
|
1179
|
+
|
|
1180
|
+
## 16. Architecture Phase Priority Matrix
|
|
1181
|
+
|
|
1182
|
+
Quick-reference table for prioritizing architecture work. Phases are from Section 10.
|
|
1183
|
+
|
|
1184
|
+
| Phase | Name | Priority | Effort | Blocks | Status |
|
|
1185
|
+
|-------|-----------------------------|----------|--------|----------------|------------|
|
|
1186
|
+
| A+E | File Separation + Frontend | High | Medium | F | COMPLETE Sprint 6+9 (HTML->index.html, JS->6 modules, app.js deleted; server.py pure Python ~1150 lines) |
|
|
1187
|
+
| B | Thread-Safe Request Context | Critical | Medium | nothing | PARTIAL (Sprint 4: per-session lock added; global env vars still used) |
|
|
1188
|
+
| C | Session Store Improvements | Medium | Medium | J | PARTIAL Sprint 5 (index file + LRU cache; LRU eviction policy and pagination still open) |
|
|
1189
|
+
| D | Input Validation | Medium | Low | nothing | COMPLETE Sprint 6 (approval/respond + file/raw hardened; all endpoints validated) |
|
|
1190
|
+
| E | Frontend Modularization | Medium | High | requires A | Pending |
|
|
1191
|
+
| F | API Design Cleanup | Low | Medium | requires A | Pending |
|
|
1192
|
+
| G | Observability | Low | Low | nothing | Partial (Sprint 7: active_streams+uptime added to /health; log rotation still pending) |
|
|
1193
|
+
| H | Authentication | Low | Medium | nothing | Pending |
|
|
1194
|
+
| I | Test Infrastructure | High | High | requires A,D | Partial(*) |
|
|
1195
|
+
| J | Performance | Low | High | requires C | Pending |
|
|
1196
|
+
|
|
1197
|
+
(*) Phase G is partial: structured request logging done in Sprint 1. Full observability
|
|
1198
|
+
(health detail, debug/stats endpoint, log rotation) remains.
|
|
1199
|
+
(*) Phase I is partial: HTTP integration test suite started in Sprint 1. Unit tests for
|
|
1200
|
+
isolated modules require Phase A file split first.
|
|
1201
|
+
|
|
1202
|
+
Recommended execution order:
|
|
1203
|
+
1. Phase B (thread safety): critical, low risk, no file changes needed
|
|
1204
|
+
2. Phase D (input validation): low effort, improves error messages immediately
|
|
1205
|
+
3. Phase A (file split): enables E, F, and full Phase I
|
|
1206
|
+
4. Phase G remainder (health detail, debug endpoint): 1-2 hours
|
|
1207
|
+
5. Phase C (session index): needed as session count grows
|
|
1208
|
+
6. Phase E (frontend modules + marked.js): biggest UX improvement
|
|
1209
|
+
7. Phase I (full test suite): after A gives us importable modules
|
|
1210
|
+
8. Phase F, H, J: lower priority, tackle when needed
|
|
1211
|
+
|
|
1212
|
+
---
|
|
1213
|
+
|
|
1214
|
+
## 17. Working Conventions for Agent Contributors
|
|
1215
|
+
|
|
1216
|
+
This section is specifically for agents (Hermes instances, subagents, Codex, etc.) that
|
|
1217
|
+
will be working on this codebase. Read this before touching any file.
|
|
1218
|
+
|
|
1219
|
+
### Before Making Any Change
|
|
1220
|
+
|
|
1221
|
+
1. Read this document (ARCHITECTURE.md) fully. Especially sections 4, 5, and the ADRs.
|
|
1222
|
+
2. Inspect the relevant module under `api/` or `static/`; `server.py` is only the routing shell.
|
|
1223
|
+
3. Check the Sprint Log (Section 15) to understand what was recently changed.
|
|
1224
|
+
4. Run the relevant test slice first to confirm baseline, for example:
|
|
1225
|
+
venv/bin/python -m pytest tests/test_regressions.py -q
|
|
1226
|
+
5. Check server health: curl -s http://127.0.0.1:8787/health
|
|
1227
|
+
|
|
1228
|
+
### Making Changes
|
|
1229
|
+
|
|
1230
|
+
Keep edits scoped to the module that owns the behavior. Use exact string
|
|
1231
|
+
matching when making mechanical patches and verify that the intended old string
|
|
1232
|
+
was found before replacing it.
|
|
1233
|
+
|
|
1234
|
+
After any change:
|
|
1235
|
+
venv/bin/python -m py_compile server.py # syntax check
|
|
1236
|
+
curl -s http://127.0.0.1:8787/health # server still alive
|
|
1237
|
+
venv/bin/python -m pytest tests/ -v # tests still pass
|
|
1238
|
+
|
|
1239
|
+
### Critical Rules (do NOT regress these)
|
|
1240
|
+
|
|
1241
|
+
These patterns have been broken and fixed multiple times. Do not re-introduce them.
|
|
1242
|
+
|
|
1243
|
+
RULE-1: deleteSession() must NEVER call newSession().
|
|
1244
|
+
Deleting does not create. If the deleted session was active and others remain,
|
|
1245
|
+
load sessions[0]. If none remain, show empty state. See Section 5.6.
|
|
1246
|
+
|
|
1247
|
+
RULE-2: /api/upload must be checked BEFORE read_body() in do_POST.
|
|
1248
|
+
read_body() consumes the request body. Upload parsing also needs the body.
|
|
1249
|
+
Order matters. See Section 4.1.
|
|
1250
|
+
|
|
1251
|
+
RULE-3: run_conversation() takes task_id=, NOT session_id=.
|
|
1252
|
+
task_id is the correct keyword argument. session_id= raises TypeError silently.
|
|
1253
|
+
|
|
1254
|
+
RULE-4: stream_delta_callback receives None as end-of-stream sentinel.
|
|
1255
|
+
The on_token callback must guard: if text is None: return
|
|
1256
|
+
|
|
1257
|
+
RULE-5: send() must capture activeSid BEFORE any await.
|
|
1258
|
+
The active session can change while awaits are pending. Capture first, guard on return.
|
|
1259
|
+
|
|
1260
|
+
RULE-6: Boot IIFE must never auto-create a session.
|
|
1261
|
+
Only two places create sessions: the + button and send() when S.session is null.
|
|
1262
|
+
|
|
1263
|
+
RULE-7: All SESSIONS dict accesses must hold LOCK.
|
|
1264
|
+
LOCK is a module-level threading.Lock(). Use: with LOCK: ...
|
|
1265
|
+
|
|
1266
|
+
RULE-8: do NOT expose tracebacks to API clients.
|
|
1267
|
+
500 responses should return {"error": "Internal server error"}, not the full traceback.
|
|
1268
|
+
(Currently traceback is exposed; fix in Phase D. Do not make it worse.)
|
|
1269
|
+
|
|
1270
|
+
RULE-9: Pattern_keys, not pattern_key, for multi-pattern approvals.
|
|
1271
|
+
The approval module may include both pattern_key (singular, legacy) and pattern_keys
|
|
1272
|
+
(plural, all matched patterns). Always iterate pattern_keys when approving.
|
|
1273
|
+
|
|
1274
|
+
### Adding New API Endpoints
|
|
1275
|
+
|
|
1276
|
+
See Section 11 for the exact code pattern. Short version:
|
|
1277
|
+
- GET: add before the 404 fallback in do_GET
|
|
1278
|
+
- POST: add after /api/upload check and after read_body(), before 404 fallback in do_POST
|
|
1279
|
+
- Always validate required fields, return 400 for missing/invalid input
|
|
1280
|
+
- Always use get_session(sid) with try/except KeyError -> 400 or 404
|
|
1281
|
+
- Add a test in test_sprint1.py or a new test file
|
|
1282
|
+
|
|
1283
|
+
### Updating This Document
|
|
1284
|
+
|
|
1285
|
+
Update ARCHITECTURE.md whenever you:
|
|
1286
|
+
- Fix a bug listed in Section 9 (update its row, mark resolved)
|
|
1287
|
+
- Complete an architecture phase (update Section 16 matrix)
|
|
1288
|
+
- Add a new endpoint (add to Section 4.1 routing table)
|
|
1289
|
+
- Discover a new pitfall or rule (add to Section 17)
|
|
1290
|
+
- Complete a sprint (add a new entry to Section 15)
|
|
1291
|
+
|
|
1292
|
+
This document is the memory of the codebase. If it is not updated, future agents will
|
|
1293
|
+
make the same mistakes again.
|
|
1294
|
+
|
|
1295
|
+
---
|
|
1296
|
+
|
|
1297
|
+
## 18. Endpoint Reference (Current)
|
|
1298
|
+
|
|
1299
|
+
Complete list of all HTTP endpoints as of Sprint 1 (v0.3).
|
|
1300
|
+
|
|
1301
|
+
### GET Endpoints
|
|
1302
|
+
|
|
1303
|
+
/ Returns full HTML app (index page)
|
|
1304
|
+
/index.html Same as /
|
|
1305
|
+
/health {"status":"ok","sessions":N}
|
|
1306
|
+
/api/session ?session_id=X -> full session + messages. 400 if no ID.
|
|
1307
|
+
/api/sessions List of all session compact() dicts, sorted by updated_at
|
|
1308
|
+
/api/list ?session_id=X&path=. -> directory listing for session workspace
|
|
1309
|
+
/api/file ?session_id=X&path=rel -> file content (text, 200KB limit)
|
|
1310
|
+
/api/chat/stream ?stream_id=X -> SSE stream. Long-lived. Emits token/tool/
|
|
1311
|
+
approval/done/error events.
|
|
1312
|
+
/api/chat/stream/status ?stream_id=X -> {"active": true/false, "stream_id": X}
|
|
1313
|
+
/api/approval/pending ?session_id=X -> {"pending": entry_or_null}
|
|
1314
|
+
/api/approval/inject_test ?session_id=X&pattern_key=K&command=C -> test-only endpoint.
|
|
1315
|
+
Injects a pending approval entry into the server process.
|
|
1316
|
+
/api/file/raw ?session_id=X&path=P -> raw file bytes with correct MIME type.
|
|
1317
|
+
Used for image preview. Path traversal protected via safe_resolve.
|
|
1318
|
+
Returns 404 JSON if file not found.
|
|
1319
|
+
|
|
1320
|
+
### POST Endpoints
|
|
1321
|
+
|
|
1322
|
+
/api/upload multipart/form-data. Fields: session_id, file. Returns filename.
|
|
1323
|
+
/api/session/new {"model"?, "workspace"?} -> new session
|
|
1324
|
+
/api/session/update {"session_id", "workspace"?, "model"?} -> updated session
|
|
1325
|
+
/api/session/delete {"session_id"} -> {"ok": true}
|
|
1326
|
+
/api/chat/start {"session_id", "message", "model"?, "workspace"?}
|
|
1327
|
+
-> {"stream_id", "session_id"}. Starts agent daemon thread.
|
|
1328
|
+
/api/chat (fallback, sync) {"session_id", "message", "model"?, "workspace"?}
|
|
1329
|
+
-> blocks until agent finishes. Returns full result.
|
|
1330
|
+
/api/approval/respond {"session_id", "choice": once|session|always|deny}
|
|
1331
|
+
-> {"ok": true, "choice": choice}
|
|
1332
|
+
|
|
1333
|
+
### GET Endpoints Added in Sprint 3
|
|
1334
|
+
|
|
1335
|
+
/api/crons All cron jobs. Returns {jobs: [...]}.
|
|
1336
|
+
/api/crons/output ?job_id=X&limit=N -> {outputs: [{filename, content}]}
|
|
1337
|
+
/api/skills All skills. Returns {skills: [{name, description, category}]}
|
|
1338
|
+
/api/skills/content ?name=X -> full skill data including SKILL.md content
|
|
1339
|
+
/api/memory MEMORY.md + USER.md + SOUL.md. Returns {memory, user, soul, *_path, *_mtime}
|
|
1340
|
+
|
|
1341
|
+
### POST Endpoints Added in Sprint 3
|
|
1342
|
+
|
|
1343
|
+
/api/crons/run {job_id} -> triggers run in daemon thread. Returns {ok, status}.
|
|
1344
|
+
/api/crons/pause {job_id} -> {ok, job} or 404.
|
|
1345
|
+
/api/crons/resume {job_id} -> {ok, job} or 404.
|
|
1346
|
+
|
|
1347
|
+
---
|
|
1348
|
+
|
|
1349
|
+
## Sprint 2 Log Entry (March 30, 2026)
|
|
1350
|
+
|
|
1351
|
+
Added to Section 15 Sprint Log.
|
|
1352
|
+
|
|
1353
|
+
### Sprint 2: Rich File Preview (March 30, 2026)
|
|
1354
|
+
|
|
1355
|
+
**Tracks:** Features (4 sub-features), Tests (8 new)
|
|
1356
|
+
**Test result:** 27/27 passing (19 Sprint 1 + 8 Sprint 2)
|
|
1357
|
+
**Backup:** server.py.sprint1.bak (Sprint 1 backup; Sprint 2 is incremental)
|
|
1358
|
+
|
|
1359
|
+
#### Features Implemented
|
|
1360
|
+
|
|
1361
|
+
**Image Preview (GET /api/file/raw)**
|
|
1362
|
+
|
|
1363
|
+
New endpoint in do_GET:
|
|
1364
|
+
|
|
1365
|
+
GET /api/file/raw?session_id=X&path=relative/path
|
|
1366
|
+
|
|
1367
|
+
- Reads raw bytes from workspace file via safe_resolve() (path traversal protected)
|
|
1368
|
+
- Looks up MIME type from MIME_MAP constant keyed by lowercase extension
|
|
1369
|
+
- Falls back to 'application/octet-stream' for unknown types
|
|
1370
|
+
- Serves bytes directly with correct Content-Type header
|
|
1371
|
+
- No MAX_FILE_BYTES size limit (images can be large; the browser handles progressive load)
|
|
1372
|
+
- Returns JSON 404 if file not found or not a file
|
|
1373
|
+
|
|
1374
|
+
Frontend: openFile() checks IMAGE_EXTS set. If image, sets <img src="/api/file/raw?...">
|
|
1375
|
+
and calls showPreview('image'). The browser loads the image natively. onerror handler
|
|
1376
|
+
shows a status message if load fails.
|
|
1377
|
+
|
|
1378
|
+
**Rendered Markdown Preview**
|
|
1379
|
+
|
|
1380
|
+
Frontend only -- uses existing GET /api/file endpoint for text content.
|
|
1381
|
+
openFile() checks MD_EXTS set. If markdown, fetches text then calls:
|
|
1382
|
+
|
|
1383
|
+
$('previewMd').innerHTML = renderMd(data.content);
|
|
1384
|
+
|
|
1385
|
+
Preview renders in .preview-md container with full typography CSS separate from the
|
|
1386
|
+
chat bubble .msg-body CSS (allows different sizing/spacing for the narrower side panel).
|
|
1387
|
+
|
|
1388
|
+
**Table Support in renderMd()**
|
|
1389
|
+
|
|
1390
|
+
Added a regex pass before paragraph wrapping:
|
|
1391
|
+
- Detects blocks of pipe-delimited rows where row[1] is a separator (|---|---|)
|
|
1392
|
+
- Converts to <table><thead><tbody> HTML
|
|
1393
|
+
- Handles any number of columns
|
|
1394
|
+
- This partially resolves B8 (renderMd missing tables)
|
|
1395
|
+
|
|
1396
|
+
**Smart File Icons in renderFileTree()**
|
|
1397
|
+
|
|
1398
|
+
New fileIcon(name, type) function maps extensions to emoji icons:
|
|
1399
|
+
- Directories: folder icon
|
|
1400
|
+
- Images: camera icon
|
|
1401
|
+
- Markdown: notepad icon
|
|
1402
|
+
- Python: snake icon
|
|
1403
|
+
- JS/TS/JSX/TSX: circuit icon
|
|
1404
|
+
- JSON/YAML/TOML: gear icon
|
|
1405
|
+
- Shell scripts: terminal icon
|
|
1406
|
+
- Everything else: document icon
|
|
1407
|
+
|
|
1408
|
+
**Preview Path Bar with Type Badge**
|
|
1409
|
+
|
|
1410
|
+
previewPath bar now has two elements:
|
|
1411
|
+
- #previewPathText: the relative file path
|
|
1412
|
+
- #previewBadge: colored badge with type label (image/md/extension)
|
|
1413
|
+
Blue for images, gold for markdown, gray for code
|
|
1414
|
+
|
|
1415
|
+
#### New Constants Added
|
|
1416
|
+
|
|
1417
|
+
IMAGE_EXTS set of image extensions: .png .jpg .jpeg .gif .svg .webp .ico .bmp
|
|
1418
|
+
MD_EXTS set of markdown extensions: .md .markdown .mdown
|
|
1419
|
+
CODE_EXTS set of code/text extensions for reference
|
|
1420
|
+
MIME_MAP dict: extension -> MIME type string
|
|
1421
|
+
|
|
1422
|
+
#### New HTML Elements
|
|
1423
|
+
|
|
1424
|
+
#previewPathText span inside preview path bar (was direct textContent on #previewPath)
|
|
1425
|
+
#previewBadge colored type badge span
|
|
1426
|
+
#previewImgWrap div centering the preview image
|
|
1427
|
+
#previewImg <img> element for image preview
|
|
1428
|
+
#previewMd div for rendered markdown HTML
|
|
1429
|
+
|
|
1430
|
+
#### Endpoint Reference Update
|
|
1431
|
+
|
|
1432
|
+
Added to Section 18:
|
|
1433
|
+
|
|
1434
|
+
GET /api/file/raw ?session_id=X&path=P -> raw file bytes with correct MIME type.
|
|
1435
|
+
Path traversal protected. 404 JSON if not found.
|
|
1436
|
+
|
|
1437
|
+
#### B8 Status Update (Section 9)
|
|
1438
|
+
|
|
1439
|
+
B8 (renderMd missing tables) is now PARTIAL: table parsing added in Sprint 2.
|
|
1440
|
+
Nested lists and complex inline HTML still not handled. Full fix remains Phase E
|
|
1441
|
+
(replace renderMd with marked.js).
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
### Sprint 3 (March 30, 2026): Panel Navigation + Feature Viewers
|
|
1445
|
+
|
|
1446
|
+
**Tracks:** Bug fixes (3), Features (3 panels + 8 API endpoints), Arch Phase D (partial)
|
|
1447
|
+
**Tests:** 48/48 passing
|
|
1448
|
+
**Backup:** server.py.sprint2.bak
|
|
1449
|
+
|
|
1450
|
+
#### New Sidebar Navigation
|
|
1451
|
+
|
|
1452
|
+
Four tabs at the top of the sidebar: Chat (default), Tasks, Skills, Memory.
|
|
1453
|
+
Implemented via `.nav-tab` / `.panel-view` CSS classes. `switchPanel(name)` activates
|
|
1454
|
+
the correct tab and panel-view, then lazy-loads panel data on first open.
|
|
1455
|
+
|
|
1456
|
+
#### Tasks Panel (Cron viewer)
|
|
1457
|
+
|
|
1458
|
+
`loadCrons()` fetches GET /api/crons, renders each job as a collapsible `.cron-item`.
|
|
1459
|
+
`toggleCron(id)` expands/collapses the body. `loadCronOutput(jobId)` auto-loads the last
|
|
1460
|
+
output file from GET /api/crons/output for each job.
|
|
1461
|
+
|
|
1462
|
+
Run Now: POST /api/crons/run starts the job in a daemon thread, returns immediately.
|
|
1463
|
+
Pause/Resume: POST /api/crons/pause and /api/crons/resume call the cron.jobs functions.
|
|
1464
|
+
|
|
1465
|
+
#### Skills Panel
|
|
1466
|
+
|
|
1467
|
+
`loadSkills()` fetches GET /api/skills, caches in `_skillsData`. `renderSkills()` groups
|
|
1468
|
+
by category, filters by search input. Clicking a skill calls `openSkill(name)` which
|
|
1469
|
+
fetches GET /api/skills/content and renders in the right panel using `showPreview('md')`.
|
|
1470
|
+
|
|
1471
|
+
#### Memory Panel
|
|
1472
|
+
|
|
1473
|
+
`loadMemory()` fetches GET /api/memory (reads MEMORY.md + USER.md from
|
|
1474
|
+
~/.hermes/memories/, and SOUL.md from ~/.hermes/), renders both as markdown via renderMd() with timestamps.
|
|
1475
|
+
|
|
1476
|
+
#### New API Endpoints (Section 18 update)
|
|
1477
|
+
|
|
1478
|
+
GET /api/crons All jobs from cron.jobs.list_jobs(include_disabled=True)
|
|
1479
|
+
GET /api/crons/output ?job_id=X&limit=N -> last N output .md files for a job
|
|
1480
|
+
POST /api/crons/run {job_id} -> triggers run_job() in daemon thread
|
|
1481
|
+
POST /api/crons/pause {job_id} -> pause_job(job_id)
|
|
1482
|
+
POST /api/crons/resume {job_id} -> resume_job(job_id)
|
|
1483
|
+
GET /api/skills All skills via tools.skills_tool.skills_list()
|
|
1484
|
+
GET /api/skills/content ?name=X -> full skill data via skill_view(name)
|
|
1485
|
+
GET /api/memory MEMORY.md + USER.md + SOUL.md content and mtimes
|
|
1486
|
+
|
|
1487
|
+
#### Phase D Input Validation Applied
|
|
1488
|
+
|
|
1489
|
+
require(body, *fields) raises ValueError with clean message on missing fields
|
|
1490
|
+
bad(handler, msg, status=400) returns clean JSON error response
|
|
1491
|
+
|
|
1492
|
+
Endpoints hardened: /api/session/update, /api/session/delete, /api/chat/start.
|
|
1493
|
+
Unknown session ID on /api/session/update now returns 404 instead of 500.
|
|
1494
|
+
|
|
1495
|
+
#### Bug Fix Details
|
|
1496
|
+
|
|
1497
|
+
B6: `newSession()` now passes `inheritWs = S.session?.workspace` to /api/session/new.
|
|
1498
|
+
Backend already accepted `workspace` param in session/new but it was never sent.
|
|
1499
|
+
|
|
1500
|
+
B10: `es.addEventListener('tool', ...)` now calls `removeThinking()` before updating
|
|
1501
|
+
status and shows a compact `.msg-role + .msg-body` tool-running row. `ensureAssistantRow()`
|
|
1502
|
+
also removes `#toolRunningRow` when first token arrives.
|
|
1503
|
+
|
|
1504
|
+
B14: `document.addEventListener('keydown', ...)` at global scope catches Cmd/Ctrl+K
|
|
1505
|
+
and calls `newSession()` if not busy.
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
### Sprint 4 (March 30, 2026): Relocation + Session Power Features + Phase A/B
|
|
1509
|
+
|
|
1510
|
+
**Tracks:** Bugs (B12, B8, TD5), Features (rename, search, file ops), Arch (Phase A/B start), Relocation
|
|
1511
|
+
**Tests:** 68/68 passing
|
|
1512
|
+
**Backup:** server.py.sprint2.bak (last full backup; Sprint 3 and 4 are incremental)
|
|
1513
|
+
|
|
1514
|
+
#### Source Relocation
|
|
1515
|
+
|
|
1516
|
+
Moved <agent-dir>/webui-mvp/ to <repo>/.
|
|
1517
|
+
Symlink: <agent-dir>/webui-mvp -> <repo>
|
|
1518
|
+
The symlink means all existing import paths (sys.path.insert for hermes-agent modules)
|
|
1519
|
+
continue working unchanged. start.sh updated to reference new canonical path.
|
|
1520
|
+
|
|
1521
|
+
Safe from: git pull, git reset --hard, git stash on hermes-agent repo.
|
|
1522
|
+
NOT safe from: git clean -fd (would delete symlink but not the target).
|
|
1523
|
+
Disk failure: still a single-copy risk. Use git init + push when ready.
|
|
1524
|
+
|
|
1525
|
+
#### Phase A: CSS Extracted
|
|
1526
|
+
|
|
1527
|
+
<repo>/static/style.css: the 23KB CSS block from the Python raw string.
|
|
1528
|
+
server.py no longer contains any CSS. GET /static/* handler serves disk files.
|
|
1529
|
+
server.py shrunk by ~200 lines.
|
|
1530
|
+
|
|
1531
|
+
#### Phase B: Per-Session Agent Lock
|
|
1532
|
+
|
|
1533
|
+
SESSION_AGENT_LOCKS = {} keyed by session_id, each value is a threading.Lock().
|
|
1534
|
+
_get_session_agent_lock(sid) returns the lock, creating it if needed.
|
|
1535
|
+
_run_agent_streaming() wraps the env var block with: with _agent_lock: ...
|
|
1536
|
+
This prevents two concurrent requests for the same session from overwriting env vars
|
|
1537
|
+
mid-execution. Two concurrent requests for DIFFERENT sessions are still unsafe (env vars
|
|
1538
|
+
are process-global). Full fix requires removing env var usage entirely (Phase B complete).
|
|
1539
|
+
|
|
1540
|
+
#### New Endpoints
|
|
1541
|
+
|
|
1542
|
+
GET /static/* Serves files from <repo>/static/ with
|
|
1543
|
+
correct Content-Type. Currently serves style.css.
|
|
1544
|
+
POST /api/session/rename {session_id, title} -> {session: compact}. Truncates to 80 chars.
|
|
1545
|
+
GET /api/sessions/search ?q=X -> sessions whose title contains q (case-insensitive).
|
|
1546
|
+
Empty q returns all sessions (same as /api/sessions).
|
|
1547
|
+
POST /api/file/delete {session_id, path} -> {ok: true}. Path traversal protected.
|
|
1548
|
+
POST /api/file/create {session_id, path, content?} -> {ok, path}. Errors if exists.
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
### Sprint 5 (March 30, 2026): Phase A Complete + Workspace + Edit + Copy
|
|
1552
|
+
|
|
1553
|
+
**Tracks:** Arch (Phase A complete, TD1/TD2/TD6/Phase C), Features (3), Tests (18)
|
|
1554
|
+
**Tests:** 86/86 passing
|
|
1555
|
+
|
|
1556
|
+
#### Phase A Complete: static/app.js
|
|
1557
|
+
|
|
1558
|
+
Extracted 902-line JavaScript from server.py HTML string to <repo>/static/app.js.
|
|
1559
|
+
server.py now: Python code + thin HTML skeleton (~875 lines, down from 1778).
|
|
1560
|
+
Layout: server.py imports nothing from static/; the HTML just has <link> and <script src>.
|
|
1561
|
+
Served via GET /static/* handler added in Sprint 4.
|
|
1562
|
+
node --check validates app.js on every sprint.
|
|
1563
|
+
|
|
1564
|
+
#### TD2: LRU SESSIONS Cache
|
|
1565
|
+
|
|
1566
|
+
SESSIONS changed to collections.OrderedDict.
|
|
1567
|
+
get_session(): SESSIONS.move_to_end(sid) on hit; on miss: load from disk, add, move_to_end, evict if over SESSIONS_MAX=100.
|
|
1568
|
+
new_session(): same eviction logic on insert.
|
|
1569
|
+
Result: memory usage capped regardless of session count.
|
|
1570
|
+
|
|
1571
|
+
#### TD1: Thread-Local Env Context
|
|
1572
|
+
|
|
1573
|
+
_thread_ctx = threading.local() added to Server Globals.
|
|
1574
|
+
_set_thread_env(**kwargs) and _clear_thread_env() set/clear _thread_ctx.env.
|
|
1575
|
+
_run_agent_streaming() calls _set_thread_env() before env var writes, _clear_thread_env() in outer finally.
|
|
1576
|
+
Process-level os.environ writes still exist as fallback (needed until terminal tool reads thread-local).
|
|
1577
|
+
|
|
1578
|
+
#### Phase C: Session Index File
|
|
1579
|
+
|
|
1580
|
+
SESSION_INDEX_FILE = SESSION_DIR / '_index.json'.
|
|
1581
|
+
_write_session_index(): builds compact() list from SESSIONS + disk files, writes JSON.
|
|
1582
|
+
Called in Session.save() -- keeps index always current.
|
|
1583
|
+
all_sessions(): reads index JSON first (one file read); overlays in-memory SESSIONS; falls back to full glob scan on error.
|
|
1584
|
+
Index files starting with '_' are skipped during full scan to avoid recursion.
|
|
1585
|
+
|
|
1586
|
+
#### New Workspace Infrastructure
|
|
1587
|
+
|
|
1588
|
+
WORKSPACES_FILE = ~/.hermes/webui-mvp/workspaces.json
|
|
1589
|
+
LAST_WORKSPACE_FILE = ~/.hermes/webui-mvp/last_workspace.txt
|
|
1590
|
+
load_workspaces() / save_workspaces() / get_last_workspace() / set_last_workspace() helpers.
|
|
1591
|
+
new_session() now calls get_last_workspace() as default instead of DEFAULT_WORKSPACE.
|
|
1592
|
+
set_last_workspace() called in /api/session/update and /api/chat/start.
|
|
1593
|
+
|
|
1594
|
+
#### New Endpoints (Sprint 5)
|
|
1595
|
+
|
|
1596
|
+
GET /api/workspaces {workspaces: [...], last: path}
|
|
1597
|
+
POST /api/workspaces/add {path, name?} -- validates exists+dir, no duplicates
|
|
1598
|
+
POST /api/workspaces/remove {path} -- removes from list, ok even if not present
|
|
1599
|
+
POST /api/workspaces/rename {path, name} -- updates display name, 404 if not found
|
|
1600
|
+
POST /api/file/save {session_id, path, content} -- write text to existing file
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
### Sprint 6 (March 31, 2026): Polish + Resize + Cron Create + Phase E
|
|
1604
|
+
|
|
1605
|
+
**Tests:** 106/106 passing
|
|
1606
|
+
**Backup:** server.py.sprint5.bak
|
|
1607
|
+
|
|
1608
|
+
#### Phase E Complete: static/index.html
|
|
1609
|
+
|
|
1610
|
+
The HTML = r triple-quoted string (197 lines, 12682 chars) was extracted to
|
|
1611
|
+
<repo>/static/index.html and served via disk read on each request.
|
|
1612
|
+
server.py is now pure Python: zero HTML/CSS/JS inline. All static content is in static/.
|
|
1613
|
+
|
|
1614
|
+
Static file layout (final):
|
|
1615
|
+
static/index.html (Sprint 6) -- HTML template
|
|
1616
|
+
static/style.css (Sprint 4) -- all CSS
|
|
1617
|
+
static/app.js (Sprint 5) -- all JavaScript
|
|
1618
|
+
|
|
1619
|
+
server.py line count progression: 1778 (S1) -> 1042 (S5) -> 903 (S6)
|
|
1620
|
+
|
|
1621
|
+
#### Phase D Complete
|
|
1622
|
+
|
|
1623
|
+
/api/approval/respond: validates session_id present; choice must be one of
|
|
1624
|
+
(once, session, always, deny); returns 400 on invalid.
|
|
1625
|
+
/api/file/raw: validates session_id present; try/except KeyError returns 404.
|
|
1626
|
+
|
|
1627
|
+
#### New Endpoints
|
|
1628
|
+
|
|
1629
|
+
POST /api/crons/create {prompt, schedule, name?, deliver?, skills?, model?}
|
|
1630
|
+
-> {ok: true, job: {...}} or 400 on invalid schedule/missing fields.
|
|
1631
|
+
Uses cron.jobs.create_job() directly.
|
|
1632
|
+
GET /api/session/export ?session_id=X
|
|
1633
|
+
-> full session JSON with Content-Disposition: attachment header.
|
|
1634
|
+
Includes all messages, workspace, model, timestamps.
|
|
1635
|
+
|
|
1636
|
+
#### Resizable Panels
|
|
1637
|
+
|
|
1638
|
+
_initResizePanels() called from boot IIFE. Creates mousedown listeners on #sidebarResize
|
|
1639
|
+
and #rightpanelResize. On mousemove: computes delta and clamps to min/max. On mouseup:
|
|
1640
|
+
saves width to localStorage. Widths restored at boot via localStorage.getItem().
|
|
1641
|
+
CSS: .resize-handle with position:absolute, width:5px, cursor:col-resize.
|
|
1642
|
+
body.resizing added during drag to suppress text selection.
|
|
1643
|
+
|
|
1644
|
+
|
|
1645
|
+
## Workspace path trust levels
|
|
1646
|
+
|
|
1647
|
+
`api/workspace.py` has two distinct trust functions — do not collapse them:
|
|
1648
|
+
|
|
1649
|
+
**`validate_workspace_to_add(path)`** — used by `/api/workspaces/add` (explicit user registration).
|
|
1650
|
+
Permissive: blocks only non-existent, non-directory, and system root paths. The user is
|
|
1651
|
+
consciously registering an external path (e.g. `/mnt/d/Projects` in WSL), so we trust intent.
|
|
1652
|
+
|
|
1653
|
+
**`resolve_trusted_workspace(path)`** — used for actual file read/write operations inside
|
|
1654
|
+
an existing workspace. Strict: path must be under home, in the saved workspace list, or under
|
|
1655
|
+
`BOOT_DEFAULT_WORKSPACE`. Prevents path traversal and unauthorized file access.
|
|
1656
|
+
|
|
1657
|
+
The distinction matters because add uses permissive validation to avoid the circular
|
|
1658
|
+
dependency: you cannot get a path into the saved list if you need the saved list to add it.
|