@bitseek/hermes-webui 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/README.md +213 -0
  2. package/bin/hermes-webui.mjs +588 -0
  3. package/package.json +25 -0
  4. package/scripts/sync-vendor.mjs +74 -0
  5. package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
  6. package/templates/systemd/hermes-webui.service +13 -0
  7. package/templates/windows/hermes-webui-task.ps1 +3 -0
  8. package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
  9. package/vendor/agent-frontend-shell/.dockerignore +7 -0
  10. package/vendor/agent-frontend-shell/.env.docker.example +89 -0
  11. package/vendor/agent-frontend-shell/.env.example +34 -0
  12. package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
  13. package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
  14. package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
  15. package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
  16. package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
  17. package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
  18. package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
  19. package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
  20. package/vendor/agent-frontend-shell/AGENTS.md +80 -0
  21. package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
  22. package/vendor/agent-frontend-shell/BUGS.md +52 -0
  23. package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
  24. package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
  25. package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
  26. package/vendor/agent-frontend-shell/DESIGN.md +173 -0
  27. package/vendor/agent-frontend-shell/Dockerfile +91 -0
  28. package/vendor/agent-frontend-shell/LICENSE +21 -0
  29. package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
  30. package/vendor/agent-frontend-shell/README.md +705 -0
  31. package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
  32. package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
  33. package/vendor/agent-frontend-shell/TESTING.md +1932 -0
  34. package/vendor/agent-frontend-shell/THEMES.md +170 -0
  35. package/vendor/agent-frontend-shell/api/__init__.py +1 -0
  36. package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
  37. package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
  38. package/vendor/agent-frontend-shell/api/auth.py +592 -0
  39. package/vendor/agent-frontend-shell/api/background.py +87 -0
  40. package/vendor/agent-frontend-shell/api/clarify.py +238 -0
  41. package/vendor/agent-frontend-shell/api/commands.py +124 -0
  42. package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
  43. package/vendor/agent-frontend-shell/api/config.py +5178 -0
  44. package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
  45. package/vendor/agent-frontend-shell/api/extensions.py +253 -0
  46. package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
  47. package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
  48. package/vendor/agent-frontend-shell/api/goals.py +608 -0
  49. package/vendor/agent-frontend-shell/api/helpers.py +474 -0
  50. package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
  51. package/vendor/agent-frontend-shell/api/metering.py +194 -0
  52. package/vendor/agent-frontend-shell/api/models.py +4210 -0
  53. package/vendor/agent-frontend-shell/api/oauth.py +770 -0
  54. package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
  55. package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
  56. package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
  57. package/vendor/agent-frontend-shell/api/providers.py +2175 -0
  58. package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
  59. package/vendor/agent-frontend-shell/api/rollback.py +320 -0
  60. package/vendor/agent-frontend-shell/api/routes.py +13990 -0
  61. package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
  62. package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
  63. package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
  64. package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
  65. package/vendor/agent-frontend-shell/api/session_events.py +45 -0
  66. package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
  67. package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
  68. package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
  69. package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
  70. package/vendor/agent-frontend-shell/api/startup.py +128 -0
  71. package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
  72. package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
  73. package/vendor/agent-frontend-shell/api/system_health.py +167 -0
  74. package/vendor/agent-frontend-shell/api/terminal.py +410 -0
  75. package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
  76. package/vendor/agent-frontend-shell/api/updates.py +1261 -0
  77. package/vendor/agent-frontend-shell/api/upload.py +322 -0
  78. package/vendor/agent-frontend-shell/api/usage.py +26 -0
  79. package/vendor/agent-frontend-shell/api/workspace.py +867 -0
  80. package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
  81. package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
  82. package/vendor/agent-frontend-shell/bootstrap.py +492 -0
  83. package/vendor/agent-frontend-shell/ctl.sh +427 -0
  84. package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
  85. package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
  86. package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
  87. package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
  88. package/vendor/agent-frontend-shell/docker_init.bash +459 -0
  89. package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
  90. package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
  91. package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
  92. package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
  93. package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
  94. package/vendor/agent-frontend-shell/docs/docker.md +337 -0
  95. package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
  96. package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
  97. package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
  98. package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
  99. package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
  100. package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
  101. package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
  102. package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
  103. package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
  104. package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
  105. package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
  106. package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
  107. package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
  108. package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
  109. package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
  110. package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
  111. package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
  112. package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
  113. package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
  114. package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
  115. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
  116. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
  117. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
  118. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
  119. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
  120. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
  121. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
  122. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
  123. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
  124. package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
  125. package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
  126. package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
  127. package/vendor/agent-frontend-shell/extensions/index.js +67 -0
  128. package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
  129. package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
  130. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
  131. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
  132. package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
  133. package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
  134. package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
  135. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
  136. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
  137. package/vendor/agent-frontend-shell/install.sh +63 -0
  138. package/vendor/agent-frontend-shell/mcp_server.py +567 -0
  139. package/vendor/agent-frontend-shell/package.json +12 -0
  140. package/vendor/agent-frontend-shell/pyproject.toml +56 -0
  141. package/vendor/agent-frontend-shell/pytest.ini +3 -0
  142. package/vendor/agent-frontend-shell/requirements.txt +5 -0
  143. package/vendor/agent-frontend-shell/server.py +624 -0
  144. package/vendor/agent-frontend-shell/start.ps1 +210 -0
  145. package/vendor/agent-frontend-shell/start.sh +65 -0
  146. package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
  147. package/vendor/agent-frontend-shell/static/boot.js +1990 -0
  148. package/vendor/agent-frontend-shell/static/commands.js +1402 -0
  149. package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
  150. package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
  151. package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
  152. package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
  153. package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
  154. package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
  155. package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
  156. package/vendor/agent-frontend-shell/static/icons.js +92 -0
  157. package/vendor/agent-frontend-shell/static/index.html +1506 -0
  158. package/vendor/agent-frontend-shell/static/login.js +177 -0
  159. package/vendor/agent-frontend-shell/static/manifest.json +53 -0
  160. package/vendor/agent-frontend-shell/static/messages.js +3521 -0
  161. package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
  162. package/vendor/agent-frontend-shell/static/panels.js +7995 -0
  163. package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
  164. package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
  165. package/vendor/agent-frontend-shell/static/style.css +4774 -0
  166. package/vendor/agent-frontend-shell/static/sw.js +173 -0
  167. package/vendor/agent-frontend-shell/static/terminal.js +632 -0
  168. package/vendor/agent-frontend-shell/static/ui.js +8997 -0
  169. package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
  170. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
  171. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
  172. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  173. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  174. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  175. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  176. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  177. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  178. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  179. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  180. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  181. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  182. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  183. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  184. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  185. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
  186. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
  187. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
  188. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  189. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  190. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  191. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
  192. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
  193. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
  194. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
  195. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
  196. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
  197. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  198. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  199. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  200. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
  201. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
  202. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
  203. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  204. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  205. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  206. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  207. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  208. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  209. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  210. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  211. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  212. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
  213. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
  214. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
  215. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
  216. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
  217. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  218. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
  219. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
  220. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  221. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
  222. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
  223. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  224. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
  225. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
  226. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  227. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  228. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  229. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  230. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
  231. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
  232. package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
  233. package/vendor/agent-frontend-shell/static/workspace.js +680 -0
