@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,1245 @@
1
+ /**
2
+ * emailInbox.js — Email inbox list in sidebar.
3
+ * Follows the session list pattern: list items, click to open as document, archive, etc.
4
+ */
5
+
6
+ import spinnerModule from './spinner.js';
7
+ import sessionModule from './sessions.js';
8
+ import { initEmailLibrary, openEmailLibrary, closeEmailLibrary, isOpen as isLibOpen, prewarmEmailLibrary } from './emailLibrary.js';
9
+ import * as Modals from './modalManager.js';
10
+ import { applyEdgeDock } from './modalSnap.js';
11
+ import { buildReplyAllCc } from './emailLibrary/replyRecipients.js';
12
+
13
+ const API_BASE = window.location.origin;
14
+ const _acct = () => window.__zeroActiveEmailAccount
15
+ ? `&account_id=${encodeURIComponent(window.__zeroActiveEmailAccount)}`
16
+ : '';
17
+
18
+ const _emailSetupHint = () => '<div style="margin-top:6px;opacity:0.72;font-size:11px;">Setup: <span style="color:var(--accent,var(--red));">Settings &rsaquo; Integrations</span></div>';
19
+
20
+ // SVG icons matching sessions.js dropdown style
21
+ const _replyIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg>';
22
+ const _archiveIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="5" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/></svg>';
23
+ const _deleteIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>';
24
+ const _unreadIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3" fill="currentColor"/></svg>';
25
+ const _starIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>';
26
+ const _starFilledIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>';
27
+ const _bellIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>';
28
+ const _icon = (svg) => `<span class="dropdown-icon">${svg}</span>`;
29
+ const _replySeparator = '---------- Previous message ----------';
30
+
31
+ function _cleanAiReplyText(text) {
32
+ if (!text) return '';
33
+ let t = String(text);
34
+ const open = /<<<\s*(?:REPLY|SUMMARY|OUTPUT)\s*>>+/i;
35
+ const close = /<<<\s*END\s*>>+/i;
36
+ const m = open.exec(t);
37
+ if (m) {
38
+ const rest = t.slice(m.index + m[0].length);
39
+ const c = close.exec(rest);
40
+ t = c ? rest.slice(0, c.index) : rest;
41
+ }
42
+ return t
43
+ .replace(/<<<\s*(?:REPLY|SUMMARY|OUTPUT)\s*>>+/gi, '')
44
+ .replace(/<<<\s*END\s*>>+/gi, '')
45
+ .trim();
46
+ }
47
+
48
+ function _shouldUseFastAiReply(data) {
49
+ const body = String(data?.body || data?.body_html || '');
50
+ const subject = String(data?.subject || '');
51
+ const atts = Array.isArray(data?.attachments) ? data.attachments : [];
52
+ if (atts.length > 0) return false;
53
+ const text = `${subject}\n${body}`.toLowerCase();
54
+ if (/\b(attach(?:ed|ment)?|pdf|document|contract|invoice|receipt|quote|estimate|proposal|question|questions|details|schedule|booking|reservation|meeting|calendar|availability|confirm|confirmation|review|sign|signature)\b/.test(text)) {
55
+ return false;
56
+ }
57
+ return body.length < 2500;
58
+ }
59
+
60
+ let _emails = [];
61
+ let _currentFolder = 'INBOX';
62
+ let _offset = 0;
63
+ let _total = 0;
64
+
65
+ // Replying to an email marks the source \Answered server-side and fires
66
+ // `email-answered`. Reflect it live in the inbox list so it shows as done
67
+ // immediately (no manual refresh needed).
68
+ window.addEventListener('email-answered', (e) => {
69
+ const uid = e.detail && e.detail.uid;
70
+ if (uid == null) return;
71
+ const em = _emails.find(x => String(x.uid) === String(uid));
72
+ if (em) { em.is_answered = true; em.is_read = true; }
73
+ document.querySelectorAll('.email-item[data-uid="' + CSS.escape(String(uid)) + '"]').forEach(item => {
74
+ item.classList.remove('email-unread');
75
+ const check = item.querySelector('.email-done-check');
76
+ if (check) check.classList.add('active');
77
+ });
78
+ });
79
+ let _loading = false;
80
+ let _expanded = false;
81
+ let _docModule = null;
82
+ let _listSpinner = null;
83
+ let _senderFilter = null; // email address (lowercased) to filter by, or null
84
+ let _senderFilterLabel = null; // display label for the active filter chip
85
+
86
+ export function init(documentModule) {
87
+ _docModule = documentModule;
88
+ _bindEvents();
89
+ // Init the library popup with a callback to open emails
90
+ initEmailLibrary({
91
+ documentModule,
92
+ onEmailClick: async (opts) => {
93
+ // Reply / AI Reply / Compose open a draft in the doc editor.
94
+ // - Desktop: dock the email to the LEFT so it stays visible beside the
95
+ // reply draft (which opens on the right) — read-while-you-reply.
96
+ // - Mobile: there's no room for a split, so minimize the email modal;
97
+ // the draft comes to the front and the inbox stays a tap away as a
98
+ // minimized chip.
99
+ // Never call closeEmailLibrary() here — that destroys state.
100
+ try {
101
+ if (Modals.isRegistered('email-lib-modal')) {
102
+ const emailModal = document.getElementById('email-lib-modal');
103
+ if (window.innerWidth > 768 && emailModal && !emailModal.classList.contains('hidden')) {
104
+ applyEdgeDock(emailModal, 'left');
105
+ }
106
+ // Mobile: do NOT pre-mount the pane here. The load path (open/inject)
107
+ // mounts it exactly once when the doc is ready; the doc-view z-index
108
+ // rule slides it up OVER the email (which stays behind). Pre-mounting
109
+ // here caused a double-mount — the early pane was torn down by the
110
+ // compose session-switch, then remounted, which looked like a doc
111
+ // flashing before the smooth slide.
112
+ }
113
+ } catch (_) {}
114
+ if (opts.compose) { _composeNew(); return; }
115
+ if (opts.email) {
116
+ await _openEmail(opts.email, null, opts.emailData, opts.mode || 'reply');
117
+ }
118
+ },
119
+ });
120
+ _watchDocOpenToReDockEmail();
121
+ }
122
+
123
+ export async function openReplyDraft(uid, folder = 'INBOX', mode = 'reply') {
124
+ if (!uid) return;
125
+ const previousFolder = _currentFolder;
126
+ _currentFolder = folder || 'INBOX';
127
+ try {
128
+ await _openEmail({ uid: String(uid), subject: '' }, null, null, mode || 'reply');
129
+ } finally {
130
+ _currentFolder = previousFolder || _currentFolder;
131
+ }
132
+ }
133
+
134
+ // When the document editor pane opens (body.doc-view turns on), make sure the
135
+ // email modal is on the LEFT — even if it was previously docked RIGHT or
136
+ // floating — so the email and the doc always end up side-by-side. The actual
137
+ // width math lives in modalSnap.js (`_anchorLeftDock` shrinks the email when
138
+ // the doc is rendered to the right).
139
+ let _docOpenObs = null;
140
+ function _watchDocOpenToReDockEmail() {
141
+ if (_docOpenObs) return;
142
+ if (typeof MutationObserver === 'undefined') return;
143
+ let last = document.body.classList.contains('doc-view');
144
+ _docOpenObs = new MutationObserver(() => {
145
+ const cur = document.body.classList.contains('doc-view');
146
+ if (cur && !last) {
147
+ if (window.innerWidth > 768) {
148
+ const emailModal = document.getElementById('email-lib-modal');
149
+ if (emailModal && !emailModal.classList.contains('hidden')) {
150
+ // Already left-docked → nothing to do (modalSnap re-anchors on its own).
151
+ if (!emailModal.classList.contains('modal-left-docked')) {
152
+ try { applyEdgeDock(emailModal, 'left'); } catch (_) {}
153
+ }
154
+ }
155
+ // Same treatment for an open email-reader modal (one specific email
156
+ // open standalone — typical "click email, click doc" flow).
157
+ document.querySelectorAll('.modal[id^="email-reader-"]').forEach(m => {
158
+ if (m.classList.contains('hidden')) return;
159
+ if (m.classList.contains('modal-left-docked')) return;
160
+ try { applyEdgeDock(m, 'left'); } catch (_) {}
161
+ });
162
+ }
163
+ }
164
+ last = cur;
165
+ });
166
+ _docOpenObs.observe(document.body, { attributes: true, attributeFilter: ['class'] });
167
+ }
168
+
169
+ function _bindEvents() {
170
+ // Clicking anywhere in the email section header opens the popup
171
+ // (except the compose button which has its own handler)
172
+ const section = document.getElementById('email-section');
173
+ const header = section?.querySelector('.section-header-flex');
174
+ if (header) {
175
+ header.style.cursor = 'pointer';
176
+ header.addEventListener('click', (e) => {
177
+ if (e.target.closest('#email-compose-btn')) return;
178
+ openEmailLibrary();
179
+ markInboxAsSeen();
180
+ });
181
+ }
182
+
183
+ // Compose button creates a new email document
184
+ const composeBtn = document.getElementById('email-compose-btn');
185
+ if (composeBtn) {
186
+ composeBtn.addEventListener('click', (e) => {
187
+ e.stopPropagation();
188
+ _composeNew();
189
+ });
190
+ }
191
+
192
+ // Initial unread count check, refresh every 60s
193
+ _refreshUnreadCount();
194
+ setInterval(_refreshUnreadCount, 60000);
195
+ prewarmEmailLibrary({ delay: 3000 });
196
+
197
+ // Deep-link: #email=<folder>:<uid> opens the library and expands that card
198
+ _maybeOpenFromHash();
199
+ window.addEventListener('hashchange', _maybeOpenFromHash);
200
+ }
201
+
202
+ function _maybeOpenFromHash() {
203
+ const h = window.location.hash || '';
204
+ const m = h.match(/^#email=([^:]+):(\d+)/);
205
+ if (!m) return;
206
+ const folder = decodeURIComponent(m[1]);
207
+ const uid = m[2];
208
+ try { openEmailLibrary({ folder, uid }); } catch (e) { console.error(e); }
209
+ // Clear the hash so reloads don't reopen
210
+ try { history.replaceState(null, '', window.location.pathname + window.location.search); } catch (_) {}
211
+ }
212
+
213
+ // Tint helper — turns the urgent-email-scanner's max_score into a dot color.
214
+ // Falls back to the default (blue / unset) when scanner is off or no urgent.
215
+ function _urgencyColor(score) {
216
+ if (score >= 3) return 'var(--color-error, #e06c75)'; // red — urgent now
217
+ if (score === 2) return '#f0ad4e'; // orange — reply soon
218
+ return ''; // default (blue / theme)
219
+ }
220
+
221
+ async function _refreshUnreadCount() {
222
+ // Default the dot to hidden — only the verified "new mail above threshold"
223
+ // path below should turn it on. Without this, a fetch error or a backend
224
+ // returning malformed data left a stale dot from a previous account/session.
225
+ const dot = document.getElementById('email-unread-dot');
226
+ if (dot && !dot._stickyState) dot.style.display = 'none';
227
+ try {
228
+ // Parallel: unread list + urgency state.
229
+ const [listRes, urgRes] = await Promise.all([
230
+ fetch(`${API_BASE}/api/email/list?folder=INBOX&limit=50&filter=unread${_acct()}`),
231
+ fetch(`${API_BASE}/api/email/urgency-state`, { credentials: 'same-origin' }).catch(() => null),
232
+ ]);
233
+ if (!listRes || !listRes.ok) return;
234
+ const data = await listRes.json();
235
+ if (!dot) return;
236
+
237
+ const emails = data.emails || [];
238
+ if (emails.length === 0) {
239
+ dot.style.display = 'none';
240
+ return;
241
+ }
242
+
243
+ // Compare highest unread UID to the last-seen threshold in localStorage
244
+ const lastSeen = parseInt(localStorage.getItem('zero-email-last-seen-uid') || '0', 10);
245
+ const maxUid = Math.max(...emails.map(e => parseInt(e.uid, 10) || 0));
246
+
247
+ // Only show dot if there's a new email above the threshold
248
+ dot.style.display = maxUid > lastSeen ? '' : 'none';
249
+
250
+ // Color the dot by urgency tier. Cache the per-uid map so the per-row
251
+ // renderer can reuse it without a second fetch.
252
+ if (dot.style.display !== 'none' && urgRes && urgRes.ok) {
253
+ try {
254
+ const ud = await urgRes.json();
255
+ window._emailUrgencyState = ud;
256
+ const tint = _urgencyColor(ud.max_score || 0);
257
+ if (tint) dot.style.backgroundColor = tint;
258
+ else dot.style.backgroundColor = '';
259
+ } catch (_) {}
260
+ } else if (dot.style.display !== 'none') {
261
+ dot.style.backgroundColor = '';
262
+ }
263
+ } catch (e) {
264
+ // Network/parse error — keep the dot hidden (default at the top).
265
+ if (dot) dot.style.display = 'none';
266
+ }
267
+ }
268
+
269
+ export function markInboxAsSeen() {
270
+ // Called when the user opens the inbox popup — clears the notif dot
271
+ try {
272
+ // Find current max UID so subsequent arrivals trigger the dot
273
+ fetch(`${API_BASE}/api/email/list?folder=INBOX&limit=1${_acct()}`)
274
+ .then(r => r.json())
275
+ .then(data => {
276
+ const emails = data.emails || [];
277
+ if (emails.length > 0) {
278
+ const maxUid = Math.max(...emails.map(e => parseInt(e.uid, 10) || 0));
279
+ localStorage.setItem('zero-email-last-seen-uid', String(maxUid));
280
+ }
281
+ const dot = document.getElementById('email-unread-dot');
282
+ if (dot) dot.style.display = 'none';
283
+ })
284
+ .catch(() => {});
285
+ } catch (e) {}
286
+ }
287
+
288
+ export async function loadEmails(append = false) {
289
+ if (_loading) return;
290
+ _loading = true;
291
+
292
+ const list = document.getElementById('email-list');
293
+ if (!list) { _loading = false; return; }
294
+
295
+ if (!append) {
296
+ list.innerHTML = '';
297
+ // Show whirlpool spinner
298
+ if (_listSpinner) { _listSpinner.destroy(); _listSpinner = null; }
299
+ const sp = spinnerModule.createWhirlpool(20);
300
+ _listSpinner = sp;
301
+ list.appendChild(sp.element);
302
+ }
303
+
304
+ try {
305
+ const fromQS = _senderFilter ? `&from=${encodeURIComponent(_senderFilter)}` : '';
306
+ const res = await fetch(`${API_BASE}/api/email/list?folder=${encodeURIComponent(_currentFolder)}&limit=50&offset=${_offset}${fromQS}${_acct()}&_=${Date.now()}`);
307
+ const data = await res.json();
308
+ if (data.error) throw new Error(data.error);
309
+
310
+ if (!append) _emails = [];
311
+ _emails.push(...(data.emails || []));
312
+ _total = data.total || 0;
313
+
314
+ // Remove spinner
315
+ if (_listSpinner) { _listSpinner.destroy(); _listSpinner = null; }
316
+
317
+ _renderList();
318
+
319
+ const unreadCount = _emails.filter(e => !e.is_read).length;
320
+ const dot = document.getElementById('email-unread-dot');
321
+ if (dot) dot.style.display = unreadCount > 0 ? '' : 'none';
322
+ } catch (e) {
323
+ console.error('Failed to load emails:', e);
324
+ if (_listSpinner) { _listSpinner.destroy(); _listSpinner = null; }
325
+ if (!append && list) {
326
+ const msg = e && e.message ? `Failed to load: ${e.message}` : 'Failed to load';
327
+ list.innerHTML = `<div class="email-loading">${msg.replace(/&/g, '&amp;').replace(/</g, '&lt;')}${_emailSetupHint()}</div>`;
328
+ }
329
+ } finally {
330
+ _loading = false;
331
+ }
332
+ }
333
+
334
+ async function loadFolders() {
335
+ try {
336
+ const res = await fetch(`${API_BASE}/api/email/folders?_=1${_acct()}`);
337
+ const data = await res.json();
338
+ const select = document.getElementById('email-folder-select');
339
+ if (!select || !data.folders) return;
340
+ _populateFolderSelect(select, data.folders);
341
+ } catch (e) {
342
+ console.error('Failed to load folders:', e);
343
+ }
344
+ }
345
+
346
+ export function sortedFolders(folders) {
347
+ const roleOf = (folder) => {
348
+ const f = String(folder || '').toLowerCase();
349
+ if (f === 'inbox') return 'inbox';
350
+ if (f.includes('sent')) return 'sent';
351
+ if (f.includes('starred') || f.includes('flagged')) return 'starred';
352
+ if (f.includes('draft')) return 'drafts';
353
+ if (f.includes('all mail') || f.includes('archive')) return 'archive';
354
+ if (f.includes('spam') || f.includes('junk')) return 'junk';
355
+ if (f.includes('trash') || f.includes('bin') || f.includes('deleted')) return 'trash';
356
+ return '';
357
+ };
358
+ const roleOrder = ['inbox', 'sent', 'starred', 'archive', 'junk', 'trash', 'drafts'];
359
+ const found = new Map();
360
+ const others = [];
361
+ for (const f of folders) {
362
+ const role = roleOf(f);
363
+ if (role && !found.has(role)) found.set(role, f);
364
+ else others.push(f);
365
+ }
366
+ return { priority: roleOrder.map(role => found.get(role)).filter(Boolean), others };
367
+ }
368
+
369
+ export function folderDisplayName(folder) {
370
+ const raw = String(folder || '');
371
+ const f = raw.toLowerCase();
372
+ if (f === 'inbox') return 'INBOX';
373
+ if (f.includes('all mail')) return 'Archive / All Mail';
374
+ if (f.includes('archive')) return 'Archive';
375
+ if (f.includes('spam')) return 'Spam';
376
+ if (f.includes('junk')) return 'Junk';
377
+ if (f.includes('trash') || f.includes('bin') || f.includes('deleted')) return 'Trash';
378
+ if (f.includes('sent')) return 'Sent';
379
+ if (f.includes('draft')) return 'Drafts';
380
+ return raw;
381
+ }
382
+
383
+ function _populateFolderSelect(select, folders) {
384
+ select.innerHTML = '';
385
+ const { priority, others } = sortedFolders(folders);
386
+
387
+ for (const folder of priority) {
388
+ const opt = document.createElement('option');
389
+ opt.value = folder;
390
+ opt.textContent = folderDisplayName(folder);
391
+ if (folder === _currentFolder) opt.selected = true;
392
+ select.appendChild(opt);
393
+ }
394
+
395
+ if (priority.length > 0 && others.length > 0) {
396
+ const sep = document.createElement('option');
397
+ sep.disabled = true;
398
+ sep.textContent = '─────────';
399
+ select.appendChild(sep);
400
+ }
401
+
402
+ for (const folder of others) {
403
+ const opt = document.createElement('option');
404
+ opt.value = folder;
405
+ opt.textContent = folderDisplayName(folder);
406
+ if (folder === _currentFolder) opt.selected = true;
407
+ select.appendChild(opt);
408
+ }
409
+ }
410
+
411
+ function _renderList() {
412
+ const list = document.getElementById('email-list');
413
+ if (!list) return;
414
+ list.innerHTML = '';
415
+
416
+ if (_senderFilter) {
417
+ const chip = document.createElement('div');
418
+ chip.className = 'email-filter-chip';
419
+ chip.innerHTML = `<span class="email-filter-chip-label">From: ${_esc(_senderFilterLabel || _senderFilter)}</span><button class="email-filter-chip-clear" title="Clear filter">&times;</button>`;
420
+ chip.querySelector('.email-filter-chip-clear').addEventListener('click', () => _clearSenderFilter());
421
+ list.appendChild(chip);
422
+ }
423
+
424
+ if (_emails.length === 0) {
425
+ const empty = document.createElement('div');
426
+ empty.className = 'email-loading';
427
+ empty.textContent = _senderFilter ? `No emails from ${_senderFilterLabel || _senderFilter}` : 'No emails';
428
+ list.appendChild(empty);
429
+ return;
430
+ }
431
+
432
+ for (const em of _emails) {
433
+ list.appendChild(_createEmailItem(em));
434
+ }
435
+
436
+ const loadMore = document.getElementById('email-load-more');
437
+ if (loadMore) {
438
+ loadMore.style.display = (_emails.length < _total) ? '' : 'none';
439
+ }
440
+ }
441
+
442
+ function _setSenderFilter(addr, label) {
443
+ _senderFilter = addr;
444
+ _senderFilterLabel = label || addr;
445
+ _offset = 0;
446
+ loadEmails(false);
447
+ }
448
+
449
+ function _clearSenderFilter() {
450
+ _senderFilter = null;
451
+ _senderFilterLabel = null;
452
+ _offset = 0;
453
+ loadEmails(false);
454
+ }
455
+
456
+ function _createEmailItem(em) {
457
+ const item = document.createElement('div');
458
+ item.className = 'list-item email-item' + (em.is_spam_verdict ? ' email-item-spam' : '');
459
+ item.setAttribute('role', 'option');
460
+ item.setAttribute('data-uid', em.uid);
461
+
462
+ let dateStr = '';
463
+ if (em.date) {
464
+ try {
465
+ const d = new Date(em.date);
466
+ const now = new Date();
467
+ const isToday = d.toDateString() === now.toDateString();
468
+ if (isToday) {
469
+ dateStr = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
470
+ } else {
471
+ dateStr = d.toLocaleDateString([], { month: 'short', day: 'numeric' });
472
+ }
473
+ } catch (_) {}
474
+ }
475
+
476
+ const senderName = em.from_name || em.from_address;
477
+ const initial = (senderName || '?')[0].toUpperCase();
478
+ const color = _senderColor(senderName);
479
+
480
+ const attachIcon = em.has_attachments
481
+ ? '<span title="Has attachments" style="opacity:0.6;display:inline-flex;flex-shrink:0;margin-left:4px;"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 17.93 8.8l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg></span>'
482
+ : '';
483
+
484
+ // Per-row dot tint: if the urgency scanner flagged this UID, override the
485
+ // per-sender pastel with red (3) / orange (2). Look up by any cached key
486
+ // ending in `:<uid>` since the per_uid map is keyed `<account_id>:<uid>`
487
+ // and the inbox list doesn't surface the account id per row.
488
+ let _unreadColor = color;
489
+ let _unreadTitle = 'Unread';
490
+ try {
491
+ const us = window._emailUrgencyState;
492
+ if (us && us.per_uid && em.uid != null) {
493
+ const suffix = ':' + String(em.uid);
494
+ for (const k of Object.keys(us.per_uid)) {
495
+ if (k.endsWith(suffix)) {
496
+ const v = us.per_uid[k] || {};
497
+ const score = v.score || 0;
498
+ if (score >= 3) { _unreadColor = 'var(--color-error, #e06c75)'; _unreadTitle = 'Urgent — ' + (v.reason || 'needs reply now'); }
499
+ else if (score === 2) { _unreadColor = '#f0ad4e'; _unreadTitle = 'Reply soon — ' + (v.reason || ''); }
500
+ break;
501
+ }
502
+ }
503
+ }
504
+ } catch (_) {}
505
+ const unreadIcon = (!em.is_read && !em.is_answered)
506
+ ? `<span class="email-unread-dot-inline" title="${_esc(_unreadTitle)}" style="display:inline-flex;align-items:center;flex-shrink:0;margin-left:4px;color:${_unreadColor}"><svg width="8" height="8" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="6"/></svg></span>`
507
+ : '';
508
+
509
+ const tags = Array.isArray(em.tags) ? em.tags : [];
510
+ const tagPills = tags.length
511
+ ? `<span class="email-tags">${tags.map(t => `<span class="email-tag email-tag-${_esc(t)}">${_esc(t)}</span>`).join('')}</span>`
512
+ : '';
513
+
514
+ const spamTag = em.is_spam_verdict
515
+ ? `<span class="email-tag email-tag-spam" title="AI flagged as spam — click ✓ to unflag">spam <button class="email-spam-unflag" data-uid="${em.uid}" title="Not spam">\u2713</button></span>`
516
+ : '';
517
+
518
+ const senderAddr = (em.from_address || '').toLowerCase();
519
+ item.innerHTML = `
520
+ <span class="email-avatar" style="background:${color}">${initial}</span>
521
+ <div class="email-item-content">
522
+ <div class="email-item-top">
523
+ <span class="email-sender email-sender-clickable" style="color:${color}" data-from-addr="${_esc(senderAddr)}" data-from-name="${_esc(senderName)}" title="Show all emails from ${_esc(senderName)}">${_esc(senderName)}</span>
524
+ <span class="email-date">${_esc(dateStr)}</span>
525
+ </div>
526
+ <div class="email-subject">${_esc(em.subject)}${unreadIcon}${attachIcon}${tagPills}${spamTag}</div>
527
+ </div>
528
+ <div class="email-menu-wrap">
529
+ <button class="hamburger email-menu-btn" title="Actions">
530
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg>
531
+ </button>
532
+ </div>
533
+ `;
534
+
535
+ // Click sender name → filter list to that sender
536
+ const senderEl = item.querySelector('.email-sender-clickable');
537
+ if (senderEl) {
538
+ senderEl.addEventListener('click', (e) => {
539
+ e.stopPropagation();
540
+ const addr = senderEl.dataset.fromAddr || '';
541
+ const name = senderEl.dataset.fromName || addr;
542
+ if (addr) _setSenderFilter(addr, name);
543
+ });
544
+ }
545
+
546
+ // Wire the "not spam" button
547
+ const unflagBtn = item.querySelector('.email-spam-unflag');
548
+ if (unflagBtn) {
549
+ unflagBtn.addEventListener('click', async (e) => {
550
+ e.stopPropagation();
551
+ try {
552
+ await fetch(`${API_BASE}/api/email/${em.uid}/unflag-spam`, {
553
+ method: 'POST', credentials: 'same-origin',
554
+ });
555
+ em.is_spam_verdict = false;
556
+ item.classList.remove('email-item-spam');
557
+ const tag = item.querySelector('.email-tag-spam');
558
+ if (tag) tag.remove();
559
+ } catch (_) {}
560
+ });
561
+ }
562
+
563
+ // Click to open — do NOT close sidebar
564
+ item.addEventListener('click', (e) => {
565
+ if (e.target.closest('.email-menu-wrap')) return;
566
+ if (item.dataset.swipeBlock === '1') return;
567
+ _openEmail(em, item);
568
+ });
569
+
570
+ const menuWrap = item.querySelector('.email-menu-wrap');
571
+ menuWrap.addEventListener('click', (e) => {
572
+ e.stopPropagation();
573
+ _showEmailMenu(em, menuWrap, item);
574
+ });
575
+
576
+ // Swipe left to archive (mobile). Mirrors sidebar-layout.js swipe pattern.
577
+ if ('ontouchstart' in window) {
578
+ let startX = 0, startY = 0, dx = 0, dy = 0, swiping = false, swiped = false;
579
+ const HORIZ_THRESHOLD = 70; // px to trigger archive
580
+ const VERT_CANCEL = 30; // px vertical motion cancels swipe (treat as scroll)
581
+
582
+ item.addEventListener('touchstart', (e) => {
583
+ if (e.target.closest('.email-menu-wrap')) return;
584
+ const t = e.touches[0];
585
+ startX = t.clientX; startY = t.clientY;
586
+ dx = 0; dy = 0; swiping = true; swiped = false;
587
+ item.style.transition = 'none';
588
+ }, { passive: true });
589
+
590
+ item.addEventListener('touchmove', (e) => {
591
+ if (!swiping) return;
592
+ const t = e.touches[0];
593
+ dx = t.clientX - startX;
594
+ dy = t.clientY - startY;
595
+ if (Math.abs(dy) > VERT_CANCEL) {
596
+ // Vertical scroll — cancel swipe
597
+ swiping = false;
598
+ item.style.transform = '';
599
+ return;
600
+ }
601
+ if (dx < 0) {
602
+ // Only swipe-left for archive; clamp at -160 so it doesn't fly off
603
+ const offset = Math.max(dx, -160);
604
+ item.style.transform = `translateX(${offset}px)`;
605
+ item.style.background = `linear-gradient(to right, transparent, transparent ${100 + offset/1.6}%, var(--red) ${100 + offset/1.6}%)`;
606
+ }
607
+ }, { passive: true });
608
+
609
+ item.addEventListener('touchend', () => {
610
+ if (!swiping) return;
611
+ swiping = false;
612
+ item.style.transition = 'transform 0.2s ease, opacity 0.2s ease';
613
+ if (dx <= -HORIZ_THRESHOLD) {
614
+ // Trigger archive — animate off-screen, suppress next click
615
+ swiped = true;
616
+ item.dataset.swipeBlock = '1';
617
+ item.style.transform = 'translateX(-100%)';
618
+ item.style.opacity = '0';
619
+ setTimeout(() => {
620
+ _archiveEmail(em);
621
+ delete item.dataset.swipeBlock;
622
+ }, 200);
623
+ } else {
624
+ // Snap back
625
+ item.style.transform = '';
626
+ item.style.background = '';
627
+ }
628
+ });
629
+
630
+ item.addEventListener('touchcancel', () => {
631
+ swiping = false;
632
+ item.style.transition = 'transform 0.2s ease';
633
+ item.style.transform = '';
634
+ item.style.background = '';
635
+ });
636
+ }
637
+
638
+ return item;
639
+ }
640
+
641
+ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply') {
642
+ const aiReplyMode = mode === 'ai-reply-fast' ? 'fast' : (mode === 'ai-reply-full' ? 'full' : '');
643
+ const wantsAiReply = mode === 'ai-reply' || !!aiReplyMode;
644
+ let aiSuggestedBody = null;
645
+ if (wantsAiReply) {
646
+ // Fall through to reply-all (not plain reply) so the generated AI
647
+ // draft addresses everyone on the original thread. On single-
648
+ // recipient emails this collapses to a regular reply since there's
649
+ // no one else to CC.
650
+ mode = 'reply-all';
651
+ }
652
+ // Show whirlpool spinner on the right side of the item (only if from sidebar)
653
+ let spinner = null;
654
+ if (itemEl) {
655
+ const sp = spinnerModule.createWhirlpool(16);
656
+ spinner = sp;
657
+ sp.element.style.cssText = 'margin:0;flex-shrink:0;';
658
+ const menuWrap = itemEl.querySelector('.email-menu-wrap');
659
+ if (menuWrap) menuWrap.style.display = 'none';
660
+ itemEl.appendChild(sp.element);
661
+ }
662
+
663
+ try {
664
+ let data = preloadedData;
665
+ if (!data) {
666
+ const res = await fetch(`${API_BASE}/api/email/read/${em.uid}?folder=${encodeURIComponent(_currentFolder)}${_acct()}`);
667
+ data = await res.json();
668
+ }
669
+ if (data.error) {
670
+ console.error('Failed to read email:', data.error);
671
+ return;
672
+ }
673
+ if (wantsAiReply) {
674
+ if (data.cached_ai_reply) {
675
+ aiSuggestedBody = _cleanAiReplyText(data.cached_ai_reply);
676
+ } else {
677
+ let draftToastTimer = null;
678
+ draftToastTimer = setTimeout(() => {
679
+ import('./ui.js').then(m => m.showToast && m.showToast('Drafting AI reply', { duration: 3000, leadingIcon: 'spinner' })).catch(() => {});
680
+ }, 450);
681
+ try {
682
+ let currentModel = '';
683
+ let currentSessionId = '';
684
+ try {
685
+ currentModel = sessionModule?.getCurrentModel() || '';
686
+ currentSessionId = sessionModule?.getCurrentSessionId() || '';
687
+ } catch (_) {}
688
+ const res = await fetch(`${API_BASE}/api/email/ai-reply`, {
689
+ method: 'POST',
690
+ headers: { 'Content-Type': 'application/json' },
691
+ body: JSON.stringify({
692
+ to: data.from_address,
693
+ subject: `Re: ${data.subject}`,
694
+ original_body: data.body,
695
+ model: currentModel,
696
+ session_id: currentSessionId,
697
+ message_id: data.message_id || '',
698
+ uid: String(em.uid || ''),
699
+ folder: _currentFolder,
700
+ fast: aiReplyMode ? aiReplyMode === 'fast' : _shouldUseFastAiReply(data),
701
+ }),
702
+ });
703
+ const result = await res.json();
704
+ if (draftToastTimer) clearTimeout(draftToastTimer);
705
+ if (result.success && result.reply) {
706
+ aiSuggestedBody = _cleanAiReplyText(result.reply);
707
+ } else {
708
+ const _msg = result.error || 'AI reply could not be generated';
709
+ console.error('AI reply generation failed:', _msg);
710
+ import('./ui.js').then(m => m.showError && m.showError('AI reply failed: ' + _msg)).catch(() => {});
711
+ return;
712
+ }
713
+ } catch (e) {
714
+ if (draftToastTimer) clearTimeout(draftToastTimer);
715
+ console.error('AI reply generation failed:', e);
716
+ import('./ui.js').then(m => m.showError && m.showError('AI reply failed: ' + (e.message || e))).catch(() => {});
717
+ return;
718
+ }
719
+ }
720
+ }
721
+
722
+ em.is_read = true;
723
+ if (itemEl) itemEl.classList.remove('email-unread');
724
+
725
+ // Addresses to exclude from Reply All. Prefer the full set of configured
726
+ // accounts (so a multi-account user's other mailboxes are excluded too),
727
+ // falling back to the single active address. Empty ⇒ no exclusion.
728
+ const myAddresses = (Array.isArray(window._myEmailAddresses) && window._myEmailAddresses.length)
729
+ ? window._myEmailAddresses
730
+ : (window._myEmailAddress ? [window._myEmailAddress] : []);
731
+
732
+ let toAddress = data.from_address;
733
+ let ccAddresses = '';
734
+ let subjectPrefix = 'Re: ';
735
+
736
+ if (mode === 'reply-all') {
737
+ // Build reply-all: TO = original sender, CC = everyone else (To + Cc minus me)
738
+ ccAddresses = buildReplyAllCc(data, myAddresses);
739
+ } else if (mode === 'forward') {
740
+ toAddress = '';
741
+ subjectPrefix = 'Fwd: ';
742
+ }
743
+
744
+ // Don't double-prefix `Re:` / `Fwd:` when the subject already starts with one.
745
+ // Replies to replies were producing `Re: Re: Re: …` which can also break
746
+ // some IMAP servers' header parsing on very long subject lines.
747
+ let _baseSubject = (data.subject || '').trim();
748
+ if (subjectPrefix === 'Re: ' && /^re\s*:/i.test(_baseSubject)) subjectPrefix = '';
749
+ else if (subjectPrefix === 'Fwd: ' && /^fwd?\s*:/i.test(_baseSubject)) subjectPrefix = '';
750
+ let content = `To: ${toAddress}\nSubject: ${subjectPrefix}${_baseSubject}`;
751
+ if (ccAddresses) content += `\nCc: ${ccAddresses}`;
752
+ if (mode !== 'forward' && data.message_id) content += `\nIn-Reply-To: ${data.message_id}`;
753
+ if (mode !== 'forward' && data.message_id) content += `\nReferences: ${data.references ? data.references + ' ' + data.message_id : data.message_id}`;
754
+ content += `\nX-Source-UID: ${em.uid}`;
755
+ content += `\nX-Source-Folder: ${_currentFolder}`;
756
+ if (data.attachments && data.attachments.length > 0) {
757
+ const attStr = data.attachments.map(a => `${a.index}:${a.filename}:${a.size}`).join('|');
758
+ content += `\nX-Attachments: ${attStr}`;
759
+ }
760
+ content += '\n---\n';
761
+
762
+ // Format the original date in a human-readable way for the quote header
763
+ let niceDate = data.date || '';
764
+ try {
765
+ if (data.date) {
766
+ const d = new Date(data.date);
767
+ if (!isNaN(d.getTime())) {
768
+ niceDate = d.toLocaleString([], {
769
+ weekday: 'short', month: 'short', day: 'numeric', year: 'numeric',
770
+ hour: '2-digit', minute: '2-digit',
771
+ });
772
+ }
773
+ }
774
+ } catch (_) {}
775
+
776
+ // Plain-text body, with HTML fallback stripped if no text part exists.
777
+ // Without this, an HTML-only email gives data.body === null/undefined
778
+ // and the reply doc opens empty (data.body.split throws).
779
+ let _origBody = (typeof data.body === 'string' && data.body.length) ? data.body : '';
780
+ if (!_origBody && typeof data.body_html === 'string' && data.body_html) {
781
+ _origBody = data.body_html
782
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
783
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
784
+ .replace(/<br\s*\/?>/gi, '\n')
785
+ .replace(/<\/p>/gi, '\n\n')
786
+ .replace(/<[^>]+>/g, '')
787
+ .replace(/&nbsp;/g, ' ')
788
+ .replace(/&amp;/g, '&')
789
+ .replace(/&lt;/g, '<')
790
+ .replace(/&gt;/g, '>')
791
+ .replace(/&quot;/g, '"')
792
+ .replace(/\n{3,}/g, '\n\n')
793
+ .trim();
794
+ }
795
+
796
+ if (mode === 'forward') {
797
+ content += `\n\n---------- Forwarded message ----------\n`;
798
+ content += `From: ${data.from_name} <${data.from_address}>\n`;
799
+ content += `Date: ${niceDate}\n`;
800
+ content += `Subject: ${data.subject}\n`;
801
+ if (data.to) content += `To: ${data.to}\n`;
802
+ content += `\n${_origBody}`;
803
+ } else {
804
+ const quotedBody = _origBody.split('\n').map(l => '> ' + l).join('\n');
805
+ // Inject AI-suggested body if present. No leading newline — the header
806
+ // block already ends with "---\n", so the reply must start on the very
807
+ // first body line, not one row down.
808
+ if (aiSuggestedBody) {
809
+ content += `${aiSuggestedBody}\n\n`;
810
+ } else {
811
+ content += '\n\n';
812
+ }
813
+ content += `${_replySeparator}\nOn ${niceDate}, ${data.from_name} <${data.from_address}> wrote:\n${quotedBody}`;
814
+ }
815
+
816
+ if (_docModule) {
817
+ // Only reuse an existing doc tab if the user really just wants to "view"
818
+ // the email again. For reply/reply-all/forward/ai-reply, always create
819
+ // a fresh draft — otherwise a previously-emptied doc (sent reply, AI
820
+ // reply that came back blank, etc.) keeps coming back instead of a
821
+ // proper pre-filled reply.
822
+ const reuseExisting = (mode === 'view' || mode === 'open');
823
+ const existingDocId = (reuseExisting && _docModule.findEmailDocId)
824
+ ? _docModule.findEmailDocId(em.uid, _currentFolder)
825
+ : null;
826
+ if (existingDocId) {
827
+ if (!_docModule.isPanelOpen()) _docModule.openPanel();
828
+ await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
829
+ await _docModule.loadDocument(existingDocId);
830
+ } else {
831
+ // If the user already has a chat session open, reuse it instead of
832
+ // spawning a new one. They asked for this explicitly — opening reply
833
+ // mid-conversation shouldn't whip them out of context.
834
+ let activeSid = '';
835
+ try { activeSid = sessionModule?.getCurrentSessionId?.() || ''; } catch {}
836
+ if (!activeSid) {
837
+ // No chat in flight — keep the old behavior of creating a scoped
838
+ // email-thread chat, then RE-READ the now-current session id. The
839
+ // POST below requires a session_id (backend 400s without one), and
840
+ // the freshly-created chat is what should own the reply draft.
841
+ await _createEmailChat(data);
842
+ try { activeSid = sessionModule?.getCurrentSessionId?.() || ''; } catch {}
843
+ }
844
+ // Guarantee a session — _createEmailChat can't make one when there's
845
+ // no enabled default-chat endpoint, which left the reply POSTing a
846
+ // null session_id → 400. Create a bare session so the draft always
847
+ // has a home regardless of chat/endpoint config.
848
+ if (!activeSid) {
849
+ try {
850
+ const _fd = new FormData();
851
+ _fd.append('name', `Email: ${(data.subject || '').slice(0, 60)}`);
852
+ _fd.append('skip_validation', 'true');
853
+ const _sres = await fetch(`${API_BASE}/api/session`, { method: 'POST', body: _fd, credentials: 'same-origin' });
854
+ if (_sres.ok) {
855
+ const _sdata = await _sres.json();
856
+ if (_sdata && _sdata.id) {
857
+ activeSid = _sdata.id;
858
+ if (sessionModule?.loadSessions) await sessionModule.loadSessions();
859
+ if (sessionModule?.selectSession) await sessionModule.selectSession(activeSid);
860
+ }
861
+ }
862
+ } catch (e) { console.error('reply: bare session create failed', e); }
863
+ }
864
+
865
+ const docRes = await fetch(`${API_BASE}/api/document`, {
866
+ method: 'POST',
867
+ headers: { 'Content-Type': 'application/json' },
868
+ body: JSON.stringify({
869
+ // Reuse the user's current chat session if there is one (so the
870
+ // reply draft lives in the chat they were just in); otherwise
871
+ // null and the new email-chat (created above) takes over.
872
+ session_id: activeSid || null,
873
+ title: data.subject,
874
+ content: content,
875
+ language: 'email',
876
+ }),
877
+ });
878
+ if (!docRes.ok) {
879
+ const errText = await docRes.text();
880
+ console.error('[reply-debug] POST /api/document failed', docRes.status, errText);
881
+ // uiModule isn't statically imported here — use the dynamic
882
+ // import pattern the rest of this file uses. (Previously this
883
+ // referenced a bare `uiModule`, throwing a ReferenceError that
884
+ // the outer catch swallowed → reply silently did nothing.)
885
+ import('./ui.js').then(m => m.showError && m.showError('Failed to create reply draft (' + docRes.status + ')')).catch(() => {});
886
+ return;
887
+ }
888
+ const doc = await docRes.json();
889
+ if (doc.id) {
890
+ const wasOpen = _docModule.isPanelOpen();
891
+ if (!wasOpen) _docModule.openPanel();
892
+ await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
893
+ // Use the doc dict from the POST directly — avoids a 404 race
894
+ // when the GET fires before the new row is visible to the read
895
+ // connection (or when caching is interfering). loadDocument's
896
+ // GET path can still be used as a fallback.
897
+ if (_docModule.injectFreshDoc) {
898
+ _docModule.injectFreshDoc(doc);
899
+ } else {
900
+ await _docModule.loadDocument(doc.id);
901
+ }
902
+ }
903
+ }
904
+ }
905
+ } catch (e) {
906
+ console.error('Failed to open email:', e);
907
+ // Surface the failure so a silent throw in the reply flow doesn't
908
+ // look like "nothing happened". Dynamic import — uiModule isn't a
909
+ // static import in this file.
910
+ const msg = e && e.message ? e.message : String(e);
911
+ import('./ui.js').then(m => m.showError && m.showError('Reply failed: ' + msg)).catch(() => {});
912
+ } finally {
913
+ if (spinner) { spinner.destroy(); spinner.element.remove(); }
914
+ if (itemEl) {
915
+ const menuWrap = itemEl.querySelector('.email-menu-wrap');
916
+ if (menuWrap) menuWrap.style.display = '';
917
+ }
918
+ }
919
+ }
920
+
921
+ function _showEmailMenu(em, anchor, itemEl) {
922
+ document.querySelectorAll('.email-dropdown').forEach(d => d.remove());
923
+
924
+ const dropdown = document.createElement('div');
925
+ dropdown.className = 'dropdown email-dropdown show';
926
+
927
+ const actions = [
928
+ { label: 'Open', icon: _replyIcon, action: () => _openEmail(em, itemEl) },
929
+ { label: 'Remind to reply', icon: _bellIcon, submenu: 'remind' },
930
+ { label: 'Archive', icon: _archiveIcon, action: () => _archiveEmail(em) },
931
+ { label: 'Delete', icon: _deleteIcon, danger: true, action: () => _deleteEmail(em) },
932
+ ];
933
+
934
+ for (const a of actions) {
935
+ const menuItem = document.createElement('div');
936
+ menuItem.className = 'dropdown-item-compact' + (a.danger ? ' dropdown-item-danger' : '');
937
+ const arrow = a.submenu ? ' <span style="margin-left:auto;opacity:0.5;">›</span>' : '';
938
+ menuItem.innerHTML = _icon(a.icon) + `<span>${a.label}</span>${arrow}`;
939
+ menuItem.addEventListener('click', (e) => {
940
+ e.stopPropagation();
941
+ if (a.submenu === 'remind') {
942
+ _showRemindSubmenu(em, dropdown);
943
+ return;
944
+ }
945
+ dropdown.remove();
946
+ a.action();
947
+ });
948
+ dropdown.appendChild(menuItem);
949
+ }
950
+
951
+ anchor.appendChild(dropdown);
952
+
953
+ const close = (e) => {
954
+ if (!dropdown.contains(e.target) && !anchor.contains(e.target)) {
955
+ dropdown.remove();
956
+ document.removeEventListener('click', close, true);
957
+ }
958
+ };
959
+ setTimeout(() => document.addEventListener('click', close, true), 10);
960
+ }
961
+
962
+ // ---- Reminder submenu (creates a Note with a reminder for this email) ----
963
+
964
+ function _showRemindSubmenu(em, parentDropdown) {
965
+ // Replace content of parent dropdown with time presets
966
+ parentDropdown.innerHTML = '';
967
+ const header = document.createElement('div');
968
+ header.className = 'dropdown-item-compact';
969
+ header.style.cssText = 'opacity:0.5;font-size:10px;pointer-events:none;text-transform:uppercase;letter-spacing:0.5px;padding-top:6px;';
970
+ header.innerHTML = '<span>Remind me</span>';
971
+ parentDropdown.appendChild(header);
972
+
973
+ const now = new Date();
974
+ const laterToday = new Date(now);
975
+ const sixPm = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 18, 0);
976
+ if (sixPm - now < 60*60*1000) laterToday.setTime(now.getTime() + 3 * 60 * 60 * 1000);
977
+ else laterToday.setTime(sixPm.getTime());
978
+
979
+ const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(8, 0, 0, 0);
980
+ const daysUntilMon = (8 - now.getDay()) % 7 || 7;
981
+ const nextWeek = new Date(now); nextWeek.setDate(now.getDate() + daysUntilMon); nextWeek.setHours(8, 0, 0, 0);
982
+
983
+ const presets = [
984
+ { label: 'Later today', sub: laterToday.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }), date: laterToday },
985
+ { label: 'Tomorrow', sub: tomorrow.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }), date: tomorrow },
986
+ { label: 'Next week', sub: nextWeek.toLocaleDateString([], { weekday: 'short' }) + ' ' + nextWeek.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }), date: nextWeek },
987
+ ];
988
+ for (const p of presets) {
989
+ const item = document.createElement('div');
990
+ item.className = 'dropdown-item-compact';
991
+ item.innerHTML = `<span>${p.label}</span><span style="margin-left:auto;opacity:0.5;font-size:10px;">${p.sub}</span>`;
992
+ item.addEventListener('click', async (e) => {
993
+ e.stopPropagation();
994
+ parentDropdown.remove();
995
+ await _createReplyReminder(em, p.date);
996
+ });
997
+ parentDropdown.appendChild(item);
998
+ }
999
+ const customItem = document.createElement('div');
1000
+ customItem.className = 'dropdown-item-compact';
1001
+ customItem.innerHTML = '<span>Pick date and time…</span>';
1002
+ customItem.addEventListener('click', async (e) => {
1003
+ e.stopPropagation();
1004
+ parentDropdown.remove();
1005
+ const tmp = document.createElement('input');
1006
+ tmp.type = 'datetime-local';
1007
+ const def = new Date(tomorrow);
1008
+ const pad = n => String(n).padStart(2, '0');
1009
+ tmp.value = `${def.getFullYear()}-${pad(def.getMonth()+1)}-${pad(def.getDate())}T${pad(def.getHours())}:${pad(def.getMinutes())}`;
1010
+ tmp.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:99999;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:6px;font-size:13px;';
1011
+ document.body.appendChild(tmp);
1012
+ tmp.focus();
1013
+ if (typeof tmp.showPicker === 'function') { try { tmp.showPicker(); } catch {} }
1014
+ // Cleanup helper — also unwires the global listeners so they don't
1015
+ // linger after dismiss.
1016
+ const _cleanup = () => {
1017
+ tmp.remove();
1018
+ document.removeEventListener('keydown', _onKey);
1019
+ document.removeEventListener('mousedown', _onDocClick, true);
1020
+ };
1021
+ const _onKey = (ev) => { if (ev.key === 'Escape') _cleanup(); };
1022
+ // Click-outside dismiss. Replaces the old blur-based auto-remove —
1023
+ // blur fires whenever the native datetime popup steals focus, so
1024
+ // the input vanished before the user could click any date. Now we
1025
+ // only dismiss when the user clicks something that is NOT the
1026
+ // input itself (the native picker popup is a browser-owned overlay
1027
+ // OUTSIDE the document, so its clicks don't fire here at all — no
1028
+ // false dismissals).
1029
+ const _onDocClick = (ev) => { if (ev.target !== tmp) _cleanup(); };
1030
+ tmp.addEventListener('change', async () => {
1031
+ if (tmp.value) {
1032
+ await _createReplyReminder(em, new Date(tmp.value));
1033
+ }
1034
+ _cleanup();
1035
+ });
1036
+ document.addEventListener('keydown', _onKey);
1037
+ // Defer the click-outside listener so the click that opened this
1038
+ // input doesn't immediately close it.
1039
+ setTimeout(() => document.addEventListener('mousedown', _onDocClick, true), 50);
1040
+ });
1041
+ parentDropdown.appendChild(customItem);
1042
+ }
1043
+
1044
+ async function _createReplyReminder(em, dueDate) {
1045
+ const pad = n => String(n).padStart(2, '0');
1046
+ const iso = `${dueDate.getFullYear()}-${pad(dueDate.getMonth()+1)}-${pad(dueDate.getDate())}T${pad(dueDate.getHours())}:${pad(dueDate.getMinutes())}`;
1047
+ const from = em.from || em.sender || 'someone';
1048
+ const payload = {
1049
+ title: `Reply: ${em.subject || '(no subject)'}`,
1050
+ content: `From: ${from}\n\nRemember to reply to this email.`,
1051
+ note_type: 'note',
1052
+ label: 'email',
1053
+ due_date: iso,
1054
+ source: 'email',
1055
+ };
1056
+ try {
1057
+ const res = await fetch(`${API_BASE}/api/notes`, {
1058
+ method: 'POST', credentials: 'same-origin',
1059
+ headers: { 'Content-Type': 'application/json' },
1060
+ body: JSON.stringify(payload),
1061
+ });
1062
+ if (!res.ok) throw new Error('Failed');
1063
+ const { showToast } = await import('./ui.js');
1064
+ const fmt = dueDate.toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
1065
+ showToast(`Reminder set for ${fmt}`);
1066
+ // Request notification permission if needed
1067
+ if ('Notification' in window && Notification.permission === 'default') {
1068
+ try { Notification.requestPermission(); } catch {}
1069
+ }
1070
+ } catch (e) {
1071
+ const { showError } = await import('./ui.js');
1072
+ showError('Failed to create reminder');
1073
+ }
1074
+ }
1075
+
1076
+ async function _archiveEmail(em) {
1077
+ try {
1078
+ await fetch(`${API_BASE}/api/email/archive/${em.uid}?folder=${encodeURIComponent(_currentFolder)}${_acct()}`, { method: 'POST' });
1079
+ _emails = _emails.filter(e => e.uid !== em.uid);
1080
+ _renderList();
1081
+ } catch (e) {
1082
+ console.error('Failed to archive:', e);
1083
+ }
1084
+ }
1085
+
1086
+ async function _deleteEmail(em) {
1087
+ const subject = em.subject || '(no subject)';
1088
+ const { styledConfirm } = await import('./ui.js');
1089
+ const ok = await styledConfirm(`Delete "${subject}"?`, { confirmText: 'Delete', cancelText: 'Cancel', danger: true });
1090
+ if (!ok) return;
1091
+ try {
1092
+ await fetch(`${API_BASE}/api/email/delete/${em.uid}?folder=${encodeURIComponent(_currentFolder)}${_acct()}`, { method: 'DELETE' });
1093
+ _emails = _emails.filter(e => e.uid !== em.uid);
1094
+ _renderList();
1095
+ } catch (e) {
1096
+ console.error('Failed to delete:', e);
1097
+ }
1098
+ }
1099
+
1100
+ async function _toggleDone(em, itemEl) {
1101
+ const newState = !em.is_answered;
1102
+ em.is_answered = newState;
1103
+ if (newState) em.is_read = true; // mark-done implies mark-read
1104
+ if (itemEl) {
1105
+ if (newState) {
1106
+ itemEl.classList.remove('email-unread');
1107
+ // Also drop any inline unread indicator dots the renderer may have added
1108
+ itemEl.querySelectorAll('.email-unread-dot, [data-unread-dot]').forEach(n => n.remove());
1109
+ }
1110
+ const check = itemEl.querySelector('.email-done-check');
1111
+ if (check) check.classList.toggle('active', newState);
1112
+ }
1113
+ try {
1114
+ if (newState) {
1115
+ await fetch(`${API_BASE}/api/email/mark-answered/${em.uid}?folder=${encodeURIComponent(_currentFolder)}${_acct()}`, { method: 'POST' });
1116
+ await fetch(`${API_BASE}/api/email/mark-read/${em.uid}?folder=${encodeURIComponent(_currentFolder)}${_acct()}`, { method: 'POST' });
1117
+ } else {
1118
+ await fetch(`${API_BASE}/api/email/clear-answered/${em.uid}?folder=${encodeURIComponent(_currentFolder)}${_acct()}`, { method: 'POST' });
1119
+ }
1120
+ } catch (e) {
1121
+ console.error('Failed to toggle done:', e);
1122
+ }
1123
+ }
1124
+
1125
+ async function _createEmailChat(emailData) {
1126
+ try {
1127
+ // Try current session's endpoint first
1128
+ const current = sessionModule.getSessions?.().find(s => s.id === sessionModule.getCurrentSessionId?.());
1129
+ let url, model, endpointId;
1130
+ if (current && current.endpoint_url && current.model) {
1131
+ url = current.endpoint_url;
1132
+ model = current.model;
1133
+ endpointId = current.endpoint_id;
1134
+ } else {
1135
+ // Fall back to default chat config
1136
+ const dcRes = await fetch(`${API_BASE}/api/default-chat`);
1137
+ const dc = await dcRes.json();
1138
+ url = dc.endpoint_url;
1139
+ model = dc.model;
1140
+ endpointId = dc.endpoint_id;
1141
+ }
1142
+
1143
+ if (url && model) {
1144
+ await sessionModule.createDirectChat(url, model, endpointId);
1145
+ // Set a helpful title in the chat meta
1146
+ const meta = document.getElementById('current-meta');
1147
+ if (meta) meta.textContent = `Email: ${(emailData.subject || '').slice(0, 60)}`;
1148
+ }
1149
+ } catch (e) {
1150
+ console.error('Failed to create email chat:', e);
1151
+ }
1152
+ }
1153
+
1154
+ async function _composeNew() {
1155
+ if (!_docModule) return;
1156
+ // NOTE: don't open the panel here. Creating the email-scoped chat below can
1157
+ // switch sessions, which tears the panel down — so an early open would mount
1158
+ // the pane, get closed, then injectFreshDoc remounts it: a visible flash
1159
+ // (doc shows for a frame, then slides up again). Mount once, at injectFreshDoc,
1160
+ // after the session + doc exist.
1161
+ try {
1162
+ // /api/document requires a session_id (returns 400 if null), so reuse
1163
+ // the active chat if there is one — otherwise spin up an email-scoped
1164
+ // chat first, same pattern the reply path uses.
1165
+ let sid = '';
1166
+ try { sid = sessionModule?.getCurrentSessionId?.() || ''; } catch (_) {}
1167
+ if (!sid) {
1168
+ await _createEmailChat({ subject: 'New Email' });
1169
+ try { sid = sessionModule?.getCurrentSessionId?.() || ''; } catch (_) {}
1170
+ }
1171
+ // Guarantee a session — _createEmailChat can't make one when there's no
1172
+ // enabled default-chat endpoint, which left compose POSTing a null
1173
+ // session_id → 400 (the draft silently never appeared). Same bare-session
1174
+ // fallback the reply flow uses.
1175
+ if (!sid) {
1176
+ try {
1177
+ const _fd = new FormData();
1178
+ _fd.append('name', 'New Email');
1179
+ _fd.append('skip_validation', 'true');
1180
+ const _sres = await fetch(`${API_BASE}/api/session`, { method: 'POST', body: _fd, credentials: 'same-origin' });
1181
+ if (_sres.ok) {
1182
+ const _sdata = await _sres.json();
1183
+ if (_sdata && _sdata.id) {
1184
+ sid = _sdata.id;
1185
+ // NOTE: intentionally do NOT loadSessions()/selectSession() here.
1186
+ // Re-selecting the (empty) session re-renders the chat and flashes
1187
+ // the welcome splash for a frame before the draft opens — the
1188
+ // "splash flickers like crazy then email opens" bug. The doc only
1189
+ // needs the session_id; the draft opens in the doc panel regardless.
1190
+ }
1191
+ }
1192
+ } catch (e) { console.error('compose: bare session create failed', e); }
1193
+ }
1194
+ if (!sid) {
1195
+ console.error('compose: could not obtain a session_id');
1196
+ import('./ui.js').then(m => m.showError && m.showError('Could not start a new email (no session).')).catch(() => {});
1197
+ return;
1198
+ }
1199
+ const res = await fetch(`${API_BASE}/api/document`, {
1200
+ method: 'POST',
1201
+ headers: { 'Content-Type': 'application/json' },
1202
+ body: JSON.stringify({
1203
+ session_id: sid,
1204
+ title: 'New Email',
1205
+ content: 'To: \nSubject: \n---\n',
1206
+ language: 'email',
1207
+ }),
1208
+ });
1209
+ if (!res.ok) {
1210
+ console.error('compose POST failed', res.status, await res.text().catch(() => ''));
1211
+ import('./ui.js').then(m => m.showError && m.showError('Failed to create new email (' + res.status + ')')).catch(() => {});
1212
+ return;
1213
+ }
1214
+ const doc = await res.json();
1215
+ if (doc.id) {
1216
+ await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
1217
+ // Use the doc dict from POST directly to avoid the GET 404 race that
1218
+ // hits a freshly-created doc on a separate read connection.
1219
+ if (_docModule.injectFreshDoc) {
1220
+ _docModule.injectFreshDoc(doc);
1221
+ } else {
1222
+ _docModule.loadDocument(doc.id);
1223
+ }
1224
+ }
1225
+ } catch (e) {
1226
+ console.error('Failed to create email:', e);
1227
+ }
1228
+ }
1229
+
1230
+ function _esc(text) {
1231
+ const div = document.createElement('div');
1232
+ div.textContent = text || '';
1233
+ return div.innerHTML;
1234
+ }
1235
+
1236
+ function _senderColor(name) {
1237
+ if (!name) return 'hsl(220, 55%, 65%)';
1238
+ const key = name.toLowerCase();
1239
+ let hash = 0;
1240
+ for (let i = 0; i < key.length; i++) {
1241
+ hash = ((hash << 5) - hash + key.charCodeAt(i)) | 0;
1242
+ }
1243
+ const hue = ((hash % 360) + 360) % 360;
1244
+ return `hsl(${hue}, 55%, 65%)`;
1245
+ }