@fraziym/axoniz 0.1.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 (512) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/agents/definition.d.ts +15 -0
  4. package/dist/agents/definition.d.ts.map +1 -0
  5. package/dist/agents/definition.js +41 -0
  6. package/dist/agents/definition.js.map +1 -0
  7. package/dist/agents/index.d.ts +12 -0
  8. package/dist/agents/index.d.ts.map +1 -0
  9. package/dist/agents/index.js +11 -0
  10. package/dist/agents/index.js.map +1 -0
  11. package/dist/agents/registry.d.ts +17 -0
  12. package/dist/agents/registry.d.ts.map +1 -0
  13. package/dist/agents/registry.js +23 -0
  14. package/dist/agents/registry.js.map +1 -0
  15. package/dist/agents/specialized.d.ts +31 -0
  16. package/dist/agents/specialized.d.ts.map +1 -0
  17. package/dist/agents/specialized.js +75 -0
  18. package/dist/agents/specialized.js.map +1 -0
  19. package/dist/awareness/index.d.ts +14 -0
  20. package/dist/awareness/index.d.ts.map +1 -0
  21. package/dist/awareness/index.js +22 -0
  22. package/dist/awareness/index.js.map +1 -0
  23. package/dist/awareness/service.d.ts +180 -0
  24. package/dist/awareness/service.d.ts.map +1 -0
  25. package/dist/awareness/service.js +789 -0
  26. package/dist/awareness/service.js.map +1 -0
  27. package/dist/cli/entry.d.ts +13 -0
  28. package/dist/cli/entry.d.ts.map +1 -0
  29. package/dist/cli/entry.js +35 -0
  30. package/dist/cli/entry.js.map +1 -0
  31. package/dist/comms/telegram.d.ts +41 -0
  32. package/dist/comms/telegram.d.ts.map +1 -0
  33. package/dist/comms/telegram.js +332 -0
  34. package/dist/comms/telegram.js.map +1 -0
  35. package/dist/core/agent.d.ts +252 -0
  36. package/dist/core/agent.d.ts.map +1 -0
  37. package/dist/core/agent.js +1399 -0
  38. package/dist/core/agent.js.map +1 -0
  39. package/dist/core/auth.d.ts +23 -0
  40. package/dist/core/auth.d.ts.map +1 -0
  41. package/dist/core/auth.js +113 -0
  42. package/dist/core/auth.js.map +1 -0
  43. package/dist/core/authority.d.ts +118 -0
  44. package/dist/core/authority.d.ts.map +1 -0
  45. package/dist/core/authority.js +402 -0
  46. package/dist/core/authority.js.map +1 -0
  47. package/dist/core/backend/async_queue.d.ts +17 -0
  48. package/dist/core/backend/async_queue.d.ts.map +1 -0
  49. package/dist/core/backend/async_queue.js +66 -0
  50. package/dist/core/backend/async_queue.js.map +1 -0
  51. package/dist/core/backend/index.d.ts +24 -0
  52. package/dist/core/backend/index.d.ts.map +1 -0
  53. package/dist/core/backend/index.js +128 -0
  54. package/dist/core/backend/index.js.map +1 -0
  55. package/dist/core/backend/llama_server.d.ts +39 -0
  56. package/dist/core/backend/llama_server.d.ts.map +1 -0
  57. package/dist/core/backend/llama_server.js +182 -0
  58. package/dist/core/backend/llama_server.js.map +1 -0
  59. package/dist/core/backend/llamacpp.d.ts +40 -0
  60. package/dist/core/backend/llamacpp.d.ts.map +1 -0
  61. package/dist/core/backend/llamacpp.js +200 -0
  62. package/dist/core/backend/llamacpp.js.map +1 -0
  63. package/dist/core/backend/lmstudio.d.ts +53 -0
  64. package/dist/core/backend/lmstudio.d.ts.map +1 -0
  65. package/dist/core/backend/lmstudio.js +274 -0
  66. package/dist/core/backend/lmstudio.js.map +1 -0
  67. package/dist/core/backend/ollama.d.ts +25 -0
  68. package/dist/core/backend/ollama.d.ts.map +1 -0
  69. package/dist/core/backend/ollama.js +86 -0
  70. package/dist/core/backend/ollama.js.map +1 -0
  71. package/dist/core/backend/openai.d.ts +27 -0
  72. package/dist/core/backend/openai.d.ts.map +1 -0
  73. package/dist/core/backend/openai.js +84 -0
  74. package/dist/core/backend/openai.js.map +1 -0
  75. package/dist/core/backend/types.d.ts +73 -0
  76. package/dist/core/backend/types.d.ts.map +1 -0
  77. package/dist/core/backend/types.js +137 -0
  78. package/dist/core/backend/types.js.map +1 -0
  79. package/dist/core/chat_store.d.ts +77 -0
  80. package/dist/core/chat_store.d.ts.map +1 -0
  81. package/dist/core/chat_store.js +332 -0
  82. package/dist/core/chat_store.js.map +1 -0
  83. package/dist/core/cli.d.ts +82 -0
  84. package/dist/core/cli.d.ts.map +1 -0
  85. package/dist/core/cli.js +602 -0
  86. package/dist/core/cli.js.map +1 -0
  87. package/dist/core/config.d.ts +127 -0
  88. package/dist/core/config.d.ts.map +1 -0
  89. package/dist/core/config.js +295 -0
  90. package/dist/core/config.js.map +1 -0
  91. package/dist/core/debug.d.ts +8 -0
  92. package/dist/core/debug.d.ts.map +1 -0
  93. package/dist/core/debug.js +8 -0
  94. package/dist/core/debug.js.map +1 -0
  95. package/dist/core/downloader.d.ts +31 -0
  96. package/dist/core/downloader.d.ts.map +1 -0
  97. package/dist/core/downloader.js +180 -0
  98. package/dist/core/downloader.js.map +1 -0
  99. package/dist/core/errors.d.ts +80 -0
  100. package/dist/core/errors.d.ts.map +1 -0
  101. package/dist/core/errors.js +134 -0
  102. package/dist/core/errors.js.map +1 -0
  103. package/dist/core/extras.d.ts +160 -0
  104. package/dist/core/extras.d.ts.map +1 -0
  105. package/dist/core/extras.js +553 -0
  106. package/dist/core/extras.js.map +1 -0
  107. package/dist/core/health.d.ts +27 -0
  108. package/dist/core/health.d.ts.map +1 -0
  109. package/dist/core/health.js +96 -0
  110. package/dist/core/health.js.map +1 -0
  111. package/dist/core/history.d.ts +50 -0
  112. package/dist/core/history.d.ts.map +1 -0
  113. package/dist/core/history.js +276 -0
  114. package/dist/core/history.js.map +1 -0
  115. package/dist/core/index.d.ts +6 -0
  116. package/dist/core/index.d.ts.map +1 -0
  117. package/dist/core/index.js +6 -0
  118. package/dist/core/index.js.map +1 -0
  119. package/dist/core/intelligence/ast_index.d.ts +255 -0
  120. package/dist/core/intelligence/ast_index.d.ts.map +1 -0
  121. package/dist/core/intelligence/ast_index.js +1369 -0
  122. package/dist/core/intelligence/ast_index.js.map +1 -0
  123. package/dist/core/intelligence/confidence.d.ts +113 -0
  124. package/dist/core/intelligence/confidence.d.ts.map +1 -0
  125. package/dist/core/intelligence/confidence.js +298 -0
  126. package/dist/core/intelligence/confidence.js.map +1 -0
  127. package/dist/core/intelligence/context_compressor.d.ts +80 -0
  128. package/dist/core/intelligence/context_compressor.d.ts.map +1 -0
  129. package/dist/core/intelligence/context_compressor.js +245 -0
  130. package/dist/core/intelligence/context_compressor.js.map +1 -0
  131. package/dist/core/intelligence/daemon.d.ts +247 -0
  132. package/dist/core/intelligence/daemon.d.ts.map +1 -0
  133. package/dist/core/intelligence/daemon.js +842 -0
  134. package/dist/core/intelligence/daemon.js.map +1 -0
  135. package/dist/core/intelligence/index.d.ts +60 -0
  136. package/dist/core/intelligence/index.d.ts.map +1 -0
  137. package/dist/core/intelligence/index.js +101 -0
  138. package/dist/core/intelligence/index.js.map +1 -0
  139. package/dist/core/intelligence/predictor.d.ts +246 -0
  140. package/dist/core/intelligence/predictor.d.ts.map +1 -0
  141. package/dist/core/intelligence/predictor.js +590 -0
  142. package/dist/core/intelligence/predictor.js.map +1 -0
  143. package/dist/core/intelligence/reflex.d.ts +84 -0
  144. package/dist/core/intelligence/reflex.d.ts.map +1 -0
  145. package/dist/core/intelligence/reflex.js +344 -0
  146. package/dist/core/intelligence/reflex.js.map +1 -0
  147. package/dist/core/intelligence/self_correction.d.ts +121 -0
  148. package/dist/core/intelligence/self_correction.d.ts.map +1 -0
  149. package/dist/core/intelligence/self_correction.js +327 -0
  150. package/dist/core/intelligence/self_correction.js.map +1 -0
  151. package/dist/core/intelligence/skill_distiller.d.ts +100 -0
  152. package/dist/core/intelligence/skill_distiller.d.ts.map +1 -0
  153. package/dist/core/intelligence/skill_distiller.js +246 -0
  154. package/dist/core/intelligence/skill_distiller.js.map +1 -0
  155. package/dist/core/intelligence/swarm.d.ts +330 -0
  156. package/dist/core/intelligence/swarm.d.ts.map +1 -0
  157. package/dist/core/intelligence/swarm.js +1064 -0
  158. package/dist/core/intelligence/swarm.js.map +1 -0
  159. package/dist/core/intelligence/trajectory.d.ts +107 -0
  160. package/dist/core/intelligence/trajectory.d.ts.map +1 -0
  161. package/dist/core/intelligence/trajectory.js +330 -0
  162. package/dist/core/intelligence/trajectory.js.map +1 -0
  163. package/dist/core/logger.d.ts +111 -0
  164. package/dist/core/logger.d.ts.map +1 -0
  165. package/dist/core/logger.js +640 -0
  166. package/dist/core/logger.js.map +1 -0
  167. package/dist/core/loop.d.ts +123 -0
  168. package/dist/core/loop.d.ts.map +1 -0
  169. package/dist/core/loop.js +423 -0
  170. package/dist/core/loop.js.map +1 -0
  171. package/dist/core/memory.d.ts +81 -0
  172. package/dist/core/memory.d.ts.map +1 -0
  173. package/dist/core/memory.js +328 -0
  174. package/dist/core/memory.js.map +1 -0
  175. package/dist/core/metrics.d.ts +29 -0
  176. package/dist/core/metrics.d.ts.map +1 -0
  177. package/dist/core/metrics.js +109 -0
  178. package/dist/core/metrics.js.map +1 -0
  179. package/dist/core/models.d.ts +51 -0
  180. package/dist/core/models.d.ts.map +1 -0
  181. package/dist/core/models.js +146 -0
  182. package/dist/core/models.js.map +1 -0
  183. package/dist/core/optimizer.d.ts +34 -0
  184. package/dist/core/optimizer.d.ts.map +1 -0
  185. package/dist/core/optimizer.js +131 -0
  186. package/dist/core/optimizer.js.map +1 -0
  187. package/dist/core/persona.d.ts +35 -0
  188. package/dist/core/persona.d.ts.map +1 -0
  189. package/dist/core/persona.js +126 -0
  190. package/dist/core/persona.js.map +1 -0
  191. package/dist/core/rate_limit.d.ts +23 -0
  192. package/dist/core/rate_limit.d.ts.map +1 -0
  193. package/dist/core/rate_limit.js +62 -0
  194. package/dist/core/rate_limit.js.map +1 -0
  195. package/dist/core/runner.d.ts +63 -0
  196. package/dist/core/runner.d.ts.map +1 -0
  197. package/dist/core/runner.js +671 -0
  198. package/dist/core/runner.js.map +1 -0
  199. package/dist/core/stream_parser.d.ts +43 -0
  200. package/dist/core/stream_parser.d.ts.map +1 -0
  201. package/dist/core/stream_parser.js +140 -0
  202. package/dist/core/stream_parser.js.map +1 -0
  203. package/dist/core/tool_schemas.d.ts +26 -0
  204. package/dist/core/tool_schemas.d.ts.map +1 -0
  205. package/dist/core/tool_schemas.js +124 -0
  206. package/dist/core/tool_schemas.js.map +1 -0
  207. package/dist/core/which.d.ts +2 -0
  208. package/dist/core/which.d.ts.map +1 -0
  209. package/dist/core/which.js +35 -0
  210. package/dist/core/which.js.map +1 -0
  211. package/dist/goals/index.d.ts +9 -0
  212. package/dist/goals/index.d.ts.map +1 -0
  213. package/dist/goals/index.js +17 -0
  214. package/dist/goals/index.js.map +1 -0
  215. package/dist/goals/service.d.ts +115 -0
  216. package/dist/goals/service.d.ts.map +1 -0
  217. package/dist/goals/service.js +415 -0
  218. package/dist/goals/service.js.map +1 -0
  219. package/dist/goals/types.d.ts +47 -0
  220. package/dist/goals/types.d.ts.map +1 -0
  221. package/dist/goals/types.js +66 -0
  222. package/dist/goals/types.js.map +1 -0
  223. package/dist/index.d.ts +22 -0
  224. package/dist/index.d.ts.map +1 -0
  225. package/dist/index.js +22 -0
  226. package/dist/index.js.map +1 -0
  227. package/dist/integrations/autonomous_startup.d.ts +51 -0
  228. package/dist/integrations/autonomous_startup.d.ts.map +1 -0
  229. package/dist/integrations/autonomous_startup.js +419 -0
  230. package/dist/integrations/autonomous_startup.js.map +1 -0
  231. package/dist/integrations/goose.d.ts +105 -0
  232. package/dist/integrations/goose.d.ts.map +1 -0
  233. package/dist/integrations/goose.js +408 -0
  234. package/dist/integrations/goose.js.map +1 -0
  235. package/dist/integrations/index.d.ts +21 -0
  236. package/dist/integrations/index.d.ts.map +1 -0
  237. package/dist/integrations/index.js +17 -0
  238. package/dist/integrations/index.js.map +1 -0
  239. package/dist/integrations/mcp.d.ts +73 -0
  240. package/dist/integrations/mcp.d.ts.map +1 -0
  241. package/dist/integrations/mcp.js +307 -0
  242. package/dist/integrations/mcp.js.map +1 -0
  243. package/dist/integrations/mempalace.d.ts +18 -0
  244. package/dist/integrations/mempalace.d.ts.map +1 -0
  245. package/dist/integrations/mempalace.js +25 -0
  246. package/dist/integrations/mempalace.js.map +1 -0
  247. package/dist/integrations/multi_agent.d.ts +82 -0
  248. package/dist/integrations/multi_agent.d.ts.map +1 -0
  249. package/dist/integrations/multi_agent.js +280 -0
  250. package/dist/integrations/multi_agent.js.map +1 -0
  251. package/dist/integrations/unified_memory.d.ts +415 -0
  252. package/dist/integrations/unified_memory.d.ts.map +1 -0
  253. package/dist/integrations/unified_memory.js +1489 -0
  254. package/dist/integrations/unified_memory.js.map +1 -0
  255. package/dist/roles/beru.yaml +48 -0
  256. package/dist/sidecar/client.d.ts +63 -0
  257. package/dist/sidecar/client.d.ts.map +1 -0
  258. package/dist/sidecar/client.js +222 -0
  259. package/dist/sidecar/client.js.map +1 -0
  260. package/dist/sidecar/index.d.ts +7 -0
  261. package/dist/sidecar/index.d.ts.map +1 -0
  262. package/dist/sidecar/index.js +6 -0
  263. package/dist/sidecar/index.js.map +1 -0
  264. package/dist/startup.d.ts +22 -0
  265. package/dist/startup.d.ts.map +1 -0
  266. package/dist/startup.js +194 -0
  267. package/dist/startup.js.map +1 -0
  268. package/dist/tools/_internal.d.ts +90 -0
  269. package/dist/tools/_internal.d.ts.map +1 -0
  270. package/dist/tools/_internal.js +375 -0
  271. package/dist/tools/_internal.js.map +1 -0
  272. package/dist/tools/_smoke.d.ts +2 -0
  273. package/dist/tools/_smoke.d.ts.map +1 -0
  274. package/dist/tools/_smoke.js +203 -0
  275. package/dist/tools/_smoke.js.map +1 -0
  276. package/dist/tools/axodex_tools.d.ts +51 -0
  277. package/dist/tools/axodex_tools.d.ts.map +1 -0
  278. package/dist/tools/axodex_tools.js +181 -0
  279. package/dist/tools/axodex_tools.js.map +1 -0
  280. package/dist/tools/code_tools.d.ts +33 -0
  281. package/dist/tools/code_tools.d.ts.map +1 -0
  282. package/dist/tools/code_tools.js +191 -0
  283. package/dist/tools/code_tools.js.map +1 -0
  284. package/dist/tools/computer_tools.d.ts +59 -0
  285. package/dist/tools/computer_tools.d.ts.map +1 -0
  286. package/dist/tools/computer_tools.js +348 -0
  287. package/dist/tools/computer_tools.js.map +1 -0
  288. package/dist/tools/file_tools.d.ts +51 -0
  289. package/dist/tools/file_tools.d.ts.map +1 -0
  290. package/dist/tools/file_tools.js +424 -0
  291. package/dist/tools/file_tools.js.map +1 -0
  292. package/dist/tools/index.d.ts +13 -0
  293. package/dist/tools/index.d.ts.map +1 -0
  294. package/dist/tools/index.js +13 -0
  295. package/dist/tools/index.js.map +1 -0
  296. package/dist/tools/shell_tools.d.ts +39 -0
  297. package/dist/tools/shell_tools.d.ts.map +1 -0
  298. package/dist/tools/shell_tools.js +163 -0
  299. package/dist/tools/shell_tools.js.map +1 -0
  300. package/dist/tools/web_tools.d.ts +31 -0
  301. package/dist/tools/web_tools.d.ts.map +1 -0
  302. package/dist/tools/web_tools.js +117 -0
  303. package/dist/tools/web_tools.js.map +1 -0
  304. package/dist/voice/index.d.ts +41 -0
  305. package/dist/voice/index.d.ts.map +1 -0
  306. package/dist/voice/index.js +58 -0
  307. package/dist/voice/index.js.map +1 -0
  308. package/dist/voice/light_daemon.d.ts +197 -0
  309. package/dist/voice/light_daemon.d.ts.map +1 -0
  310. package/dist/voice/light_daemon.js +746 -0
  311. package/dist/voice/light_daemon.js.map +1 -0
  312. package/dist/voice/setup.d.ts +63 -0
  313. package/dist/voice/setup.d.ts.map +1 -0
  314. package/dist/voice/setup.js +443 -0
  315. package/dist/voice/setup.js.map +1 -0
  316. package/dist/voice/stt.d.ts +148 -0
  317. package/dist/voice/stt.d.ts.map +1 -0
  318. package/dist/voice/stt.js +361 -0
  319. package/dist/voice/stt.js.map +1 -0
  320. package/dist/voice/tts.d.ts +168 -0
  321. package/dist/voice/tts.d.ts.map +1 -0
  322. package/dist/voice/tts.js +698 -0
  323. package/dist/voice/tts.js.map +1 -0
  324. package/dist/voice/voice_loop.d.ts +101 -0
  325. package/dist/voice/voice_loop.d.ts.map +1 -0
  326. package/dist/voice/voice_loop.js +463 -0
  327. package/dist/voice/voice_loop.js.map +1 -0
  328. package/dist/voice/wake_word.d.ts +153 -0
  329. package/dist/voice/wake_word.d.ts.map +1 -0
  330. package/dist/voice/wake_word.js +458 -0
  331. package/dist/voice/wake_word.js.map +1 -0
  332. package/dist/voice/wav.d.ts +99 -0
  333. package/dist/voice/wav.d.ts.map +1 -0
  334. package/dist/voice/wav.js +306 -0
  335. package/dist/voice/wav.js.map +1 -0
  336. package/dist/web/broker.d.ts +33 -0
  337. package/dist/web/broker.d.ts.map +1 -0
  338. package/dist/web/broker.js +58 -0
  339. package/dist/web/broker.js.map +1 -0
  340. package/dist/web/open_browser.d.ts +2 -0
  341. package/dist/web/open_browser.d.ts.map +1 -0
  342. package/dist/web/open_browser.js +42 -0
  343. package/dist/web/open_browser.js.map +1 -0
  344. package/dist/web/server.d.ts +54 -0
  345. package/dist/web/server.d.ts.map +1 -0
  346. package/dist/web/server.js +831 -0
  347. package/dist/web/server.js.map +1 -0
  348. package/dist/web/static/app.js +4121 -0
  349. package/dist/web/static/index.html +2281 -0
  350. package/dist/web/static/js/MODULE_SUMMARY.md +124 -0
  351. package/dist/web/static/js/a11y.js +165 -0
  352. package/dist/web/static/js/admin.js +2145 -0
  353. package/dist/web/static/js/assistant.js +475 -0
  354. package/dist/web/static/js/calendar/reminders.js +114 -0
  355. package/dist/web/static/js/calendar/utils.js +166 -0
  356. package/dist/web/static/js/calendar.js +3348 -0
  357. package/dist/web/static/js/censor.js +356 -0
  358. package/dist/web/static/js/chat.js +4546 -0
  359. package/dist/web/static/js/chatRenderer.js +2294 -0
  360. package/dist/web/static/js/chatStream.js +276 -0
  361. package/dist/web/static/js/codeRunner.js +398 -0
  362. package/dist/web/static/js/color/hex.js +14 -0
  363. package/dist/web/static/js/colorPicker.js +453 -0
  364. package/dist/web/static/js/compare/icons.js +77 -0
  365. package/dist/web/static/js/compare/index.js +1482 -0
  366. package/dist/web/static/js/compare/models.js +104 -0
  367. package/dist/web/static/js/compare/panes.js +817 -0
  368. package/dist/web/static/js/compare/probe.js +78 -0
  369. package/dist/web/static/js/compare/scoreboard.js +223 -0
  370. package/dist/web/static/js/compare/selector.js +1335 -0
  371. package/dist/web/static/js/compare/state.js +58 -0
  372. package/dist/web/static/js/compare/stream.js +695 -0
  373. package/dist/web/static/js/compare/vote.js +254 -0
  374. package/dist/web/static/js/cookbook-diagnosis.js +694 -0
  375. package/dist/web/static/js/cookbook-hwfit.js +1888 -0
  376. package/dist/web/static/js/cookbook.js +2093 -0
  377. package/dist/web/static/js/cookbookDownload.js +633 -0
  378. package/dist/web/static/js/cookbookProgressSignal.js +29 -0
  379. package/dist/web/static/js/cookbookRunning.js +3506 -0
  380. package/dist/web/static/js/cookbookServe.js +2128 -0
  381. package/dist/web/static/js/document.js +9731 -0
  382. package/dist/web/static/js/documentLibrary.js +3376 -0
  383. package/dist/web/static/js/dragSort.js +265 -0
  384. package/dist/web/static/js/editor/ai-inpaint.js +374 -0
  385. package/dist/web/static/js/editor/ai-models.js +273 -0
  386. package/dist/web/static/js/editor/ai-rembg.js +230 -0
  387. package/dist/web/static/js/editor/ai-tool-runner.js +147 -0
  388. package/dist/web/static/js/editor/ai-tools-misc.js +216 -0
  389. package/dist/web/static/js/editor/build/controls.js +366 -0
  390. package/dist/web/static/js/editor/build/popups.js +112 -0
  391. package/dist/web/static/js/editor/build/right-panel.js +200 -0
  392. package/dist/web/static/js/editor/build/toolbar.js +73 -0
  393. package/dist/web/static/js/editor/build/topbar.js +131 -0
  394. package/dist/web/static/js/editor/build/transform-popup.js +109 -0
  395. package/dist/web/static/js/editor/canvas-coords.js +21 -0
  396. package/dist/web/static/js/editor/canvas-events.js +197 -0
  397. package/dist/web/static/js/editor/canvas-transforms.js +132 -0
  398. package/dist/web/static/js/editor/checkerboard.js +24 -0
  399. package/dist/web/static/js/editor/clipboard-and-drop.js +133 -0
  400. package/dist/web/static/js/editor/composite-helpers.js +83 -0
  401. package/dist/web/static/js/editor/filters/blur.js +118 -0
  402. package/dist/web/static/js/editor/filters/edge-feather.js +70 -0
  403. package/dist/web/static/js/editor/fx/adj-popup.js +677 -0
  404. package/dist/web/static/js/editor/fx/filter-string.js +38 -0
  405. package/dist/web/static/js/editor/fx/histogram.js +67 -0
  406. package/dist/web/static/js/editor/fx/pixel-pass.js +249 -0
  407. package/dist/web/static/js/editor/harmonize-masks.js +116 -0
  408. package/dist/web/static/js/editor/history-panel.js +176 -0
  409. package/dist/web/static/js/editor/keyboard-shortcuts.js +271 -0
  410. package/dist/web/static/js/editor/layer-helpers.js +131 -0
  411. package/dist/web/static/js/editor/layer-panel.js +600 -0
  412. package/dist/web/static/js/editor/mask-utils.js +91 -0
  413. package/dist/web/static/js/editor/shortcuts-popover.js +149 -0
  414. package/dist/web/static/js/editor/slider-ux.js +191 -0
  415. package/dist/web/static/js/editor/snap.js +107 -0
  416. package/dist/web/static/js/editor/state.js +257 -0
  417. package/dist/web/static/js/editor/stroke-pipeline.js +162 -0
  418. package/dist/web/static/js/editor/stroke-tool-sliders.js +64 -0
  419. package/dist/web/static/js/editor/tools/clone.js +81 -0
  420. package/dist/web/static/js/editor/tools/crop.js +137 -0
  421. package/dist/web/static/js/editor/tools/flood-fill.js +72 -0
  422. package/dist/web/static/js/editor/tools/lasso-mask.js +171 -0
  423. package/dist/web/static/js/editor/tools/lasso.js +65 -0
  424. package/dist/web/static/js/editor/tools/move.js +79 -0
  425. package/dist/web/static/js/editor/tools/stroke.js +123 -0
  426. package/dist/web/static/js/editor/tools/transform-drag.js +174 -0
  427. package/dist/web/static/js/editor/tools/transform-handles.js +271 -0
  428. package/dist/web/static/js/editor/tools/transform-session.js +381 -0
  429. package/dist/web/static/js/editor/tools/wand.js +46 -0
  430. package/dist/web/static/js/editor/wire-import.js +148 -0
  431. package/dist/web/static/js/editor/wire-inpaint-controls.js +164 -0
  432. package/dist/web/static/js/editor/wire-merge-buttons.js +103 -0
  433. package/dist/web/static/js/editor/wire-selection-controls.js +170 -0
  434. package/dist/web/static/js/editor/wire-topbar-menus.js +174 -0
  435. package/dist/web/static/js/editor/wire-topbar-overflow.js +49 -0
  436. package/dist/web/static/js/editor/wire-topbar.js +188 -0
  437. package/dist/web/static/js/emailInbox.js +1245 -0
  438. package/dist/web/static/js/emailLibrary/replyRecipients.js +27 -0
  439. package/dist/web/static/js/emailLibrary/signatureFold.js +339 -0
  440. package/dist/web/static/js/emailLibrary/state.js +34 -0
  441. package/dist/web/static/js/emailLibrary/utils.js +202 -0
  442. package/dist/web/static/js/emailLibrary.js +5217 -0
  443. package/dist/web/static/js/emojiPicker.js +311 -0
  444. package/dist/web/static/js/escMenuStack.js +102 -0
  445. package/dist/web/static/js/fileHandler.js +295 -0
  446. package/dist/web/static/js/gallery.js +2835 -0
  447. package/dist/web/static/js/galleryEditor.js +3798 -0
  448. package/dist/web/static/js/group.js +955 -0
  449. package/dist/web/static/js/init.js +405 -0
  450. package/dist/web/static/js/keyboard-shortcuts.js +292 -0
  451. package/dist/web/static/js/langIcons.js +187 -0
  452. package/dist/web/static/js/markdown/tableRow.js +19 -0
  453. package/dist/web/static/js/markdown.js +806 -0
  454. package/dist/web/static/js/memory.js +1418 -0
  455. package/dist/web/static/js/modalManager.js +1552 -0
  456. package/dist/web/static/js/modalSnap.js +782 -0
  457. package/dist/web/static/js/model/matchKey.js +19 -0
  458. package/dist/web/static/js/modelPicker.js +695 -0
  459. package/dist/web/static/js/modelSort.js +33 -0
  460. package/dist/web/static/js/models.js +642 -0
  461. package/dist/web/static/js/notes.js +5054 -0
  462. package/dist/web/static/js/package.json +1 -0
  463. package/dist/web/static/js/platform.js +47 -0
  464. package/dist/web/static/js/presets.js +1110 -0
  465. package/dist/web/static/js/providers.js +93 -0
  466. package/dist/web/static/js/rag.js +177 -0
  467. package/dist/web/static/js/research/jobs.js +335 -0
  468. package/dist/web/static/js/research/panel.js +1233 -0
  469. package/dist/web/static/js/researchSynapse.js +225 -0
  470. package/dist/web/static/js/search-chat.js +201 -0
  471. package/dist/web/static/js/search.js +52 -0
  472. package/dist/web/static/js/section-management.js +260 -0
  473. package/dist/web/static/js/sessions.js +3108 -0
  474. package/dist/web/static/js/settings.js +4991 -0
  475. package/dist/web/static/js/sidebar-layout.js +573 -0
  476. package/dist/web/static/js/signature.js +504 -0
  477. package/dist/web/static/js/skills.js +1908 -0
  478. package/dist/web/static/js/slashAutocomplete.js +267 -0
  479. package/dist/web/static/js/slashCommands.js +6099 -0
  480. package/dist/web/static/js/spinner.js +406 -0
  481. package/dist/web/static/js/storage.js +123 -0
  482. package/dist/web/static/js/tasks.js +2699 -0
  483. package/dist/web/static/js/theme.js +2086 -0
  484. package/dist/web/static/js/tileManager.js +386 -0
  485. package/dist/web/static/js/tourAutoplay.js +133 -0
  486. package/dist/web/static/js/tourHints.js +179 -0
  487. package/dist/web/static/js/tts-ai.js +521 -0
  488. package/dist/web/static/js/ui.js +1288 -0
  489. package/dist/web/static/js/util/ordinal.js +13 -0
  490. package/dist/web/static/js/voiceRecorder.js +283 -0
  491. package/dist/web/static/js/windowDrag.js +326 -0
  492. package/dist/web/static/js/windowResize.js +233 -0
  493. package/dist/web/static/landing.html +824 -0
  494. package/dist/web/static/lib/docx.umd.min.js +20116 -0
  495. package/dist/web/static/lib/highlight.min.js +1213 -0
  496. package/dist/web/static/lib/html2pdf.bundle.min.js +3 -0
  497. package/dist/web/static/lib/mammoth.browser.min.js +21 -0
  498. package/dist/web/static/lib/qrcode.min.js +8 -0
  499. package/dist/web/static/lib/xlsx.full.min.js +24 -0
  500. package/dist/web/static/login.html +582 -0
  501. package/dist/web/static/manifest.json +15 -0
  502. package/dist/web/static/style.css +35733 -0
  503. package/dist/web/static/sw.js +144 -0
  504. package/dist/workflows/engine.d.ts +162 -0
  505. package/dist/workflows/engine.d.ts.map +1 -0
  506. package/dist/workflows/engine.js +625 -0
  507. package/dist/workflows/engine.js.map +1 -0
  508. package/dist/workflows/index.d.ts +8 -0
  509. package/dist/workflows/index.d.ts.map +1 -0
  510. package/dist/workflows/index.js +14 -0
  511. package/dist/workflows/index.js.map +1 -0
  512. package/package.json +112 -0