@@ -0,0 +1,238 @@
1
+ """Clarify prompt state for the WebUI.
2
+
3
+ This mirrors the approval flow structure, but the response is a free-form
4
+ clarification string instead of an approval decision.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import queue
10
+ import threading
11
+ import time
12
+ import uuid
13
+ from typing import Optional
14
+
15
+ from api.session_events import publish_session_list_changed
16
+
17
+
18
+ DEFAULT_TIMEOUT_SECONDS = 120
19
+ _lock = threading.Lock()
20
+ _pending: dict[str, dict] = {}
21
+ _gateway_queues: dict[str, list] = {}
22
+ _gateway_notify_cbs: dict[str, object] = {}
23
+
24
+ # ── SSE subscriber registry ─────────────────────────────────────────────
25
+ _clarify_sse_subscribers: dict[str, list[queue.Queue]] = {}
26
+
27
+
28
+ class _ClarifyEntry:
29
+ """One pending clarify request inside a session."""
30
+
31
+ __slots__ = ("event", "data", "result", "clarify_id")
32
+
33
+ def __init__(self, data: dict):
34
+ self.event = threading.Event()
35
+ self.data = data
36
+ self.result: Optional[str] = None
37
+ self.clarify_id: str = data.get("clarify_id", "") or uuid.uuid4().hex[:12]
38
+
39
+
40
+ def register_gateway_notify(session_key: str, cb) -> None:
41
+ """Register a per-session callback for sending clarify requests to the UI."""
42
+ with _lock:
43
+ _gateway_notify_cbs[session_key] = cb
44
+
45
+
46
+ def _clear_queue_locked(session_key: str) -> list[_ClarifyEntry]:
47
+ entries = _gateway_queues.pop(session_key, [])
48
+ _pending.pop(session_key, None)
49
+ return entries
50
+
51
+
52
+ def unregister_gateway_notify(session_key: str) -> None:
53
+ """Unregister the per-session callback and unblock any waiting clarify prompt."""
54
+ with _lock:
55
+ _gateway_notify_cbs.pop(session_key, None)
56
+ entries = _clear_queue_locked(session_key)
57
+ if entries:
58
+ publish_session_list_changed("attention_cleared")
59
+ for entry in entries:
60
+ entry.event.set()
61
+
62
+
63
+ def clear_pending(session_key: str) -> int:
64
+ """Clear any pending clarify prompts for the session without removing the callback."""
65
+ with _lock:
66
+ entries = _clear_queue_locked(session_key)
67
+ if entries:
68
+ publish_session_list_changed("attention_cleared")
69
+ for entry in entries:
70
+ entry.event.set()
71
+ return len(entries)
72
+
73
+
74
+ def _with_timeout_metadata(data: dict) -> dict:
75
+ item = dict(data or {})
76
+ requested_at = float(item.get("requested_at") or time.time())
77
+ timeout_seconds = int(item.get("timeout_seconds") or DEFAULT_TIMEOUT_SECONDS)
78
+ expires_at = float(item.get("expires_at") or requested_at + timeout_seconds)
79
+ item["requested_at"] = requested_at
80
+ item["timeout_seconds"] = timeout_seconds
81
+ item["expires_at"] = expires_at
82
+ return item
83
+
84
+
85
+ def _clarify_sse_notify(session_id: str, head: dict | None, total: int) -> None:
86
+ """Push a clarify event to all SSE subscribers for a session."""
87
+ payload = {"pending": dict(head) if head else None, "pending_count": total}
88
+ for q in _clarify_sse_subscribers.get(session_id, ()):
89
+ try:
90
+ q.put_nowait(payload)
91
+ except queue.Full:
92
+ pass # drop if subscriber is slow
93
+
94
+
95
+ def sse_subscribe(session_id: str) -> queue.Queue:
96
+ """Register a bounded Queue for SSE push to a given session."""
97
+ q: queue.Queue = queue.Queue(maxsize=16)
98
+ with _lock:
99
+ _clarify_sse_subscribers.setdefault(session_id, []).append(q)
100
+ return q
101
+
102
+
103
+ def sse_unsubscribe(session_id: str, q: queue.Queue) -> None:
104
+ """Remove a subscriber Queue; clean up empty session entries."""
105
+ with _lock:
106
+ subs = _clarify_sse_subscribers.get(session_id)
107
+ if subs:
108
+ try:
109
+ subs.remove(q)
110
+ except ValueError:
111
+ pass
112
+ if not subs:
113
+ _clarify_sse_subscribers.pop(session_id, None)
114
+
115
+
116
+ def submit_pending(session_key: str, data: dict) -> _ClarifyEntry:
117
+ """Queue a pending clarify request and notify the UI callback if registered."""
118
+ data = _with_timeout_metadata(data)
119
+ with _lock:
120
+ gw_queue = _gateway_queues.setdefault(session_key, [])
121
+ # De-duplicate while unresolved: if the most recent pending clarify is
122
+ # semantically identical, reuse it instead of stacking duplicates.
123
+ if gw_queue:
124
+ last = gw_queue[-1]
125
+ if (
126
+ str(last.data.get("question", "")) == str(data.get("question", ""))
127
+ and list(last.data.get("choices_offered") or [])
128
+ == list(data.get("choices_offered") or [])
129
+ ):
130
+ entry = last
131
+ # Dedup re-uses the existing entry with its original clarify_id.
132
+ # If a future caller pre-populates clarify_id in data, it is
133
+ # silently discarded here — the original entry's id wins.
134
+ # Today no caller sets clarify_id (it's generated by __init__),
135
+ # so this is a non-issue.
136
+ cb = _gateway_notify_cbs.get(session_key)
137
+ # Keep _pending aligned to the oldest unresolved entry.
138
+ _pending[session_key] = gw_queue[0].data
139
+ if cb:
140
+ try:
141
+ cb(dict(entry.data))
142
+ except Exception:
143
+ pass
144
+ # Safe to call while holding _lock: publish() only takes the
145
+ # leaf _SESSION_EVENTS_LOCK and never re-acquires this lock.
146
+ publish_session_list_changed("attention_pending")
147
+ return entry
148
+
149
+ entry = _ClarifyEntry(data)
150
+ # Ensure clarify_id is present in the serialised data the frontend receives.
151
+ entry.data["clarify_id"] = entry.clarify_id
152
+ gw_queue.append(entry)
153
+ _pending[session_key] = gw_queue[0].data
154
+ cb = _gateway_notify_cbs.get(session_key)
155
+ # Notify SSE subscribers from inside _lock for ordering guarantees.
156
+ _clarify_sse_notify(session_key, dict(gw_queue[0].data), len(gw_queue))
157
+ publish_session_list_changed("attention_pending")
158
+ if cb:
159
+ try:
160
+ cb(data)
161
+ except Exception:
162
+ pass
163
+ return entry
164
+
165
+
166
+ def get_pending(session_key: str) -> dict | None:
167
+ """Return the oldest pending clarify request for this session, if any."""
168
+ with _lock:
169
+ queue = _gateway_queues.get(session_key) or []
170
+ if queue:
171
+ return dict(queue[0].data)
172
+ pending = _pending.get(session_key)
173
+ return dict(pending) if pending else None
174
+
175
+
176
+ def has_pending(session_key: str) -> bool:
177
+ with _lock:
178
+ return bool(_gateway_queues.get(session_key))
179
+
180
+
181
+ def pending_count(session_key: str) -> int:
182
+ """Return the number of unresolved clarify prompts for a session."""
183
+ with _lock:
184
+ queue = _gateway_queues.get(session_key) or []
185
+ if queue:
186
+ return len(queue)
187
+ return 1 if _pending.get(session_key) else 0
188
+
189
+
190
+ def resolve_clarify(session_key: str, response: str, resolve_all: bool = False) -> int:
191
+ """Resolve the oldest pending clarify request for a session."""
192
+ with _lock:
193
+ q = _gateway_queues.get(session_key)
194
+ if not q:
195
+ _pending.pop(session_key, None)
196
+ return 0
197
+ entries = list(q) if resolve_all else [q.pop(0)]
198
+ if q:
199
+ _pending[session_key] = q[0].data
200
+ _clarify_sse_notify(session_key, dict(q[0].data), len(q))
201
+ else:
202
+ _clear_queue_locked(session_key)
203
+ _clarify_sse_notify(session_key, None, 0)
204
+ publish_session_list_changed("attention_resolved")
205
+ count = 0
206
+ for entry in entries:
207
+ entry.result = response
208
+ entry.event.set()
209
+ count += 1
210
+ return count
211
+
212
+
213
+ def resolve_clarify_by_id(session_key: str, clarify_id: str, response: str) -> bool:
214
+ """Resolve a specific pending clarify request by its stable id.
215
+
216
+ Returns True if the id was found and resolved, False otherwise.
217
+ """
218
+ with _lock:
219
+ q = _gateway_queues.get(session_key)
220
+ if not q:
221
+ _pending.pop(session_key, None)
222
+ return False
223
+ for i, entry in enumerate(q):
224
+ if entry.clarify_id == clarify_id:
225
+ q.pop(i)
226
+ if q:
227
+ _pending[session_key] = q[0].data
228
+ _clarify_sse_notify(session_key, dict(q[0].data), len(q))
229
+ else:
230
+ _clear_queue_locked(session_key)
231
+ _clarify_sse_notify(session_key, None, 0)
232
+ # Safe to call while holding _lock: publish() only takes the
233
+ # leaf _SESSION_EVENTS_LOCK and never re-acquires this lock.
234
+ publish_session_list_changed("attention_resolved")
235
+ entry.result = response
236
+ entry.event.set()
237
+ return True
238
+ return False
@@ -0,0 +1,124 @@
1
+ """Expose hermes-agent's COMMAND_REGISTRY to the webui frontend.
2
+
3
+ This module is the single integration point with hermes_cli.commands.
4
+ If hermes-agent is unavailable the endpoint degrades to an empty list
5
+ so the frontend can still load with WEBUI_ONLY commands.
6
+ """
7
+ from __future__ import annotations
8
+ import logging
9
+ from typing import Any
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Commands that are gateway_only in the agent registry -- webui never
14
+ # wants to expose them (sethome, restart, update etc.) even if a future
15
+ # agent version drops the gateway_only flag. /commands is the agent's
16
+ # own command-listing command; webui has its own /help that calls
17
+ # cmdHelp() locally, so /commands would be redundant and confusing.
18
+ _NEVER_EXPOSE: frozenset[str] = frozenset({
19
+ 'sethome', 'restart', 'update', 'commands',
20
+ })
21
+
22
+
23
+ def list_commands(_registry=None) -> list[dict[str, Any]]:
24
+ """Return COMMAND_REGISTRY entries as JSON-friendly dicts.
25
+
26
+ Returns empty list if hermes_cli is not installed (graceful
27
+ degradation -- the frontend has its own fallback minimum set).
28
+
29
+ Args:
30
+ _registry: Optional injected registry for testing. When None
31
+ (production), imports COMMAND_REGISTRY from hermes_cli.
32
+ """
33
+ if _registry is None:
34
+ try:
35
+ from hermes_cli.commands import COMMAND_REGISTRY as _registry
36
+ except ImportError:
37
+ logger.warning("hermes_cli.commands not importable -- /api/commands returns []")
38
+ return []
39
+
40
+ out: list[dict[str, Any]] = []
41
+ for cmd in _registry:
42
+ if cmd.gateway_only:
43
+ continue
44
+ if cmd.name in _NEVER_EXPOSE:
45
+ continue
46
+ out.append({
47
+ 'name': cmd.name,
48
+ 'description': cmd.description,
49
+ 'category': cmd.category,
50
+ 'aliases': list(cmd.aliases),
51
+ 'args_hint': cmd.args_hint,
52
+ 'subcommands': list(cmd.subcommands),
53
+ 'cli_only': bool(cmd.cli_only),
54
+ 'gateway_only': bool(cmd.gateway_only),
55
+ })
56
+
57
+ # Include plugin-registered slash commands
58
+ try:
59
+ from hermes_cli.plugins import get_plugin_commands
60
+ plugin_cmds = get_plugin_commands() or {}
61
+ existing_names = {c['name'] for c in out}
62
+ for cmd_name, cmd_info in plugin_cmds.items():
63
+ if cmd_name in existing_names or cmd_name in _NEVER_EXPOSE:
64
+ continue
65
+ out.append({
66
+ 'name': cmd_name,
67
+ 'description': str(cmd_info.get('description', 'Plugin command')),
68
+ 'category': 'Plugin',
69
+ 'aliases': [],
70
+ 'args_hint': str(cmd_info.get('args_hint', '')),
71
+ 'subcommands': [],
72
+ 'cli_only': False,
73
+ 'gateway_only': False,
74
+ })
75
+ except Exception:
76
+ pass
77
+
78
+ return out
79
+
80
+
81
+ def execute_plugin_command(command: str) -> str:
82
+ """Execute a plugin-registered slash command and return printable output.
83
+
84
+ Unknown commands raise ``KeyError`` so the HTTP layer can return 404.
85
+ Plugin handler failures are returned as output text instead of surfacing as
86
+ transport errors, matching Hermes' existing slash-command UX.
87
+ """
88
+
89
+ raw = str(command or "").strip()
90
+ if not raw:
91
+ raise ValueError("command is required")
92
+
93
+ cmd_text = raw[1:] if raw.startswith("/") else raw
94
+ cmd_parts = cmd_text.split(maxsplit=1)
95
+ cmd_base = (cmd_parts[0] if cmd_parts else "").strip().lower()
96
+ cmd_arg = cmd_parts[1] if len(cmd_parts) > 1 else ""
97
+ if not cmd_base:
98
+ raise ValueError("command is required")
99
+
100
+ try:
101
+ from hermes_cli.plugins import (
102
+ get_plugin_command_handler,
103
+ resolve_plugin_command_result,
104
+ )
105
+ except ImportError as exc:
106
+ raise RuntimeError("plugin command runtime unavailable") from exc
107
+
108
+ try:
109
+ handler = get_plugin_command_handler(cmd_base)
110
+ except Exception as exc:
111
+ raise RuntimeError(f"plugin command lookup failed: {exc}") from exc
112
+
113
+ if not handler:
114
+ raise KeyError(cmd_base)
115
+
116
+ try:
117
+ result = resolve_plugin_command_result(handler(cmd_arg))
118
+ return str(result or "(no output)")
119
+ except Exception as exc:
120
+ # Don't leak raw exception str (paths, env, internal state) to the
121
+ # user-facing chat. Type name is enough for the user to know what
122
+ # class of failure occurred; full traceback lives in the server log.
123
+ logger.warning("Plugin command %r failed", cmd_base, exc_info=exc)
124
+ return f"Plugin command error: {type(exc).__name__}"
@@ -0,0 +1,134 @@
1
+ """
2
+ Shared helpers for session compression anchor metadata.
3
+
4
+ Manual compression anchoring versus automatic compression paths
5
+ ===============================================================
6
+
7
+ When ``auto_compression=True`` is passed to ``visible_messages_for_anchor()``,
8
+ the function accepts a broader set of message content types (including
9
+ provider-style ``input_text`` / ``output_text`` parts) and metadata markers
10
+ (``reasoning``, ``thinking``, etc.) from any non-tool role. This enables the
11
+ streaming auto-compression path to determine which messages should anchor
12
+ compression UI metadata without being limited to the legacy manual-compression
13
+ rules.
14
+
15
+ When ``auto_compression=False`` (the default), the function applies the
16
+ historical manual-compression rules: only plain ``text`` content parts from
17
+ non-assistant roles are counted.
18
+
19
+ Why this module exists
20
+ ======================
21
+
22
+ Compression anchoring needs to identify which messages in a session transcript
23
+ are semantically significant enough to seed the compression UI metadata (e.g.,
24
+ message count, token budget display). The original implementation hard-coded
25
+ these rules in multiple places. This module consolidates the logic so that:
26
+
27
+ 1. Manual compression anchoring (CLI/legacy path) uses the stricter ruleset.
28
+ 2. Automatic compression (streaming/agent path) can leverage the relaxed ruleset
29
+ when it knows it is handling provider-style messages.
30
+
31
+ Callers specify ``auto_compression=True`` when the messages may originate from
32
+ an automatic/compression-aware pipeline, and ``False`` (default) for manual
33
+ compression contexts.
34
+ """
35
+
36
+
37
+ def _content_text(content, *, part_types):
38
+ if isinstance(content, list):
39
+ return "\n".join(
40
+ str(part.get("text") or part.get("content") or "")
41
+ for part in content
42
+ if isinstance(part, dict) and part.get("type") in part_types
43
+ ).strip()
44
+ return str(content or "").strip()
45
+
46
+
47
+ def _content_has_part_type(content, part_types):
48
+ if not isinstance(content, list):
49
+ return False
50
+ return any(
51
+ isinstance(part, dict) and part.get("type") in part_types
52
+ for part in content
53
+ )
54
+
55
+
56
+ def is_context_compression_marker(message):
57
+ """Return true for synthetic compression/reference cards, not user turns."""
58
+ if not isinstance(message, dict):
59
+ return False
60
+ role = message.get("role")
61
+ if not role or role == "tool":
62
+ return False
63
+ text = _content_text(
64
+ message.get("content", ""),
65
+ part_types={"text", "input_text", "output_text"},
66
+ ).lower().lstrip()
67
+ return (
68
+ text.startswith("[context compaction")
69
+ or text.startswith("context compaction")
70
+ or text.startswith("[your active task list was preserved across context compression]")
71
+ or text.startswith("[session arc summary")
72
+ )
73
+
74
+
75
+ def _is_context_compression_marker(message):
76
+ """Backward-compatible alias for callers that have not switched yet."""
77
+ return is_context_compression_marker(message)
78
+
79
+
80
+ def visible_messages_for_anchor(messages, *, auto_compression: bool = False):
81
+ """Return transcript messages that can anchor compression UI metadata.
82
+
83
+ Manual compression historically only counted plain ``text`` content parts
84
+ for non-assistant messages, while the streaming auto-compression path also
85
+ accepted provider-style ``input_text`` / ``output_text`` parts and metadata
86
+ markers on any non-tool role. Keep that difference explicit at the call site
87
+ instead of carrying two near-identical helper implementations.
88
+ """
89
+ out = []
90
+ text_part_types = {"text", "input_text", "output_text"} if auto_compression else {"text"}
91
+ for message in messages or []:
92
+ if not isinstance(message, dict):
93
+ continue
94
+ role = message.get("role")
95
+ if not role or role == "tool":
96
+ continue
97
+ if _is_context_compression_marker(message):
98
+ continue
99
+
100
+ content = message.get("content", "")
101
+ has_attachments = bool(message.get("attachments"))
102
+ text = _content_text(content, part_types=text_part_types)
103
+
104
+ if auto_compression:
105
+ has_tool_calls = bool(
106
+ isinstance(message.get("tool_calls"), list) and message.get("tool_calls")
107
+ )
108
+ has_tool_use = _content_has_part_type(content, {"tool_use"})
109
+ has_reasoning = bool(message.get("reasoning"))
110
+ if not text:
111
+ has_reasoning = has_reasoning or _content_has_part_type(
112
+ content,
113
+ {"thinking", "reasoning"},
114
+ )
115
+ if text or has_attachments or has_tool_calls or has_tool_use or has_reasoning:
116
+ out.append(message)
117
+ continue
118
+
119
+ if role == "assistant":
120
+ has_tool_calls = bool(
121
+ isinstance(message.get("tool_calls"), list) and message.get("tool_calls")
122
+ )
123
+ has_tool_use = _content_has_part_type(content, {"tool_use"})
124
+ has_reasoning = bool(message.get("reasoning")) or _content_has_part_type(
125
+ content,
126
+ {"thinking", "reasoning"},
127
+ )
128
+ if text or has_attachments or has_tool_calls or has_tool_use or has_reasoning:
129
+ out.append(message)
130
+ continue
131
+
132
+ if text or has_attachments:
133
+ out.append(message)
134
+ return out