@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,800 @@
1
+ const ONBOARDING={status:null,step:0,steps:['system','setup','workspace','password','finish'],form:{provider:'openrouter',workspace:'',model:'',password:'',apiKey:'',baseUrl:''},active:false,probe:{status:'idle',error:null,detail:'',models:null,probedKey:''}};
2
+
3
+ // ── Onboarding base-URL probe (#1499) ───────────────────────────────────────
4
+ // Probes <base_url>/models so the wizard can validate the configured endpoint
5
+ // before persisting AND populate the model dropdown from the live catalog.
6
+ // Probe state lives on ONBOARDING.probe; the dropdown render and the
7
+ // nextOnboardingStep gate both consult it.
8
+
9
+ let _onboardingProbeTimer=null;
10
+
11
+ function _onboardingProbeKey(provider,baseUrl,apiKey){
12
+ return `${provider||''}|${(baseUrl||'').trim().replace(/\/+$/,'')}|${apiKey||''}`;
13
+ }
14
+
15
+ function _setOnboardingProbeState(patch){
16
+ ONBOARDING.probe={...ONBOARDING.probe,...patch};
17
+ // Re-render body so probe status / model dropdown reflect new state.
18
+ _renderOnboardingBody();
19
+ }
20
+
21
+ async function _runOnboardingProbe({force=false}={}){
22
+ const provider=ONBOARDING.form.provider;
23
+ const cat=_getOnboardingSetupProvider(provider);
24
+ if(!cat||!cat.requires_base_url){
25
+ _setOnboardingProbeState({status:'idle',error:null,detail:'',models:null,probedKey:''});
26
+ return ONBOARDING.probe;
27
+ }
28
+ const baseUrl=(ONBOARDING.form.baseUrl||'').trim();
29
+ if(!baseUrl){
30
+ _setOnboardingProbeState({status:'idle',error:null,detail:'',models:null,probedKey:''});
31
+ return ONBOARDING.probe;
32
+ }
33
+ const apiKey=(ONBOARDING.form.apiKey||'').trim();
34
+ const key=_onboardingProbeKey(provider,baseUrl,apiKey);
35
+ if(!force&&ONBOARDING.probe.probedKey===key&&ONBOARDING.probe.status!=='probing'){
36
+ return ONBOARDING.probe;
37
+ }
38
+ _setOnboardingProbeState({status:'probing',error:null,detail:'',probedKey:key});
39
+ try{
40
+ const res=await api('/api/onboarding/probe',{method:'POST',body:JSON.stringify({provider,base_url:baseUrl,api_key:apiKey||undefined})});
41
+ if(res&&res.ok){
42
+ _setOnboardingProbeState({status:'ok',error:null,detail:'',models:Array.isArray(res.models)?res.models:[],probedKey:key});
43
+ // If the user hasn't picked a model yet (or their pick is no longer in
44
+ // the list), default to the first probed model so Continue isn't blocked
45
+ // on an empty selection.
46
+ const stillPresent=ONBOARDING.form.model&&(res.models||[]).some(m=>m.id===ONBOARDING.form.model);
47
+ if(!stillPresent&&(res.models||[]).length>0){
48
+ ONBOARDING.form.model=res.models[0].id;
49
+ _renderOnboardingBody();
50
+ }
51
+ }else{
52
+ const err=(res&&res.error)||'unreachable';
53
+ const detail=(res&&res.detail)||'';
54
+ _setOnboardingProbeState({status:'error',error:err,detail,models:null,probedKey:key});
55
+ }
56
+ }catch(e){
57
+ _setOnboardingProbeState({status:'error',error:'unreachable',detail:(e&&e.message)||String(e),models:null,probedKey:key});
58
+ }
59
+ return ONBOARDING.probe;
60
+ }
61
+
62
+ function _scheduleOnboardingProbe(){
63
+ if(_onboardingProbeTimer)clearTimeout(_onboardingProbeTimer);
64
+ _onboardingProbeTimer=setTimeout(()=>{_runOnboardingProbe();},400);
65
+ }
66
+
67
+ function _onboardingProbeMessage(probe){
68
+ if(!probe||probe.status==='idle')return '';
69
+ if(probe.status==='probing')return t('onboarding_probe_probing')||'Testing connection…';
70
+ if(probe.status==='ok'){
71
+ const n=(probe.models||[]).length;
72
+ const tmpl=t('onboarding_probe_ok')||'Connected. {n} model(s) available.';
73
+ return tmpl.replace('{n}',String(n));
74
+ }
75
+ // status === 'error'
76
+ const errKey='onboarding_probe_error_'+probe.error;
77
+ const localized=t(errKey);
78
+ // i18n.js's `t()` returns the key itself when missing — fall back to a generic message.
79
+ const heading=(localized&&localized!==errKey)?localized:(t('onboarding_probe_error_generic')||'Could not reach the configured base URL.');
80
+ const detail=probe.detail?` (${probe.detail})`:'';
81
+ return heading+detail;
82
+ }
83
+
84
+ function _getOnboardingSetupProviders(){
85
+ return (((ONBOARDING.status||{}).setup||{}).providers)||[];
86
+ }
87
+
88
+ function _getOnboardingSetupProvider(id){
89
+ return _getOnboardingSetupProviders().find(p=>p.id===id)||null;
90
+ }
91
+
92
+ function _getOnboardingSetupCategories(){
93
+ return (((ONBOARDING.status||{}).setup||{}).categories)||[];
94
+ }
95
+
96
+ /** Render the provider <select> with <optgroup> per category. */
97
+ function _renderProviderSelectOptions(selectedId){
98
+ const providers=_getOnboardingSetupProviders();
99
+ const categories=_getOnboardingSetupCategories();
100
+ const provMap={};
101
+ providers.forEach(p=>{provMap[p.id]=p;});
102
+ if(!categories.length){
103
+ // Fallback: flat list when no categories are available.
104
+ return providers.map(p=>`<option value="${esc(p.id)}">${esc(p.label)}${p.quick?' — '+esc(t('onboarding_quick_setup_badge')):''}</option>`).join('');
105
+ }
106
+ return categories.map(cat=>{
107
+ const opts=cat.providers.map(pid=>{
108
+ const p=provMap[pid];
109
+ if(!p)return '';
110
+ return `<option value="${esc(p.id)}"${p.id===selectedId?' selected':''}>${esc(p.label)}${p.quick?' — '+esc(t('onboarding_quick_setup_badge')):''}</option>`;
111
+ }).join('');
112
+ return `<optgroup label="${esc(t('provider_category_'+cat.id)||cat.label)}">${opts}</optgroup>`;
113
+ }).join('');
114
+ }
115
+
116
+ function _getOnboardingCurrentSetup(){
117
+ return (((ONBOARDING.status||{}).setup||{}).current)||{};
118
+ }
119
+
120
+ function _onboardingStepMeta(key){
121
+ return ({
122
+ system:{title:t('onboarding_step_system_title'),desc:t('onboarding_step_system_desc')},
123
+ setup:{title:t('onboarding_step_setup_title'),desc:t('onboarding_step_setup_desc')},
124
+ workspace:{title:t('onboarding_step_workspace_title'),desc:t('onboarding_step_workspace_desc')},
125
+ password:{title:t('onboarding_step_password_title'),desc:t('onboarding_step_password_desc')},
126
+ finish:{title:t('onboarding_step_finish_title'),desc:t('onboarding_step_finish_desc')}
127
+ })[key];
128
+ }
129
+
130
+ function _renderOnboardingSteps(){
131
+ const wrap=$('onboardingSteps');
132
+ if(!wrap)return;
133
+ wrap.innerHTML='';
134
+ ONBOARDING.steps.forEach((key,idx)=>{
135
+ const meta=_onboardingStepMeta(key);
136
+ const item=document.createElement('div');
137
+ item.className='onboarding-step'+(idx===ONBOARDING.step?' active':idx<ONBOARDING.step?' done':'');
138
+ item.innerHTML=`<div class="onboarding-step-index">${idx+1}</div><div><div class="onboarding-step-title">${meta.title}</div><div class="onboarding-step-desc">${meta.desc}</div></div>`;
139
+ wrap.appendChild(item);
140
+ });
141
+ }
142
+
143
+ function _setOnboardingNotice(msg,kind='info'){
144
+ const el=$('onboardingNotice');
145
+ if(!el)return;
146
+ if(!msg){el.style.display='none';el.textContent='';el.className='onboarding-status';return;}
147
+ el.style.display='block';
148
+ el.className='onboarding-status '+kind;
149
+ el.textContent=msg;
150
+ }
151
+
152
+ function _getOnboardingWorkspaceChoices(){
153
+ const items=((ONBOARDING.status||{}).workspaces||{}).items||[];
154
+ return items.length?items:[{name:'Home',path:ONBOARDING.form.workspace||''}];
155
+ }
156
+
157
+ function _getOnboardingProviderModelChoices(){
158
+ const provider=_getOnboardingSetupProvider(ONBOARDING.form.provider);
159
+ // Probe-discovered models (#1499) take precedence over the static catalog
160
+ // for providers with requires_base_url=True. The catalog ships an empty
161
+ // list for self-hosted providers (lmstudio, ollama, custom) — without the
162
+ // probe the user had nothing to pick from.
163
+ if(provider&&provider.requires_base_url&&ONBOARDING.probe&&ONBOARDING.probe.status==='ok'&&Array.isArray(ONBOARDING.probe.models)&&ONBOARDING.probe.models.length){
164
+ return ONBOARDING.probe.models;
165
+ }
166
+ return provider?(provider.models||[]):[];
167
+ }
168
+
169
+ function _renderOnboardingBaseUrlField(showBaseUrl){
170
+ // Renders the base_url input PLUS the probe status banner / Test button
171
+ // when the active provider has requires_base_url=True (#1499). Returns
172
+ // the empty string when the active provider does not require a base URL,
173
+ // so the existing call sites can continue to template-interpolate this in
174
+ // place of the previous inline `<label …>` snippet.
175
+ if(!showBaseUrl)return '';
176
+ const probe=ONBOARDING.probe||{status:'idle'};
177
+ const msg=_onboardingProbeMessage(probe);
178
+ let banner='';
179
+ if(msg){
180
+ const cls={ok:'onboarding-probe-ok',probing:'onboarding-probe-probing',error:'onboarding-probe-error'}[probe.status]||'';
181
+ banner=`<p class="onboarding-copy onboarding-probe-banner ${cls}">${esc(msg)}</p>`;
182
+ }
183
+ const testBtnLabel=t('onboarding_probe_test_button')||'Test connection';
184
+ const testBtnDisabled=(probe.status==='probing')?'disabled':'';
185
+ return `<label class="onboarding-field"><span>${t('onboarding_base_url_label')}</span><input id="onboardingBaseUrlInput" value="${esc(ONBOARDING.form.baseUrl||'')}" placeholder="${t('onboarding_base_url_placeholder')}" oninput="ONBOARDING.form.baseUrl=this.value;_scheduleOnboardingProbe()" onblur="_runOnboardingProbe()"></label><div class="onboarding-probe-row"><button type="button" class="onboarding-probe-btn" ${testBtnDisabled} onclick="_runOnboardingProbe({force:true})">${esc(testBtnLabel)}</button></div>${banner}`;
186
+ }
187
+
188
+ function _renderOnboardingApiKeyField(){
189
+ // Renders the API-key input. For providers flagged `key_optional` in the
190
+ // setup catalog (lmstudio, ollama, custom — typically self-hosted servers
191
+ // that run keyless by default), the field shows an "(optional)" hint and
192
+ // empty input is accepted on Continue. Pre-#1499-third-sub-bug-fix the
193
+ // wizard required a non-empty string here even for keyless installs, which
194
+ // forced users to type random gibberish to clear onboarding.
195
+ const provider=_getOnboardingSetupProvider(ONBOARDING.form.provider);
196
+ const keyOptional=!!(provider&&provider.key_optional);
197
+ const labelKey=keyOptional?'onboarding_api_key_label_optional':'onboarding_api_key_label';
198
+ const placeholderKey=keyOptional?'onboarding_api_key_placeholder_optional':'onboarding_api_key_placeholder';
199
+ const helpHtml=keyOptional?`<p class="onboarding-copy onboarding-api-key-help">${esc(t('onboarding_api_key_help_keyless')||'')}</p>`:'';
200
+ return `<label class="onboarding-field" id="onboardingApiKeyField"><span>${t(labelKey)}</span><input id="onboardingApiKeyInput" type="password" value="${esc(ONBOARDING.form.apiKey||'')}" placeholder="${t(placeholderKey)}" oninput="ONBOARDING.form.apiKey=this.value" onblur="_runOnboardingProbe()"></label>${helpHtml}`;
201
+ }
202
+
203
+ function _getOnboardingSelectedModel(){
204
+ return ONBOARDING.form.model||'';
205
+ }
206
+
207
+ function _renderOnboardingModelField(){
208
+ const choices=_getOnboardingProviderModelChoices();
209
+ if(ONBOARDING.form.provider==='custom'){
210
+ return `<label class="onboarding-field"><span>${t('onboarding_model_label')}</span><input id="onboardingModelInput" value="${esc(_getOnboardingSelectedModel())}" placeholder="${t('onboarding_custom_model_placeholder')}" oninput="ONBOARDING.form.model=this.value"></label><p class="onboarding-copy">${t('onboarding_custom_model_help')}</p>`;
211
+ }
212
+ const options=choices.map(m=>`<option value="${esc(m.id)}">${esc(m.label)}</option>`).join('');
213
+ return `<label class="onboarding-field"><span>${t('onboarding_model_label')}</span><select id="onboardingModelSelect" onchange="ONBOARDING.form.model=this.value">${options}</select></label><p class="onboarding-copy">${t('onboarding_workspace_help')}</p>`;
214
+ }
215
+
216
+ function _renderOnboardingProviderOAuthField(provider){
217
+ if(!provider||provider.oauth_provider!=='anthropic')return '';
218
+ return `<div class="onboarding-oauth-card onboarding-oauth-pending" style="margin-top:12px">
219
+ <div class="onboarding-oauth-icon">🔑</div>
220
+ <div style="flex:1">
221
+ <strong>Use Claude Code OAuth instead</strong>
222
+ <p style="margin-top:6px;color:var(--muted);font-size:13px"><strong>Claude Code subscription credentials are not the same as an Anthropic API key.</strong> Use this path only when you want Hermes to use Claude Code credentials already available on the server, or start a short polling flow while you complete <code>claude setup-token</code> on the host.</p>
223
+ <div style="margin-top:10px;display:flex;gap:8px;align-items:center;flex-wrap:wrap"><button class="sm-btn" id="anthropicOAuthBtn" onclick="startAnthropicOAuth()" type="button">Login with Claude Code</button></div>
224
+ <div id="anthropicOAuthFlow" style="display:none;margin-top:12px"></div>
225
+ </div>
226
+ </div>`;
227
+ }
228
+
229
+ function _providerStatusLabel(system){
230
+ if(system.chat_ready) return t('onboarding_check_provider_ready');
231
+ if(system.provider_configured) return t('onboarding_check_provider_partial');
232
+ return t('onboarding_check_provider_pending');
233
+ }
234
+
235
+ function _renderOnboardingBody(){
236
+ const body=$('onboardingBody');
237
+ if(!body||!ONBOARDING.status)return;
238
+ const key=ONBOARDING.steps[ONBOARDING.step];
239
+ const system=ONBOARDING.status.system||{};
240
+ const settings=ONBOARDING.status.settings||{};
241
+ const setup=ONBOARDING.status.setup||{};
242
+ const nextBtn=$('onboardingNextBtn');
243
+ const backBtn=$('onboardingBackBtn');
244
+ if(backBtn) backBtn.style.display=ONBOARDING.step>0?'':'none';
245
+ if(nextBtn) nextBtn.textContent=key==='finish'?t('onboarding_open'):t('onboarding_continue');
246
+
247
+ if(key==='system'){
248
+ const hermesOk=system.hermes_found&&system.imports_ok;
249
+ const setupOk=!!system.chat_ready;
250
+ _setOnboardingNotice(system.provider_note|| (setupOk?t('onboarding_notice_system_ready'):t('onboarding_notice_system_unavailable')),setupOk?'success':(hermesOk?'info':'warn'));
251
+ body.innerHTML=`
252
+ <div class="onboarding-panel-grid">
253
+ <div class="onboarding-check ${hermesOk?'ok':'warn'}"><strong>${t('onboarding_check_agent')}</strong><span>${hermesOk?t('onboarding_check_agent_ready'):t('onboarding_check_agent_missing')}</span></div>
254
+ <div class="onboarding-check ${(setupOk?'ok':system.provider_configured?'warn':'muted')}"><strong>${t('onboarding_check_provider')}</strong><span>${_providerStatusLabel(system)}</span></div>
255
+ <div class="onboarding-check ${(settings.password_enabled?'ok':'muted')}"><strong>${t('onboarding_check_password')}</strong><span>${settings.password_enabled?t('onboarding_check_password_enabled'):t('onboarding_check_password_disabled')}</span></div>
256
+ </div>
257
+ <div class="onboarding-copy">
258
+ <p><strong>${t('onboarding_config_file')}</strong> ${esc(system.config_path||t('onboarding_unknown'))}</p>
259
+ <p><strong>${t('onboarding_env_file')}</strong> ${esc(system.env_path||t('onboarding_unknown'))}</p>
260
+ <p>${esc(system.provider_note||'')}</p>
261
+ ${system.current_provider?`<p><strong>${t('onboarding_current_provider')}</strong> ${esc(system.current_provider)}${system.current_model?` — ${esc(system.current_model)}`:''}</p>`:''}
262
+ ${system.current_base_url?`<p><strong>${t('onboarding_base_url_label')}</strong> ${esc(system.current_base_url)}</p>`:''}
263
+ ${system.missing_modules&&system.missing_modules.length?`<p><strong>${t('onboarding_missing_imports')}</strong> ${esc(system.missing_modules.join(', '))}</p>`:''}
264
+ </div>`;
265
+ return;
266
+ }
267
+
268
+ if(key==='setup'){
269
+ const selectedId=ONBOARDING.form.provider;
270
+ const groupedOptions=_renderProviderSelectOptions(selectedId);
271
+ const provider=_getOnboardingSetupProvider(selectedId)||_getOnboardingSetupProviders()[0]||null;
272
+ const showBaseUrl=provider&&provider.requires_base_url;
273
+ const keyHelp=provider
274
+ ? (provider.id==='anthropic'
275
+ ? 'Anthropic API key path: paste an Anthropic Console API key here. This is separate from a Claude Code subscription; use the Claude Code OAuth card if you want subscription credentials instead.'
276
+ : `${t('onboarding_api_key_help_prefix')} ${esc(provider.env_var)}.`)
277
+ : '';
278
+
279
+ // OAuth provider path: configured via CLI, no API key input needed.
280
+ const currentIsOauth=!!(ONBOARDING.status.setup||{}).current_is_oauth;
281
+ const currentProviderName=((ONBOARDING.status.setup||{}).current||{}).provider||'';
282
+ if(currentIsOauth){
283
+ const isReady=!!(ONBOARDING.status.system||{}).chat_ready;
284
+ const providerLabel=esc(currentProviderName);
285
+ const codexOauthPendingBody=currentProviderName==='openai-codex'
286
+ ? 'This instance is configured to use <strong>openai-codex</strong>, which uses OAuth rather than an API key. Use the button below to authenticate with ChatGPT, then continue once provider status refreshes.'
287
+ : t('onboarding_oauth_provider_not_ready_body').replace('{provider}',providerLabel);
288
+ if(isReady){
289
+ _setOnboardingNotice(t('onboarding_notice_setup_already_ready'),'success');
290
+ body.innerHTML=`
291
+ <div class="onboarding-oauth-card onboarding-oauth-ready">
292
+ <div class="onboarding-oauth-icon">âś“</div>
293
+ <div>
294
+ <strong>${t('onboarding_oauth_provider_ready_title')}</strong>
295
+ <p>${t('onboarding_oauth_provider_ready_body').replace('{provider}',providerLabel)}</p>
296
+ </div>
297
+ </div>
298
+ <p class="onboarding-copy" style="margin-top:20px">${t('onboarding_oauth_switch_hint')}</p>
299
+ <label class="onboarding-field">
300
+ <span>${t('onboarding_provider_label')}</span>
301
+ <select id="onboardingProviderSelect" onchange="syncOnboardingProvider(this.value)">${groupedOptions}</select>
302
+ </label>
303
+ ${_renderOnboardingApiKeyField()}
304
+ ${_renderOnboardingBaseUrlField(showBaseUrl)}
305
+ <p class="onboarding-copy">${keyHelp}</p>`;
306
+ } else {
307
+ _setOnboardingNotice(t('onboarding_notice_setup_required'),'warn');
308
+ body.innerHTML=`
309
+ <div class="onboarding-oauth-card onboarding-oauth-pending">
310
+ <div class="onboarding-oauth-icon">âš </div>
311
+ <div style="flex:1">
312
+ <strong>${t('onboarding_oauth_provider_not_ready_title')}</strong>
313
+ <p>${codexOauthPendingBody}</p>
314
+ ${currentProviderName==='openai-codex'?`<div style="margin-top:12px;display:flex;gap:8px;align-items:center;flex-wrap:wrap"><button class="sm-btn" id="codexOAuthBtn" onclick="startCodexOAuth()" type="button">${t('oauth_login_codex')}</button></div><div id="codexOAuthFlow" style="display:none;margin-top:12px"></div>`:''}
315
+ </div>
316
+ </div>
317
+ <p class="onboarding-copy" style="margin-top:20px">${t('onboarding_oauth_switch_hint')}</p>
318
+ <label class="onboarding-field">
319
+ <span>${t('onboarding_provider_label')}</span>
320
+ <select id="onboardingProviderSelect" onchange="syncOnboardingProvider(this.value)">${groupedOptions}</select>
321
+ </label>
322
+ ${_renderOnboardingApiKeyField()}
323
+ ${_renderOnboardingBaseUrlField(showBaseUrl)}
324
+ <p class="onboarding-copy">${keyHelp}</p>`;
325
+ }
326
+ return;
327
+ }
328
+
329
+ _setOnboardingNotice(system.chat_ready?t('onboarding_notice_setup_already_ready'):t('onboarding_notice_setup_required'),system.chat_ready?'success':'info');
330
+ body.innerHTML=`
331
+ <label class="onboarding-field">
332
+ <span>${t('onboarding_provider_label')}</span>
333
+ <select id="onboardingProviderSelect" onchange="syncOnboardingProvider(this.value)">${groupedOptions}</select>
334
+ </label>
335
+ ${_renderOnboardingApiKeyField()}
336
+ ${_renderOnboardingProviderOAuthField(provider)}
337
+ ${_renderOnboardingBaseUrlField(showBaseUrl)}
338
+ <p class="onboarding-copy">${keyHelp}</p>
339
+ ${showBaseUrl?`<p class="onboarding-copy">${t('onboarding_base_url_help')}</p>`:''}
340
+ <p class="onboarding-copy">${esc(setup.unsupported_note||'')||''}</p>`;
341
+ return;
342
+ }
343
+
344
+ if(key==='workspace'){
345
+ const workspaceOptions=_getOnboardingWorkspaceChoices().map(ws=>`<option value="${esc(ws.path)}">${esc(ws.name||ws.path)} — ${esc(ws.path)}</option>`).join('');
346
+ _setOnboardingNotice(t('onboarding_notice_workspace'), 'info');
347
+ body.innerHTML=`
348
+ <label class="onboarding-field">
349
+ <span>${t('onboarding_workspace_label')}</span>
350
+ <select id="onboardingWorkspaceSelect" onchange="syncOnboardingWorkspaceSelect(this.value)">${workspaceOptions}</select>
351
+ </label>
352
+ <label class="onboarding-field">
353
+ <span>${t('onboarding_workspace_or_path')}</span>
354
+ <input id="onboardingWorkspaceInput" value="${esc(ONBOARDING.form.workspace||'')}" placeholder="${t('onboarding_workspace_placeholder')}" oninput="ONBOARDING.form.workspace=this.value">
355
+ </label>
356
+ ${_renderOnboardingModelField()}`;
357
+ const wsSel=$('onboardingWorkspaceSelect');
358
+ if(wsSel && ONBOARDING.form.workspace) wsSel.value=ONBOARDING.form.workspace;
359
+ const modelSel=$('onboardingModelSelect');
360
+ if(modelSel && ONBOARDING.form.model) modelSel.value=ONBOARDING.form.model;
361
+ return;
362
+ }
363
+
364
+ if(key==='password'){
365
+ _setOnboardingNotice(settings.password_enabled?t('onboarding_notice_password_enabled'):t('onboarding_notice_password_recommended'), settings.password_enabled?'success':'info');
366
+ body.innerHTML=`
367
+ <label class="onboarding-field">
368
+ <span>${t('onboarding_password_label')}</span>
369
+ <input id="onboardingPasswordInput" type="password" value="${esc(ONBOARDING.form.password||'')}" placeholder="${t('onboarding_password_placeholder')}" oninput="ONBOARDING.form.password=this.value">
370
+ </label>
371
+ <p class="onboarding-copy">${t('onboarding_password_help')}</p>`;
372
+ return;
373
+ }
374
+
375
+ const provider=_getOnboardingSetupProvider(ONBOARDING.form.provider);
376
+ _setOnboardingNotice(t('onboarding_notice_finish'), 'success');
377
+ body.innerHTML=`
378
+ <div class="onboarding-summary">
379
+ <div><strong>${t('onboarding_provider_label')}</strong><span>${esc((provider&&provider.label)||ONBOARDING.form.provider||t('onboarding_not_set'))}</span></div>
380
+ <div><strong>${t('onboarding_model_label')}</strong><span>${esc(_getOnboardingSelectedModel()||t('onboarding_not_set'))}</span></div>
381
+ <div><strong>${t('onboarding_workspace_label')}</strong><span>${esc(ONBOARDING.form.workspace||t('onboarding_not_set'))}</span></div>
382
+ <div><strong>${t('onboarding_check_password')}</strong><span>${t(_getOnboardingPasswordSummaryKey(settings))}</span></div>
383
+ </div>
384
+ ${ONBOARDING.form.baseUrl?`<p class="onboarding-copy"><strong>${t('onboarding_base_url_label')}</strong> ${esc(ONBOARDING.form.baseUrl)}</p>`:''}
385
+ <p class="onboarding-copy">${t('onboarding_finish_help')}</p>`;
386
+ }
387
+
388
+ function _getOnboardingPasswordSummaryKey(settings){
389
+ const hasExistingPassword=!!(settings&&settings.password_enabled);
390
+ const hasNewPassword=!!((ONBOARDING.form.password||'').trim());
391
+ if(hasNewPassword) return hasExistingPassword?'onboarding_password_will_replace':'onboarding_password_will_enable';
392
+ return hasExistingPassword?'onboarding_password_keep_existing':'onboarding_password_remains_disabled';
393
+ }
394
+
395
+ function syncOnboardingWorkspaceSelect(value){
396
+ ONBOARDING.form.workspace=value;
397
+ const input=$('onboardingWorkspaceInput');
398
+ if(input) input.value=value;
399
+ }
400
+
401
+ function syncOnboardingProvider(value){
402
+ const provider=_getOnboardingSetupProvider(value);
403
+ ONBOARDING.form.provider=value;
404
+ if(provider){
405
+ if(!ONBOARDING.form.model || !_getOnboardingProviderModelChoices().some(m=>m.id===ONBOARDING.form.model) || value==='custom'){
406
+ ONBOARDING.form.model=provider.default_model||'';
407
+ }
408
+ if(provider.requires_base_url){
409
+ ONBOARDING.form.baseUrl=ONBOARDING.form.baseUrl||provider.default_base_url||'';
410
+ }else{
411
+ ONBOARDING.form.baseUrl=provider.default_base_url||'';
412
+ }
413
+ }
414
+ _renderOnboardingBody();
415
+ }
416
+
417
+ async function loadOnboardingWizard(){
418
+ try{
419
+ const status=await api('/api/onboarding/status');
420
+ ONBOARDING.status=status;
421
+ const current=((status.setup||{}).current)||{};
422
+ ONBOARDING.form.provider=current.provider||'openrouter';
423
+ ONBOARDING.form.workspace=(status.workspaces&&status.workspaces.last)||status.settings.default_workspace||'';
424
+ ONBOARDING.form.model=status.settings.default_model||current.model||'';
425
+ ONBOARDING.form.password='';
426
+ ONBOARDING.form.apiKey='';
427
+ ONBOARDING.form.baseUrl=current.base_url||'';
428
+ ONBOARDING.active=!status.completed;
429
+ if(!ONBOARDING.active) return false;
430
+ $('onboardingOverlay').style.display='flex';
431
+ _renderOnboardingSteps();
432
+ _renderOnboardingBody();
433
+ return true;
434
+ }catch(e){
435
+ console.warn('onboarding status failed',e);
436
+ return false;
437
+ }
438
+ }
439
+
440
+ function prevOnboardingStep(){
441
+ if(ONBOARDING.step===0)return;
442
+ ONBOARDING.step--;
443
+ _renderOnboardingSteps();
444
+ _renderOnboardingBody();
445
+ }
446
+
447
+ async function _saveOnboardingProviderSetup(){
448
+ const provider=(ONBOARDING.form.provider||'').trim();
449
+ const model=(ONBOARDING.form.model||'').trim();
450
+ const apiKey=(ONBOARDING.form.apiKey||'').trim();
451
+ const baseUrl=(ONBOARDING.form.baseUrl||'').trim();
452
+ const current=_getOnboardingCurrentSetup();
453
+ const isUnchanged=current.provider===provider&&((current.model||'')===model)&&((current.base_url||'')===baseUrl);
454
+ // Skip the POST when nothing changed. We also skip when the provider is
455
+ // unsupported/OAuth-based and already working — chat_ready may be false for
456
+ // providers not in the quick-setup list (e.g. minimax-cn) even though they are
457
+ // fully configured. Posting in that case would either be a no-op (the server
458
+ // just marks complete for unsupported providers) or could silently overwrite
459
+ // config.yaml if the user accidentally changed the provider dropdown.
460
+ const currentIsOauth=!!(ONBOARDING.status&&ONBOARDING.status.setup&&ONBOARDING.status.setup.current_is_oauth);
461
+ if(isUnchanged && !apiKey && ((ONBOARDING.status.system||{}).chat_ready || currentIsOauth)) return;
462
+ const body={provider,model};
463
+ if(apiKey) body.api_key=apiKey;
464
+ if(baseUrl) body.base_url=baseUrl;
465
+ const status=await api('/api/onboarding/setup',{method:'POST',body:JSON.stringify(body)});
466
+ ONBOARDING.status=status;
467
+ }
468
+
469
+ async function _saveOnboardingDefaults(){
470
+ const workspace=(ONBOARDING.form.workspace||'').trim();
471
+ const model=(ONBOARDING.form.model||'').trim();
472
+ const password=(ONBOARDING.form.password||'').trim();
473
+ if(!workspace) throw new Error(t('onboarding_error_choose_workspace'));
474
+ if(!model) throw new Error(t('onboarding_error_choose_model'));
475
+ const known=_getOnboardingWorkspaceChoices().some(ws=>ws.path===workspace);
476
+ if(!known){
477
+ await api('/api/workspaces/add',{method:'POST',body:JSON.stringify({path:workspace})});
478
+ }
479
+ // Model persisted by /api/onboarding/setup — no /api/default-model call needed here
480
+ const body={default_workspace:workspace};
481
+ if(password) body._set_password=password;
482
+ const saved=await api('/api/settings',{method:'POST',body:JSON.stringify(body)});
483
+ if(ONBOARDING.status){
484
+ ONBOARDING.status.settings={...(ONBOARDING.status.settings||{}),password_enabled:!!saved.auth_enabled};
485
+ }
486
+ try{localStorage.setItem('hermes-webui-model',model)}catch{}
487
+ if($('modelSelect')) _applyModelToDropdown(model,$('modelSelect'));
488
+ }
489
+
490
+ async function _finishOnboarding(){
491
+ await _saveOnboardingProviderSetup();
492
+ await _saveOnboardingDefaults();
493
+ const done=await api('/api/onboarding/complete',{method:'POST',body:'{}'});
494
+ ONBOARDING.status=done;
495
+ ONBOARDING.active=false;
496
+ $('onboardingOverlay').style.display='none';
497
+ showToast(t('onboarding_complete'));
498
+ await loadWorkspaceList();
499
+ if(typeof renderSessionList==='function') await renderSessionList();
500
+ if(!S.session && typeof newSession==='function'){
501
+ await newSession(true);
502
+ await renderSessionList();
503
+ }
504
+ }
505
+
506
+ async function skipOnboarding(){
507
+ try{
508
+ // Mark onboarding completed server-side without changing any config
509
+ await api('/api/onboarding/complete',{method:'POST',body:'{}'});
510
+ ONBOARDING.active=false;
511
+ $('onboardingOverlay').style.display='none';
512
+ showToast(t('onboarding_skipped')||'Setup skipped');
513
+ }catch(e){
514
+ _setOnboardingNotice((e.message||String(e)),'warn');
515
+ }
516
+ }
517
+
518
+ async function nextOnboardingStep(){
519
+ try{
520
+ if(ONBOARDING.steps[ONBOARDING.step]==='setup'){
521
+ ONBOARDING.form.provider=(($('onboardingProviderSelect')||{}).value||ONBOARDING.form.provider||'').trim();
522
+ ONBOARDING.form.apiKey=(($('onboardingApiKeyInput')||{}).value||'').trim();
523
+ ONBOARDING.form.baseUrl=(($('onboardingBaseUrlInput')||{}).value||ONBOARDING.form.baseUrl||'').trim();
524
+ if(!ONBOARDING.form.provider) throw new Error(t('onboarding_error_provider_required'));
525
+ if(ONBOARDING.form.provider==='custom' && !ONBOARDING.form.baseUrl) throw new Error(t('onboarding_error_base_url_required'));
526
+ // For self-hosted providers (requires_base_url=True), gate Continue on a
527
+ // successful probe of <base_url>/models — otherwise the wizard would
528
+ // happily persist an unreachable URL and finish in 200ms with no
529
+ // outbound HTTP, exactly the bug in #1499. Run the probe synchronously
530
+ // here, then check status; the probe is idempotent & cached on
531
+ // (provider, baseUrl, apiKey) so this rarely triggers a second network
532
+ // call when the user already saw a green banner.
533
+ const cat=_getOnboardingSetupProvider(ONBOARDING.form.provider);
534
+ if(cat&&cat.requires_base_url){
535
+ if(!ONBOARDING.form.baseUrl) throw new Error(t('onboarding_error_base_url_required'));
536
+ await _runOnboardingProbe();
537
+ if(ONBOARDING.probe.status!=='ok'){
538
+ // Surface the same localized error string the inline banner shows.
539
+ const msg=_onboardingProbeMessage(ONBOARDING.probe)||t('onboarding_error_probe_failed')||'Could not reach the configured base URL.';
540
+ throw new Error(msg);
541
+ }
542
+ }
543
+ }
544
+ if(ONBOARDING.steps[ONBOARDING.step]==='workspace'){
545
+ ONBOARDING.form.workspace=(($('onboardingWorkspaceInput')||{}).value||ONBOARDING.form.workspace||'').trim();
546
+ ONBOARDING.form.model=(($('onboardingModelInput')||{}).value||($('onboardingModelSelect')||{}).value||ONBOARDING.form.model||'').trim();
547
+ if(!ONBOARDING.form.workspace) throw new Error(t('onboarding_error_workspace_required'));
548
+ if(!ONBOARDING.form.model) throw new Error(t('onboarding_error_model_required'));
549
+ }
550
+ if(ONBOARDING.steps[ONBOARDING.step]==='password'){
551
+ ONBOARDING.form.password=(($('onboardingPasswordInput')||{}).value||'').trim();
552
+ }
553
+ if(ONBOARDING.step===ONBOARDING.steps.length-1){
554
+ await _finishOnboarding();
555
+ return;
556
+ }
557
+ ONBOARDING.step++;
558
+ _renderOnboardingSteps();
559
+ _renderOnboardingBody();
560
+ }catch(e){
561
+ _setOnboardingNotice(e.message||String(e),'warn');
562
+ }
563
+ }
564
+
565
+ /* ── Codex OAuth device-code flow ── */
566
+ let _codexOAuthPollTimer=null;
567
+ let _codexOAuthFlowId=null;
568
+
569
+ function _clearCodexOAuthPoll(){
570
+ if(_codexOAuthPollTimer){clearTimeout(_codexOAuthPollTimer);_codexOAuthPollTimer=null;}
571
+ }
572
+
573
+ function _setCodexOAuthButton(enabled){
574
+ const btn=$('codexOAuthBtn');
575
+ if(btn){btn.disabled=!enabled;btn.textContent=enabled?t('oauth_login_codex'):'...';}
576
+ }
577
+
578
+ async function copyCodexOAuthCode(code){
579
+ try{
580
+ await navigator.clipboard.writeText(code||'');
581
+ showToast('Code copied');
582
+ }catch(e){
583
+ showToast(code||'');
584
+ }
585
+ }
586
+
587
+ async function cancelCodexOAuth(){
588
+ const flowDiv=$('codexOAuthFlow');
589
+ const flowId=_codexOAuthFlowId;
590
+ _clearCodexOAuthPoll();
591
+ _codexOAuthFlowId=null;
592
+ if(flowId){
593
+ try{await api('/api/onboarding/oauth/cancel',{method:'POST',body:JSON.stringify({flow_id:flowId})});}catch(e){}
594
+ }
595
+ _setCodexOAuthButton(true);
596
+ if(flowDiv){
597
+ flowDiv.innerHTML=`<div class="onboarding-oauth-card"><div class="onboarding-oauth-icon">⏹</div><div><strong>OAuth login cancelled</strong><p style="margin-top:6px;color:var(--muted);font-size:13px">Start again whenever you're ready.</p></div></div>`;
598
+ }
599
+ }
600
+
601
+ function _renderCodexOAuthTerminal(status,message){
602
+ const flowDiv=$('codexOAuthFlow');
603
+ if(!flowDiv)return;
604
+ const ok=status==='success';
605
+ const icon=ok?'✅':status==='expired'?'⌛':status==='cancelled'?'⏹':'❌';
606
+ const title=ok?t('oauth_codex_success'):(status==='expired'?t('oauth_codex_expired'):(status==='cancelled'?'OAuth login cancelled':t('oauth_codex_error')));
607
+ flowDiv.innerHTML=`
608
+ <div class="onboarding-oauth-card ${ok?'onboarding-oauth-ready':''}" ${ok?'':'style="border-color:var(--error,#e55)"'}>
609
+ <div class="onboarding-oauth-icon">${icon}</div>
610
+ <div><strong>${title}</strong><p style="margin-top:6px;color:var(--muted);font-size:13px">${esc(message||'')}</p></div>
611
+ </div>`;
612
+ }
613
+
614
+ async function _pollCodexOAuth(){
615
+ const flowId=_codexOAuthFlowId;
616
+ if(!flowId)return;
617
+ try{
618
+ const resp=await api('/api/onboarding/oauth/poll?flow_id='+encodeURIComponent(flowId));
619
+ const status=(resp&&resp.status)||'error';
620
+ if(status==='pending'){
621
+ _codexOAuthPollTimer=setTimeout(_pollCodexOAuth,3000);
622
+ return;
623
+ }
624
+ _clearCodexOAuthPoll();
625
+ _codexOAuthFlowId=null;
626
+ _setCodexOAuthButton(true);
627
+ if(status==='success'){
628
+ _renderCodexOAuthTerminal('success','Credentials saved to the Hermes credential pool. Refreshing provider status…');
629
+ showToast(t('oauth_codex_success'));
630
+ try{await loadOnboardingWizard();}catch(e){}
631
+ }else if(status==='expired'){
632
+ _renderCodexOAuthTerminal('expired','The code expired. Start a new login flow to try again.');
633
+ }else if(status==='cancelled'){
634
+ _renderCodexOAuthTerminal('cancelled','The login flow was cancelled.');
635
+ }else{
636
+ _renderCodexOAuthTerminal('error',(resp&&resp.error)||'OAuth login failed. Please try again.');
637
+ }
638
+ }catch(e){
639
+ _clearCodexOAuthPoll();
640
+ _codexOAuthFlowId=null;
641
+ _setCodexOAuthButton(true);
642
+ _renderCodexOAuthTerminal('error',(e&&e.message)||String(e));
643
+ }
644
+ }
645
+
646
+ async function startCodexOAuth(){
647
+ const flowDiv=$('codexOAuthFlow');
648
+ if(!flowDiv)return;
649
+ _clearCodexOAuthPoll();
650
+ _codexOAuthFlowId=null;
651
+ _setCodexOAuthButton(false);
652
+ flowDiv.style.display='block';
653
+ flowDiv.innerHTML=`<div class="onboarding-oauth-card onboarding-oauth-pending"><div class="onboarding-oauth-icon">⏳</div><div><strong>${t('oauth_codex_polling')}</strong><p>Starting device-code flow…</p></div></div>`;
654
+ try{
655
+ const resp=await api('/api/onboarding/oauth/start',{method:'POST',body:JSON.stringify({provider:'openai-codex'})});
656
+ if(resp.error) throw new Error(resp.error);
657
+ const{flow_id,user_code,verification_uri}=resp;
658
+ if(!flow_id||!user_code||!verification_uri) throw new Error('Invalid OAuth response');
659
+ _codexOAuthFlowId=flow_id;
660
+ flowDiv.innerHTML=`
661
+ <div class="onboarding-oauth-card onboarding-oauth-pending">
662
+ <div class="onboarding-oauth-icon">đź“‹</div>
663
+ <div style="flex:1">
664
+ <strong>${t('oauth_codex_step1')}</strong>
665
+ <p><a href="${esc(verification_uri)}" target="_blank" rel="noopener" style="color:var(--accent);word-break:break-all">${esc(verification_uri)}</a></p>
666
+ <p style="margin-top:8px"><strong>${t('oauth_codex_step2')}</strong></p>
667
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:4px">
668
+ <code style="display:inline-block;font-size:18px;letter-spacing:0.1em;background:rgba(255,255,255,.08);padding:6px 14px;border-radius:8px;user-select:all">${esc(user_code)}</code>
669
+ <button class="sm-btn" type="button" onclick="copyCodexOAuthCode('${esc(user_code)}')">Copy code</button>
670
+ <button class="sm-btn" type="button" onclick="cancelCodexOAuth()">Cancel</button>
671
+ </div>
672
+ <p style="margin-top:8px;color:var(--muted);font-size:13px">${t('oauth_codex_polling')}</p>
673
+ </div>
674
+ </div>`;
675
+ _codexOAuthPollTimer=setTimeout(_pollCodexOAuth,Math.max(1000,Number(resp.poll_interval_seconds||3)*1000));
676
+ }catch(e){
677
+ _clearCodexOAuthPoll();
678
+ _codexOAuthFlowId=null;
679
+ _renderCodexOAuthTerminal('error',(e&&e.message)||String(e));
680
+ _setCodexOAuthButton(true);
681
+ }
682
+ }
683
+
684
+ /* ── Anthropic / Claude Code credential-link flow ── */
685
+ let _anthropicOAuthPollTimer=null;
686
+ let _anthropicOAuthFlowId=null;
687
+
688
+ function _clearAnthropicOAuthPoll(){
689
+ if(_anthropicOAuthPollTimer){clearTimeout(_anthropicOAuthPollTimer);_anthropicOAuthPollTimer=null;}
690
+ }
691
+
692
+ function _setAnthropicOAuthButton(enabled){
693
+ const btn=$('anthropicOAuthBtn');
694
+ if(btn){btn.disabled=!enabled;btn.textContent=enabled?'Login with Claude Code':'...';}
695
+ }
696
+
697
+ async function cancelAnthropicOAuth(){
698
+ const flowDiv=$('anthropicOAuthFlow');
699
+ const flowId=_anthropicOAuthFlowId;
700
+ _clearAnthropicOAuthPoll();
701
+ _anthropicOAuthFlowId=null;
702
+ if(flowId){
703
+ try{await api('/api/onboarding/oauth/cancel',{method:'POST',body:JSON.stringify({flow_id:flowId,provider:'anthropic'})});}catch(e){}
704
+ }
705
+ _setAnthropicOAuthButton(true);
706
+ if(flowDiv){
707
+ flowDiv.innerHTML=`<div class="onboarding-oauth-card"><div class="onboarding-oauth-icon">⏹</div><div><strong>Claude Code OAuth cancelled</strong><p style="margin-top:6px;color:var(--muted);font-size:13px">Start again whenever you're ready.</p></div></div>`;
708
+ }
709
+ }
710
+
711
+ function _renderAnthropicOAuthTerminal(status,message){
712
+ const flowDiv=$('anthropicOAuthFlow');
713
+ if(!flowDiv)return;
714
+ const ok=status==='success';
715
+ const icon=ok?'✅':status==='expired'?'⌛':status==='cancelled'?'⏹':'❌';
716
+ const title=ok?'Claude Code OAuth linked':(status==='expired'?'Claude Code polling expired':(status==='cancelled'?'Claude Code OAuth cancelled':'Claude Code OAuth failed'));
717
+ flowDiv.style.display='block';
718
+ flowDiv.innerHTML=`
719
+ <div class="onboarding-oauth-card ${ok?'onboarding-oauth-ready':''}" ${ok?'':'style="border-color:var(--error,#e55)"'}>
720
+ <div class="onboarding-oauth-icon">${icon}</div>
721
+ <div><strong>${title}</strong><p style="margin-top:6px;color:var(--muted);font-size:13px">${esc(message||'')}</p></div>
722
+ </div>`;
723
+ }
724
+
725
+ async function _pollAnthropicOAuth(){
726
+ const flowId=_anthropicOAuthFlowId;
727
+ if(!flowId)return;
728
+ try{
729
+ const resp=await api('/api/onboarding/oauth/poll?flow_id='+encodeURIComponent(flowId));
730
+ const status=(resp&&resp.status)||'error';
731
+ if(status==='pending'){
732
+ _anthropicOAuthPollTimer=setTimeout(_pollAnthropicOAuth,3000);
733
+ return;
734
+ }
735
+ _clearAnthropicOAuthPoll();
736
+ _anthropicOAuthFlowId=null;
737
+ _setAnthropicOAuthButton(true);
738
+ if(status==='success'){
739
+ _renderAnthropicOAuthTerminal('success','Hermes is now linked to Claude Code credentials. Refreshing provider status…');
740
+ showToast('Claude Code OAuth linked');
741
+ try{await loadOnboardingWizard();}catch(e){}
742
+ }else if(status==='expired'){
743
+ _renderAnthropicOAuthTerminal('expired','Claude Code credentials were not detected before this flow expired. Start a new flow to try again.');
744
+ }else if(status==='cancelled'){
745
+ _renderAnthropicOAuthTerminal('cancelled','The login flow was cancelled.');
746
+ }else{
747
+ _renderAnthropicOAuthTerminal('error',(resp&&resp.error)||'Claude Code OAuth linking failed. Please try again.');
748
+ }
749
+ }catch(e){
750
+ _clearAnthropicOAuthPoll();
751
+ _anthropicOAuthFlowId=null;
752
+ _setAnthropicOAuthButton(true);
753
+ _renderAnthropicOAuthTerminal('error',(e&&e.message)||String(e));
754
+ }
755
+ }
756
+
757
+ async function startAnthropicOAuth(){
758
+ const flowDiv=$('anthropicOAuthFlow');
759
+ if(!flowDiv)return;
760
+ _clearAnthropicOAuthPoll();
761
+ _anthropicOAuthFlowId=null;
762
+ _setAnthropicOAuthButton(false);
763
+ flowDiv.style.display='block';
764
+ flowDiv.innerHTML=`<div class="onboarding-oauth-card onboarding-oauth-pending"><div class="onboarding-oauth-icon">⏳</div><div><strong>Checking Claude Code credentials…</strong><p>Hermes is checking for existing Claude Code OAuth credentials on this server.</p></div></div>`;
765
+ try{
766
+ const resp=await api('/api/onboarding/oauth/start',{method:'POST',body:JSON.stringify({provider:'anthropic'})});
767
+ if(resp.error) throw new Error(resp.error);
768
+ const{flow_id,status,action_required}=resp;
769
+ if(!flow_id) throw new Error('Invalid OAuth response');
770
+ _anthropicOAuthFlowId=flow_id;
771
+ if(status==='success'){
772
+ _clearAnthropicOAuthPoll();
773
+ _anthropicOAuthFlowId=null;
774
+ _setAnthropicOAuthButton(true);
775
+ _renderAnthropicOAuthTerminal('success','Hermes is now linked to Claude Code credentials. Refreshing provider status…');
776
+ showToast('Claude Code OAuth linked');
777
+ try{await loadOnboardingWizard();}catch(e){}
778
+ return;
779
+ }
780
+ flowDiv.innerHTML=`
781
+ <div class="onboarding-oauth-card onboarding-oauth-pending">
782
+ <div class="onboarding-oauth-icon">🖥️</div>
783
+ <div style="flex:1">
784
+ <strong>Complete Claude Code login on this host</strong>
785
+ <p style="margin-top:6px">${esc(action_required||"Run 'claude setup-token' on the server, then return here. Hermes will detect the credential automatically.")}</p>
786
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px">
787
+ <code style="display:inline-block;background:rgba(255,255,255,.08);padding:6px 10px;border-radius:8px;user-select:all">claude setup-token</code>
788
+ <button class="sm-btn" type="button" onclick="cancelAnthropicOAuth()">Cancel</button>
789
+ </div>
790
+ <p style="margin-top:8px;color:var(--muted);font-size:13px">Waiting for Claude Code credentials...</p>
791
+ </div>
792
+ </div>`;
793
+ _anthropicOAuthPollTimer=setTimeout(_pollAnthropicOAuth,Math.max(1000,Number(resp.poll_interval_seconds||3)*1000));
794
+ }catch(e){
795
+ _clearAnthropicOAuthPoll();
796
+ _anthropicOAuthFlowId=null;
797
+ _renderAnthropicOAuthTerminal('error',(e&&e.message)||String(e));
798
+ _setAnthropicOAuthButton(true);
799
+ }
800
+ }