@@ -0,0 +1,955 @@
1
+ // static/js/group.js
2
+ // Group Chat — multi-model conversations (parallel or round-robin)
3
+
4
+ import uiModule from './ui.js';
5
+ import markdownModule from './markdown.js';
6
+ import chatRenderer from './chatRenderer.js';
7
+ import spinnerModule from './spinner.js';
8
+ import { providerLogo } from './providers.js';
9
+ import { PROMPT_TEMPLATES, getAllPresets } from './presets.js';
10
+ import { sortModelObjects } from './modelSort.js';
11
+ import Storage from './storage.js';
12
+
13
+ let API_BASE = '';
14
+ let _active = false;
15
+ let _models = []; // [{mid, display, url, endpointId}]
16
+ let _participantSessions = []; // session IDs for each model
17
+ const _groupParticipants = []; // module-level participants list
18
+ let _abortControllers = [];
19
+ let _mode = 'round-robin'; // 'parallel' or 'round-robin'
20
+ let _roundRobinIdx = 0;
21
+ let _parentSessionId = null;
22
+ const GROUP_STATE_KEY = 'zero-group-state';
23
+
24
+ export function init(apiBase) {
25
+ API_BASE = apiBase;
26
+ // Initialize Group tab inside Characters modal
27
+ setTimeout(_initGroupTab, 500);
28
+ }
29
+
30
+ function _initGroupTab() {
31
+ const participantsEl = document.getElementById('group-participants');
32
+ const addBtn = document.getElementById('group-add-btn');
33
+ const startBtn = document.getElementById('save-custom-preset'); // main footer "Start" button
34
+ const modeBtn = document.getElementById('group-mode-btn');
35
+ if (!participantsEl || !addBtn) return;
36
+
37
+ // _groupParticipants is at module scope
38
+ let _modelsCache = null;
39
+
40
+ async function _getModels() {
41
+ if (_modelsCache) return _modelsCache;
42
+ let items = (window.modelsModule && window.modelsModule.getCachedItems) ? window.modelsModule.getCachedItems() : [];
43
+ if (!items || items.length === 0) {
44
+ try {
45
+ const res = await fetch(API_BASE + '/api/models', { credentials: 'same-origin' });
46
+ items = (await res.json()).items || [];
47
+ } catch (e) {}
48
+ }
49
+ const result = [];
50
+ const seen = new Set();
51
+ items.forEach(item => {
52
+ if (item.offline) return;
53
+ (item.models || []).concat(item.models_extra || []).forEach((mid, i) => {
54
+ if (seen.has(mid)) return;
55
+ seen.add(mid);
56
+ const display = ((item.models_display || []).concat(item.models_extra_display || []))[i] || mid;
57
+ result.push({ mid, display: display.split('/').pop(), url: item.url, endpointId: item.endpoint_id });
58
+ });
59
+ });
60
+ _modelsCache = sortModelObjects(result);
61
+ return _modelsCache;
62
+ }
63
+
64
+ function _render() {
65
+ participantsEl.innerHTML = '';
66
+ _groupParticipants.forEach((p, idx) => {
67
+ const row = document.createElement('div');
68
+ row.style.cssText = 'display:flex;align-items:center;gap:6px;padding:4px 6px;background:color-mix(in srgb, var(--fg) 3%, transparent);border-radius:6px;';
69
+ const label = p.character ? p.character.name : (p.model ? p.model.display : '?');
70
+ const sublabel = p.model ? p.model.display : '';
71
+ row.innerHTML = `
72
+ <span style="flex:1;min-width:0;">
73
+ <span style="font-size:12px;font-weight:500;">${uiModule.esc(label)}</span>
74
+ ${sublabel && sublabel !== label ? '<span style="font-size:10px;opacity:0.35;margin-left:4px;">' + uiModule.esc(sublabel) + '</span>' : ''}
75
+ </span>
76
+ <button style="background:none;border:none;color:var(--fg);opacity:0.5;cursor:pointer;font-size:16px;padding:0 4px;line-height:1;position:relative;top:-4px;" data-idx="${idx}" title="Remove">&times;</button>
77
+ `;
78
+ row.querySelector('button').addEventListener('click', () => { _groupParticipants.splice(idx, 1); _render(); });
79
+ participantsEl.appendChild(row);
80
+ });
81
+ // startBtn is shared — don't disable it
82
+ }
83
+
84
+ addBtn.addEventListener('click', async () => {
85
+ const [models, characters] = await Promise.all([_getModels(), _getCharacterList()]);
86
+
87
+ const picker = document.createElement('div');
88
+ picker.style.cssText = 'display:flex;gap:4px;align-items:center;';
89
+
90
+ const charSel = document.createElement('select');
91
+ charSel.className = 'preset-input';
92
+ charSel.style.cssText = 'font-size:11px;flex:1;height:26px;';
93
+ charSel.innerHTML = '<option value="">Empty...</option>' +
94
+ characters.map(c => '<option value="' + c.id + '">' + uiModule.esc(c.name) + '</option>').join('');
95
+
96
+ const modelSel = document.createElement('select');
97
+ modelSel.className = 'preset-input';
98
+ modelSel.style.cssText = 'font-size:11px;flex:1;height:26px;';
99
+ modelSel.innerHTML = '<option value="">Model…</option>' +
100
+ models.map(m => '<option value="' + m.mid + '">' + uiModule.esc(m.display) + '</option>').join('');
101
+
102
+ // Auto-add when model is selected
103
+ modelSel.addEventListener('change', () => {
104
+ if (!modelSel.value) return;
105
+ if (_groupParticipants.length >= 8) { uiModule.showToast('Max 8'); return; }
106
+ const entry = { character: null, model: null };
107
+ entry.model = models.find(m => m.mid === modelSel.value) || null;
108
+ if (charSel.value) entry.character = characters.find(c => c.id === charSel.value) || null;
109
+ _groupParticipants.push(entry);
110
+ picker.remove();
111
+ _render();
112
+ });
113
+
114
+ picker.appendChild(charSel);
115
+ picker.appendChild(modelSel);
116
+ participantsEl.appendChild(picker);
117
+ });
118
+
119
+ // Mode toggle — same style as Compare's parallel button
120
+ if (modeBtn) {
121
+ const ICON_PAR = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></svg>';
122
+ const ICON_SEQ = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="8" y1="6" x2="20" y2="6"/><line x1="8" y1="12" x2="20" y2="12"/><line x1="8" y1="18" x2="20" y2="18"/><circle cx="4" cy="6" r="1.5" fill="currentColor"/><circle cx="4" cy="12" r="1.5" fill="currentColor"/><circle cx="4" cy="18" r="1.5" fill="currentColor"/></svg>';
123
+ modeBtn.addEventListener('click', () => {
124
+ _mode = _mode === 'parallel' ? 'round-robin' : 'parallel';
125
+ modeBtn.classList.toggle('active', _mode === 'parallel');
126
+ modeBtn.innerHTML = (_mode === 'parallel' ? ICON_PAR : ICON_SEQ) + '<span class="compare-toggle-label">' + (_mode === 'parallel' ? 'Parallel' : 'Sequential') + '</span>';
127
+ });
128
+ }
129
+
130
+ // Hook into the main "Start" button — only act when Group tab is active
131
+ if (startBtn) startBtn.addEventListener('click', async () => {
132
+ const activeTab = document.querySelector('.preset-tab.active');
133
+ if (!activeTab || activeTab.dataset.chartab !== 'group') return;
134
+ // Get default model from current session as fallback
135
+ const _defaultModel = (window.sessionModule && window.sessionModule.getSessions) ?
136
+ (() => {
137
+ const s = window.sessionModule.getSessions().find(x => x.id === window.sessionModule.getCurrentSessionId());
138
+ if (s) return { mid: s.model, display: s.model.split('/').pop(), url: s.endpoint_url, endpointId: '' };
139
+ return null;
140
+ })() : null;
141
+
142
+ const picked = _groupParticipants.map(p => {
143
+ let m = p.model ? { ...p.model } : (_defaultModel ? { ..._defaultModel } : null);
144
+ if (!m || !m.url) {
145
+ console.warn('[group] Participant has no valid model:', p);
146
+ return null;
147
+ }
148
+ if (p.character) m.character = { characterId: p.character.id, characterName: p.character.name, characterPrompt: p.character.prompt };
149
+ return m;
150
+ }).filter(Boolean);
151
+
152
+ if (picked.length < 2) { uiModule.showToast('Need at least 2 participants — add models or characters'); return; }
153
+
154
+ const modal = document.getElementById('custom-preset-modal');
155
+ if (modal) modal.classList.add('hidden');
156
+
157
+ setActive(true);
158
+ if (window._syncGroupIndicator) window._syncGroupIndicator(true);
159
+ if (window.sessionModule) window.sessionModule.setCurrentSessionId(null);
160
+ const box = document.getElementById('chat-history');
161
+ if (box) box.innerHTML = '';
162
+
163
+ await startGroup(picked, 'group-' + Date.now());
164
+
165
+ // Auto-save as preset if 2+ participants
166
+ if (picked.length >= 2) {
167
+ const presetData = {
168
+ id: 'grp-' + Date.now(),
169
+ name: picked.map(p => p._groupName || p.character?.characterName || p.display).join(' & '),
170
+ mode: _mode,
171
+ participants: picked.map(p => ({
172
+ modelId: p.mid,
173
+ modelDisplay: p.display,
174
+ characterId: p.character?.characterId || null,
175
+ characterName: p.character?.characterName || null,
176
+ })),
177
+ };
178
+ try {
179
+ const existing = await fetch(API_BASE + '/api/presets/groups', { credentials: 'same-origin' }).then(r => r.json());
180
+ const groups = existing.groups || [];
181
+ // Don't duplicate if same participants
182
+ const sig = presetData.participants.map(p => p.modelId + ':' + (p.characterId || '')).sort().join(',');
183
+ const exists = groups.some(g => (g.participants || []).map(p => p.modelId + ':' + (p.characterId || '')).sort().join(',') === sig);
184
+ if (!exists) {
185
+ groups.push(presetData);
186
+ await fetch(API_BASE + '/api/presets/groups', {
187
+ method: 'POST', credentials: 'same-origin',
188
+ headers: { 'Content-Type': 'application/json' },
189
+ body: JSON.stringify({ groups }),
190
+ });
191
+ }
192
+ } catch (e) {}
193
+ }
194
+
195
+ uiModule.showToast('Group chat ready — ' + picked.length + ' participants');
196
+ });
197
+
198
+ const groupTab = document.querySelector('.preset-tab[data-chartab="group"]');
199
+ if (groupTab) groupTab.addEventListener('click', () => {
200
+ _modelsCache = null;
201
+ if (startBtn) startBtn.textContent = 'Start Group';
202
+ _loadGroupPresets();
203
+ if (_groupParticipants.length === 0) {
204
+ setTimeout(() => addBtn.click(), 100);
205
+ }
206
+ });
207
+
208
+ // Load and render saved group presets
209
+ async function _loadGroupPresets() {
210
+ try {
211
+ const res = await fetch(API_BASE + '/api/presets/groups', { credentials: 'same-origin' });
212
+ const data = await res.json();
213
+ const groups = data.groups || [];
214
+ // Render presets above participant list
215
+ let presetsDiv = document.getElementById('group-presets-list');
216
+ if (!presetsDiv) {
217
+ presetsDiv = document.createElement('div');
218
+ presetsDiv.id = 'group-presets-list';
219
+ presetsDiv.style.cssText = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;';
220
+ participantsEl.parentNode.insertBefore(presetsDiv, participantsEl);
221
+ }
222
+ presetsDiv.innerHTML = '';
223
+ if (groups.length === 0) return;
224
+ groups.forEach((g, idx) => {
225
+ const chip = document.createElement('button');
226
+ chip.className = 'preset-save-btn';
227
+ chip.style.cssText = 'padding:3px 10px;font-size:11px;background:color-mix(in srgb, var(--fg) 5%, transparent);border:1px solid var(--border);';
228
+ const chipLabel = document.createElement('span');
229
+ chipLabel.textContent = g.name || 'Group ' + (idx + 1);
230
+ chip.appendChild(chipLabel);
231
+ const chipX = document.createElement('span');
232
+ chipX.textContent = ' \u00d7';
233
+ chipX.style.cssText = 'opacity:0.4;margin-left:4px;cursor:pointer;';
234
+ chipX.addEventListener('click', (ev) => {
235
+ ev.stopPropagation();
236
+ groups.splice(idx, 1);
237
+ fetch(API_BASE + '/api/presets/groups', {
238
+ method: 'POST', credentials: 'same-origin',
239
+ headers: { 'Content-Type': 'application/json' },
240
+ body: JSON.stringify({ groups }),
241
+ }).then(() => _loadGroupPresets());
242
+ });
243
+ chip.appendChild(chipX);
244
+ chip.title = (g.participants || []).map(p => p.characterName || p.modelDisplay || '?').join(', ');
245
+ chip.addEventListener('click', async () => {
246
+ // Load preset participants
247
+ const [models, chars] = await Promise.all([_getModels(), _getCharacterList()]);
248
+ _groupParticipants.length = 0;
249
+ (g.participants || []).forEach(p => {
250
+ const model = models.find(m => m.mid === p.modelId) || models[0];
251
+ const entry = { model: model || null, character: null };
252
+ if (p.characterId) {
253
+ entry.character = chars.find(c => c.id === p.characterId) || null;
254
+ }
255
+ if (entry.model) _groupParticipants.push(entry);
256
+ });
257
+ _mode = g.mode || 'parallel';
258
+ _render();
259
+ });
260
+ // Long-press / right-click to delete
261
+ chip.addEventListener('contextmenu', async (e) => {
262
+ e.preventDefault();
263
+ if (await window.styledConfirm('Delete preset "' + (g.name || 'Group') + '"?', { confirmText: 'Delete', danger: true })) {
264
+ groups.splice(idx, 1);
265
+ fetch(API_BASE + '/api/presets/groups', {
266
+ method: 'POST', credentials: 'same-origin',
267
+ headers: { 'Content-Type': 'application/json' },
268
+ body: JSON.stringify({ groups }),
269
+ }).then(() => _loadGroupPresets());
270
+ }
271
+ });
272
+ presetsDiv.appendChild(chip);
273
+ });
274
+ } catch (e) { console.warn('[group] Failed to load presets:', e); }
275
+ }
276
+ // Restore button text when switching away from Group tab
277
+ document.querySelectorAll('.preset-tab[data-chartab]').forEach(tab => {
278
+ if (tab.dataset.chartab !== 'group') {
279
+ tab.addEventListener('click', () => {
280
+ if (startBtn) startBtn.textContent = 'Start';
281
+ });
282
+ }
283
+ });
284
+ }
285
+
286
+ async function _getCharacterList() {
287
+ // Built-in characters from PROMPT_TEMPLATES
288
+ const chars = PROMPT_TEMPLATES.filter(t => t.isCharacter).map(t => ({
289
+ id: t.id, name: t.name, prompt: t.prompt,
290
+ }));
291
+ // User-created characters from presets
292
+ try {
293
+ const allPresets = getAllPresets();
294
+ if (allPresets && allPresets.custom && allPresets.custom.character_name) {
295
+ chars.push({
296
+ id: 'custom',
297
+ name: allPresets.custom.character_name,
298
+ prompt: allPresets.custom.system_prompt || allPresets.custom.prompt || '',
299
+ });
300
+ }
301
+ } catch (e) {}
302
+ // Load user templates and wait for them before returning.
303
+ // The endpoint returns a JSON array directly (not {templates:[...]}).
304
+ // All user templates are personas by definition — no isCharacter filter needed.
305
+ try {
306
+ const r = await fetch(API_BASE + '/api/presets/templates', { credentials: 'same-origin' });
307
+ const data = await r.json();
308
+ const templates = Array.isArray(data) ? data : (data.templates || []);
309
+ templates.forEach(t => {
310
+ if (t.id && t.name && !chars.find(c => c.id === t.id)) {
311
+ chars.push({ id: t.id, name: t.name, prompt: t.system_prompt || t.prompt || '' });
312
+ }
313
+ });
314
+ } catch (e) {}
315
+ return chars;
316
+ }
317
+
318
+ export function isActive() { return _active; }
319
+ export function setActive(v) { _active = v; }
320
+ export function getMode() { return _mode; }
321
+ export function setMode(m) { _mode = m; }
322
+
323
+ // ── Model Picker ─────────────────────────────────────
324
+
325
+ export async function showModelPicker() {
326
+ return new Promise((resolve) => {
327
+ const overlay = document.createElement('div');
328
+ overlay.className = 'modal';
329
+ overlay.id = 'group-model-picker';
330
+
331
+ const content = document.createElement('div');
332
+ content.className = 'modal-content';
333
+ content.style.width = 'min(480px, 92vw)';
334
+
335
+ // Header
336
+ const header = document.createElement('div');
337
+ header.className = 'modal-header';
338
+ header.innerHTML = '<h4>Group Chat — Select Models</h4>';
339
+ const closeBtn = document.createElement('button');
340
+ closeBtn.className = 'close-btn';
341
+ closeBtn.innerHTML = '&#x2716;';
342
+ closeBtn.addEventListener('click', () => { overlay.remove(); resolve(null); });
343
+ header.appendChild(closeBtn);
344
+
345
+ // Body
346
+ const body = document.createElement('div');
347
+ body.className = 'modal-body';
348
+
349
+ // Mode toggle
350
+ const modeRow = document.createElement('div');
351
+ modeRow.style.cssText = 'display:flex;gap:8px;margin-bottom:10px;align-items:center;font-size:12px;';
352
+ modeRow.innerHTML = `
353
+ <label style="display:flex;align-items:center;gap:4px;cursor:pointer;">
354
+ <input type="radio" name="group-mode" value="parallel" ${_mode === 'parallel' ? 'checked' : ''}> All respond
355
+ </label>
356
+ <label style="display:flex;align-items:center;gap:4px;cursor:pointer;">
357
+ <input type="radio" name="group-mode" value="round-robin" ${_mode === 'round-robin' ? 'checked' : ''}> Round-robin
358
+ </label>
359
+ `;
360
+ body.appendChild(modeRow);
361
+
362
+ // Search
363
+ const search = document.createElement('input');
364
+ search.type = 'text';
365
+ search.placeholder = 'Filter models…';
366
+ search.className = 'memory-search-input';
367
+ search.style.marginBottom = '8px';
368
+ body.appendChild(search);
369
+
370
+ // Model list
371
+ const list = document.createElement('div');
372
+ list.style.cssText = 'max-height:300px;overflow-y:auto;display:flex;flex-direction:column;gap:2px;';
373
+ body.appendChild(list);
374
+
375
+ // Selected count + start button
376
+ const footer = document.createElement('div');
377
+ footer.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-top:10px;';
378
+ footer.innerHTML = `
379
+ <span id="group-selected-count" style="font-size:11px;opacity:0.5;">0 selected</span>
380
+ <button id="group-start-btn" class="btn-primary" disabled style="padding:6px 16px;font-size:12px;">Start Group Chat</button>
381
+ `;
382
+ body.appendChild(footer);
383
+
384
+ content.appendChild(header);
385
+ content.appendChild(body);
386
+ overlay.appendChild(content);
387
+ overlay.style.display = 'flex';
388
+ document.body.appendChild(overlay);
389
+
390
+ // Get all available models — try cached first, fetch if empty
391
+ const selected = new Set();
392
+ let _cachedModels = null;
393
+ async function getAllModels() {
394
+ if (_cachedModels) return _cachedModels;
395
+ let items = (window.modelsModule && window.modelsModule.getCachedItems) ? window.modelsModule.getCachedItems() : [];
396
+ // Fallback: fetch from API if cache is empty
397
+ if (!items || items.length === 0) {
398
+ try {
399
+ const res = await fetch(API_BASE + '/api/models', { credentials: 'same-origin' });
400
+ const data = await res.json();
401
+ items = data.items || [];
402
+ } catch (e) { console.warn('[group] Failed to fetch models:', e); }
403
+ }
404
+ const result = [];
405
+ const seen = new Set();
406
+ items.forEach(item => {
407
+ if (item.offline) return;
408
+ (item.models || []).concat(item.models_extra || []).forEach((mid, i) => {
409
+ if (seen.has(mid)) return;
410
+ seen.add(mid);
411
+ const display = ((item.models_display || []).concat(item.models_extra_display || []))[i] || mid;
412
+ result.push({ mid, display: display.split('/').pop(), url: item.url, endpointId: item.endpoint_id, epName: item.endpoint_name || '' });
413
+ });
414
+ });
415
+ _cachedModels = sortModelObjects(result);
416
+ return _cachedModels;
417
+ }
418
+
419
+ async function render(filter) {
420
+ list.innerHTML = '<div style="opacity:0.4;padding:8px;font-size:12px;">Loading models…</div>';
421
+ const all = await getAllModels();
422
+ const q = (filter || '').toLowerCase();
423
+ all.forEach(m => {
424
+ if (q && !m.mid.toLowerCase().includes(q) && !m.display.toLowerCase().includes(q) && !m.epName.toLowerCase().includes(q)) return;
425
+ const row = document.createElement('div');
426
+ row.className = 'memory-item';
427
+ row.style.cssText = 'padding:6px 8px;cursor:pointer;' + (selected.has(m.mid) ? 'background:color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);' : '');
428
+ const logo = providerLogo(m.mid);
429
+ row.innerHTML = `
430
+ <input type="checkbox" ${selected.has(m.mid) ? 'checked' : ''} style="margin-right:6px;">
431
+ ${logo ? '<span style="opacity:0.5;margin-right:4px;">' + logo + '</span>' : ''}
432
+ <span style="flex:1;font-size:12px;">${uiModule.esc(m.display)}</span>
433
+ <span style="font-size:10px;opacity:0.3;">${uiModule.esc(m.epName)}</span>
434
+ `;
435
+ row.addEventListener('click', (e) => {
436
+ if (e.target.tagName === 'INPUT') return;
437
+ const cb = row.querySelector('input[type=checkbox]');
438
+ cb.checked = !cb.checked;
439
+ cb.dispatchEvent(new Event('change'));
440
+ });
441
+ row.querySelector('input').addEventListener('change', (e) => {
442
+ if (e.target.checked) {
443
+ if (selected.size >= 8) { e.target.checked = false; uiModule.showToast('Max 8 models'); return; }
444
+ selected.add(m.mid);
445
+ } else {
446
+ selected.delete(m.mid);
447
+ }
448
+ document.getElementById('group-selected-count').textContent = selected.size + ' selected';
449
+ document.getElementById('group-start-btn').disabled = selected.size < 2;
450
+ row.style.background = selected.has(m.mid) ? 'color-mix(in srgb, var(--accent, var(--red)) 12%, transparent)' : '';
451
+ });
452
+ list.appendChild(row);
453
+ });
454
+ }
455
+
456
+ search.addEventListener('input', () => render(search.value));
457
+ render();
458
+
459
+ // Mode toggle
460
+ modeRow.querySelectorAll('input[name=group-mode]').forEach(r => {
461
+ r.addEventListener('change', () => { _mode = r.value; });
462
+ });
463
+
464
+ // Start button
465
+ document.getElementById('group-start-btn').addEventListener('click', async () => {
466
+ const all = await getAllModels();
467
+ const picked = all.filter(m => selected.has(m.mid));
468
+
469
+ // Step 2: Character assignment
470
+ body.innerHTML = '';
471
+ const stepTitle = document.createElement('div');
472
+ stepTitle.style.cssText = 'font-size:12px;opacity:0.5;margin-bottom:8px;';
473
+ stepTitle.textContent = 'Assign characters (optional)';
474
+ body.appendChild(stepTitle);
475
+
476
+ // Build character options
477
+ const characters = await _getCharacterList();
478
+ const assignments = {}; // mid -> {characterId, characterName, characterPrompt}
479
+
480
+ for (const m of picked) {
481
+ const row = document.createElement('div');
482
+ row.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--border);';
483
+ const logo = providerLogo(m.mid);
484
+ row.innerHTML = `
485
+ ${logo ? '<span style="opacity:0.5;">' + logo + '</span>' : ''}
486
+ <span style="flex:1;font-size:12px;font-weight:500;">${uiModule.esc(m.display)}</span>
487
+ `;
488
+ const sel = document.createElement('select');
489
+ sel.style.cssText = 'font-size:11px;padding:3px 6px;border-radius:4px;border:1px solid var(--border);background:var(--bg);color:var(--fg);max-width:140px;';
490
+ let optsHtml = '<option value="">No character</option>';
491
+ characters.forEach(c => {
492
+ optsHtml += `<option value="${c.id}">${uiModule.esc(c.name)}</option>`;
493
+ });
494
+ sel.innerHTML = optsHtml;
495
+ sel.addEventListener('change', () => {
496
+ if (sel.value) {
497
+ const ch = characters.find(c => c.id === sel.value);
498
+ assignments[m.mid] = { characterId: ch.id, characterName: ch.name, characterPrompt: ch.prompt };
499
+ } else {
500
+ delete assignments[m.mid];
501
+ }
502
+ });
503
+ row.appendChild(sel);
504
+ body.appendChild(row);
505
+ }
506
+
507
+ // Go button
508
+ const goBtn = document.createElement('button');
509
+ goBtn.className = 'btn-primary';
510
+ goBtn.style.cssText = 'margin-top:10px;padding:6px 16px;font-size:12px;width:100%;';
511
+ goBtn.textContent = 'Start Group Chat';
512
+ goBtn.addEventListener('click', () => {
513
+ // Attach character info to picked models
514
+ picked.forEach(m => {
515
+ if (assignments[m.mid]) {
516
+ m.character = assignments[m.mid];
517
+ }
518
+ });
519
+ overlay.remove();
520
+ resolve(picked);
521
+ });
522
+ body.appendChild(goBtn);
523
+ });
524
+
525
+ // Click outside to close
526
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.remove(); resolve(null); } });
527
+ search.focus();
528
+ });
529
+ }
530
+
531
+ // ── Start / Stop ─────────────────────────────────────
532
+
533
+ export async function startGroup(models, parentSessionId) {
534
+ _models = models;
535
+ _active = true;
536
+ _roundRobinIdx = 0;
537
+ _participantSessions = [];
538
+
539
+ // Create a real parent session for persistence
540
+ const groupName = '[GRP] ' + models.map(m => m._groupName || m.character?.characterName || m.display).join(', ');
541
+ try {
542
+ const pfd = new FormData();
543
+ pfd.append('name', groupName);
544
+ pfd.append('endpoint_url', models[0].url);
545
+ pfd.append('model', models[0].mid);
546
+ pfd.append('skip_validation', 'true');
547
+ if (models[0].endpointId) pfd.append('endpoint_id', models[0].endpointId);
548
+ const pres = await fetch(`${API_BASE}/api/session`, { method: 'POST', body: pfd, credentials: 'same-origin' });
549
+ const pdata = await pres.json();
550
+ _parentSessionId = pdata.id;
551
+ // Register as group session for sidebar icon
552
+ try {
553
+ const storedGroupSessions = Storage.getJSON('zero-group-sessions', []);
554
+ const gids = Array.isArray(storedGroupSessions) ? storedGroupSessions : [];
555
+ if (!gids.includes(_parentSessionId)) { gids.push(_parentSessionId); localStorage.setItem('zero-group-sessions', JSON.stringify(gids)); }
556
+ } catch (e) {}
557
+ } catch (e) {
558
+ console.error('[group] Failed to create parent session:', e);
559
+ _parentSessionId = parentSessionId || 'group-' + Date.now();
560
+ }
561
+
562
+ // Create a hidden session per model
563
+ for (const m of models) {
564
+ try {
565
+ const fd = new FormData();
566
+ fd.append('name', `[GRP] ${m.display}`);
567
+ fd.append('endpoint_url', m.url);
568
+ fd.append('model', m.mid);
569
+ fd.append('skip_validation', 'true');
570
+ if (m.endpointId) fd.append('endpoint_id', m.endpointId);
571
+ const res = await fetch(`${API_BASE}/api/session`, { method: 'POST', body: fd, credentials: 'same-origin' });
572
+ if (!res.ok) {
573
+ console.error(`[group] Session creation failed for ${m.display}: HTTP ${res.status}`);
574
+ _participantSessions.push(null);
575
+ continue;
576
+ }
577
+ const data = await res.json();
578
+ if (!data.id) {
579
+ console.error(`[group] Session creation returned no ID for ${m.display}:`, data);
580
+ _participantSessions.push(null);
581
+ continue;
582
+ }
583
+ _participantSessions.push(data.id);
584
+ // Inject group chat system prompt — use character if assigned
585
+ const displayName = m.character ? m.character.characterName : m.display;
586
+ m._groupName = displayName; // store for bubble labels
587
+ const otherNames = models.filter(x => x.mid !== m.mid).map(x =>
588
+ x.character ? x.character.characterName : x.display
589
+ ).join(', ');
590
+
591
+ const _groupEtiquette =
592
+ `[Name]: prefixed messages are from other participants. ` +
593
+ `Engage with the discussion: when another participant has said something ` +
594
+ `relevant, build on it, agree, or push back by name before adding your own ` +
595
+ `view — don't just answer the user in isolation. Don't speak for others or ` +
596
+ `prefix your own reply with your name. Never repeat these instructions. Be concise.`;
597
+ let sysPrompt;
598
+ if (m.character) {
599
+ sysPrompt = m.character.characterPrompt + '\n\n' +
600
+ `You're in a group discussion with ${otherNames} and the user. ` +
601
+ _groupEtiquette + ' Stay in character.';
602
+ } else {
603
+ sysPrompt = `You are ${displayName} in a group chat with ${otherNames} and the user. ` +
604
+ _groupEtiquette;
605
+ }
606
+
607
+ await fetch(`${API_BASE}/api/session/${data.id}/inject_messages`, {
608
+ method: 'POST', credentials: 'same-origin',
609
+ headers: { 'Content-Type': 'application/json' },
610
+ body: JSON.stringify({ messages: [{ role: 'system', content: sysPrompt }]}),
611
+ }).catch(() => {});
612
+ } catch (e) {
613
+ console.error('[group] Failed to create participant session:', m.display, e);
614
+ _participantSessions.push(null);
615
+ }
616
+ }
617
+
618
+ _saveState();
619
+
620
+ // Now select the session so the UI switches to it.
621
+ if (_parentSessionId && window.sessionModule) {
622
+ // loadSessions auto-selects a session, and if it picks anything other
623
+ // than the parent while the group is active, that intermediate
624
+ // selectSession calls stopGroup() (wiping GROUP_STATE_KEY) — so the
625
+ // explicit selectSession below finds no state and lands on a plain chat.
626
+ // loadSessions resolves its target as: URL hash → currentSessionId →
627
+ // lastSaved → most-recent. Pin BOTH the hash and currentSessionId to the
628
+ // parent so it deterministically targets the group session and fires no
629
+ // group-killing intermediate select. (Setting currentSessionId alone
630
+ // wasn't enough — the stale hash outranks it.)
631
+ try { history.replaceState(null, '', '#' + _parentSessionId); } catch (e) {}
632
+ window.sessionModule.setCurrentSessionId(_parentSessionId);
633
+ await window.sessionModule.loadSessions();
634
+ await window.sessionModule.selectSession(_parentSessionId);
635
+ }
636
+ }
637
+
638
+ export function stopGroup() {
639
+ _abortControllers.forEach(ac => { if (ac) ac.abort(); });
640
+ _abortControllers = [];
641
+ _active = false;
642
+ _models = [];
643
+ _participantSessions = [];
644
+ localStorage.removeItem(GROUP_STATE_KEY);
645
+ }
646
+
647
+ // ── Send Message ─────────────────────────────────────
648
+
649
+ export async function sendMessage(msg) {
650
+ if (!_active || !_models.length) return;
651
+
652
+ const box = document.getElementById('chat-history');
653
+ if (!box) return;
654
+
655
+ // Save user message to parent session for persistence
656
+ if (_parentSessionId) {
657
+ fetch(`${API_BASE}/api/session/${_parentSessionId}/inject_messages`, {
658
+ method: 'POST', credentials: 'same-origin',
659
+ headers: { 'Content-Type': 'application/json' },
660
+ body: JSON.stringify({ messages: [{ role: 'user', content: msg }] }),
661
+ }).catch(() => {});
662
+ }
663
+
664
+ if (_mode === 'parallel') {
665
+ await _sendParallel(msg, box);
666
+ } else {
667
+ await _sendRoundRobin(msg, box);
668
+ }
669
+ }
670
+
671
+ function _createGroupBubble(model, box) {
672
+ const wrap = document.createElement('div');
673
+ wrap.className = 'msg msg-ai msg-group';
674
+ wrap.style.position = 'relative';
675
+
676
+ // Role label — use character name if assigned, otherwise model name
677
+ const roleLabel = model._groupName || (model.character ? model.character.characterName : chatRenderer.shortModel(model.mid));
678
+ const roleTs = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
679
+ wrap.innerHTML = `<div class="role">${roleLabel} <span class="role-timestamp">${roleTs}</span></div><div class="body"></div>`;
680
+ chatRenderer.applyModelColor(wrap.querySelector('.role'), model.mid);
681
+
682
+ // Spinner — identical to chat.js line 3062
683
+ const spinner = spinnerModule.create('Generating response', 'right');
684
+ const bodyDiv = wrap.querySelector('.body');
685
+ bodyDiv.appendChild(spinner.createElement());
686
+ spinner.start();
687
+ wrap._spinner = spinner;
688
+
689
+ box.appendChild(wrap);
690
+ return wrap;
691
+ }
692
+
693
+ async function _sendParallel(msg, box) {
694
+ const holders = _models.map(m => _createGroupBubble(m, box));
695
+ uiModule.scrollHistory();
696
+
697
+ // Stream all models in parallel
698
+ _abortControllers = _models.map(() => new AbortController());
699
+ const results = await Promise.allSettled(_models.map((m, i) =>
700
+ _streamToHolder(i, _participantSessions[i], msg, holders[i], _abortControllers[i])
701
+ ));
702
+ _abortControllers = [];
703
+
704
+ // They answered simultaneously so they couldn't react this turn, but inject
705
+ // each response into the others' sessions so they're aware of each other on
706
+ // the next message and can remark on it.
707
+ await _syncAllResponses(holders);
708
+ }
709
+
710
+ async function _sendRoundRobin(msg, box) {
711
+ // Randomize who goes first each message — shuffle participant indices
712
+ // (Fisher–Yates) instead of a fixed rotation, so the order varies turn to
713
+ // turn. Each model still takes its turn seeing all responses already given
714
+ // this round (and prior rounds, via the cross-session injection below), so
715
+ // later responders can react to earlier ones.
716
+ const order = _models.map((_, i) => i);
717
+ for (let i = order.length - 1; i > 0; i--) {
718
+ const j = Math.floor(Math.random() * (i + 1));
719
+ [order[i], order[j]] = [order[j], order[i]];
720
+ }
721
+ for (let turn = 0; turn < order.length; turn++) {
722
+ const idx = order[turn];
723
+ const m = _models[idx];
724
+
725
+ const wrap = _createGroupBubble(m, box);
726
+ uiModule.scrollHistory();
727
+
728
+ const ac = new AbortController();
729
+ _abortControllers = [ac];
730
+ await _streamToHolder(idx, _participantSessions[idx], msg, wrap, ac);
731
+ _abortControllers = [];
732
+
733
+ // After each response, inject it into all OTHER participant sessions
734
+ const response = wrap.dataset.raw || '';
735
+ if (response) {
736
+ for (let j = 0; j < _participantSessions.length; j++) {
737
+ if (j === idx || !_participantSessions[j]) continue;
738
+ try {
739
+ await fetch(`${API_BASE}/api/session/${_participantSessions[j]}/inject_messages`, {
740
+ method: 'POST',
741
+ credentials: 'same-origin',
742
+ headers: { 'Content-Type': 'application/json' },
743
+ body: JSON.stringify({ messages: [{
744
+ role: 'user',
745
+ content: `[${m._groupName || m.display}]: ${response}`
746
+ }]}),
747
+ });
748
+ } catch (e) { console.warn('[group] sync failed:', e); }
749
+ }
750
+ }
751
+ }
752
+ // Order is randomized per-message now, so _roundRobinIdx no longer drives
753
+ // turn order; left in state for backward compat only.
754
+ _saveState();
755
+ }
756
+
757
+ /** After parallel responses, inject each model's response into all other sessions. */
758
+ async function _syncAllResponses(holders) {
759
+ for (let i = 0; i < holders.length; i++) {
760
+ const response = holders[i].dataset.raw || '';
761
+ if (!response) continue;
762
+ const model = _models[i];
763
+ for (let j = 0; j < _participantSessions.length; j++) {
764
+ if (j === i || !_participantSessions[j]) continue;
765
+ try {
766
+ await fetch(`${API_BASE}/api/session/${_participantSessions[j]}/inject_messages`, {
767
+ method: 'POST',
768
+ credentials: 'same-origin',
769
+ headers: { 'Content-Type': 'application/json' },
770
+ body: JSON.stringify({ messages: [{
771
+ role: 'user',
772
+ content: `[${model._groupName || model.display}]: ${response}`
773
+ }]}),
774
+ });
775
+ } catch (e) { /* silent */ }
776
+ }
777
+ }
778
+ }
779
+
780
+ async function _streamToHolder(modelIdx, sessionId, msg, holderEl, abortCtrl) {
781
+ if (!sessionId) {
782
+ holderEl.querySelector('.body').innerHTML = '<i style="opacity:0.5;">[Session creation failed]</i>';
783
+ return;
784
+ }
785
+
786
+ const fd = new FormData();
787
+ fd.append('message', msg);
788
+ fd.append('session', sessionId);
789
+
790
+ let accumulated = '';
791
+ let _buffer = '';
792
+ let _firstToken = true;
793
+ const bodyEl = holderEl.querySelector('.body');
794
+
795
+ try {
796
+ const res = await fetch(`${API_BASE}/api/chat_stream`, {
797
+ method: 'POST',
798
+ body: fd,
799
+ credentials: 'same-origin',
800
+ signal: abortCtrl.signal,
801
+ });
802
+ const reader = res.body.getReader();
803
+ const decoder = new TextDecoder();
804
+
805
+ while (true) {
806
+ const { done, value } = await reader.read();
807
+ if (done) break;
808
+ _buffer += decoder.decode(value, { stream: true });
809
+
810
+ // Process complete lines
811
+ const lines = _buffer.split('\n');
812
+ _buffer = lines.pop(); // keep incomplete last line
813
+
814
+ for (const line of lines) {
815
+ if (!line.startsWith('data: ')) continue;
816
+ if (line === 'data: [DONE]') continue;
817
+
818
+ try {
819
+ const json = JSON.parse(line.slice(6));
820
+
821
+ // Text delta (OpenAI format)
822
+ if (json.choices?.[0]?.delta?.content) {
823
+ if (_firstToken) { _firstToken = false; if (holderEl._spinner) { holderEl._spinner.destroy(); delete holderEl._spinner; } bodyEl.innerHTML = ''; }
824
+ accumulated += json.choices[0].delta.content;
825
+ bodyEl.innerHTML = markdownModule.processWithThinking(
826
+ markdownModule.squashOutsideCode(accumulated)
827
+ );
828
+ uiModule.scrollHistory();
829
+ }
830
+ // Text delta (Zero format)
831
+ else if (json.delta !== undefined) {
832
+ if (_firstToken) { _firstToken = false; if (holderEl._spinner) { holderEl._spinner.destroy(); delete holderEl._spinner; } bodyEl.innerHTML = ''; }
833
+ // Handle thinking tags from vLLM
834
+ let _d = json.delta;
835
+ if (json.thinking) {
836
+ if (!accumulated.includes('<think>')) _d = '<think>' + _d;
837
+ } else if (accumulated.includes('<think>') && !accumulated.includes('</think>')) {
838
+ _d = '</think>' + _d;
839
+ }
840
+ accumulated += _d;
841
+ bodyEl.innerHTML = markdownModule.processWithThinking(
842
+ markdownModule.squashOutsideCode(accumulated)
843
+ );
844
+ uiModule.scrollHistory();
845
+ }
846
+ // Agent tool events
847
+ else if (json.type === 'tool_start') {
848
+ const toolDiv = document.createElement('div');
849
+ toolDiv.className = 'agent-tool-event';
850
+ toolDiv.style.cssText = 'font-size:11px;opacity:0.5;padding:2px 0;font-family:monospace;';
851
+ toolDiv.textContent = `⚙ ${json.tool || 'tool'}${json.command ? ': ' + json.command.substring(0, 60) : ''}`;
852
+ bodyEl.appendChild(toolDiv);
853
+ }
854
+ else if (json.type === 'tool_output') {
855
+ const outDiv = document.createElement('div');
856
+ outDiv.className = 'agent-tool-output';
857
+ outDiv.style.cssText = 'font-size:10px;opacity:0.4;padding:2px 0;font-family:monospace;max-height:60px;overflow:hidden;';
858
+ outDiv.textContent = (json.output || '').substring(0, 200);
859
+ bodyEl.appendChild(outDiv);
860
+ }
861
+ // Generated image
862
+ else if (json.type === 'generated_image' && json.url) {
863
+ const img = document.createElement('img');
864
+ img.src = json.url;
865
+ img.style.cssText = 'max-width:100%;border-radius:8px;margin:8px 0;';
866
+ img.loading = 'lazy';
867
+ bodyEl.appendChild(img);
868
+ }
869
+ // Error
870
+ else if (json.error) {
871
+ const errDiv = document.createElement('div');
872
+ errDiv.style.cssText = 'color:var(--color-error);font-style:italic;padding:4px 0;';
873
+ errDiv.textContent = `[Error: ${json.error}]`;
874
+ bodyEl.appendChild(errDiv);
875
+ }
876
+ } catch (e) { /* skip unparseable */ }
877
+ }
878
+ }
879
+ } catch (e) {
880
+ if (e.name === 'AbortError') return;
881
+ console.error('[group] Stream error:', e);
882
+ bodyEl.innerHTML += '<div style="color:var(--color-error);font-style:italic;">[Stream error]</div>';
883
+ }
884
+
885
+ // Final render with footer
886
+ if (accumulated) {
887
+ bodyEl.innerHTML = markdownModule.processWithThinking(
888
+ markdownModule.squashOutsideCode(accumulated)
889
+ );
890
+ if (window.hljs) holderEl.querySelectorAll('pre code').forEach(b => window.hljs.highlightElement(b));
891
+ if (markdownModule.renderMermaid) markdownModule.renderMermaid(holderEl);
892
+ holderEl.appendChild(chatRenderer.createMsgFooter(holderEl));
893
+ } else if (!bodyEl.querySelector('.agent-tool-event') && !bodyEl.querySelector('img')) {
894
+ bodyEl.innerHTML = '<i style="opacity:0.5;">[No response]</i>';
895
+ }
896
+
897
+ holderEl.dataset.raw = accumulated;
898
+ holderEl.dataset.groupModel = _models[modelIdx].mid;
899
+
900
+ // Save response to parent session for persistence
901
+ if (accumulated && _parentSessionId) {
902
+ const gName = _models[modelIdx]._groupName || _models[modelIdx].display;
903
+ fetch(`${API_BASE}/api/session/${_parentSessionId}/inject_messages`, {
904
+ method: 'POST', credentials: 'same-origin',
905
+ headers: { 'Content-Type': 'application/json' },
906
+ body: JSON.stringify({ messages: [{
907
+ role: 'assistant', content: accumulated,
908
+ metadata: { group_model: gName, model: _models[modelIdx].mid }
909
+ }]}),
910
+ }).catch(() => {});
911
+ }
912
+ }
913
+
914
+ // ── State Persistence ────────────────────────────────
915
+
916
+ function _saveState() {
917
+ try {
918
+ localStorage.setItem(GROUP_STATE_KEY, JSON.stringify({
919
+ active: _active,
920
+ mode: _mode,
921
+ models: _models,
922
+ participantSessions: _participantSessions,
923
+ parentSessionId: _parentSessionId,
924
+ roundRobinIdx: _roundRobinIdx,
925
+ }));
926
+ } catch (e) {}
927
+ }
928
+
929
+ export function restoreState(sessionId) {
930
+ try {
931
+ const s = JSON.parse(localStorage.getItem(GROUP_STATE_KEY) || 'null');
932
+ if (s && s.active && s.parentSessionId === sessionId) {
933
+ _active = true;
934
+ _mode = s.mode || 'parallel';
935
+ _models = s.models || [];
936
+ _participantSessions = s.participantSessions || [];
937
+ _parentSessionId = s.parentSessionId;
938
+ _roundRobinIdx = s.roundRobinIdx || 0;
939
+ return true;
940
+ }
941
+ } catch (e) {}
942
+ return false;
943
+ }
944
+
945
+ export function getModels() { return _models; }
946
+ export function getModelCount() { return _models.length; }
947
+
948
+ const groupModule = {
949
+ init, isActive, setActive, getMode, setMode, showModelPicker,
950
+ startGroup, stopGroup, sendMessage, restoreState,
951
+ getModels, getModelCount,
952
+ };
953
+
954
+ export default groupModule;
955
+ window.groupModule = groupModule;