@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,1402 @@
1
+ // ── Slash commands ──────────────────────────────────────────────────────────
2
+ // Built-in commands intercepted before send(). Each command runs locally
3
+ // (no round-trip to the agent) and shows feedback via toast or local message.
4
+
5
+ const COMMANDS=[
6
+ // noEcho:true = action-only commands that don't produce a chat response.
7
+ // Commands without noEcho get a user message echoed to the chat (#840).
8
+ {name:'help', desc:t('cmd_help'), fn:cmdHelp},
9
+ {name:'clear', desc:t('cmd_clear'), fn:cmdClear, noEcho:true},
10
+ {name:'compress', desc:t('cmd_compress'), fn:cmdCompress, arg:'[focus topic]', noEcho:true},
11
+ {name:'compact', desc:t('cmd_compact_alias'), fn:cmdCompact, noEcho:true},
12
+ {name:'model', desc:t('cmd_model'), fn:cmdModel, arg:'model_name', subArgs:'models', noEcho:true},
13
+ {name:'workspace', desc:t('cmd_workspace'), fn:cmdWorkspace, arg:'name', noEcho:true},
14
+ {name:'terminal', desc:t('cmd_terminal'), fn:cmdTerminal, noEcho:true},
15
+ {name:'new', desc:t('cmd_new'), fn:cmdNew, noEcho:true},
16
+ {name:'usage', desc:t('cmd_usage'), fn:cmdUsage, noEcho:true},
17
+ {name:'theme', desc:t('cmd_theme'), fn:cmdTheme, arg:'name', noEcho:true},
18
+ {name:'personality', desc:t('cmd_personality'), fn:cmdPersonality, arg:'name', subArgs:'personalities'},
19
+ {name:'skills', desc:t('cmd_skills'), fn:cmdSkills, arg:'query'},
20
+ {name:'stop', desc:t('cmd_stop'), fn:cmdStop, noEcho:true},
21
+ {name:'goal', desc:t('cmd_goal'), fn:cmdGoal, arg:'[status|pause|resume|clear|text]', subArgs:['status','pause','resume','clear']},
22
+ {name:'queue', desc:t('cmd_queue'), fn:cmdQueue, arg:'message', noEcho:true},
23
+ {name:'interrupt', desc:t('cmd_interrupt'), fn:cmdInterrupt, arg:'message', noEcho:true},
24
+ {name:'steer', desc:t('cmd_steer'), fn:cmdSteer, arg:'message', noEcho:true},
25
+ {name:'title', desc:t('cmd_title'), fn:cmdTitle, arg:'[title]'},
26
+ {name:'retry', desc:t('cmd_retry'), fn:cmdRetry, noEcho:true},
27
+ {name:'undo', desc:t('cmd_undo'), fn:cmdUndo, noEcho:true},
28
+ {name:'btw', desc:t('cmd_btw'), fn:cmdBtw, arg:'question', noEcho:true},
29
+ {name:'background',desc:t('cmd_background'),fn:cmdBackground,arg:'prompt', noEcho:true},
30
+ {name:'status', desc:t('cmd_status'), fn:cmdStatus},
31
+ {name:'voice', desc:t('cmd_voice'), fn:cmdVoice, noEcho:true},
32
+ {name:'reasoning', desc:t('cmd_reasoning'), fn:cmdReasoning, arg:'show|hide|none|minimal|low|medium|high|xhigh|max', subArgs:['show','hide','none','minimal','low','medium','high','xhigh','max'], noEcho:true},
33
+ {name:'yolo', desc:t('cmd_yolo'), fn:cmdYolo, noEcho:true},
34
+ {name:'branch', desc:t('cmd_branch'), fn:cmdBranch, arg:'[name]', noEcho:true},
35
+ ];
36
+
37
+ const SLASH_SUBARG_SOURCES={
38
+ model:{desc:t('cmd_model'), subArgs:'models'},
39
+ personality:{desc:t('cmd_personality'), subArgs:'personalities'},
40
+ };
41
+
42
+ function parseCommand(text){
43
+ if(!text.startsWith('/'))return null;
44
+ const parts=text.slice(1).split(/\s+/);
45
+ const name=parts[0].toLowerCase();
46
+ const args=parts.slice(1).join(' ').trim();
47
+ return {name,args};
48
+ }
49
+
50
+ function executeCommand(text){
51
+ const parsed=parseCommand(text);
52
+ if(!parsed)return null;
53
+ const cmd=COMMANDS.find(c=>c.name===parsed.name);
54
+ if(!cmd)return null;
55
+ // A handler may return `false` to opt out of interception — e.g. /reasoning
56
+ // with an effort level falls through so the agent's own handler sees it,
57
+ // preserving the pre-existing pass-through behaviour for that subcommand.
58
+ if(cmd.fn(parsed.args)===false)return null;
59
+ // Return noEcho flag so send() knows whether to echo the command as a user message (#840).
60
+ return {noEcho:!!cmd.noEcho};
61
+ }
62
+
63
+ function getMatchingCommands(prefix){
64
+ const q=prefix.toLowerCase();
65
+ const matches=COMMANDS.filter(c=>c.name.startsWith(q)).map(c=>({...c,source:'builtin'}));
66
+ const seen=new Set(matches.map(c=>c.name));
67
+ for(const [name, spec] of Object.entries(SLASH_SUBARG_SOURCES)){
68
+ if(!name.startsWith(q)||seen.has(name))continue;
69
+ matches.push({
70
+ name,
71
+ desc:spec.desc,
72
+ arg:'name',
73
+ source:'subarg-command',
74
+ });
75
+ seen.add(name);
76
+ }
77
+ for(const skill of _skillCommandCache){
78
+ if(!skill.name.startsWith(q)||seen.has(skill.name))continue;
79
+ matches.push(skill);
80
+ seen.add(skill.name);
81
+ }
82
+ // Include agent/plugin commands from /api/commands metadata
83
+ for(const cmd of (_agentCommandCache||[])){
84
+ const name=String(cmd&&cmd.name||'').toLowerCase();
85
+ if(!name.startsWith(q)||seen.has(name))continue;
86
+ if(cmd.cli_only)continue;
87
+ matches.push({
88
+ name,
89
+ desc:String(cmd&&cmd.description||'').trim()||'Agent command',
90
+ source:cmd.category==='Plugin'?'plugin':'agent',
91
+ });
92
+ seen.add(name);
93
+ }
94
+ return matches;
95
+ }
96
+
97
+ let _slashModelCache=null;
98
+ let _slashModelCachePromise=null;
99
+ let _slashPersonalityCache=null;
100
+ let _slashPersonalityCachePromise=null;
101
+ let _agentCommandCache=null;
102
+ let _agentCommandCachePromise=null;
103
+
104
+ // Invalidate the /api/models slash-suggestion cache. Called by panels.js
105
+ // after a provider is added or removed so the next /model autocomplete
106
+ // rebuilds from a fresh /api/models response (#1539). Returning a function
107
+ // rather than letting callers poke the module-local lets/promises directly
108
+ // keeps the cache shape encapsulated to this module.
109
+ function _invalidateSlashModelCache(){
110
+ _slashModelCache=null;
111
+ _slashModelCachePromise=null;
112
+ }
113
+ // Expose on window when available. Guarded by typeof so the module is
114
+ // importable in headless test contexts (vm.runInContext) that don't
115
+ // define a window global — see tests/test_cli_only_slash_commands.py.
116
+ if(typeof window!=='undefined'){
117
+ window._invalidateSlashModelCache=_invalidateSlashModelCache;
118
+ }
119
+
120
+ function _normalizeSlashSubArg(value){
121
+ return String(value||'').trim();
122
+ }
123
+
124
+ function _getSlashModelSubArgsFromDom(){
125
+ const sel=$('modelSelect');
126
+ if(!sel) return [];
127
+ const values=[];
128
+ for(const opt of Array.from(sel.options||[])){
129
+ const value=_normalizeSlashSubArg(opt.value||opt.textContent||'');
130
+ if(value) values.push(value);
131
+ }
132
+ return Array.from(new Set(values)).sort((a,b)=>a.localeCompare(b));
133
+ }
134
+
135
+ async function _loadSlashModelSubArgs(force=false){
136
+ const domValues=_getSlashModelSubArgsFromDom();
137
+ if(domValues.length&&!force){
138
+ _slashModelCache=domValues;
139
+ return domValues;
140
+ }
141
+ if(_slashModelCache&&!force) return _slashModelCache;
142
+ if(_slashModelCachePromise&&!force) return _slashModelCachePromise;
143
+ _slashModelCachePromise=(async()=>{
144
+ try{
145
+ const data=await api('/api/models');
146
+ const values=[];
147
+ for(const group of (data&&data.groups)||[]){
148
+ for(const model of (group&&group.models)||[]){
149
+ const id=_normalizeSlashSubArg(model&&model.id);
150
+ if(id) values.push(id);
151
+ }
152
+ // Include extra_models (the catalog tail that doesn't render as
153
+ // <option> entries when the picker is capped) so /model autocomplete
154
+ // covers the full catalog. The trimming is purely a dropdown
155
+ // scannability concern — the slash command exists precisely so
156
+ // power users can reach any model by typing its name. #1567.
157
+ for(const model of (group&&group.extra_models)||[]){
158
+ const id=_normalizeSlashSubArg(model&&model.id);
159
+ if(id) values.push(id);
160
+ }
161
+ }
162
+ const deduped=Array.from(new Set(values)).sort((a,b)=>a.localeCompare(b));
163
+ _slashModelCache=deduped;
164
+ return deduped;
165
+ }catch(_){
166
+ _slashModelCache=domValues;
167
+ return domValues;
168
+ }finally{
169
+ _slashModelCachePromise=null;
170
+ }
171
+ })();
172
+ return _slashModelCachePromise;
173
+ }
174
+
175
+ async function _loadSlashPersonalitySubArgs(force=false){
176
+ if(_slashPersonalityCache&&!force) return _slashPersonalityCache;
177
+ if(_slashPersonalityCachePromise&&!force) return _slashPersonalityCachePromise;
178
+ _slashPersonalityCachePromise=(async()=>{
179
+ try{
180
+ const data=await api('/api/personalities');
181
+ const values=['none'];
182
+ for(const p of (data&&data.personalities)||[]){
183
+ const name=_normalizeSlashSubArg(p&&p.name);
184
+ if(name) values.push(name);
185
+ }
186
+ const deduped=Array.from(new Set(values)).sort((a,b)=>a.localeCompare(b));
187
+ _slashPersonalityCache=deduped;
188
+ return deduped;
189
+ }catch(_){
190
+ _slashPersonalityCache=['none'];
191
+ return _slashPersonalityCache;
192
+ }finally{
193
+ _slashPersonalityCachePromise=null;
194
+ }
195
+ })();
196
+ return _slashPersonalityCachePromise;
197
+ }
198
+
199
+ function _getSlashSubArgOptions(spec){
200
+ if(Array.isArray(spec)) return Promise.resolve(spec.slice());
201
+ if(spec==='models') return _loadSlashModelSubArgs();
202
+ if(spec==='personalities') return _loadSlashPersonalitySubArgs();
203
+ return Promise.resolve([]);
204
+ }
205
+
206
+ let _agentCommandCacheReady=false;
207
+ async function loadAgentCommandMetadata(force=false){
208
+ if(_agentCommandCacheReady&&!force)return _agentCommandCache||[];
209
+ if(_agentCommandCachePromise&&!force)return _agentCommandCachePromise;
210
+ _agentCommandCachePromise=(async()=>{
211
+ try{
212
+ const data=await api('/api/commands');
213
+ _agentCommandCache=Array.isArray(data&&data.commands)?data.commands:[];
214
+ }catch(_){
215
+ _agentCommandCache=[];
216
+ }finally{
217
+ _agentCommandCacheReady=true;
218
+ _agentCommandCachePromise=null;
219
+ }
220
+ return _agentCommandCache;
221
+ })();
222
+ return _agentCommandCachePromise;
223
+ }
224
+
225
+ async function getAgentCommandMetadata(name){
226
+ const needle=String(name||'').trim().toLowerCase();
227
+ if(!needle) return null;
228
+ const commands=await loadAgentCommandMetadata();
229
+ return commands.find(cmd=>{
230
+ if(String(cmd&&cmd.name||'').toLowerCase()===needle) return true;
231
+ return Array.isArray(cmd&&cmd.aliases)&&cmd.aliases.some(a=>String(a||'').toLowerCase()===needle);
232
+ })||null;
233
+ }
234
+
235
+ function cliOnlyCommandResponse(cmdName, meta){
236
+ const name=String((meta&&meta.name)||cmdName||'').trim();
237
+ const desc=String((meta&&meta.description)||'').trim();
238
+ const detail=desc?`\n\n${desc}`:'';
239
+ let extra='';
240
+ if(name==='browser'){
241
+ extra='\n\nBrowser tools in WebUI must be configured server-side with the agent/browser environment. Once configured, ask the model to use browser tools directly; `/browser` itself only works in `hermes chat`.';
242
+ }
243
+ return `\`/${name}\` is a Hermes CLI-only command and cannot run inside the WebUI.${detail}${extra}`;
244
+ }
245
+
246
+ async function executeAgentPluginCommand(text,_meta){
247
+ const command=String(text||'').trim();
248
+ if(!command) throw new Error('command is required');
249
+ const data=await api('/api/commands/exec',{
250
+ method:'POST',
251
+ body:JSON.stringify({command})
252
+ });
253
+ return String(data&&data.output||'(no output)');
254
+ }
255
+
256
+ function _parseSlashAutocomplete(text){
257
+ if(!text.startsWith('/')||text.indexOf('\n')!==-1) return null;
258
+ const raw=text.slice(1);
259
+ const hasSpace=/\s/.test(raw);
260
+ const parts=raw.split(/\s+/);
261
+ const cmdName=(parts[0]||'').toLowerCase();
262
+ const command=COMMANDS.find(c=>c.name===cmdName);
263
+ const subArgSource=(command&&command.subArgs)?command:SLASH_SUBARG_SOURCES[cmdName];
264
+ if(!hasSpace||!subArgSource){
265
+ return {kind:'commands', query:raw};
266
+ }
267
+ const argText=raw.slice(cmdName.length).replace(/^\s+/,'');
268
+ return {kind:'subargs', command:{name:cmdName, desc:subArgSource.desc, subArgs:subArgSource.subArgs}, query:argText.toLowerCase(), rawQuery:argText};
269
+ }
270
+
271
+ async function getSlashAutocompleteMatches(text){
272
+ const parsed=_parseSlashAutocomplete(text);
273
+ if(!parsed) return [];
274
+ if(parsed.kind==='commands') return getMatchingCommands(parsed.query);
275
+ const options=await _getSlashSubArgOptions(parsed.command.subArgs);
276
+ return options
277
+ .filter(opt=>String(opt).toLowerCase().startsWith(parsed.query))
278
+ .map(opt=>({
279
+ name:parsed.command.name,
280
+ value:String(opt),
281
+ desc:parsed.command.desc,
282
+ source:'subarg',
283
+ parent:parsed.command.name,
284
+ }));
285
+ }
286
+
287
+ function _compressionAnchorMessageKey(m){
288
+ if(!m||!m.role||m.role==='tool') return null;
289
+ let content='';
290
+ try{
291
+ content=typeof msgContent==='function' ? String(msgContent(m)||'') : String(m.content||'');
292
+ }catch(_){
293
+ content=String(m.content||'');
294
+ }
295
+ const norm=content.replace(/\s+/g,' ').trim().slice(0,160);
296
+ const ts=m._ts||m.timestamp||null;
297
+ const attachments=Array.isArray(m.attachments)?m.attachments.length:0;
298
+ if(!norm && !attachments && !ts) return null;
299
+ return {role:String(m.role||''), ts, text:norm, attachments};
300
+ }
301
+
302
+ // ── Command handlers ────────────────────────────────────────────────────────
303
+
304
+ function cmdHelp(){
305
+ const lines=COMMANDS.map(c=>{
306
+ const usage=c.arg ? (String(c.arg).startsWith('[') ? ` ${c.arg}` : ` <${c.arg}>`) : '';
307
+ return ` /${c.name}${usage} — ${c.desc}`;
308
+ });
309
+ const msg={role:'assistant',content:t('available_commands')+'\n'+lines.join('\n')};
310
+ S.messages.push(msg);
311
+ renderMessages();
312
+ showToast(t('type_slash'));
313
+ }
314
+
315
+ function cmdClear(){
316
+ if(!S.session)return;
317
+ S.messages=[];S.toolCalls=[];
318
+ clearLiveToolCards();
319
+ if(typeof clearCompressionUi==='function') clearCompressionUi();
320
+ renderMessages();
321
+ $('emptyState').style.display='';
322
+ showToast(t('conversation_cleared'));
323
+ }
324
+
325
+ async function cmdModel(args){
326
+ if(!args){showToast(t('model_usage'));return;}
327
+ const sel=$('modelSelect');
328
+ if(!sel)return;
329
+ let q=args.toLowerCase();
330
+ // Resolve alias before fuzzy matching the dropdown.
331
+ // Fetch /api/models which now includes an "aliases" key.
332
+ try {
333
+ const resp=await fetch('/api/models');
334
+ if(resp.ok){
335
+ const data=await resp.json();
336
+ const aliases=data.aliases||{};
337
+ for(const [alias,modelId] of Object.entries(aliases)){
338
+ if(alias.toLowerCase()===q){
339
+ q=modelId.toLowerCase(); // resolve alias to real model id e.g. "deepseek/deepseek-v4-flash"
340
+ break;
341
+ }
342
+ }
343
+ }
344
+ } catch(_){/* non-critical, fall through to fuzzy match */}
345
+ // First: try exact match within active provider's optgroup.
346
+ // Use _findModelInDropdown (ui.js) which supports preferredProviderId.
347
+ const preferred=(S&&S.session&&S.session.model_provider)||window._activeProvider||null;
348
+ let match=(typeof _findModelInDropdown==='function')?_findModelInDropdown(q,sel,preferred):null;
349
+ // Fallback: fuzzy match across all options
350
+ if(!match){
351
+ for(const opt of sel.options){
352
+ if(opt.value.toLowerCase().includes(q)||opt.textContent.toLowerCase().includes(q)){
353
+ match=opt.value;break;
354
+ }
355
+ }
356
+ }
357
+ // Fallback: if q has provider/ prefix (e.g. "deepseek/deepseek-v4-flash"),
358
+ // try the bare model name (which is how options appear for the active provider)
359
+ if(!match && q.includes('/')){
360
+ const bare=q.slice(q.lastIndexOf('/')+1);
361
+ for(const opt of sel.options){
362
+ if(opt.value.toLowerCase().includes(bare)||opt.textContent.toLowerCase().includes(bare)){
363
+ match=opt.value;break;
364
+ }
365
+ }
366
+ // Cross-provider fallback: if still no match, the model is from a
367
+ // different provider not in the dropdown. Call /api/session/update directly.
368
+ if(!match && S&&S.session&&S.session.session_id){
369
+ const provider=q.slice(0,q.indexOf('/'));
370
+ try{
371
+ const resp=await fetch('/api/session/update',{
372
+ method:'POST',
373
+ headers:{'Content-Type':'application/json'},
374
+ body:JSON.stringify({
375
+ session_id:S.session.session_id,
376
+ model:q,
377
+ model_provider:provider,
378
+ }),
379
+ });
380
+ if(resp.ok){
381
+ S.session.model=q;
382
+ S.session.model_provider=provider;
383
+ if(typeof syncTopbar==='function') syncTopbar();
384
+ showToast(t('switched_to')+q);
385
+ return;
386
+ }
387
+ }catch(_){/* fall through to "no model match" */}
388
+ }
389
+ }
390
+ if(!match){showToast(t('no_model_match')+`"${args}"`);return;}
391
+ sel.value=match;
392
+ await sel.onchange();
393
+ showToast(t('switched_to')+match);
394
+ }
395
+
396
+ async function cmdWorkspace(args){
397
+ if(!args){showToast(t('workspace_usage'));return;}
398
+ try{
399
+ const data=await api('/api/workspaces');
400
+ const q=args.toLowerCase();
401
+ const ws=(data.workspaces||[]).find(w=>
402
+ (w.name||'').toLowerCase().includes(q)||w.path.toLowerCase().includes(q)
403
+ );
404
+ if(!ws){showToast(t('no_workspace_match')+`"${args}"`);return;}
405
+ if(typeof switchToWorkspace==='function') await switchToWorkspace(ws.path, ws.name||ws.path);
406
+ else showToast(t('switched_workspace')+(ws.name||ws.path));
407
+ }catch(e){showToast(t('workspace_switch_failed')+e.message);}
408
+ }
409
+
410
+ async function cmdTerminal(){
411
+ if(!S.session&&typeof newSession==='function'){
412
+ if(!S._profileSwitchWorkspace&&!S._profileDefaultWorkspace){
413
+ try{
414
+ const data=await api('/api/workspaces');
415
+ const first=(data.workspaces||[])[0];
416
+ S._profileSwitchWorkspace=data.last||(first&&first.path)||null;
417
+ }catch(_){}
418
+ }
419
+ await newSession();
420
+ if(typeof renderSessionList==='function') await renderSessionList();
421
+ }
422
+ if(!S.session||!S.session.workspace){
423
+ showToast(t('terminal_no_workspace_title'),2600,'warning');
424
+ if(typeof syncTerminalButton==='function') syncTerminalButton();
425
+ return;
426
+ }
427
+ if(typeof toggleComposerTerminal==='function') await toggleComposerTerminal(true);
428
+ }
429
+
430
+ async function cmdNew(){
431
+ if(typeof clearCompressionUi==='function') clearCompressionUi();
432
+ await newSession();
433
+ await renderSessionList();
434
+ $('msg').focus();
435
+ showToast(t('new_session'));
436
+ }
437
+
438
+ function _manualCompressionVisibleMessages(){
439
+ return (S.messages||[]).filter(m=>{
440
+ if(!m||!m.role||m.role==='tool') return false;
441
+ if(m.role==='assistant'){
442
+ const hasTc=Array.isArray(m.tool_calls)&&m.tool_calls.length>0;
443
+ const hasTu=Array.isArray(m.content)&&m.content.some(p=>p&&p.type==='tool_use');
444
+ if(hasTc||hasTu|| (typeof _messageHasReasoningPayload==='function' && _messageHasReasoningPayload(m))) return true;
445
+ }
446
+ return typeof msgContent==='function' ? !!msgContent(m) || !!m.attachments?.length : !!m.content || !!m.attachments?.length;
447
+ });
448
+ }
449
+
450
+ function _manualCompressionSleep(ms){
451
+ return new Promise(resolve=>setTimeout(resolve, ms));
452
+ }
453
+
454
+ async function _pollManualCompressionResult(sid){
455
+ let delay=700;
456
+ while(true){
457
+ const data=await api(`/api/session/compress/status?session_id=${encodeURIComponent(sid)}`);
458
+ if(data&&data.status==='done') return data;
459
+ if(data&&data.status==='error'){
460
+ const err=new Error(data.error||'Compression failed');
461
+ err.status=data.error_status||400;
462
+ throw err;
463
+ }
464
+ if(data&&data.status==='idle') throw new Error('Compression job is no longer available');
465
+ await _manualCompressionSleep(delay);
466
+ delay=Math.min(2000, delay+300);
467
+ }
468
+ }
469
+
470
+ async function _applyManualCompressionResult(data, focusTopic, visibleCount, commandText){
471
+ if(data&&data.session){
472
+ const currentSid=S.session&&S.session.session_id;
473
+ if(data.session.session_id&&data.session.session_id!==currentSid){
474
+ await loadSession(data.session.session_id);
475
+ }else{
476
+ S.session=data.session;
477
+ S.messages=data.session.messages||[];
478
+ S.toolCalls=data.session.tool_calls||[];
479
+ clearLiveToolCards();
480
+ try{localStorage.setItem('hermes-webui-session',S.session.session_id);}catch(_){}
481
+ if(typeof _setActiveSessionUrl==='function') _setActiveSessionUrl(S.session.session_id);
482
+ syncTopbar();
483
+ renderMessages();
484
+ await renderSessionList();
485
+ updateQueueBadge(S.session.session_id);
486
+ }
487
+ }
488
+ const summary=data&&data.summary;
489
+ if(typeof setCompressionUi==='function'&&S.session){
490
+ const referenceMsg=(S.messages||[]).find(m=>typeof _isContextCompactionMessage==='function'&&_isContextCompactionMessage(m));
491
+ const messageRef=referenceMsg?msgContent(referenceMsg)||String(referenceMsg.content||''):'';
492
+ const summaryRef=summary&&typeof summary.reference_message==='string' ? String(summary.reference_message||'').trim() : '';
493
+ // Prefer the persisted compaction handoff when it already exists in session state.
494
+ // The short summary fallback is only for environments where that message is unavailable.
495
+ const referenceText=messageRef || summaryRef;
496
+ const effectiveFocus=(data&&data.focus_topic)||focusTopic||'';
497
+ setCompressionUi({
498
+ sessionId:S.session.session_id,
499
+ phase:'done',
500
+ focusTopic:effectiveFocus,
501
+ commandText:effectiveFocus?`/compress ${effectiveFocus}`:(commandText||'/compress'),
502
+ beforeCount:visibleCount,
503
+ summary:summary||null,
504
+ referenceText,
505
+ anchorVisibleIdx: data?.session?.compression_anchor_visible_idx,
506
+ anchorMessageKey: data?.session?.compression_anchor_message_key||null,
507
+ });
508
+ }
509
+ if(typeof setComposerStatus==='function') setComposerStatus('');
510
+ renderMessages();
511
+ if(typeof _setCompressionSessionLock==='function') _setCompressionSessionLock(null);
512
+ }
513
+
514
+ async function resumeManualCompressionForSession(sid){
515
+ if(!sid) return;
516
+ try{
517
+ const status=await api(`/api/session/compress/status?session_id=${encodeURIComponent(sid)}`);
518
+ if(!status||status.status!=='running') return;
519
+ const visibleMessages=_manualCompressionVisibleMessages();
520
+ const visibleCount=visibleMessages.length;
521
+ const anchorMessageKey=_compressionAnchorMessageKey(visibleMessages[visibleMessages.length-1]||null);
522
+ if(typeof setBusy==='function') setBusy(true);
523
+ if(typeof setComposerStatus==='function') setComposerStatus(t('compressing'));
524
+ if(typeof setCompressionUi==='function'){
525
+ setCompressionUi({
526
+ sessionId:sid,
527
+ phase:'running',
528
+ focusTopic:status.focus_topic||'',
529
+ commandText:status.focus_topic?`/compress ${status.focus_topic}`:'/compress',
530
+ beforeCount:visibleCount,
531
+ anchorVisibleIdx:Math.max(0, visibleCount-1),
532
+ anchorMessageKey,
533
+ });
534
+ }
535
+ renderMessages();
536
+ const done=await _pollManualCompressionResult(sid);
537
+ if(!S.session||S.session.session_id!==sid) return;
538
+ await _applyManualCompressionResult(done, status.focus_topic||'', visibleCount, status.focus_topic?`/compress ${status.focus_topic}`:'/compress');
539
+ }catch(e){
540
+ // No active compression job or transient server error — not a real failure.
541
+ // 404: route missed or session gone; 5xx: backend exception during status check.
542
+ if(e&&(!e.status||e.status===404||e.status>=500)) return;
543
+ if(S.session&&S.session.session_id===sid&&typeof setCompressionUi==='function'){
544
+ const visibleMessages=_manualCompressionVisibleMessages();
545
+ setCompressionUi({
546
+ sessionId:sid,
547
+ phase:'error',
548
+ focusTopic:'',
549
+ commandText:'/compress',
550
+ beforeCount:visibleMessages.length,
551
+ errorText:`Compression failed: ${e.message}`,
552
+ anchorVisibleIdx:Math.max(0, visibleMessages.length-1),
553
+ anchorMessageKey:null,
554
+ });
555
+ renderMessages();
556
+ }
557
+ }finally{
558
+ if(S.session&&S.session.session_id===sid){
559
+ if(typeof _setCompressionSessionLock==='function') _setCompressionSessionLock(null);
560
+ if(typeof setBusy==='function') setBusy(false);
561
+ if(typeof setComposerStatus==='function') setComposerStatus('');
562
+ }
563
+ }
564
+ }
565
+
566
+ async function _runManualCompression(focusTopic){
567
+ if(!S.session){showToast(t('no_active_session'));return;}
568
+ let visibleCount=0;
569
+ try{
570
+ const sid=S.session.session_id;
571
+ // Preflight: verify the viewed session still exists before compressing.
572
+ // This avoids a confusing "not found" toast when the UI is stale.
573
+ try{
574
+ const live=await api(`/api/session?session_id=${encodeURIComponent(sid)}`);
575
+ if(!live||!live.session||live.session.session_id!==sid){
576
+ throw new Error('session no longer available');
577
+ }
578
+ S.session=live.session;
579
+ S.messages=live.session.messages||[];
580
+ S.toolCalls=live.session.tool_calls||[];
581
+ if(typeof _messagesTruncated!=='undefined') _messagesTruncated=false;
582
+ }catch(preflightErr){
583
+ if(typeof clearCompressionUi==='function') clearCompressionUi();
584
+ if(typeof _setCompressionSessionLock==='function') _setCompressionSessionLock(null);
585
+ if(typeof setBusy==='function') setBusy(false);
586
+ if(typeof setComposerStatus==='function') setComposerStatus('');
587
+ renderMessages();
588
+ showToast('Compression failed: '+(preflightErr.message||'session no longer available'));
589
+ return;
590
+ }
591
+ if(typeof setBusy==='function') setBusy(true);
592
+ const body={session_id:sid};
593
+ if(focusTopic) body.focus_topic=focusTopic;
594
+ const visibleMessages=_manualCompressionVisibleMessages();
595
+ visibleCount=visibleMessages.length;
596
+ const anchorVisibleIdx=Math.max(0, visibleCount - 1);
597
+ const anchorMessageKey=_compressionAnchorMessageKey(visibleMessages[visibleMessages.length-1]||null);
598
+ const commandText=focusTopic?`/compress ${focusTopic}`:'/compress';
599
+ if(typeof setCompressionUi==='function'){
600
+ setCompressionUi({
601
+ sessionId:S.session.session_id,
602
+ phase:'running',
603
+ focusTopic:focusTopic||'',
604
+ commandText,
605
+ beforeCount:visibleCount,
606
+ anchorVisibleIdx,
607
+ anchorMessageKey,
608
+ });
609
+ }
610
+ if(typeof setComposerStatus==='function') setComposerStatus(t('compressing'));
611
+ renderMessages();
612
+ const started=await api('/api/session/compress/start',{method:'POST',body:JSON.stringify(body)});
613
+ if(started&&started.status==='error'){
614
+ const err=new Error(started.error||'Compression failed');
615
+ err.status=started.error_status||400;
616
+ throw err;
617
+ }
618
+ const data=(started&&started.status==='done')?started:await _pollManualCompressionResult(sid);
619
+ await _applyManualCompressionResult(data, focusTopic, visibleCount, commandText);
620
+ }catch(e){
621
+ if(typeof setCompressionUi==='function'){
622
+ const currentSid=S.session&&S.session.session_id;
623
+ setCompressionUi({
624
+ sessionId:currentSid||'',
625
+ phase:'error',
626
+ focusTopic:(focusTopic||'').trim(),
627
+ commandText:focusTopic?`/compress ${focusTopic}`:'/compress',
628
+ beforeCount:(S.messages||[]).filter(m=>m&&m.role&&m.role!=='tool').length,
629
+ errorText:`Compression failed: ${e.message}`,
630
+ anchorVisibleIdx: Math.max(0, visibleCount - 1),
631
+ anchorMessageKey:null,
632
+ });
633
+ }
634
+ if(typeof _setCompressionSessionLock==='function') _setCompressionSessionLock(null);
635
+ if(typeof setBusy==='function') setBusy(false);
636
+ if(typeof setComposerStatus==='function') setComposerStatus('');
637
+ renderMessages();
638
+ showToast('Compression failed: '+e.message);
639
+ return;
640
+ }
641
+ if(typeof setBusy==='function') setBusy(false);
642
+ }
643
+
644
+ async function cmdCompress(args){
645
+ await _runManualCompression((args||'').trim());
646
+ }
647
+
648
+ async function cmdCompact(args){
649
+ await _runManualCompression((args||'').trim());
650
+ }
651
+
652
+ async function cmdUsage(){
653
+ const next=!window._showTokenUsage;
654
+ window._showTokenUsage=next;
655
+ try{
656
+ await api('/api/settings',{method:'POST',body:JSON.stringify({show_token_usage:next})});
657
+ }catch(e){}
658
+ // Update the settings checkbox if the panel is open
659
+ const cb=$('settingsShowTokenUsage');
660
+ if(cb) cb.checked=next;
661
+ renderMessages();
662
+ showToast(next?t('token_usage_on'):t('token_usage_off'));
663
+ }
664
+
665
+ async function cmdTheme(args){
666
+ const themes=['system','dark','light'];
667
+ const skins=(_SKINS||[]).map(s=>(s.value||s.name).toLowerCase());
668
+ const legacyThemes=Object.keys(_LEGACY_THEME_MAP||{});
669
+ const val=(args||'').toLowerCase().trim();
670
+ // Check if it's a theme
671
+ if(themes.includes(val)||legacyThemes.includes(val)){
672
+ const appearance=_normalizeAppearance(
673
+ val,
674
+ legacyThemes.includes(val)?null:localStorage.getItem('hermes-skin')
675
+ );
676
+ localStorage.setItem('hermes-theme',appearance.theme);
677
+ localStorage.setItem('hermes-skin',appearance.skin);
678
+ _applyTheme(appearance.theme);
679
+ _applySkin(appearance.skin);
680
+ try{await api('/api/settings',{method:'POST',body:JSON.stringify({theme:appearance.theme,skin:appearance.skin})});}catch(e){}
681
+ const sel=$('settingsTheme');
682
+ if(sel)sel.value=appearance.theme;
683
+ const skinSel=$('settingsSkin');
684
+ if(skinSel)skinSel.value=appearance.skin;
685
+ if(typeof _syncThemePicker==='function') _syncThemePicker(appearance.theme);
686
+ if(typeof _syncSkinPicker==='function') _syncSkinPicker(appearance.skin);
687
+ showToast(t('theme_set')+appearance.theme+(legacyThemes.includes(val)?` + ${appearance.skin}`:''));
688
+ return;
689
+ }
690
+ // Check if it's a skin
691
+ if(skins.includes(val)){
692
+ const appearance=_normalizeAppearance(localStorage.getItem('hermes-theme'),val);
693
+ localStorage.setItem('hermes-theme',appearance.theme);
694
+ localStorage.setItem('hermes-skin',appearance.skin);
695
+ _applyTheme(appearance.theme);
696
+ _applySkin(appearance.skin);
697
+ try{await api('/api/settings',{method:'POST',body:JSON.stringify({theme:appearance.theme,skin:appearance.skin})});}catch(e){}
698
+ const sel=$('settingsSkin');
699
+ if(sel)sel.value=appearance.skin;
700
+ const themeSel=$('settingsTheme');
701
+ if(themeSel)themeSel.value=appearance.theme;
702
+ if(typeof _syncThemePicker==='function') _syncThemePicker(appearance.theme);
703
+ if(typeof _syncSkinPicker==='function') _syncSkinPicker(appearance.skin);
704
+ showToast(t('theme_set')+appearance.skin);
705
+ return;
706
+ }
707
+ showToast(t('theme_usage')+themes.join('|')+' | '+skins.join('|')+' | legacy:'+legacyThemes.join('|'));
708
+ }
709
+
710
+ async function cmdSkills(args){
711
+ try{
712
+ const data = await api('/api/skills');
713
+ let skills = data.skills || [];
714
+ if(args){
715
+ const q = args.toLowerCase();
716
+ skills = skills.filter(s =>
717
+ (s.name||'').toLowerCase().includes(q) ||
718
+ (s.description||'').toLowerCase().includes(q) ||
719
+ (s.category||'').toLowerCase().includes(q)
720
+ );
721
+ }
722
+ if(!skills.length){
723
+ const msg = {role:'assistant', content: args ? `No skills matching "${args}".` : 'No skills found.'};
724
+ S.messages.push(msg); renderMessages(); return;
725
+ }
726
+ // Group by category
727
+ const byCategory = {};
728
+ skills.forEach(s => {
729
+ const cat = s.category || 'General';
730
+ if(!byCategory[cat]) byCategory[cat] = [];
731
+ byCategory[cat].push(s);
732
+ });
733
+ const lines = [];
734
+ for(const [cat, items] of Object.entries(byCategory).sort()){
735
+ lines.push(`**${cat}**`);
736
+ items.forEach(s => {
737
+ const desc = s.description ? ` — ${s.description.slice(0,80)}${s.description.length>80?'...':''}` : '';
738
+ lines.push(` \`${s.name}\`${desc}`);
739
+ });
740
+ lines.push('');
741
+ }
742
+ const header = args
743
+ ? `Skills matching "${args}" (${skills.length}):\n\n`
744
+ : `Available skills (${skills.length}):\n\n`;
745
+ S.messages.push({role:'assistant', content: header + lines.join('\n')});
746
+ renderMessages();
747
+ showToast(t('type_slash'));
748
+ }catch(e){
749
+ showToast('Failed to load skills: '+e.message);
750
+ }
751
+ }
752
+
753
+ async function cmdPersonality(args){
754
+ if(!S.session){showToast(t('no_active_session'));return;}
755
+ if(!args){
756
+ // List available personalities
757
+ try{
758
+ const data=await api('/api/personalities');
759
+ if(!data.personalities||!data.personalities.length){
760
+ showToast(t('no_personalities'));
761
+ return;
762
+ }
763
+ const list=data.personalities.map(p=>` **${p.name}**${p.description?' — '+p.description:''}`).join('\n');
764
+ S.messages.push({role:'assistant',content:t('available_personalities')+'\n\n'+list+t('personality_switch_hint')});
765
+ renderMessages();
766
+ }catch(e){showToast(t('personalities_load_failed'));}
767
+ return;
768
+ }
769
+ const name=args.trim();
770
+ if(name.toLowerCase()==='none'||name.toLowerCase()==='default'||name.toLowerCase()==='clear'){
771
+ try{
772
+ await api('/api/personality/set',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,name:''})});
773
+ showToast(t('personality_cleared'));
774
+ }catch(e){showToast(t('failed_colon')+e.message);}
775
+ return;
776
+ }
777
+ try{
778
+ const res=await api('/api/personality/set',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,name})});
779
+ S.messages.push({role:'assistant',content:t('personality_set')+`**${name}**`});
780
+ renderMessages();
781
+ showToast(t('personality_set')+name);
782
+ }catch(e){showToast(t('failed_colon')+e.message);}
783
+ }
784
+
785
+ async function cmdStop(){
786
+ if(!S.session){showToast(t('no_active_session'));return;}
787
+ if(!S.activeStreamId){showToast(t('no_active_task'));return;}
788
+ if(typeof cancelStream==='function'){await cancelStream();showToast(t('stream_stopped'));}
789
+ else showToast(t('cancel_unavailable'));
790
+ }
791
+
792
+ async function cmdGoal(args){
793
+ if(!S.session){await newSession();await renderSessionList();}
794
+ if(!S.session||!S.session.session_id){showToast(t('no_active_session'));return;}
795
+ const activeSid=S.session.session_id;
796
+ try{
797
+ const r=await api('/api/goal',{method:'POST',body:JSON.stringify({
798
+ session_id:activeSid,
799
+ args:args||'',
800
+ workspace:S.session.workspace,
801
+ model:S.session.model||($('modelSelect')&&$('modelSelect').value)||'',
802
+ model_provider:S.session.model_provider||null,
803
+ profile:S.activeProfile||S.session.profile||'default',
804
+ })});
805
+ const msg = (() => {
806
+ const raw = String((r && r.message) || '').trim();
807
+ const key = String((r && r.message_key) || '').trim();
808
+ const args = Array.isArray(r && r.message_args) ? r.message_args : [];
809
+ if (raw.includes('\n')) return raw;
810
+ if (key && typeof t === 'function') {
811
+ const translated = String(t(key, ...args));
812
+ if (translated && translated !== key) return translated;
813
+ }
814
+ return raw;
815
+ })();
816
+ if(msg){
817
+ S.messages.push({role:'assistant',content:msg,_ts:Date.now()/1000,_goalStatus:true,_transient:true});
818
+ renderMessages({preserveScroll:true});
819
+ showToast(msg.split('\n')[0],2600);
820
+ }
821
+ if(!r||!r.stream_id)return;
822
+ S.toolCalls=[];
823
+ if(typeof clearLiveToolCards==='function')clearLiveToolCards();
824
+ appendThinking();setBusy(true);
825
+ setComposerStatus(t('goal_working_toward'));
826
+ S.activeStreamId=r.stream_id;
827
+ if(S.session&&S.session.session_id===activeSid){
828
+ S.session.active_stream_id=r.stream_id;
829
+ if(typeof r.pending_started_at==='number')S.session.pending_started_at=r.pending_started_at;
830
+ if(r.effective_model)S.session.model=r.effective_model;
831
+ if(r.effective_model_provider)S.session.model_provider=r.effective_model_provider;
832
+ }
833
+ INFLIGHT[activeSid]={messages:[...S.messages],uploaded:[],toolCalls:[]};
834
+ if(typeof markInflight==='function')markInflight(activeSid,r.stream_id);
835
+ if(typeof saveInflightState==='function')saveInflightState(activeSid,{streamId:r.stream_id,messages:INFLIGHT[activeSid].messages,uploaded:[],toolCalls:[]});
836
+ startApprovalPolling(activeSid);
837
+ startClarifyPolling(activeSid);
838
+ if(typeof _fetchYoloState==='function')_fetchYoloState(activeSid);
839
+ attachLiveStream(activeSid,r.stream_id,[]);
840
+ if(typeof renderSessionList==='function')void renderSessionList();
841
+ }catch(e){
842
+ const err=String((e&&e.message)||e||'Goal command failed');
843
+ S.messages.push({role:'assistant',content:`**Goal command failed:** ${err}`,_ts:Date.now()/1000,_error:true});
844
+ renderMessages({preserveScroll:true});
845
+ showToast(err,3000);
846
+ }
847
+ }
848
+
849
+ // ── Busy-input mode commands ──────────────────────────────────────────────
850
+ // These commands let users override the default busy_input_mode setting for a
851
+ // specific message. They are only meaningful while the agent is running.
852
+
853
+ /**
854
+ * /queue <message> — Explicitly queue a message for the next turn.
855
+ * Works regardless of the busy_input_mode setting.
856
+ */
857
+ async function cmdQueue(args){
858
+ const msg=(args||'').trim();
859
+ if(!msg){showToast(t('cmd_queue_no_msg'));return;}
860
+ // If nothing is running, /queue <msg> just sends like a normal message
861
+ if(!S.busy){
862
+ const inp=$('msg');
863
+ if(inp){inp.value=msg;}
864
+ if(typeof send==='function'){await send();}
865
+ return;
866
+ }
867
+ if(!S.session){showToast(t('no_active_session'));return;}
868
+ queueSessionMessage(S.session.session_id,{text:msg,files:[...S.pendingFiles],model:S.session&&S.session.model||($('modelSelect')&&$('modelSelect').value)||'',profile:S.activeProfile||'default'});
869
+ updateQueueBadge(S.session.session_id);
870
+ S.pendingFiles=[];renderTray();
871
+ showToast(t('cmd_queue_confirm'),2000);
872
+ }
873
+
874
+ /**
875
+ * /interrupt <message> — Cancel the current turn and send a new message.
876
+ * Calls cancelStream() then queues the message so the drain picks it up.
877
+ */
878
+ async function cmdInterrupt(args){
879
+ const msg=(args||'').trim();
880
+ if(!msg){showToast(t('cmd_interrupt_no_msg'));return;}
881
+ // If nothing is running, /interrupt <msg> just sends like a normal message
882
+ if(!S.busy||!S.activeStreamId){
883
+ const inp=$('msg');
884
+ if(inp){inp.value=msg;}
885
+ if(typeof send==='function'){await send();}
886
+ return;
887
+ }
888
+ if(!S.session){showToast(t('no_active_session'));return;}
889
+ // Queue the message first (before cancel sets busy=false and drains)
890
+ queueSessionMessage(S.session.session_id,{text:msg,files:[...S.pendingFiles],model:S.session&&S.session.model||($('modelSelect')&&$('modelSelect').value)||'',profile:S.activeProfile||'default'});
891
+ updateQueueBadge(S.session.session_id);
892
+ S.pendingFiles=[];renderTray();
893
+ // Cancel the active stream; setBusy(false) will drain the queue
894
+ if(typeof cancelStream==='function'){await cancelStream();}
895
+ showToast(t('cmd_interrupt_confirm'),2000);
896
+ }
897
+
898
+ /**
899
+ * /steer <message> — Inject a steering hint mid-task without interrupting.
900
+ *
901
+ * Calls POST /api/chat/steer which looks up the cached AIAgent for this
902
+ * session and calls agent.steer(text). The agent's run loop appends the
903
+ * steer text to the next tool-result message so the model sees it on its
904
+ * next iteration — same pathway as the CLI's /steer command.
905
+ *
906
+ * Falls back to interrupt mode when the agent isn't running, isn't cached,
907
+ * or doesn't support steer (older hermes-agent versions).
908
+ */
909
+ async function cmdSteer(args){
910
+ const msg=(args||'').trim();
911
+ if(!msg){showToast(t('cmd_steer_no_msg'));return;}
912
+ // If nothing is running, /steer <msg> just sends like a normal message
913
+ if(!S.busy||!S.activeStreamId){
914
+ const inp=$('msg');
915
+ if(inp){inp.value=msg;}
916
+ if(typeof send==='function'){await send();}
917
+ return;
918
+ }
919
+ if(!S.session){showToast(t('no_active_session'));return;}
920
+ await _trySteer(msg, /*explicitSteer=*/true);
921
+ }
922
+
923
+ /**
924
+ * Shared implementation for /steer and the busy_input_mode='steer' path.
925
+ *
926
+ * Tries the real steer endpoint first. On any non-accept response (no cached
927
+ * agent, agent lacks steer, stream dead, etc.) falls back to interrupt+queue:
928
+ * queues the message and cancels the stream so the drain re-sends it.
929
+ *
930
+ * @param {string} msg - The steer text.
931
+ * @param {boolean} explicitSteer - True if the user explicitly invoked /steer
932
+ * (vs the busy-mode auto-fallback). Affects toast wording only.
933
+ */
934
+ function _showSteerIndicator(text){
935
+ const inner=document.getElementById('msgInner');
936
+ if(!inner) return;
937
+ // Remove any existing steer indicator
938
+ const old=inner.querySelector('.steer-indicator');
939
+ if(old) old.remove();
940
+ const el=document.createElement('div');
941
+ el.className='steer-indicator';
942
+ const badge=document.createElement('span');
943
+ badge.className='steer-badge';
944
+ badge.textContent='Steer';
945
+ const body=document.createElement('span');
946
+ body.className='steer-body';
947
+ body.textContent=text.length>120?text.slice(0,117)+'…':text;
948
+ el.appendChild(badge);
949
+ el.appendChild(body);
950
+ inner.appendChild(el);
951
+ if(typeof scrollToBottom==='function') scrollToBottom();
952
+ }
953
+
954
+ async function _trySteer(msg, explicitSteer){
955
+ let result=null;
956
+ try{
957
+ result=await api('/api/chat/steer',{
958
+ method:'POST',
959
+ body:JSON.stringify({session_id:S.session.session_id,text:msg}),
960
+ });
961
+ }catch(e){
962
+ // Network or server error — fall back to interrupt
963
+ result={accepted:false, fallback:'network_error'};
964
+ }
965
+ if(result&&result.accepted){
966
+ // Show a transient steer indicator in the chat (NOT in S.messages — it must
967
+ // survive the done event's S.messages=d.session.messages replacement).
968
+ // The indicator self-removes when the turn completes (done/cancel/error
969
+ // all call renderMessages which rebuilds msgInner).
970
+ _showSteerIndicator(msg);
971
+ showToast(t('cmd_steer_delivered'),2500);
972
+ return;
973
+ }
974
+ // Fall back to interrupt: queue the message + cancel the stream so the
975
+ // drain in setBusy(false) re-sends it as a fresh turn.
976
+ queueSessionMessage(S.session.session_id,{text:msg,files:[...S.pendingFiles],model:S.session&&S.session.model||($('modelSelect')&&$('modelSelect').value)||'',profile:S.activeProfile||'default'});
977
+ updateQueueBadge(S.session.session_id);
978
+ S.pendingFiles=[];renderTray();
979
+ if(typeof cancelStream==='function'){await cancelStream();}
980
+ // Toast wording differs based on why we're falling back so the user
981
+ // understands what just happened.
982
+ const reason=(result&&result.fallback)||'unknown';
983
+ if(explicitSteer){
984
+ showToast(t('cmd_steer_fallback'),2500);
985
+ } else if(reason==='no_cached_agent'||reason==='not_running'||reason==='stream_dead'){
986
+ // Busy mode hit the steer path before the agent was ready —
987
+ // interrupt is the natural fallback, no need to call out steer.
988
+ showToast(t('busy_interrupt_confirm'),2000);
989
+ } else {
990
+ showToast(t('busy_steer_fallback'),2500);
991
+ }
992
+ }
993
+
994
+ async function cmdTitle(args){
995
+ if(!S.session){showToast(t('no_active_session'));return;}
996
+ const name=(args||'').trim();
997
+ if(!name){
998
+ S.messages.push({role:'assistant',content:`${t('title_current')}: **${S.session.title||t('untitled')}**\n\n${t('title_change_hint')}`});
999
+ renderMessages();return;
1000
+ }
1001
+ try{
1002
+ const r=await api('/api/session/rename',{method:'POST',body:JSON.stringify({session_id:S.session.session_id,title:name})});
1003
+ if(r&&r.error){showToast(r.error);return;}
1004
+ S.session.title=(r&&r.session&&r.session.title)||name;
1005
+ if(typeof syncTopbar==='function')syncTopbar();
1006
+ if(typeof renderSessionList==='function')renderSessionList();
1007
+ showToast(`${t('title_set')} "${S.session.title}"`);
1008
+ S.messages.push({role:'assistant',content:`${t('title_set')} **${S.session.title}**`});
1009
+ renderMessages();
1010
+ }catch(e){showToast(t('failed_colon')+e.message);}
1011
+ }
1012
+ async function cmdRetry(){
1013
+ if(!S.session){showToast(t('no_active_session'));return;}
1014
+ if(S.session.is_cli_session){showToast(t('cmd_webui_only_session'));return;}
1015
+ const activeSid=S.session.session_id;
1016
+ try{
1017
+ const r=await api('/api/session/retry',{method:'POST',body:JSON.stringify({session_id:activeSid})});
1018
+ if(r&&r.error){showToast(r.error);return;}
1019
+ if(!S.session||S.session.session_id!==activeSid)return;
1020
+ const data=await api('/api/session?session_id='+encodeURIComponent(activeSid));
1021
+ if(data&&data.session){S.messages=data.session.messages||[];S.toolCalls=[];if(typeof clearLiveToolCards==='function')clearLiveToolCards();if(typeof _messagesTruncated!=='undefined')_messagesTruncated=false;renderMessages();}
1022
+ $('msg').value=r.last_user_text||'';if(typeof autoResize==='function')autoResize();await send();
1023
+ }catch(e){showToast(t('retry_failed')+e.message);}
1024
+ }
1025
+ async function cmdUndo(){
1026
+ if(!S.session){showToast(t('no_active_session'));return;}
1027
+ if(S.session.is_cli_session){showToast(t('cmd_webui_only_session'));return;}
1028
+ const activeSid=S.session.session_id;
1029
+ try{
1030
+ const r=await api('/api/session/undo',{method:'POST',body:JSON.stringify({session_id:activeSid})});
1031
+ if(r&&r.error){showToast(r.error);return;}
1032
+ if(!S.session||S.session.session_id!==activeSid)return;
1033
+ const data=await api('/api/session?session_id='+encodeURIComponent(activeSid));
1034
+ if(data&&data.session){S.messages=data.session.messages||[];S.toolCalls=[];if(typeof clearLiveToolCards==='function')clearLiveToolCards();if(typeof _messagesTruncated!=='undefined')_messagesTruncated=false;renderMessages();}
1035
+ showToast(`↩ ${t('undid_n_messages')} ${r.removed_count} ${t('undid_messages_suffix')}`);
1036
+ }catch(e){showToast(t('undo_failed')+e.message);}
1037
+ }
1038
+ async function undoLastExchange(){await cmdUndo();}
1039
+ async function cmdBtw(args){
1040
+ if(!S.session){showToast(t('no_active_session'));return;}
1041
+ const question=(args||'').trim();
1042
+ if(!question){showToast(t('cmd_btw_usage'));return;}
1043
+ showToast(t('btw_asking'));
1044
+ const activeSid=S.session.session_id;
1045
+ try{
1046
+ const r=await api('/api/btw',{method:'POST',body:JSON.stringify({session_id:activeSid,question})});
1047
+ if(r&&r.error){showToast(r.error);return;}
1048
+ // Connect to the ephemeral SSE stream
1049
+ const streamId=r.stream_id;
1050
+ const parentSid=r.parent_session_id;
1051
+ if(typeof attachBtwStream==='function') attachBtwStream(parentSid,streamId,question);
1052
+ }catch(e){showToast(t('btw_failed')+e.message);}
1053
+ }
1054
+ async function cmdBackground(args){
1055
+ if(!S.session){showToast(t('no_active_session'));return;}
1056
+ const prompt=(args||'').trim();
1057
+ if(!prompt){showToast(t('cmd_background_usage'));return;}
1058
+ showToast(t('bg_running'));
1059
+ const activeSid=S.session.session_id;
1060
+ try{
1061
+ const r=await api('/api/background',{method:'POST',body:JSON.stringify({session_id:activeSid,prompt})});
1062
+ if(r&&r.error){showToast(r.error);return;}
1063
+ // Show background badge and start polling
1064
+ if(typeof showBackgroundBadge==='function') showBackgroundBadge(r.task_id);
1065
+ if(typeof startBackgroundPolling==='function') startBackgroundPolling(activeSid,r.task_id,prompt);
1066
+ }catch(e){showToast(t('bg_failed')+e.message);}
1067
+ }
1068
+ function _formatStatusTimestamp(value){
1069
+ if(value===undefined||value===null||value==='') return t('status_unknown');
1070
+ let date;
1071
+ if(typeof value==='number') date=new Date(value < 1000000000000 ? value*1000 : value);
1072
+ else date=new Date(value);
1073
+ if(Number.isNaN(date.getTime())) return t('status_unknown');
1074
+ return date.toLocaleString();
1075
+ }
1076
+ function _formatStatusTokens(s){
1077
+ const lastUsage=(typeof S!=='undefined'&&(S.lastUsage||s.last_usage))||{};
1078
+ const input=Number(s.input_tokens??lastUsage.input_tokens??0)||0;
1079
+ const output=Number(s.output_tokens??lastUsage.output_tokens??0)||0;
1080
+ const total=Number(s.total_tokens??lastUsage.total_tokens??(input+output))||0;
1081
+ const cost=Number(s.estimated_cost??lastUsage.estimated_cost??0)||0;
1082
+ if(!total&&!cost) return t('status_no_tokens');
1083
+ const fmtNum=n=>Number(n||0).toLocaleString();
1084
+ return `${fmtNum(input)} in / ${fmtNum(output)} out${cost?` (~$${cost.toFixed(4)})`:''}`;
1085
+ }
1086
+ function _statusProviderForSession(s){
1087
+ if(s.model_provider) return String(s.model_provider);
1088
+ if(window._activeProvider) return String(window._activeProvider);
1089
+ const model=String(s.model||'');
1090
+ return model.includes('/') ? model.split('/')[0] : '';
1091
+ }
1092
+ function _statusCardFromSession(s){
1093
+ const provider=_statusProviderForSession(s);
1094
+ const model=s.model||(($('modelSelect')&&$('modelSelect').value)||t('usage_default_model'));
1095
+ const running=!!(s.active_stream_id||S.activeStreamId||S.busy);
1096
+ const profile=s.profile||S.activeProfile||'default';
1097
+ const workspace=s.workspace||S.currentDir||t('status_unknown');
1098
+ const rows=[
1099
+ {label:t('status_session_id'), value:s.session_id||t('status_unknown')},
1100
+ {label:t('status_title'), value:s.title||t('untitled')},
1101
+ {label:t('status_model'), value:model},
1102
+ {label:t('status_provider'), value:provider||t('status_unknown')},
1103
+ {label:t('status_profile'), value:profile},
1104
+ {label:t('status_workspace'), value:workspace},
1105
+ {label:t('status_personality'), value:s.personality||t('usage_personality_none')},
1106
+ {label:t('status_started'), value:_formatStatusTimestamp(s.created_at)},
1107
+ {label:t('status_updated'), value:_formatStatusTimestamp(s.updated_at||s.last_message_at)},
1108
+ {label:t('status_tokens'), value:_formatStatusTokens(s)},
1109
+ {label:t('status_messages'), value:String(s.message_count??(S.messages||[]).filter(m=>m&&m.role&&m.role!=='tool').length)},
1110
+ {label:t('status_agent_running'), value:running?t('status_yes'):t('status_no')},
1111
+ ];
1112
+ return {
1113
+ title:t('status_heading'),
1114
+ subtitle:t('status_ephemeral'),
1115
+ sessionId:s.session_id||'',
1116
+ rows,
1117
+ };
1118
+ }
1119
+ function cmdStatus(){
1120
+ if(!S.session){showToast(t('no_active_session'));return;}
1121
+ S.messages.push({
1122
+ role:'assistant',
1123
+ content:'',
1124
+ _ephemeral:true,
1125
+ _statusCard:_statusCardFromSession(S.session),
1126
+ _ts:Date.now()/1000,
1127
+ });
1128
+ renderMessages();
1129
+ }
1130
+ function cmdReasoning(args){
1131
+ const arg=(args||'').trim().toLowerCase();
1132
+ const BRAIN='\uD83E\uDDE0';
1133
+ // Matches hermes_constants.VALID_REASONING_EFFORTS + 'none' (CLI parity).
1134
+ const EFFORTS=['none','minimal','low','medium','high','xhigh','max'];
1135
+ // Shared status renderer used by the no-args branch and as a fallback.
1136
+ function _fmtStatus(st){
1137
+ const vis=(st && st.show_reasoning===false)?'off':'on';
1138
+ const eff=(st && st.reasoning_effort)||'default';
1139
+ return BRAIN+' Reasoning effort: '+eff+' \u00B7 display: '+vis
1140
+ +' | /reasoning show|hide|none|minimal|low|medium|high|xhigh|max';
1141
+ }
1142
+ if(!arg){
1143
+ // Status — read from the same config.yaml keys the CLI uses.
1144
+ const q=(typeof _reasoningEffortQuery==='function')?_reasoningEffortQuery():'';
1145
+ api('/api/reasoning'+q).then(function(st){showToast(_fmtStatus(st));})
1146
+ .catch(function(){showToast(BRAIN+' /reasoning — status unavailable');});
1147
+ return true;
1148
+ }
1149
+ if(arg==='show'||arg==='on'||arg==='hide'||arg==='off'){
1150
+ const on=(arg==='show'||arg==='on');
1151
+ // Update the UI render gate immediately for responsiveness.
1152
+ window._showThinking=on;
1153
+ if(typeof renderMessages==='function') renderMessages();
1154
+ // Persist via /api/reasoning → config.yaml display.show_reasoning
1155
+ // (CLI reads the same key). Also mirror into WebUI settings.json
1156
+ // show_thinking so boot.js picks it up on reload without hitting
1157
+ // /api/reasoning on every page load.
1158
+ api('/api/reasoning',{method:'POST',body:JSON.stringify({display:arg})}).catch(function(){});
1159
+ api('/api/settings',{method:'POST',body:JSON.stringify({show_thinking:on})}).catch(function(){});
1160
+ showToast(BRAIN+' Thinking blocks: '+(on?'on':'off')+' (saved)');
1161
+ return true;
1162
+ }
1163
+ if(EFFORTS.includes(arg)){
1164
+ // Persist via /api/reasoning → config.yaml agent.reasoning_effort.
1165
+ // Takes effect on the NEXT session/turn (agent re-reads config at
1166
+ // construction time), matching CLI semantics where `/reasoning high`
1167
+ // also forces an agent re-init.
1168
+ api('/api/reasoning',{method:'POST',body:JSON.stringify({effort:arg})})
1169
+ .then(function(st){
1170
+ const eff=(st && st.reasoning_effort)||arg;
1171
+ showToast(BRAIN+' Reasoning effort: '+eff+' (saved; applies to next turn)');
1172
+ if(typeof _applyReasoningChip==='function') _applyReasoningChip(eff, st||{});
1173
+ })
1174
+ .catch(function(e){
1175
+ showToast(BRAIN+' Failed to set effort: '+(e && e.message ? e.message : arg));
1176
+ });
1177
+ return true;
1178
+ }
1179
+ showToast('Unknown argument: '+arg+' \u2014 use show|hide|'+EFFORTS.join('|'));
1180
+ return true;
1181
+ }
1182
+ function cmdVoice(){
1183
+ const mic=document.getElementById('btnMic');
1184
+ if(mic&&mic.style.display!=='none'&&!mic.disabled){try{mic.click();return;}catch(_){}}
1185
+ showToast(t('cmd_voice_use_mic'));
1186
+ }
1187
+
1188
+ // ── YOLO mode toggle ──
1189
+ // Session-scoped: skips all approval prompts for the current session.
1190
+ // Toggles on/off; state is not persisted across page reloads.
1191
+ async function cmdYolo(){
1192
+ const sid=S.session&&S.session.session_id;
1193
+ if(!sid){showToast(t('yolo_no_session'));return;}
1194
+ try{
1195
+ // Check current state first to toggle
1196
+ const status=await api('/api/session/yolo?session_id='+encodeURIComponent(sid));
1197
+ const enable=!status.yolo_enabled;
1198
+ await api('/api/session/yolo',{
1199
+ method:'POST',
1200
+ body:JSON.stringify({session_id:sid,enabled:enable}),
1201
+ });
1202
+ _yoloEnabled=enable;
1203
+ _updateYoloPill();
1204
+ showToast(enable?t('yolo_enabled'):t('yolo_disabled'));
1205
+ if(enable){
1206
+ // Dismiss any visible approval card
1207
+ hideApprovalCard(true);
1208
+ }
1209
+ }catch(e){showToast('YOLO: '+e.message);}
1210
+ }
1211
+
1212
+ // ── Branch / fork command ──
1213
+ // Forks the current conversation into a new session (#465).
1214
+ // /branch → full history copy
1215
+ // /branch My Name → full history copy with custom title
1216
+ async function cmdBranch(args){
1217
+ if(!S.session){showToast(t('no_active_session'));return;}
1218
+ const customTitle=(args||'').trim()||null;
1219
+ try{
1220
+ const data=await api('/api/session/branch',{
1221
+ method:'POST',
1222
+ body:JSON.stringify({
1223
+ session_id:S.session.session_id,
1224
+ title:customTitle||undefined,
1225
+ }),
1226
+ });
1227
+ if(data&&data.session_id){
1228
+ await loadSession(data.session_id);
1229
+ if(typeof renderSessionList==='function') await renderSessionList();
1230
+ showToast(t('branch_forked'));
1231
+ }
1232
+ }catch(e){showToast(t('branch_failed')+e.message);}
1233
+ }
1234
+
1235
+ // ── Fork from a specific message point ──
1236
+ // Called from the "Fork from here" button on message hover actions.
1237
+ // msgIdx is 1-based within the currently loaded tail window (rawIdx+1).
1238
+ // When the session is truncated (_oldestIdx > 0), msgIdx alone would be
1239
+ // a local-window count, but the backend expects an absolute message count
1240
+ // from the beginning of the full transcript. We capture the absolute
1241
+ // count (_oldestIdx + msgIdx) BEFORE awaiting _ensureAllMessagesLoaded,
1242
+ // which resets _oldestIdx to 0 after its wholesale replace. See #2184.
1243
+ async function forkFromMessage(msgIdx){
1244
+ if(!S.session||S.busy)return;
1245
+ const initialSid = S.session.session_id;
1246
+ // Capture the absolute keep_count before any async work that may
1247
+ // reset _oldestIdx. _oldestIdx is 0 when the full transcript is
1248
+ // already loaded, so short/already-full sessions send msgIdx unchanged.
1249
+ const absoluteKeepCount = _oldestIdx + msgIdx;
1250
+ // Ensure the full transcript is loaded so the forked session renders
1251
+ // correctly and subsequent operations see the complete history.
1252
+ if(typeof _ensureAllMessagesLoaded==='function'){
1253
+ await _ensureAllMessagesLoaded();
1254
+ }
1255
+ if(!S.session || S.session.session_id !== initialSid) return;
1256
+ try{
1257
+ const data=await api('/api/session/branch',{
1258
+ method:'POST',
1259
+ body:JSON.stringify({
1260
+ session_id:initialSid,
1261
+ keep_count:absoluteKeepCount,
1262
+ }),
1263
+ });
1264
+ if(data&&data.session_id){
1265
+ await loadSession(data.session_id);
1266
+ if(typeof _ensureAllMessagesLoaded==='function') await _ensureAllMessagesLoaded();
1267
+ if(typeof renderSessionList==='function') await renderSessionList();
1268
+ showToast(t('branch_forked'));
1269
+ }
1270
+ }catch(e){showToast(t('branch_failed')+e.message);}
1271
+ }
1272
+
1273
+ let _skillCommandCache=[];
1274
+ let _skillCommandLoadPromise=null;
1275
+ let _skillCommandCacheReady=false;
1276
+ function _skillCommandSlug(name){
1277
+ const raw=String(name||'').trim().toLowerCase();
1278
+ if(!raw)return'';
1279
+ return raw.replace(/[\s_]+/g,'-').replace(/[^a-z0-9-]/g,'').replace(/-{2,}/g,'-').replace(/^-+|-+$/g,'');
1280
+ }
1281
+ function _buildSkillCommandEntry(skill){
1282
+ const skillName=String(skill&&skill.name||'').trim();
1283
+ const slug=_skillCommandSlug(skillName);
1284
+ if(!slug)return null;
1285
+ if(COMMANDS.some(c=>c.name===slug)) return null;
1286
+ return{name:slug,desc:String(skill&&skill.description||'').trim()||t('slash_skill_desc'),source:'skill',skillName};
1287
+ }
1288
+ async function loadSkillCommands(force=false){
1289
+ if(_skillCommandCacheReady&&!force)return _skillCommandCache;
1290
+ if(_skillCommandLoadPromise&&!force)return _skillCommandLoadPromise;
1291
+ _skillCommandLoadPromise=(async()=>{
1292
+ try{
1293
+ const data=await api('/api/skills');
1294
+ const deduped=new Map();
1295
+ for(const skill of (data&&data.skills)||[]){const entry=_buildSkillCommandEntry(skill);if(entry&&!deduped.has(entry.name))deduped.set(entry.name,entry);}
1296
+ _skillCommandCache=Array.from(deduped.values()).sort((a,b)=>a.name.localeCompare(b.name));
1297
+ }catch(_){_skillCommandCache=[];}
1298
+ finally{_skillCommandCacheReady=true;_skillCommandLoadPromise=null;}
1299
+ return _skillCommandCache;
1300
+ })();
1301
+ return _skillCommandLoadPromise;
1302
+ }
1303
+ function refreshSlashCommandDropdown(){
1304
+ const ta=$('msg');if(!ta)return;
1305
+ const text=ta.value||'';
1306
+ if(!text.startsWith('/')||text.indexOf('\n')!==-1){hideCmdDropdown();return;}
1307
+ getSlashAutocompleteMatches(text).then(matches=>{
1308
+ if(($('msg').value||'')!==text) return;
1309
+ if(matches.length)showCmdDropdown(matches);else hideCmdDropdown();
1310
+ });
1311
+ }
1312
+ function ensureSkillCommandsLoadedForAutocomplete(){
1313
+ if(_skillCommandCacheReady||_skillCommandLoadPromise)return;
1314
+ loadSkillCommands().then(()=>{refreshSlashCommandDropdown();});
1315
+ // Also preload agent/plugin command metadata for autocomplete
1316
+ if(!_agentCommandCacheReady&&!_agentCommandCachePromise){
1317
+ loadAgentCommandMetadata().then(()=>{refreshSlashCommandDropdown();});
1318
+ }
1319
+ }
1320
+
1321
+ // ── Autocomplete dropdown ───────────────────────────────────────────────────
1322
+
1323
+ let _cmdSelectedIdx=-1;
1324
+
1325
+ function showCmdDropdown(matches){
1326
+ const dd=$('cmdDropdown');
1327
+ if(!dd)return;
1328
+ dd.innerHTML='';
1329
+ _cmdSelectedIdx=matches.length?0:-1;
1330
+ for(let i=0;i<matches.length;i++){
1331
+ const c=matches[i];
1332
+ const el=document.createElement('div');
1333
+ el.className='cmd-item';
1334
+ if(i===_cmdSelectedIdx) el.classList.add('selected');
1335
+ el.dataset.idx=i;
1336
+ const isSubArg=c.source==='subarg';
1337
+ const usage=(!isSubArg&&c.arg)?` <span class="cmd-item-arg">${esc(c.arg)}</span>`:'';
1338
+ const badge=c.source==='skill'?`<span class="cmd-item-badge cmd-item-badge-skill">${esc(t('slash_skill_badge'))}</span>`:'';
1339
+ if(c.source==='skill') el.classList.add('cmd-item-skill');
1340
+ const nameHtml=isSubArg
1341
+ ? `<div class="cmd-item-name"><span class="cmd-item-parent">/${esc(c.parent)}</span> <span class="cmd-item-subarg">${esc(c.value)}</span></div>`
1342
+ : `<div class="cmd-item-name">/${esc(c.name)}${usage}${badge}</div>`;
1343
+ const descHtml=`<div class="cmd-item-desc">${esc(c.desc)}</div>`;
1344
+ el.innerHTML=`${nameHtml}${descHtml}`;
1345
+ el.onmousedown=(e)=>{
1346
+ e.preventDefault();
1347
+ const nextValue=isSubArg?('/'+c.parent+' '+c.value):('/'+c.name+(c.arg?' ':''));
1348
+ $('msg').value=nextValue;
1349
+ $('msg').focus();
1350
+ if(!isSubArg&&c.source!=='skill'&&nextValue.endsWith(' ')&&typeof getSlashAutocompleteMatches==='function'){
1351
+ getSlashAutocompleteMatches(nextValue).then(matches=>{
1352
+ if(($('msg').value||'')!==nextValue) return;
1353
+ if(matches.length) showCmdDropdown(matches);
1354
+ else hideCmdDropdown();
1355
+ });
1356
+ }else{
1357
+ hideCmdDropdown();
1358
+ }
1359
+ };
1360
+ dd.appendChild(el);
1361
+ }
1362
+ dd.classList.add('open');
1363
+ }
1364
+
1365
+ function hideCmdDropdown(){
1366
+ const dd=$('cmdDropdown');
1367
+ if(dd)dd.classList.remove('open');
1368
+ _cmdSelectedIdx=-1;
1369
+ }
1370
+
1371
+ function navigateCmdDropdown(dir){
1372
+ const dd=$('cmdDropdown');
1373
+ if(!dd)return;
1374
+ const items=dd.querySelectorAll('.cmd-item');
1375
+ if(!items.length)return;
1376
+ items.forEach(el=>el.classList.remove('selected'));
1377
+ _cmdSelectedIdx+=dir;
1378
+ if(_cmdSelectedIdx<0)_cmdSelectedIdx=items.length-1;
1379
+ if(_cmdSelectedIdx>=items.length)_cmdSelectedIdx=0;
1380
+ items[_cmdSelectedIdx].classList.add('selected');
1381
+ // Scroll the newly highlighted item into view so it stays visible when the
1382
+ // dropdown overflows and the user navigates with keyboard (#838).
1383
+ items[_cmdSelectedIdx].scrollIntoView({block:'nearest'});
1384
+ }
1385
+
1386
+ function selectCmdDropdownItem(){
1387
+ const dd=$('cmdDropdown');
1388
+ if(!dd)return;
1389
+ const items=dd.querySelectorAll('.cmd-item');
1390
+ if(_cmdSelectedIdx>=0&&_cmdSelectedIdx<items.length){
1391
+ items[_cmdSelectedIdx].onmousedown({preventDefault:()=>{}});
1392
+ } else if(items.length===1){
1393
+ items[0].onmousedown({preventDefault:()=>{}});
1394
+ }
1395
+ hideCmdDropdown();
1396
+ }
1397
+
1398
+ // ── Handler aliases (for test-discoverable command registration) ──────────────
1399
+ // The COMMANDS array above is the authoritative dispatch table. These aliases
1400
+ // allow tooling and tests to discover command handlers by name independently.
1401
+ const HANDLERS = {};
1402
+ HANDLERS.skills = cmdSkills;