onbuzz 4.6.4 → 4.7.1

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 (966) hide show
  1. package/bin/cli.js +8 -0
  2. package/package.json +1 -1
  3. package/scripts/debug-balance-probe.mjs +35 -0
  4. package/src/__tests__/autoUpdateConfig.test.js +80 -0
  5. package/src/__tests__/releaseWorkflow.test.js +72 -0
  6. package/src/core/__tests__/messageProcessor.sourceHeader.test.js +7 -2
  7. package/src/index.js +16 -0
  8. package/src/interfaces/__tests__/accountRoutes.test.js +487 -0
  9. package/src/interfaces/__tests__/apiKeyManagerContract.test.js +39 -0
  10. package/src/interfaces/__tests__/authRoutes.test.js +8 -8
  11. package/src/interfaces/accountRoutes.js +398 -0
  12. package/src/interfaces/webServer.js +104 -15
  13. package/src/services/__tests__/apiKeyManager.test.js +4 -6
  14. package/src/services/__tests__/authCacheStore.test.js +118 -0
  15. package/src/services/__tests__/channelRelay.integration.test.js +49 -12
  16. package/src/services/__tests__/messageSource.test.js +6 -2
  17. package/src/services/__tests__/telegramBlockParser.test.js +183 -0
  18. package/src/services/__tests__/telegramService.test.js +838 -0
  19. package/src/services/apiKeyManager.js +12 -99
  20. package/src/services/authCacheStore.js +140 -0
  21. package/src/services/messageSource.js +11 -11
  22. package/src/services/telegramBlockParser.js +145 -0
  23. package/src/services/telegramService.js +1273 -431
  24. package/src/tools/__tests__/webTool.e2e.test.js +9 -1
  25. package/src/tools/webTool.js +20 -4
  26. package/src/utilities/__tests__/secretsCipher.test.js +97 -0
  27. package/src/utilities/__tests__/updateNotifier.test.js +139 -0
  28. package/src/utilities/secretsCipher.js +116 -0
  29. package/src/utilities/updateNotifier.js +140 -0
  30. package/web-ui/build/index.html +2 -2
  31. package/web-ui/build/static/1c-CuE_dLW8.js +1 -0
  32. package/web-ui/build/static/abap-LOu14tBm.js +1 -0
  33. package/web-ui/build/static/abnf-CJpfHQLV.js +1 -0
  34. package/web-ui/build/static/abnf-LdAi7OAY.js +1 -0
  35. package/web-ui/build/static/accesslog-BvrhdZIJ.js +1 -0
  36. package/web-ui/build/static/actionscript-B3NHp9II.js +1 -0
  37. package/web-ui/build/static/actionscript-DVfqv5DS.js +1 -0
  38. package/web-ui/build/static/ada-BVkvHgSu.js +1 -0
  39. package/web-ui/build/static/ada-d33rlRPC.js +1 -0
  40. package/web-ui/build/static/agda-t1mot-jT.js +1 -0
  41. package/web-ui/build/static/al-D2Aw8sYi.js +1 -0
  42. package/web-ui/build/static/angelscript-DGjq6ZLh.js +1 -0
  43. package/web-ui/build/static/antlr4-DQWExQhj.js +1 -0
  44. package/web-ui/build/static/apache-CRX2mGHA.js +1 -0
  45. package/web-ui/build/static/apacheconf-BBhhyVVR.js +1 -0
  46. package/web-ui/build/static/apex-DIJyAF3g.js +1 -0
  47. package/web-ui/build/static/apl-COGQUhmX.js +1 -0
  48. package/web-ui/build/static/applescript-DLPa_bvL.js +1 -0
  49. package/web-ui/build/static/applescript-iEepawXM.js +1 -0
  50. package/web-ui/build/static/aql-vDYotUbW.js +1 -0
  51. package/web-ui/build/static/arcade-5bpnh2Oi.js +1 -0
  52. package/web-ui/build/static/arduino-4OxI8HS8.js +1 -0
  53. package/web-ui/build/static/arduino-zJUNF7Xl.js +1 -0
  54. package/web-ui/build/static/arff-DHcdYO6-.js +1 -0
  55. package/web-ui/build/static/armasm-DVyTHmiD.js +1 -0
  56. package/web-ui/build/static/asciidoc-BRYIR6BF.js +1 -0
  57. package/web-ui/build/static/asciidoc-bd_scw1t.js +1 -0
  58. package/web-ui/build/static/asm6502-DfRcm1MU.js +1 -0
  59. package/web-ui/build/static/asmatmel--fvKJXxu.js +1 -0
  60. package/web-ui/build/static/aspectj-Cnm2Ccyg.js +1 -0
  61. package/web-ui/build/static/aspnet-BK2pQltF.js +1 -0
  62. package/web-ui/build/static/autohotkey-COkKlASn.js +1 -0
  63. package/web-ui/build/static/autohotkey-DNYipPv0.js +1 -0
  64. package/web-ui/build/static/autoit-DiwhqkEo.js +1 -0
  65. package/web-ui/build/static/autoit-DkQ_YDdk.js +1 -0
  66. package/web-ui/build/static/avisynth-DFrkQWrl.js +1 -0
  67. package/web-ui/build/static/avrasm-BbFtsKVm.js +1 -0
  68. package/web-ui/build/static/avro-idl-06_wFrxE.js +1 -0
  69. package/web-ui/build/static/awk-BNO1Hu3Y.js +1 -0
  70. package/web-ui/build/static/axapta-CW0rBEQf.js +1 -0
  71. package/web-ui/build/static/bash-DAP5H41u.js +1 -0
  72. package/web-ui/build/static/bash-DsCT4fHh.js +1 -0
  73. package/web-ui/build/static/basic-DqCcX17x.js +1 -0
  74. package/web-ui/build/static/basic-J3jqGQaz.js +1 -0
  75. package/web-ui/build/static/batch-BzKb0QuF.js +1 -0
  76. package/web-ui/build/static/bbcode-Cxys0qL9.js +1 -0
  77. package/web-ui/build/static/bicep-DPpVhf9I.js +1 -0
  78. package/web-ui/build/static/birb-BDF00uY2.js +1 -0
  79. package/web-ui/build/static/bison-DerSCeTc.js +1 -0
  80. package/web-ui/build/static/bnf--Nxb91Mj.js +1 -0
  81. package/web-ui/build/static/bnf-DKp1OqQh.js +1 -0
  82. package/web-ui/build/static/brainfuck-BgKburBi.js +1 -0
  83. package/web-ui/build/static/brainfuck-CLRabJhl.js +1 -0
  84. package/web-ui/build/static/brightscript-DJ0LrgUU.js +1 -0
  85. package/web-ui/build/static/bro-FVn3137a.js +1 -0
  86. package/web-ui/build/static/bsl-DGcfC5Qq.js +1 -0
  87. package/web-ui/build/static/c-CtBaJxj_.js +1 -0
  88. package/web-ui/build/static/c-DcJBPsOs.js +1 -0
  89. package/web-ui/build/static/c-like-BMAPMOT0.js +1 -0
  90. package/web-ui/build/static/cal-Dwvf5LQJ.js +1 -0
  91. package/web-ui/build/static/capnproto-Be5xeQu8.js +1 -0
  92. package/web-ui/build/static/ceylon-BV9kQegc.js +1 -0
  93. package/web-ui/build/static/cfscript-DqyqkqRV.js +1 -0
  94. package/web-ui/build/static/chaiscript-ab8S5sGz.js +1 -0
  95. package/web-ui/build/static/cil-DU5bEU16.js +1 -0
  96. package/web-ui/build/static/clean-CqKiC4l-.js +1 -0
  97. package/web-ui/build/static/clojure-CdaWManm.js +1 -0
  98. package/web-ui/build/static/clojure-cVIUydqy.js +1 -0
  99. package/web-ui/build/static/clojure-repl-mc7sqsOd.js +1 -0
  100. package/web-ui/build/static/cmake-D2ntzyPv.js +1 -0
  101. package/web-ui/build/static/cmake-Dt7gdCZl.js +1 -0
  102. package/web-ui/build/static/cobol-BYMIYxJY.js +1 -0
  103. package/web-ui/build/static/coffeescript-COUOYE5O.js +1 -0
  104. package/web-ui/build/static/coffeescript-FDyQzH4X.js +1 -0
  105. package/web-ui/build/static/concurnas-D8GglsJx.js +1 -0
  106. package/web-ui/build/static/coq-BvEL_B0N.js +1 -0
  107. package/web-ui/build/static/coq-C6NZeAuz.js +1 -0
  108. package/web-ui/build/static/cos-CjjeNVfD.js +1 -0
  109. package/web-ui/build/static/cpp-BdQQ9k01.js +1 -0
  110. package/web-ui/build/static/cpp-iYGopslr.js +1 -0
  111. package/web-ui/build/static/crmsh-COhOWlQB.js +1 -0
  112. package/web-ui/build/static/crystal-DqVfuZe7.js +1 -0
  113. package/web-ui/build/static/crystal-fkI7WHLA.js +1 -0
  114. package/web-ui/build/static/csharp-BKyMR69a.js +1 -0
  115. package/web-ui/build/static/csharp-KJwk5tFh.js +1 -0
  116. package/web-ui/build/static/cshtml-D-0Bd5yF.js +1 -0
  117. package/web-ui/build/static/csp-C6xrFQUZ.js +1 -0
  118. package/web-ui/build/static/csp-CKOZZjS5.js +1 -0
  119. package/web-ui/build/static/css-DhttYfIQ.js +1 -0
  120. package/web-ui/build/static/css-extras-Cwksdn14.js +1 -0
  121. package/web-ui/build/static/csv-3xOycrIm.js +1 -0
  122. package/web-ui/build/static/cypher-DIQ_IXvU.js +1 -0
  123. package/web-ui/build/static/d-CA3ONaPZ.js +1 -0
  124. package/web-ui/build/static/d-Cpj6yr1P.js +1 -0
  125. package/web-ui/build/static/dart-CStXueOh.js +1 -0
  126. package/web-ui/build/static/dart-DyyN2hCZ.js +1 -0
  127. package/web-ui/build/static/dataweave-C1Z1lf_p.js +1 -0
  128. package/web-ui/build/static/dax-Bra_xxin.js +1 -0
  129. package/web-ui/build/static/delphi-BSp32sTR.js +1 -0
  130. package/web-ui/build/static/dhall-DqULvWhD.js +1 -0
  131. package/web-ui/build/static/diff-DZWp0eai.js +1 -0
  132. package/web-ui/build/static/diff-Dl7Q9ZLJ.js +1 -0
  133. package/web-ui/build/static/django-BKb-9srn.js +1 -0
  134. package/web-ui/build/static/django-D4ZBsr7A.js +1 -0
  135. package/web-ui/build/static/dns-BIl1lqfE.js +1 -0
  136. package/web-ui/build/static/dns-zone-file-BHOpqtwv.js +1 -0
  137. package/web-ui/build/static/docker-DhYlo5KM.js +1 -0
  138. package/web-ui/build/static/dockerfile-DkKcxplU.js +1 -0
  139. package/web-ui/build/static/dos-BL8RAxlP.js +1 -0
  140. package/web-ui/build/static/dot-BLokvOTb.js +1 -0
  141. package/web-ui/build/static/dsconfig-C-HwSveF.js +1 -0
  142. package/web-ui/build/static/dts-D7N7kY9U.js +1 -0
  143. package/web-ui/build/static/dust-BDDbpTgO.js +1 -0
  144. package/web-ui/build/static/ebnf-BXq1reNT.js +1 -0
  145. package/web-ui/build/static/ebnf-DbPDw4Bq.js +1 -0
  146. package/web-ui/build/static/editorconfig-w2ZfPGuw.js +1 -0
  147. package/web-ui/build/static/eiffel-c2MQNBkW.js +1 -0
  148. package/web-ui/build/static/ejs-3QktTSvM.js +1 -0
  149. package/web-ui/build/static/elixir-BpckBbUo.js +1 -0
  150. package/web-ui/build/static/elixir-CdGx--Zx.js +1 -0
  151. package/web-ui/build/static/elm-BZQYORvs.js +1 -0
  152. package/web-ui/build/static/elm-CuB52FFf.js +1 -0
  153. package/web-ui/build/static/erb-BBShdpRp.js +1 -0
  154. package/web-ui/build/static/erb-C6oyD8uR.js +1 -0
  155. package/web-ui/build/static/erlang-CTR2NiVN.js +1 -0
  156. package/web-ui/build/static/erlang-Gyvyd8Bt.js +1 -0
  157. package/web-ui/build/static/erlang-repl-DQ9M2hTB.js +1 -0
  158. package/web-ui/build/static/etlua-WvCpEBL7.js +1 -0
  159. package/web-ui/build/static/excel-Cti8qyDh.js +1 -0
  160. package/web-ui/build/static/excel-formula-CbyjGhLT.js +1 -0
  161. package/web-ui/build/static/factor-DsS7xUlQ.js +1 -0
  162. package/web-ui/build/static/false-By7DT7NY.js +1 -0
  163. package/web-ui/build/static/firestore-security-rules-D818j5mX.js +1 -0
  164. package/web-ui/build/static/fix-Z9CNHO5B.js +1 -0
  165. package/web-ui/build/static/flix-CQ5tU7oG.js +1 -0
  166. package/web-ui/build/static/flow-TAcj5V8G.js +1 -0
  167. package/web-ui/build/static/fortran-CSo4-IB9.js +1 -0
  168. package/web-ui/build/static/fortran-D8ckpcuk.js +1 -0
  169. package/web-ui/build/static/fsharp-CX5IhzHG.js +1 -0
  170. package/web-ui/build/static/fsharp-CjgdWVzI.js +1 -0
  171. package/web-ui/build/static/ftl-BniZfpaP.js +1 -0
  172. package/web-ui/build/static/gams-Dx_Ej7b0.js +1 -0
  173. package/web-ui/build/static/gap-BW8rQPKU.js +1 -0
  174. package/web-ui/build/static/gauss-DP_j2AoM.js +1 -0
  175. package/web-ui/build/static/gcode-C_p0RHK2.js +1 -0
  176. package/web-ui/build/static/gcode-CvPzqlIr.js +1 -0
  177. package/web-ui/build/static/gdscript-BQxanI3X.js +1 -0
  178. package/web-ui/build/static/gedcom-DX22sB95.js +1 -0
  179. package/web-ui/build/static/gherkin-BuM2TN92.js +1 -0
  180. package/web-ui/build/static/gherkin-SyKtGAnl.js +1 -0
  181. package/web-ui/build/static/git-CZsBJbLj.js +1 -0
  182. package/web-ui/build/static/glsl-0gMMTGpT.js +1 -0
  183. package/web-ui/build/static/glsl-DdxyDP7O.js +1 -0
  184. package/web-ui/build/static/gml-Bmf5f_3a.js +1 -0
  185. package/web-ui/build/static/gml-DygwvUqd.js +1 -0
  186. package/web-ui/build/static/gn-CmJ9lVHR.js +1 -0
  187. package/web-ui/build/static/go-DcXIw3ql.js +1 -0
  188. package/web-ui/build/static/go-Dp-1fwju.js +1 -0
  189. package/web-ui/build/static/go-module-CVe5dTg-.js +1 -0
  190. package/web-ui/build/static/golo-Dqz3BdPW.js +1 -0
  191. package/web-ui/build/static/gradle-PY8Q_ptd.js +1 -0
  192. package/web-ui/build/static/graphql-7ynBfuuQ.js +1 -0
  193. package/web-ui/build/static/groovy-CKe3t1A3.js +1 -0
  194. package/web-ui/build/static/groovy-DK_6c_Dy.js +1 -0
  195. package/web-ui/build/static/haml-BjjIreuG.js +1 -0
  196. package/web-ui/build/static/haml-knbHtzU7.js +1 -0
  197. package/web-ui/build/static/handlebars-Bvicc2ZP.js +1 -0
  198. package/web-ui/build/static/handlebars-CWjisUes.js +1 -0
  199. package/web-ui/build/static/haskell-0vOFJmtj.js +1 -0
  200. package/web-ui/build/static/haskell-BjwWvVul.js +1 -0
  201. package/web-ui/build/static/haxe-CXIYhI9-.js +1 -0
  202. package/web-ui/build/static/haxe-Msa_nat5.js +1 -0
  203. package/web-ui/build/static/hcl-DADfqiqO.js +1 -0
  204. package/web-ui/build/static/hlsl-BoecwvZ1.js +1 -0
  205. package/web-ui/build/static/hoon-DoMcF4bN.js +1 -0
  206. package/web-ui/build/static/hpkp-DOkWWV9B.js +1 -0
  207. package/web-ui/build/static/hsp-KvanktXQ.js +1 -0
  208. package/web-ui/build/static/hsts-DP5sAe8p.js +1 -0
  209. package/web-ui/build/static/htmlbars-DpevdeqY.js +1 -0
  210. package/web-ui/build/static/http-CTyudxeK.js +1 -0
  211. package/web-ui/build/static/http-D0_pALtR.js +1 -0
  212. package/web-ui/build/static/hy-KpsNGTaA.js +1 -0
  213. package/web-ui/build/static/ichigojam-B9szb3eY.js +1 -0
  214. package/web-ui/build/static/icon-nqAgdt4t.js +1 -0
  215. package/web-ui/build/static/icu-message-format-DvgC-cKZ.js +1 -0
  216. package/web-ui/build/static/idris-D5VcDmFg.js +1 -0
  217. package/web-ui/build/static/iecst-LbbDhK7w.js +1 -0
  218. package/web-ui/build/static/ignore-rThoz4zA.js +1 -0
  219. package/web-ui/build/static/index-Cm1gK3R7.css +1 -0
  220. package/web-ui/build/static/index-D6blIL5t.js +13 -0
  221. package/web-ui/build/static/index-DqCutaVi.js +1164 -0
  222. package/web-ui/build/static/index-_UVy_UGY.js +1 -0
  223. package/web-ui/build/static/inform7-B2u0-vkK.js +1 -0
  224. package/web-ui/build/static/inform7-Cj2uMx5P.js +1 -0
  225. package/web-ui/build/static/ini-DDC_WfpB.js +1 -0
  226. package/web-ui/build/static/ini-Dafi1wkS.js +1 -0
  227. package/web-ui/build/static/io-Cs5RmCdQ.js +1 -0
  228. package/web-ui/build/static/irpf90-5KuyCx_k.js +1 -0
  229. package/web-ui/build/static/isbl-CZGAUFN3.js +1 -0
  230. package/web-ui/build/static/j-C2U5CNx3.js +1 -0
  231. package/web-ui/build/static/java-B-L3bHbG.js +1 -0
  232. package/web-ui/build/static/java-Dhfb499l.js +1 -0
  233. package/web-ui/build/static/javadoc-_ln2gn5I.js +1 -0
  234. package/web-ui/build/static/javadoclike-CnV_ddKg.js +1 -0
  235. package/web-ui/build/static/javascript-DBUUUlif.js +1 -0
  236. package/web-ui/build/static/javastacktrace-BS1l3Yf6.js +1 -0
  237. package/web-ui/build/static/jboss-cli-RdKEfNzL.js +1 -0
  238. package/web-ui/build/static/jexl-Bu5vdWnx.js +1 -0
  239. package/web-ui/build/static/jolie-LS18mxou.js +1 -0
  240. package/web-ui/build/static/jq-u9QEjWr_.js +1 -0
  241. package/web-ui/build/static/js-extras-C1cS_85G.js +1 -0
  242. package/web-ui/build/static/js-templates-DQxgBfhh.js +1 -0
  243. package/web-ui/build/static/jsdoc-DhLXAtzJ.js +1 -0
  244. package/web-ui/build/static/json-B8DgcmbV.js +1 -0
  245. package/web-ui/build/static/json-ByLJzbD8.js +1 -0
  246. package/web-ui/build/static/json5-DSnccLNQ.js +1 -0
  247. package/web-ui/build/static/jsonp-CcwF40Vu.js +1 -0
  248. package/web-ui/build/static/jsstacktrace-7YvddNWa.js +1 -0
  249. package/web-ui/build/static/jsx-DnHkAc3-.js +1 -0
  250. package/web-ui/build/static/julia-BmadVpf3.js +1 -0
  251. package/web-ui/build/static/julia-GSKCoIBJ.js +1 -0
  252. package/web-ui/build/static/julia-repl-ks3HTctE.js +1 -0
  253. package/web-ui/build/static/keepalived-CWDkM2-Z.js +1 -0
  254. package/web-ui/build/static/keyman-C9jA4-3Y.js +1 -0
  255. package/web-ui/build/static/kotlin-BqzUXT-K.js +1 -0
  256. package/web-ui/build/static/kotlin-BxhEUQAU.js +1 -0
  257. package/web-ui/build/static/kumir-BFYLWJq-.js +1 -0
  258. package/web-ui/build/static/kusto-CQRzYXm2.js +1 -0
  259. package/web-ui/build/static/lasso-D44z_uI8.js +1 -0
  260. package/web-ui/build/static/latex-BEJdTqYC.js +1 -0
  261. package/web-ui/build/static/latex-CStS4ql1.js +1 -0
  262. package/web-ui/build/static/latte-BPXasl8T.js +1 -0
  263. package/web-ui/build/static/ldif-Wxrk2-xQ.js +1 -0
  264. package/web-ui/build/static/leaf-DrTXK-pT.js +1 -0
  265. package/web-ui/build/static/less-BmoAe98U.js +1 -0
  266. package/web-ui/build/static/less-DzWEFsZj.js +1 -0
  267. package/web-ui/build/static/lilypond-C3ZnRm_L.js +1 -0
  268. package/web-ui/build/static/liquid-eXmypvP_.js +1 -0
  269. package/web-ui/build/static/lisp-DcidyoAl.js +1 -0
  270. package/web-ui/build/static/lisp-DhmUig93.js +1 -0
  271. package/web-ui/build/static/livecodeserver-D5lnfCkg.js +1 -0
  272. package/web-ui/build/static/livescript-Bpb_EpXS.js +1 -0
  273. package/web-ui/build/static/livescript-rF5whmjG.js +1 -0
  274. package/web-ui/build/static/llvm-BD9QG7T-.js +1 -0
  275. package/web-ui/build/static/llvm-CVP-jOx8.js +1 -0
  276. package/web-ui/build/static/log-DRzysjS2.js +1 -0
  277. package/web-ui/build/static/lolcode-BmKgPOwi.js +1 -0
  278. package/web-ui/build/static/lsl-DEO2mV0F.js +1 -0
  279. package/web-ui/build/static/lua-BNDDIa9F.js +1 -0
  280. package/web-ui/build/static/lua-C5n4hyV-.js +1 -0
  281. package/web-ui/build/static/magma-UZvvdXue.js +1 -0
  282. package/web-ui/build/static/makefile-BDQ4tARf.js +1 -0
  283. package/web-ui/build/static/makefile-Bjg-ueA2.js +1 -0
  284. package/web-ui/build/static/markdown-BTXvpQos.js +1 -0
  285. package/web-ui/build/static/markdown-BvMdFo1Y.js +1 -0
  286. package/web-ui/build/static/markup-templating-xSAHnnIS.js +1 -0
  287. package/web-ui/build/static/mathematica-CqNsUy6n.js +1 -0
  288. package/web-ui/build/static/matlab-D-A1WMHc.js +1 -0
  289. package/web-ui/build/static/matlab-Us_SxUWG.js +1 -0
  290. package/web-ui/build/static/maxima-BSyBp09v.js +1 -0
  291. package/web-ui/build/static/maxscript-wyb-3dgT.js +1 -0
  292. package/web-ui/build/static/mel-Bpo7JK_A.js +1 -0
  293. package/web-ui/build/static/mel-eZCDX4iS.js +1 -0
  294. package/web-ui/build/static/mercury-CEgBiaXI.js +1 -0
  295. package/web-ui/build/static/mermaid-BJIHmQsO.js +1 -0
  296. package/web-ui/build/static/mipsasm-c70dMcw9.js +1 -0
  297. package/web-ui/build/static/mizar-CeCpdHS-.js +1 -0
  298. package/web-ui/build/static/mizar-DDlOFZXr.js +1 -0
  299. package/web-ui/build/static/mojolicious-b4UGVld0.js +1 -0
  300. package/web-ui/build/static/mongodb-DwkU2uSm.js +1 -0
  301. package/web-ui/build/static/monkey-CNkGqOfo.js +1 -0
  302. package/web-ui/build/static/monkey-DwmdAwUx.js +1 -0
  303. package/web-ui/build/static/moonscript-C2vv4alq.js +1 -0
  304. package/web-ui/build/static/moonscript-GOmGojxm.js +1 -0
  305. package/web-ui/build/static/n1ql-B-uZXYG_.js +1 -0
  306. package/web-ui/build/static/n1ql-CqKgc7lF.js +1 -0
  307. package/web-ui/build/static/n4js-CLkxsn24.js +1 -0
  308. package/web-ui/build/static/nand2tetris-hdl-CAJAzliK.js +1 -0
  309. package/web-ui/build/static/naniscript-DxWjsrnl.js +1 -0
  310. package/web-ui/build/static/nasm-C-GI38un.js +1 -0
  311. package/web-ui/build/static/neon-DWKBFo--.js +1 -0
  312. package/web-ui/build/static/nevod-DGDg1w8a.js +1 -0
  313. package/web-ui/build/static/nginx-BQbAompS.js +1 -0
  314. package/web-ui/build/static/nginx-BQbgG8J2.js +1 -0
  315. package/web-ui/build/static/nim-Bf864jik.js +1 -0
  316. package/web-ui/build/static/nim-DG4Tf5sg.js +1 -0
  317. package/web-ui/build/static/nix-BBq54ypW.js +1 -0
  318. package/web-ui/build/static/nix-DiVQTSyM.js +1 -0
  319. package/web-ui/build/static/node-repl-Dg0rNxhv.js +1 -0
  320. package/web-ui/build/static/nsis-BG8Lm28v.js +1 -0
  321. package/web-ui/build/static/nsis-CwA_MNyF.js +1 -0
  322. package/web-ui/build/static/objectivec-83FM4mu0.js +1 -0
  323. package/web-ui/build/static/objectivec-CbDgPZGc.js +1 -0
  324. package/web-ui/build/static/ocaml-CXrksX9b.js +1 -0
  325. package/web-ui/build/static/ocaml-aktipfk_.js +1 -0
  326. package/web-ui/build/static/opencl-B1Wu4nP5.js +1 -0
  327. package/web-ui/build/static/openqasm-BnUjC0Rq.js +1 -0
  328. package/web-ui/build/static/openscad-CNEF75nQ.js +1 -0
  329. package/web-ui/build/static/oxygene-B5nQHVvu.js +1 -0
  330. package/web-ui/build/static/oz-Dw07HFRJ.js +1 -0
  331. package/web-ui/build/static/parigp-Dkh7PqN8.js +1 -0
  332. package/web-ui/build/static/parser-CAW1btwt.js +1 -0
  333. package/web-ui/build/static/parser3-DO4cmbnD.js +1 -0
  334. package/web-ui/build/static/pascal-B3XwEzrN.js +1 -0
  335. package/web-ui/build/static/pascaligo-CMKmAZ_B.js +1 -0
  336. package/web-ui/build/static/pcaxis-B5RrdB6p.js +1 -0
  337. package/web-ui/build/static/peoplecode-CGkgPJl9.js +1 -0
  338. package/web-ui/build/static/perl-BWBuZ5Vt.js +1 -0
  339. package/web-ui/build/static/perl-CW0_w5xG.js +1 -0
  340. package/web-ui/build/static/pf-B1aiG4iq.js +1 -0
  341. package/web-ui/build/static/pgsql-BgXadqYx.js +1 -0
  342. package/web-ui/build/static/php-BpB9heTH.js +1 -0
  343. package/web-ui/build/static/php-S3LJn70M.js +1 -0
  344. package/web-ui/build/static/php-extras-Dmblzt0I.js +1 -0
  345. package/web-ui/build/static/php-template-BS_uyUS1.js +1 -0
  346. package/web-ui/build/static/phpdoc-BqP_4LKt.js +1 -0
  347. package/web-ui/build/static/plaintext-BvDBRLjE.js +1 -0
  348. package/web-ui/build/static/plsql-DEj_qOIm.js +1 -0
  349. package/web-ui/build/static/pony-DpvrOLZf.js +1 -0
  350. package/web-ui/build/static/powerquery-C0Wobunx.js +1 -0
  351. package/web-ui/build/static/powershell-CY7ZBlTW.js +1 -0
  352. package/web-ui/build/static/powershell-DiEDDrOo.js +1 -0
  353. package/web-ui/build/static/processing-IXybgMWQ.js +1 -0
  354. package/web-ui/build/static/processing-i136WolF.js +1 -0
  355. package/web-ui/build/static/profile-guIih27Q.js +1 -0
  356. package/web-ui/build/static/prolog-CvmO0VDt.js +1 -0
  357. package/web-ui/build/static/prolog-Dmf4klwf.js +1 -0
  358. package/web-ui/build/static/promql-DObZhyF5.js +1 -0
  359. package/web-ui/build/static/properties-CK08qg7k.js +1 -0
  360. package/web-ui/build/static/properties-DmwePx7J.js +1 -0
  361. package/web-ui/build/static/protobuf-BDkUYcAx.js +1 -0
  362. package/web-ui/build/static/protobuf-CBMNyvhU.js +1 -0
  363. package/web-ui/build/static/psl-eW3BB0n6.js +1 -0
  364. package/web-ui/build/static/pug-D0LF-keh.js +1 -0
  365. package/web-ui/build/static/puppet-BUYZQfB-.js +1 -0
  366. package/web-ui/build/static/puppet-Dbo5UDT-.js +1 -0
  367. package/web-ui/build/static/pure-C5ukDHhg.js +1 -0
  368. package/web-ui/build/static/purebasic-B8eqrPTw.js +1 -0
  369. package/web-ui/build/static/purebasic-BA8g0-l-.js +1 -0
  370. package/web-ui/build/static/purescript-CKa_o5qx.js +1 -0
  371. package/web-ui/build/static/python-C-iEDsum.js +1 -0
  372. package/web-ui/build/static/python-CJcHyEGQ.js +1 -0
  373. package/web-ui/build/static/python-repl-WvDxhQ8t.js +1 -0
  374. package/web-ui/build/static/q-Ben5hioy.js +1 -0
  375. package/web-ui/build/static/q-CenUx0Za.js +1 -0
  376. package/web-ui/build/static/qml-CkUJB-7R.js +1 -0
  377. package/web-ui/build/static/qml-G6uEqbt_.js +1 -0
  378. package/web-ui/build/static/qore-CASrYvaT.js +1 -0
  379. package/web-ui/build/static/qsharp-CJMvScJV.js +1 -0
  380. package/web-ui/build/static/r-C3qOLTEa.js +1 -0
  381. package/web-ui/build/static/r-pFTIkWVX.js +1 -0
  382. package/web-ui/build/static/racket-Dj2Pnxz8.js +1 -0
  383. package/web-ui/build/static/reason-BLo81vJj.js +1 -0
  384. package/web-ui/build/static/reasonml-Uso3eiRd.js +1 -0
  385. package/web-ui/build/static/regex-CRRrWtDv.js +1 -0
  386. package/web-ui/build/static/rego-Pd9N41dQ.js +1 -0
  387. package/web-ui/build/static/renpy-B5nJRJ6W.js +1 -0
  388. package/web-ui/build/static/rest-C4KktBYV.js +1 -0
  389. package/web-ui/build/static/rib-WhwJFXjw.js +1 -0
  390. package/web-ui/build/static/rip-BRMu9y5Q.js +1 -0
  391. package/web-ui/build/static/roboconf-BC-HzyUK.js +1 -0
  392. package/web-ui/build/static/roboconf-Wo-TPzlW.js +1 -0
  393. package/web-ui/build/static/robotframework-Cvw8u8mL.js +1 -0
  394. package/web-ui/build/static/routeros-1EUCLy_j.js +1 -0
  395. package/web-ui/build/static/rsl-DcVNeKkF.js +1 -0
  396. package/web-ui/build/static/ruby-B1fqRC3j.js +1 -0
  397. package/web-ui/build/static/ruby-Bl1wXNzL.js +1 -0
  398. package/web-ui/build/static/ruleslanguage-C3rzl2Ma.js +1 -0
  399. package/web-ui/build/static/rust-4iyLrpFQ.js +1 -0
  400. package/web-ui/build/static/rust-V8MRv3os.js +1 -0
  401. package/web-ui/build/static/sas-ClkrCklD.js +1 -0
  402. package/web-ui/build/static/sas-CyFFy3mS.js +1 -0
  403. package/web-ui/build/static/sass-Cege3fu1.js +1 -0
  404. package/web-ui/build/static/scala-DNb7CDvN.js +1 -0
  405. package/web-ui/build/static/scala-c5k_CS9h.js +1 -0
  406. package/web-ui/build/static/scheme-BXrEBzXP.js +1 -0
  407. package/web-ui/build/static/scheme-COq0pstl.js +1 -0
  408. package/web-ui/build/static/scilab-CRMCfvft.js +1 -0
  409. package/web-ui/build/static/scss-C6MDSdKd.js +1 -0
  410. package/web-ui/build/static/scss-D1INSi-r.js +1 -0
  411. package/web-ui/build/static/shell-BHTFz9Xv.js +1 -0
  412. package/web-ui/build/static/shell-session-B6KXgRjz.js +1 -0
  413. package/web-ui/build/static/smali-CXGHC3zj.js +1 -0
  414. package/web-ui/build/static/smali-DFRkIqFc.js +1 -0
  415. package/web-ui/build/static/smalltalk-DWI2TbuJ.js +1 -0
  416. package/web-ui/build/static/smalltalk-jkgxBKRZ.js +1 -0
  417. package/web-ui/build/static/smarty-BPJMXwnj.js +1 -0
  418. package/web-ui/build/static/sml-BHT8CCeD.js +1 -0
  419. package/web-ui/build/static/sml-D5fkPJU1.js +1 -0
  420. package/web-ui/build/static/solidity-BfFvCWqp.js +1 -0
  421. package/web-ui/build/static/solution-file-DmSVfk_X.js +1 -0
  422. package/web-ui/build/static/soy-CXqMJVf8.js +1 -0
  423. package/web-ui/build/static/sparql-BxIpMyhJ.js +1 -0
  424. package/web-ui/build/static/splunk-spl-DMekyRuD.js +1 -0
  425. package/web-ui/build/static/sqf-DeQ-Lyaa.js +1 -0
  426. package/web-ui/build/static/sqf-hkl5BOBV.js +1 -0
  427. package/web-ui/build/static/sql-Di-rn3Od.js +1 -0
  428. package/web-ui/build/static/sql-DuF5aX9R.js +1 -0
  429. package/web-ui/build/static/sql_more-CCmzwYYA.js +1 -0
  430. package/web-ui/build/static/squirrel-fGLPlBkB.js +1 -0
  431. package/web-ui/build/static/stan-BveEpl1w.js +1 -0
  432. package/web-ui/build/static/stan-CM09XtOD.js +1 -0
  433. package/web-ui/build/static/stata-DeFUbv91.js +1 -0
  434. package/web-ui/build/static/step21-DnUE2ZTl.js +1 -0
  435. package/web-ui/build/static/stylus-Bz9XG6KK.js +1 -0
  436. package/web-ui/build/static/stylus-h33ZUR4b.js +1 -0
  437. package/web-ui/build/static/subunit-v4SRMSnP.js +1 -0
  438. package/web-ui/build/static/swift-BMCxsbHv.js +1 -0
  439. package/web-ui/build/static/swift-qFBaTSsZ.js +1 -0
  440. package/web-ui/build/static/systemd-9w7KWsst.js +1 -0
  441. package/web-ui/build/static/t4-cs-DdBrJkO9.js +1 -0
  442. package/web-ui/build/static/t4-templating-B-3iQJ4y.js +1 -0
  443. package/web-ui/build/static/t4-vb-BBuTbPK8.js +1 -0
  444. package/web-ui/build/static/taggerscript-BPvYYpaJ.js +1 -0
  445. package/web-ui/build/static/tap-DiFULuZV.js +1 -0
  446. package/web-ui/build/static/tap-Q9DMgrzh.js +1 -0
  447. package/web-ui/build/static/tcl-BZcwcRUU.js +1 -0
  448. package/web-ui/build/static/tcl-C4DHKxjE.js +1 -0
  449. package/web-ui/build/static/textile-ByLAjlo8.js +1 -0
  450. package/web-ui/build/static/thrift-C_isEMNl.js +1 -0
  451. package/web-ui/build/static/toml-C6ffwJpv.js +1 -0
  452. package/web-ui/build/static/tp-aV8nCrpp.js +1 -0
  453. package/web-ui/build/static/tremor-Bs1SSWo9.js +1 -0
  454. package/web-ui/build/static/tsx-DdryYBa4.js +1 -0
  455. package/web-ui/build/static/tt2-BeCMXvbZ.js +1 -0
  456. package/web-ui/build/static/turtle-B2FzT5Ov.js +1 -0
  457. package/web-ui/build/static/twig-fVEYqAxR.js +1 -0
  458. package/web-ui/build/static/twig-p9k58-hx.js +1 -0
  459. package/web-ui/build/static/typescript-8bhStN7p.js +1 -0
  460. package/web-ui/build/static/typescript-D2zmnuDR.js +1 -0
  461. package/web-ui/build/static/typoscript-DQePQcKL.js +1 -0
  462. package/web-ui/build/static/unrealscript-BZYSnJjB.js +1 -0
  463. package/web-ui/build/static/uorazor-CGaRtqR5.js +1 -0
  464. package/web-ui/build/static/uri-PJ3dz1qj.js +1 -0
  465. package/web-ui/build/static/v-CI9qDsYZ.js +1 -0
  466. package/web-ui/build/static/vala-B4ses542.js +1 -0
  467. package/web-ui/build/static/vala-BLxHpeKp.js +1 -0
  468. package/web-ui/build/static/vbnet-B7Dzk1rR.js +1 -0
  469. package/web-ui/build/static/vbnet-HOI9SMLZ.js +1 -0
  470. package/web-ui/build/static/vbscript-ERv5iDpi.js +1 -0
  471. package/web-ui/build/static/vbscript-html-D-mjpNAX.js +1 -0
  472. package/web-ui/build/static/velocity-B754qmeE.js +1 -0
  473. package/web-ui/build/static/verilog-3PA83Bxs.js +1 -0
  474. package/web-ui/build/static/verilog-D-lqP5ci.js +1 -0
  475. package/web-ui/build/static/vhdl-BjRYdIRF.js +1 -0
  476. package/web-ui/build/static/vhdl-DgrorvTt.js +1 -0
  477. package/web-ui/build/static/vim-3Oixnlee.js +1 -0
  478. package/web-ui/build/static/vim-BJTKcXdU.js +1 -0
  479. package/web-ui/build/static/visual-basic-BG8-3339.js +1 -0
  480. package/web-ui/build/static/warpscript-CEIgv5UG.js +1 -0
  481. package/web-ui/build/static/wasm-BJlpp9rs.js +1 -0
  482. package/web-ui/build/static/web-idl-Dlh9DutO.js +1 -0
  483. package/web-ui/build/static/wiki-CCcmRzkd.js +1 -0
  484. package/web-ui/build/static/wolfram--Xk-fAUi.js +1 -0
  485. package/web-ui/build/static/wren-B9Ajd9OC.js +1 -0
  486. package/web-ui/build/static/x86asm-DJJ72Zy-.js +1 -0
  487. package/web-ui/build/static/xeora-C0tNsSDj.js +1 -0
  488. package/web-ui/build/static/xl-CzEpyjRJ.js +1 -0
  489. package/web-ui/build/static/xml-CdtQTsBH.js +1 -0
  490. package/web-ui/build/static/xml-doc-TtPAAmc6.js +1 -0
  491. package/web-ui/build/static/xojo-B92fAnQE.js +1 -0
  492. package/web-ui/build/static/xquery-D0Z1FZVO.js +1 -0
  493. package/web-ui/build/static/xquery-DwlcRJ7Q.js +1 -0
  494. package/web-ui/build/static/yaml-DySZjPvD.js +1 -0
  495. package/web-ui/build/static/yaml-YgOVzvAS.js +1 -0
  496. package/web-ui/build/static/yang-BJokKPTo.js +1 -0
  497. package/web-ui/build/static/zephir-BWMUFIRD.js +1 -0
  498. package/web-ui/build/static/zig-Bpzj48cM.js +1 -0
  499. package/web-ui/build/static/1c-4BRB87En.js +0 -1
  500. package/web-ui/build/static/abap-BXIqiAfL.js +0 -1
  501. package/web-ui/build/static/abnf-0gk1i0eD.js +0 -1
  502. package/web-ui/build/static/abnf-jcv7vN9a.js +0 -1
  503. package/web-ui/build/static/accesslog-bXE2aOZl.js +0 -1
  504. package/web-ui/build/static/actionscript-CF-bqawj.js +0 -1
  505. package/web-ui/build/static/actionscript-CodwLKar.js +0 -1
  506. package/web-ui/build/static/ada-BBEDI84F.js +0 -1
  507. package/web-ui/build/static/ada-Kb6JvaxP.js +0 -1
  508. package/web-ui/build/static/agda-UvnMXUUo.js +0 -1
  509. package/web-ui/build/static/al-D5QZ0ijQ.js +0 -1
  510. package/web-ui/build/static/angelscript-RpvZxbeT.js +0 -1
  511. package/web-ui/build/static/antlr4-BPyUvfii.js +0 -1
  512. package/web-ui/build/static/apache-CtM2pGik.js +0 -1
  513. package/web-ui/build/static/apacheconf-D-i9eISH.js +0 -1
  514. package/web-ui/build/static/apex-Dhn_SJi4.js +0 -1
  515. package/web-ui/build/static/apl-HWtdSaYy.js +0 -1
  516. package/web-ui/build/static/applescript-Br9NrmTP.js +0 -1
  517. package/web-ui/build/static/applescript-DLL0TuGw.js +0 -1
  518. package/web-ui/build/static/aql-MjCgNRUX.js +0 -1
  519. package/web-ui/build/static/arcade--DFvEPVv.js +0 -1
  520. package/web-ui/build/static/arduino-Cqw5W_fL.js +0 -1
  521. package/web-ui/build/static/arduino-DIDBbB7R.js +0 -1
  522. package/web-ui/build/static/arff-GGvbnOhc.js +0 -1
  523. package/web-ui/build/static/armasm-DKLRDLs1.js +0 -1
  524. package/web-ui/build/static/asciidoc-CN5qfm0K.js +0 -1
  525. package/web-ui/build/static/asciidoc-CeQEWYOj.js +0 -1
  526. package/web-ui/build/static/asm6502-B1l2Hc9j.js +0 -1
  527. package/web-ui/build/static/asmatmel-BAojNNWJ.js +0 -1
  528. package/web-ui/build/static/aspectj-DqZFhwdh.js +0 -1
  529. package/web-ui/build/static/aspnet-C0kQPqSr.js +0 -1
  530. package/web-ui/build/static/autohotkey-DY25td-Z.js +0 -1
  531. package/web-ui/build/static/autohotkey-DlqXINgk.js +0 -1
  532. package/web-ui/build/static/autoit-By7J2Y34.js +0 -1
  533. package/web-ui/build/static/autoit-nsu9sBga.js +0 -1
  534. package/web-ui/build/static/avisynth-DmhvVsr-.js +0 -1
  535. package/web-ui/build/static/avrasm-CHfBwwoQ.js +0 -1
  536. package/web-ui/build/static/avro-idl-BKgIWEIF.js +0 -1
  537. package/web-ui/build/static/awk-CpAb4d4E.js +0 -1
  538. package/web-ui/build/static/axapta-CM4aTfCZ.js +0 -1
  539. package/web-ui/build/static/bash-D1KTDlk-.js +0 -1
  540. package/web-ui/build/static/bash-DShkskxb.js +0 -1
  541. package/web-ui/build/static/basic-C4CfKvhg.js +0 -1
  542. package/web-ui/build/static/basic-CkQS4VwL.js +0 -1
  543. package/web-ui/build/static/batch-DZtrClI1.js +0 -1
  544. package/web-ui/build/static/bbcode-CY2FsfaO.js +0 -1
  545. package/web-ui/build/static/bicep-BeSEpm_3.js +0 -1
  546. package/web-ui/build/static/birb-huu22Mwu.js +0 -1
  547. package/web-ui/build/static/bison-CmbxfR57.js +0 -1
  548. package/web-ui/build/static/bnf-c2l_4HXN.js +0 -1
  549. package/web-ui/build/static/bnf-wucEDTqY.js +0 -1
  550. package/web-ui/build/static/brainfuck-CzWBg5U7.js +0 -1
  551. package/web-ui/build/static/brainfuck-I_YQ4sma.js +0 -1
  552. package/web-ui/build/static/brightscript-LOkD4eYW.js +0 -1
  553. package/web-ui/build/static/bro-CLJ7Afqo.js +0 -1
  554. package/web-ui/build/static/bsl-DbUgC2FQ.js +0 -1
  555. package/web-ui/build/static/c-C0S2D2ge.js +0 -1
  556. package/web-ui/build/static/c-KH25O_pL.js +0 -1
  557. package/web-ui/build/static/c-like-DtAFxyl_.js +0 -1
  558. package/web-ui/build/static/cal-uGiVJBNC.js +0 -1
  559. package/web-ui/build/static/capnproto-DSLchjOW.js +0 -1
  560. package/web-ui/build/static/ceylon-Bbxk66YL.js +0 -1
  561. package/web-ui/build/static/cfscript-CjDdgKbl.js +0 -1
  562. package/web-ui/build/static/chaiscript-CnRXaMae.js +0 -1
  563. package/web-ui/build/static/cil-CM9ygF3R.js +0 -1
  564. package/web-ui/build/static/clean-ppAH0SYy.js +0 -1
  565. package/web-ui/build/static/clojure-C5LVccGq.js +0 -1
  566. package/web-ui/build/static/clojure-DtLXiuuC.js +0 -1
  567. package/web-ui/build/static/clojure-repl-fSdrYuup.js +0 -1
  568. package/web-ui/build/static/cmake-AD1fuzSY.js +0 -1
  569. package/web-ui/build/static/cmake-D-o08Yta.js +0 -1
  570. package/web-ui/build/static/cobol-bDh1KHf9.js +0 -1
  571. package/web-ui/build/static/coffeescript-D8InK8kf.js +0 -1
  572. package/web-ui/build/static/coffeescript-MKWUOXb5.js +0 -1
  573. package/web-ui/build/static/concurnas-CR56nj6R.js +0 -1
  574. package/web-ui/build/static/coq-DDLBsBJu.js +0 -1
  575. package/web-ui/build/static/coq-WbhtANeW.js +0 -1
  576. package/web-ui/build/static/cos-Bfmre3op.js +0 -1
  577. package/web-ui/build/static/cpp-Cdy6A7gI.js +0 -1
  578. package/web-ui/build/static/cpp-De2KZejr.js +0 -1
  579. package/web-ui/build/static/crmsh-uf3vbKnN.js +0 -1
  580. package/web-ui/build/static/crystal-BHMyEVPP.js +0 -1
  581. package/web-ui/build/static/crystal-DXAQwftg.js +0 -1
  582. package/web-ui/build/static/csharp-Ci2MRG6L.js +0 -1
  583. package/web-ui/build/static/csharp-D8MV2a7f.js +0 -1
  584. package/web-ui/build/static/cshtml-kVj3v3uY.js +0 -1
  585. package/web-ui/build/static/csp-B83CCdVc.js +0 -1
  586. package/web-ui/build/static/csp-DUO7zlPh.js +0 -1
  587. package/web-ui/build/static/css-BWsx7mtO.js +0 -1
  588. package/web-ui/build/static/css-extras-oHch2Y5C.js +0 -1
  589. package/web-ui/build/static/csv-DIf0iNlc.js +0 -1
  590. package/web-ui/build/static/cypher-B1rOF_f3.js +0 -1
  591. package/web-ui/build/static/d-BUgLS87u.js +0 -1
  592. package/web-ui/build/static/d-ClM8XTCr.js +0 -1
  593. package/web-ui/build/static/dart-D5rVGtrb.js +0 -1
  594. package/web-ui/build/static/dart-Oal__k4m.js +0 -1
  595. package/web-ui/build/static/dataweave-B0uxnNDC.js +0 -1
  596. package/web-ui/build/static/dax-B9z6d1P6.js +0 -1
  597. package/web-ui/build/static/delphi-KMlx8q8A.js +0 -1
  598. package/web-ui/build/static/dhall-DLWFgSR8.js +0 -1
  599. package/web-ui/build/static/diff-DDMGnyhE.js +0 -1
  600. package/web-ui/build/static/diff-yg1oXq3U.js +0 -1
  601. package/web-ui/build/static/django-BDtkIu_c.js +0 -1
  602. package/web-ui/build/static/django-Bkn9HlyQ.js +0 -1
  603. package/web-ui/build/static/dns-Bx1VHfo6.js +0 -1
  604. package/web-ui/build/static/dns-zone-file-Bgn5zn_J.js +0 -1
  605. package/web-ui/build/static/docker-CTdrZJHc.js +0 -1
  606. package/web-ui/build/static/dockerfile-B7ziWbf_.js +0 -1
  607. package/web-ui/build/static/dos-cbBC5Icj.js +0 -1
  608. package/web-ui/build/static/dot-JXSVTLEH.js +0 -1
  609. package/web-ui/build/static/dsconfig-sEua2C2o.js +0 -1
  610. package/web-ui/build/static/dts-Zem_gT_O.js +0 -1
  611. package/web-ui/build/static/dust-B-dZ6-uQ.js +0 -1
  612. package/web-ui/build/static/ebnf-CXfeZoXF.js +0 -1
  613. package/web-ui/build/static/ebnf-raQs4FpT.js +0 -1
  614. package/web-ui/build/static/editorconfig-hp8ARbHf.js +0 -1
  615. package/web-ui/build/static/eiffel-tO3jaG00.js +0 -1
  616. package/web-ui/build/static/ejs-BGmD5Rvl.js +0 -1
  617. package/web-ui/build/static/elixir-CF0JDN9L.js +0 -1
  618. package/web-ui/build/static/elixir-CFSil_Ou.js +0 -1
  619. package/web-ui/build/static/elm-As6XaD-g.js +0 -1
  620. package/web-ui/build/static/elm-pw5GpqTZ.js +0 -1
  621. package/web-ui/build/static/erb-BrpCJIyN.js +0 -1
  622. package/web-ui/build/static/erb-D2_eJU9C.js +0 -1
  623. package/web-ui/build/static/erlang-5rSv6NMZ.js +0 -1
  624. package/web-ui/build/static/erlang-RCAxVMBd.js +0 -1
  625. package/web-ui/build/static/erlang-repl-BN0h_6XR.js +0 -1
  626. package/web-ui/build/static/etlua-BMtMnKtv.js +0 -1
  627. package/web-ui/build/static/excel-CRNS57iM.js +0 -1
  628. package/web-ui/build/static/excel-formula-BNw56R2m.js +0 -1
  629. package/web-ui/build/static/factor-7lsloWTf.js +0 -1
  630. package/web-ui/build/static/false-CQUvGnfd.js +0 -1
  631. package/web-ui/build/static/firestore-security-rules-WC1PabFS.js +0 -1
  632. package/web-ui/build/static/fix-Cs6kLeYV.js +0 -1
  633. package/web-ui/build/static/flix-1fdMgw2I.js +0 -1
  634. package/web-ui/build/static/flow-CRPduKSH.js +0 -1
  635. package/web-ui/build/static/fortran-C-3TUt5R.js +0 -1
  636. package/web-ui/build/static/fortran-CA4bbZCj.js +0 -1
  637. package/web-ui/build/static/fsharp-BScjCFae.js +0 -1
  638. package/web-ui/build/static/fsharp-BtHWuW4o.js +0 -1
  639. package/web-ui/build/static/ftl-Re-S2zvF.js +0 -1
  640. package/web-ui/build/static/gams-Cr12l-cM.js +0 -1
  641. package/web-ui/build/static/gap-H0O9-Zju.js +0 -1
  642. package/web-ui/build/static/gauss-CW8jwURx.js +0 -1
  643. package/web-ui/build/static/gcode-CIxfYtg9.js +0 -1
  644. package/web-ui/build/static/gcode-CVqRWYeB.js +0 -1
  645. package/web-ui/build/static/gdscript-BltZCaKl.js +0 -1
  646. package/web-ui/build/static/gedcom-nMT5ety_.js +0 -1
  647. package/web-ui/build/static/gherkin-B4M7CGOo.js +0 -1
  648. package/web-ui/build/static/gherkin-DumuqAQF.js +0 -1
  649. package/web-ui/build/static/git-BBiBlZ5H.js +0 -1
  650. package/web-ui/build/static/glsl-D8ckiRVo.js +0 -1
  651. package/web-ui/build/static/glsl-Z4VdDyPa.js +0 -1
  652. package/web-ui/build/static/gml-BHuSndHE.js +0 -1
  653. package/web-ui/build/static/gml-DZpfN_A4.js +0 -1
  654. package/web-ui/build/static/gn-BjpAHNh_.js +0 -1
  655. package/web-ui/build/static/go-BeZ9IgZB.js +0 -1
  656. package/web-ui/build/static/go-HEXrcgIc.js +0 -1
  657. package/web-ui/build/static/go-module-BGdPb7sP.js +0 -1
  658. package/web-ui/build/static/golo-Cr70KUbI.js +0 -1
  659. package/web-ui/build/static/gradle-C7MXrDnB.js +0 -1
  660. package/web-ui/build/static/graphql-CJK0wQjf.js +0 -1
  661. package/web-ui/build/static/groovy-Bwe7hkFG.js +0 -1
  662. package/web-ui/build/static/groovy-Dc6WDoKa.js +0 -1
  663. package/web-ui/build/static/haml-BSsAjltt.js +0 -1
  664. package/web-ui/build/static/haml-CZih7X_4.js +0 -1
  665. package/web-ui/build/static/handlebars-CN8hCpsy.js +0 -1
  666. package/web-ui/build/static/handlebars-XBLc7i13.js +0 -1
  667. package/web-ui/build/static/haskell-BH9WhK10.js +0 -1
  668. package/web-ui/build/static/haskell-Djf2J1vd.js +0 -1
  669. package/web-ui/build/static/haxe-BllD-W9e.js +0 -1
  670. package/web-ui/build/static/haxe-DY8Hyn7u.js +0 -1
  671. package/web-ui/build/static/hcl-CfktAP2o.js +0 -1
  672. package/web-ui/build/static/hlsl-B2JSOD9d.js +0 -1
  673. package/web-ui/build/static/hoon-BILyDobB.js +0 -1
  674. package/web-ui/build/static/hpkp-Bb8rXx4p.js +0 -1
  675. package/web-ui/build/static/hsp-BmjeJC21.js +0 -1
  676. package/web-ui/build/static/hsts-DJhW57Fu.js +0 -1
  677. package/web-ui/build/static/htmlbars-CJe_ZLjw.js +0 -1
  678. package/web-ui/build/static/http-D7SlABvQ.js +0 -1
  679. package/web-ui/build/static/http-DEUJqmEm.js +0 -1
  680. package/web-ui/build/static/hy-DVfsgEbr.js +0 -1
  681. package/web-ui/build/static/ichigojam-CPdz2bOt.js +0 -1
  682. package/web-ui/build/static/icon-DYK9RbU9.js +0 -1
  683. package/web-ui/build/static/icu-message-format-HiZB_alB.js +0 -1
  684. package/web-ui/build/static/idris-Clk3tJIE.js +0 -1
  685. package/web-ui/build/static/iecst-D7wkeksq.js +0 -1
  686. package/web-ui/build/static/ignore-GSm-L3jG.js +0 -1
  687. package/web-ui/build/static/index-BJe5td0v.js +0 -13
  688. package/web-ui/build/static/index-BYvljeMN.js +0 -1
  689. package/web-ui/build/static/index-CLelJq9j.js +0 -1156
  690. package/web-ui/build/static/index-f5UnFSsY.css +0 -1
  691. package/web-ui/build/static/inform7-D3gc0hSc.js +0 -1
  692. package/web-ui/build/static/inform7-DLdvZTxX.js +0 -1
  693. package/web-ui/build/static/ini-CVcWx1TA.js +0 -1
  694. package/web-ui/build/static/ini-CxYBdsvZ.js +0 -1
  695. package/web-ui/build/static/io-D760QF0i.js +0 -1
  696. package/web-ui/build/static/irpf90-DfkXdZut.js +0 -1
  697. package/web-ui/build/static/isbl-Bl3W-krY.js +0 -1
  698. package/web-ui/build/static/j-DyJj9FSc.js +0 -1
  699. package/web-ui/build/static/java-CLw1cH97.js +0 -1
  700. package/web-ui/build/static/java-vXaHBzP4.js +0 -1
  701. package/web-ui/build/static/javadoc-p5hIe48g.js +0 -1
  702. package/web-ui/build/static/javadoclike-OXi-sQ0q.js +0 -1
  703. package/web-ui/build/static/javascript-CTSqES8o.js +0 -1
  704. package/web-ui/build/static/javastacktrace-PC9-3moA.js +0 -1
  705. package/web-ui/build/static/jboss-cli-CxOZKDLi.js +0 -1
  706. package/web-ui/build/static/jexl-C4kSXhD1.js +0 -1
  707. package/web-ui/build/static/jolie-CntM8cWx.js +0 -1
  708. package/web-ui/build/static/jq-D_4bTCm0.js +0 -1
  709. package/web-ui/build/static/js-extras-Cx9VAZJd.js +0 -1
  710. package/web-ui/build/static/js-templates-BqyJUDxJ.js +0 -1
  711. package/web-ui/build/static/jsdoc-CNHLgAph.js +0 -1
  712. package/web-ui/build/static/json-BXIFrbVC.js +0 -1
  713. package/web-ui/build/static/json-DRUrJL_F.js +0 -1
  714. package/web-ui/build/static/json5-EsTrOpXr.js +0 -1
  715. package/web-ui/build/static/jsonp-RijEuTJY.js +0 -1
  716. package/web-ui/build/static/jsstacktrace-dM13uE1y.js +0 -1
  717. package/web-ui/build/static/jsx-Cf0j02DZ.js +0 -1
  718. package/web-ui/build/static/julia-BLrtj5Dj.js +0 -1
  719. package/web-ui/build/static/julia-D0PJgQhi.js +0 -1
  720. package/web-ui/build/static/julia-repl-BLnLCve4.js +0 -1
  721. package/web-ui/build/static/keepalived-SvzEYloD.js +0 -1
  722. package/web-ui/build/static/keyman-B5l2mmmE.js +0 -1
  723. package/web-ui/build/static/kotlin-B_6COR1e.js +0 -1
  724. package/web-ui/build/static/kotlin-vd1Xdz6D.js +0 -1
  725. package/web-ui/build/static/kumir-8mIZWgJQ.js +0 -1
  726. package/web-ui/build/static/kusto-CoV2oHqt.js +0 -1
  727. package/web-ui/build/static/lasso-BjjUNa_o.js +0 -1
  728. package/web-ui/build/static/latex-C9KOfuFk.js +0 -1
  729. package/web-ui/build/static/latex-DhLttY-I.js +0 -1
  730. package/web-ui/build/static/latte-CaYNaOoK.js +0 -1
  731. package/web-ui/build/static/ldif-DFxnpH3G.js +0 -1
  732. package/web-ui/build/static/leaf-DrMpC83a.js +0 -1
  733. package/web-ui/build/static/less-DUoYpEFn.js +0 -1
  734. package/web-ui/build/static/less-nh0XCcut.js +0 -1
  735. package/web-ui/build/static/lilypond-BlqjLr3W.js +0 -1
  736. package/web-ui/build/static/liquid-BVGHSG0T.js +0 -1
  737. package/web-ui/build/static/lisp-CEK8BtYQ.js +0 -1
  738. package/web-ui/build/static/lisp-D_jGHxhc.js +0 -1
  739. package/web-ui/build/static/livecodeserver-CFUDw_SK.js +0 -1
  740. package/web-ui/build/static/livescript-C5xzpiLM.js +0 -1
  741. package/web-ui/build/static/livescript-CKoPcK0c.js +0 -1
  742. package/web-ui/build/static/llvm-BBOVIeTl.js +0 -1
  743. package/web-ui/build/static/llvm-ioC5bCUB.js +0 -1
  744. package/web-ui/build/static/log-CPYTqm4T.js +0 -1
  745. package/web-ui/build/static/lolcode-BkT93adT.js +0 -1
  746. package/web-ui/build/static/lsl-M98YPNRW.js +0 -1
  747. package/web-ui/build/static/lua-D7Lum5n9.js +0 -1
  748. package/web-ui/build/static/lua-DHyr0QwE.js +0 -1
  749. package/web-ui/build/static/magma-BwMFjwQu.js +0 -1
  750. package/web-ui/build/static/makefile-BFYZ-SfG.js +0 -1
  751. package/web-ui/build/static/makefile-D6oyR2lD.js +0 -1
  752. package/web-ui/build/static/markdown-6jtg6KZm.js +0 -1
  753. package/web-ui/build/static/markdown-DTLtPgeP.js +0 -1
  754. package/web-ui/build/static/markup-templating-K8uoAIqY.js +0 -1
  755. package/web-ui/build/static/mathematica-cW5hOuB2.js +0 -1
  756. package/web-ui/build/static/matlab-CwSNWEq8.js +0 -1
  757. package/web-ui/build/static/matlab-MQ972BY-.js +0 -1
  758. package/web-ui/build/static/maxima-CMSFLri-.js +0 -1
  759. package/web-ui/build/static/maxscript-BzcoS2KA.js +0 -1
  760. package/web-ui/build/static/mel-Cd20pAs-.js +0 -1
  761. package/web-ui/build/static/mel-KWG5N49g.js +0 -1
  762. package/web-ui/build/static/mercury-eW1ljJh0.js +0 -1
  763. package/web-ui/build/static/mermaid-D3zeCM84.js +0 -1
  764. package/web-ui/build/static/mipsasm-B5g6Ndra.js +0 -1
  765. package/web-ui/build/static/mizar-C_nZ0Ts9.js +0 -1
  766. package/web-ui/build/static/mizar-CuTNEv2t.js +0 -1
  767. package/web-ui/build/static/mojolicious-C1Xq--lT.js +0 -1
  768. package/web-ui/build/static/mongodb-C_3giArw.js +0 -1
  769. package/web-ui/build/static/monkey-DQ1olPd-.js +0 -1
  770. package/web-ui/build/static/monkey-n4Tbrep7.js +0 -1
  771. package/web-ui/build/static/moonscript-CmnQJuIL.js +0 -1
  772. package/web-ui/build/static/moonscript-DXgmWjLy.js +0 -1
  773. package/web-ui/build/static/n1ql-531a3W-1.js +0 -1
  774. package/web-ui/build/static/n1ql-BYH9EvME.js +0 -1
  775. package/web-ui/build/static/n4js-Cy0LcM-V.js +0 -1
  776. package/web-ui/build/static/nand2tetris-hdl-CKOztBDD.js +0 -1
  777. package/web-ui/build/static/naniscript-oLFpxBGE.js +0 -1
  778. package/web-ui/build/static/nasm-DLPnwFkM.js +0 -1
  779. package/web-ui/build/static/neon-Cn03wGUR.js +0 -1
  780. package/web-ui/build/static/nevod-C_VRvbdf.js +0 -1
  781. package/web-ui/build/static/nginx-60SpLv4t.js +0 -1
  782. package/web-ui/build/static/nginx-DZLHid-q.js +0 -1
  783. package/web-ui/build/static/nim-BnrHdqGC.js +0 -1
  784. package/web-ui/build/static/nim-Ds3pmwtE.js +0 -1
  785. package/web-ui/build/static/nix-Dl-dS0w4.js +0 -1
  786. package/web-ui/build/static/nix-jL62FzTz.js +0 -1
  787. package/web-ui/build/static/node-repl-BAuDzNZd.js +0 -1
  788. package/web-ui/build/static/nsis-CEnFjfNr.js +0 -1
  789. package/web-ui/build/static/nsis-CcCz6HIu.js +0 -1
  790. package/web-ui/build/static/objectivec-CZXfVowS.js +0 -1
  791. package/web-ui/build/static/objectivec-CuaFU-Ef.js +0 -1
  792. package/web-ui/build/static/ocaml-BGAzfwAx.js +0 -1
  793. package/web-ui/build/static/ocaml-BoO1lU26.js +0 -1
  794. package/web-ui/build/static/opencl-DAnD5xP-.js +0 -1
  795. package/web-ui/build/static/openqasm-CO7gdVj9.js +0 -1
  796. package/web-ui/build/static/openscad-B2wMEMQR.js +0 -1
  797. package/web-ui/build/static/oxygene-1erv93gF.js +0 -1
  798. package/web-ui/build/static/oz-D3334gi7.js +0 -1
  799. package/web-ui/build/static/parigp-rQZTN8zi.js +0 -1
  800. package/web-ui/build/static/parser-B4ZSZ1ig.js +0 -1
  801. package/web-ui/build/static/parser3-CCCjkYLG.js +0 -1
  802. package/web-ui/build/static/pascal-DeC9Pvom.js +0 -1
  803. package/web-ui/build/static/pascaligo-CT2u6UpO.js +0 -1
  804. package/web-ui/build/static/pcaxis-CvlRg6t4.js +0 -1
  805. package/web-ui/build/static/peoplecode-BYahULz2.js +0 -1
  806. package/web-ui/build/static/perl-CKgvyY59.js +0 -1
  807. package/web-ui/build/static/perl-E7sx7Izv.js +0 -1
  808. package/web-ui/build/static/pf-D9wnFBt7.js +0 -1
  809. package/web-ui/build/static/pgsql-CrlBdRAn.js +0 -1
  810. package/web-ui/build/static/php-D2O-B3y7.js +0 -1
  811. package/web-ui/build/static/php-extras-BKqZuzgt.js +0 -1
  812. package/web-ui/build/static/php-nfXl9WfB.js +0 -1
  813. package/web-ui/build/static/php-template-Cg8ERLNQ.js +0 -1
  814. package/web-ui/build/static/phpdoc-Z1u90OwH.js +0 -1
  815. package/web-ui/build/static/plaintext-DkNVeXWx.js +0 -1
  816. package/web-ui/build/static/plsql-DO5fJNhS.js +0 -1
  817. package/web-ui/build/static/pony-DXIqq5Bk.js +0 -1
  818. package/web-ui/build/static/powerquery-B2ovxJZi.js +0 -1
  819. package/web-ui/build/static/powershell-BVeqqari.js +0 -1
  820. package/web-ui/build/static/powershell-CskV4qWh.js +0 -1
  821. package/web-ui/build/static/processing-D90SJpnq.js +0 -1
  822. package/web-ui/build/static/processing-DP2mf97Z.js +0 -1
  823. package/web-ui/build/static/profile-H6zk8v_f.js +0 -1
  824. package/web-ui/build/static/prolog-AUfxYiJV.js +0 -1
  825. package/web-ui/build/static/prolog-Dz2WdsVN.js +0 -1
  826. package/web-ui/build/static/promql-CloMJmMW.js +0 -1
  827. package/web-ui/build/static/properties-CTl3mnrP.js +0 -1
  828. package/web-ui/build/static/properties-D2RfteTZ.js +0 -1
  829. package/web-ui/build/static/protobuf-Be9oqQCu.js +0 -1
  830. package/web-ui/build/static/protobuf-BgG8zUnY.js +0 -1
  831. package/web-ui/build/static/psl-Bqx8iN9e.js +0 -1
  832. package/web-ui/build/static/pug-CHfFC0Wg.js +0 -1
  833. package/web-ui/build/static/puppet-__1ifzZS.js +0 -1
  834. package/web-ui/build/static/puppet-pbtRCMgd.js +0 -1
  835. package/web-ui/build/static/pure-B9STuf4F.js +0 -1
  836. package/web-ui/build/static/purebasic-7ADyn-Ce.js +0 -1
  837. package/web-ui/build/static/purebasic-Bc06YQcD.js +0 -1
  838. package/web-ui/build/static/purescript-CU-dDebr.js +0 -1
  839. package/web-ui/build/static/python-BEsXrEbi.js +0 -1
  840. package/web-ui/build/static/python-CZFrgH2X.js +0 -1
  841. package/web-ui/build/static/python-repl-CI9GmJce.js +0 -1
  842. package/web-ui/build/static/q-E7xvTGRo.js +0 -1
  843. package/web-ui/build/static/q-hvg9aQoY.js +0 -1
  844. package/web-ui/build/static/qml-DkZWcxEv.js +0 -1
  845. package/web-ui/build/static/qml-DpdWla8G.js +0 -1
  846. package/web-ui/build/static/qore-Dsm7ACR_.js +0 -1
  847. package/web-ui/build/static/qsharp-BBec6MJR.js +0 -1
  848. package/web-ui/build/static/r-CTVkq_Ky.js +0 -1
  849. package/web-ui/build/static/r-DHGNCupF.js +0 -1
  850. package/web-ui/build/static/racket-hoSh4YPx.js +0 -1
  851. package/web-ui/build/static/reason-CQPUIbsm.js +0 -1
  852. package/web-ui/build/static/reasonml-BXtFFSTZ.js +0 -1
  853. package/web-ui/build/static/regex-BNkF_ufb.js +0 -1
  854. package/web-ui/build/static/rego-BXHrjItC.js +0 -1
  855. package/web-ui/build/static/renpy-CBQvK_5t.js +0 -1
  856. package/web-ui/build/static/rest-Bf8hzmF1.js +0 -1
  857. package/web-ui/build/static/rib-D0a4KzlP.js +0 -1
  858. package/web-ui/build/static/rip-DchN4BQN.js +0 -1
  859. package/web-ui/build/static/roboconf-HXnyGNTm.js +0 -1
  860. package/web-ui/build/static/roboconf-zfK6mgmU.js +0 -1
  861. package/web-ui/build/static/robotframework-DnP-vXyd.js +0 -1
  862. package/web-ui/build/static/routeros-CDirYxZY.js +0 -1
  863. package/web-ui/build/static/rsl-BCK_msc-.js +0 -1
  864. package/web-ui/build/static/ruby--mXX6GNK.js +0 -1
  865. package/web-ui/build/static/ruby-BIdQYq0Y.js +0 -1
  866. package/web-ui/build/static/ruleslanguage-BD07s5RU.js +0 -1
  867. package/web-ui/build/static/rust-Bgni9V7U.js +0 -1
  868. package/web-ui/build/static/rust-OywmQ2ox.js +0 -1
  869. package/web-ui/build/static/sas-B6-82htH.js +0 -1
  870. package/web-ui/build/static/sas-k9XEScbR.js +0 -1
  871. package/web-ui/build/static/sass-BOvNBStT.js +0 -1
  872. package/web-ui/build/static/scala-At07fmMd.js +0 -1
  873. package/web-ui/build/static/scala-DUOfqSaU.js +0 -1
  874. package/web-ui/build/static/scheme-B87lC0bK.js +0 -1
  875. package/web-ui/build/static/scheme-BS7OzWRX.js +0 -1
  876. package/web-ui/build/static/scilab-Bmn4jjcu.js +0 -1
  877. package/web-ui/build/static/scss-DTeB95yE.js +0 -1
  878. package/web-ui/build/static/scss-xilnl6Dx.js +0 -1
  879. package/web-ui/build/static/shell-CKetyy4y.js +0 -1
  880. package/web-ui/build/static/shell-session-B6rat2c7.js +0 -1
  881. package/web-ui/build/static/smali-C9O9CXOD.js +0 -1
  882. package/web-ui/build/static/smali-CBkgbinS.js +0 -1
  883. package/web-ui/build/static/smalltalk-CuUjFHiz.js +0 -1
  884. package/web-ui/build/static/smalltalk-D0xhFL9b.js +0 -1
  885. package/web-ui/build/static/smarty-CY0N0d5x.js +0 -1
  886. package/web-ui/build/static/sml-BVAw3fjB.js +0 -1
  887. package/web-ui/build/static/sml-CO9XB1ax.js +0 -1
  888. package/web-ui/build/static/solidity-BNixD4Dx.js +0 -1
  889. package/web-ui/build/static/solution-file-DHyGXf2y.js +0 -1
  890. package/web-ui/build/static/soy-Cxgjntsl.js +0 -1
  891. package/web-ui/build/static/sparql-CDLqu9vy.js +0 -1
  892. package/web-ui/build/static/splunk-spl-Cmkl_Xmw.js +0 -1
  893. package/web-ui/build/static/sqf-CIGKolw3.js +0 -1
  894. package/web-ui/build/static/sqf-Cfp_lC6-.js +0 -1
  895. package/web-ui/build/static/sql-DHOGwYxe.js +0 -1
  896. package/web-ui/build/static/sql-QIw1Q2yi.js +0 -1
  897. package/web-ui/build/static/sql_more-C0b3-InT.js +0 -1
  898. package/web-ui/build/static/squirrel-DASToo1F.js +0 -1
  899. package/web-ui/build/static/stan-DHMkcFZg.js +0 -1
  900. package/web-ui/build/static/stan-No3nsdnQ.js +0 -1
  901. package/web-ui/build/static/stata-Brkzu5Nv.js +0 -1
  902. package/web-ui/build/static/step21-D2l5M5Ml.js +0 -1
  903. package/web-ui/build/static/stylus-Bx6FaZN_.js +0 -1
  904. package/web-ui/build/static/stylus-CVkTaPns.js +0 -1
  905. package/web-ui/build/static/subunit-CDrsAMuL.js +0 -1
  906. package/web-ui/build/static/swift-BSw_pS-l.js +0 -1
  907. package/web-ui/build/static/swift-BWyqwIo8.js +0 -1
  908. package/web-ui/build/static/systemd-BwqpYcJK.js +0 -1
  909. package/web-ui/build/static/t4-cs-CwsJLOxV.js +0 -1
  910. package/web-ui/build/static/t4-templating-BfxY01xf.js +0 -1
  911. package/web-ui/build/static/t4-vb-BhsKKFhp.js +0 -1
  912. package/web-ui/build/static/taggerscript-Bkew95or.js +0 -1
  913. package/web-ui/build/static/tap-DHovcHbS.js +0 -1
  914. package/web-ui/build/static/tap-DPl9uPo4.js +0 -1
  915. package/web-ui/build/static/tcl-BeozHO-f.js +0 -1
  916. package/web-ui/build/static/tcl-Df_8ItQH.js +0 -1
  917. package/web-ui/build/static/textile-B3m_f58b.js +0 -1
  918. package/web-ui/build/static/thrift-D20n-HpW.js +0 -1
  919. package/web-ui/build/static/toml-C-fTAonK.js +0 -1
  920. package/web-ui/build/static/tp-hGbXe5Eo.js +0 -1
  921. package/web-ui/build/static/tremor-BaVOtRFz.js +0 -1
  922. package/web-ui/build/static/tsx-ChPInNl0.js +0 -1
  923. package/web-ui/build/static/tt2-DAIJ8cpz.js +0 -1
  924. package/web-ui/build/static/turtle-B7z_y-PK.js +0 -1
  925. package/web-ui/build/static/twig-TUEkve6W.js +0 -1
  926. package/web-ui/build/static/twig-stKDItz7.js +0 -1
  927. package/web-ui/build/static/typescript-BPdM-QOM.js +0 -1
  928. package/web-ui/build/static/typescript-DjSY4MvL.js +0 -1
  929. package/web-ui/build/static/typoscript-BQHgQtEL.js +0 -1
  930. package/web-ui/build/static/unrealscript-g8yNPQQ5.js +0 -1
  931. package/web-ui/build/static/uorazor-CnuMiQz9.js +0 -1
  932. package/web-ui/build/static/uri-CfioiemU.js +0 -1
  933. package/web-ui/build/static/v-B69Oau14.js +0 -1
  934. package/web-ui/build/static/vala-C0PjlK3l.js +0 -1
  935. package/web-ui/build/static/vala-COgjD3gU.js +0 -1
  936. package/web-ui/build/static/vbnet-CklOnyuR.js +0 -1
  937. package/web-ui/build/static/vbnet-MxHUC5Mz.js +0 -1
  938. package/web-ui/build/static/vbscript-BxQnA97w.js +0 -1
  939. package/web-ui/build/static/vbscript-html-DOtde7dD.js +0 -1
  940. package/web-ui/build/static/velocity-l8B9m66X.js +0 -1
  941. package/web-ui/build/static/verilog-DF6dF-Vp.js +0 -1
  942. package/web-ui/build/static/verilog-Dgb9SDRX.js +0 -1
  943. package/web-ui/build/static/vhdl-0GarU_AK.js +0 -1
  944. package/web-ui/build/static/vhdl-BfPQnOdI.js +0 -1
  945. package/web-ui/build/static/vim-Likg_-II.js +0 -1
  946. package/web-ui/build/static/vim-k53Sj7A3.js +0 -1
  947. package/web-ui/build/static/visual-basic-BNVZCzlV.js +0 -1
  948. package/web-ui/build/static/warpscript-BkLK5iuW.js +0 -1
  949. package/web-ui/build/static/wasm-CQhA1ece.js +0 -1
  950. package/web-ui/build/static/web-idl-DmNsI0kE.js +0 -1
  951. package/web-ui/build/static/wiki-B3z4pmra.js +0 -1
  952. package/web-ui/build/static/wolfram-Dsf21d_g.js +0 -1
  953. package/web-ui/build/static/wren-BZXZlzrL.js +0 -1
  954. package/web-ui/build/static/x86asm-DpWGZhJU.js +0 -1
  955. package/web-ui/build/static/xeora-8xPZN4DH.js +0 -1
  956. package/web-ui/build/static/xl-XkdWYMGl.js +0 -1
  957. package/web-ui/build/static/xml-BBdROEWt.js +0 -1
  958. package/web-ui/build/static/xml-doc-Cjs4OpTe.js +0 -1
  959. package/web-ui/build/static/xojo-nCen1FPx.js +0 -1
  960. package/web-ui/build/static/xquery-CBFkAxo1.js +0 -1
  961. package/web-ui/build/static/xquery-Dwsc1weX.js +0 -1
  962. package/web-ui/build/static/yaml-DzSAh6jl.js +0 -1
  963. package/web-ui/build/static/yaml-Lg8gnEUU.js +0 -1
  964. package/web-ui/build/static/yang-Bd-v2b0k.js +0 -1
  965. package/web-ui/build/static/zephir-wbJOizIa.js +0 -1
  966. package/web-ui/build/static/zig-Cz384XjE.js +0 -1
@@ -1,18 +1,43 @@
1
1
  /**
2
- * TelegramService — Remote agent interface via Telegram Bot
2
+ * TelegramService — Conversational agent interface over Telegram.
3
3
  *
4
- * Purpose:
5
- * - Full conversational interface with agents from phone
6
- * - @agent-name prefix routing (sticky session for no-prefix)
7
- * - Smart response formatting (markdown, code blocks, images, inline keyboards)
8
- * - On-demand notifications (/watch)
9
- * - Prompt/credential relay for interactive agent flows
4
+ * Public surface (unchanged):
5
+ * getTelegramService(logger) singleton
6
+ * .setOrchestrator() .setAgentPool() .setWebSocketManager() .setFlowExecutor()
7
+ * .autoConnect() / .connect(botToken) / .disconnect()
8
+ * .getStatus() / .sendTestMessage()
9
+ * .getBridgedChannels(agentId) / .isAgentBridged(agentId)
10
10
  *
11
- * Architecture:
12
- * - Long polling (no webhook, works behind NAT)
13
- * - Intercepts WebSocket broadcasts to capture agent responses
14
- * - Routes user messages to agents via orchestrator.processRequest()
15
- * - Optional dependency system works without it
11
+ * Capabilities (UX-focused):
12
+ * Multi-chat any number of private chats can /start the bot; the
13
+ * first-chat-wins lock is gone. Each chat keeps its own sticky agent
14
+ * + watch state. (`chats: Map<chatId, ChatState>`)
15
+ * Group chatbots in Telegram groups respond ONLY when @mentioned
16
+ * by their username (Telegram convention; prevents bot spam).
17
+ * • Voice notes — downloaded, base64-posted to the backend's
18
+ * /llm/transcribe endpoint (which proxies to whichever Azure AI
19
+ * Foundry speech-to-text deployment is in the model catalog —
20
+ * gpt-4o-mini-transcribe by default), then handled as text. Falls
21
+ * back to a friendly "voice not configured" message when the
22
+ * backend route returns 503.
23
+ * • Typing indicator + "thinking" placeholder — emitted before the
24
+ * orchestrator call so the user sees instant feedback; replaced
25
+ * with the real reply via editMessageText.
26
+ * • Rich reply blocks via `<actions>` / `<image>` / `<attachment>`
27
+ * tags inside `<external>` — see telegramBlockParser.js.
28
+ * • Errors and timeouts always relayed (not gated by /watch).
29
+ * • Prompt-request timeout sends a "your prompt expired" message
30
+ * instead of silently dropping the request.
31
+ * • Paginated `/agents` and `/flows` for large pools.
32
+ * • Long code-only replies are sent as a `.txt` document so
33
+ * Telegram's MarkdownV2 escaping never mangles them.
34
+ *
35
+ * Config persistence (backward-compatible):
36
+ * Old: `{ chatId, watchEnabled }` (scalar)
37
+ * New: `{ chats: [{ chatId, addedAt, watchEnabled, lastAgentId }], … }`
38
+ * On load, a legacy scalar `chatId` is migrated to a single-entry
39
+ * chats array. Writes always emit the new shape; the legacy field
40
+ * is dropped silently.
16
41
  */
17
42
 
18
43
  import { promises as fs } from 'fs';
@@ -20,18 +45,22 @@ import path from 'path';
20
45
  import { getUserDataPaths, ensureUserDataDirs } from '../utilities/userDataDir.js';
21
46
  import { filterContentForExternalRelay, resolveBlockTargets } from './channelFilter.js';
22
47
  import { createTelegramSource } from './messageSource.js';
48
+ import { parseTelegramBlock } from './telegramBlockParser.js';
23
49
 
24
50
  const TELEGRAM_STATUS = {
25
51
  DISCONNECTED: 'disconnected',
26
- CONNECTING: 'connecting',
27
- CONNECTED: 'connected',
28
- FAILED: 'failed'
52
+ CONNECTING: 'connecting',
53
+ CONNECTED: 'connected',
54
+ FAILED: 'failed',
29
55
  };
30
56
 
31
- const MAX_MESSAGE_LENGTH = 4000; // Telegram limit is 4096, leave room for formatting
57
+ const MAX_MESSAGE_LENGTH = 4000; // Telegram limit is 4096 leave room for headers
32
58
  const NOTIFICATION_BATCH_INTERVAL_MS = 10000;
33
- const PROMPT_TIMEOUT_MS = 300000; // 5 minutes
34
- const PROMPT_REMINDER_MS = 180000; // 3 minutes
59
+ const PROMPT_REMINDER_MS = 180000; // 3 min — "still waiting" nudge
60
+ const PROMPT_TIMEOUT_MS = 600000; // 10 min — hard timeout, send cancel
61
+ const PAGE_SIZE = 8; // /agents and /flows page size
62
+ const CODE_AS_FILE_THRESHOLD = 3200; // chars; above this, send code as .txt
63
+ const IMAGE_PENDING_TTL_MS = 5 * 60 * 1000; // 5 min — buffer window for imageGenerated → stream_complete
35
64
 
36
65
  class TelegramService {
37
66
  constructor(logger = null) {
@@ -46,29 +75,70 @@ class TelegramService {
46
75
  // Bot state
47
76
  this.bot = null;
48
77
  this.status = TELEGRAM_STATUS.DISCONNECTED;
49
- this.chatId = null;
50
- this.lastAgentId = null;
51
- this.activeAgentIds = new Set(); // all agents user has addressed from Telegram
52
-
53
- // Relay state
54
- this.pendingRelays = new Map();
55
- this.replyContext = null; // current expected reply
56
-
57
- // Notifications
58
- this.watchEnabled = false;
59
- this.notificationQueue = [];
60
- this.notificationTimer = null;
78
+ this.botUsername = null; // populated by getMe() at connect
79
+
80
+ /**
81
+ * Per-chat state. Keyed by chatId (string). Each value:
82
+ * {
83
+ * type: 'private' | 'group' | 'supergroup' | 'channel',
84
+ * title?: string,
85
+ * addedAt: ISO string,
86
+ * watchEnabled: boolean,
87
+ * lastAgentId: string|null,
88
+ * activeAgentIds: Set<string>,
89
+ * // Tracking for in-flight "🔄 Thinking" placeholders:
90
+ * thinkingByAgentId: Map<agentId, messageId>,
91
+ * }
92
+ */
93
+ this.chats = new Map();
94
+
95
+ // Relay state (global — keyed by requestId)
96
+ this.pendingRelays = new Map(); // requestId → { type, agentId, chatId, timeoutId, reminderId }
97
+ this.replyContext = new Map(); // chatId → { type, requestId, agentId }
98
+
99
+ // Notifications (batched per-chat)
100
+ this.notificationQueue = new Map(); // chatId → string[]
101
+ this.notificationTimers = new Map(); // chatId → timeout handle
61
102
 
62
103
  // Config
63
104
  this.dataDir = null;
64
105
  this.configPath = null;
65
106
  this.config = {};
66
107
 
108
+ // Backend (for transcription) — falls back to the same default
109
+ // the rest of the CLI uses. webServer.js sets this via
110
+ // setBackendBaseUrl().
111
+ this.backendBaseUrl = 'https://autopilot-api.azurewebsites.net';
112
+ // Platform API key resolver. May be set as either:
113
+ // a) a function () => string|null (preferred — re-resolves on
114
+ // every call, so signin/signout after boot is handled
115
+ // automatically)
116
+ // b) a plain string (legacy — captured once)
117
+ // Both are exposed through `_resolvePlatformKey()` below.
118
+ this._platformApiKey = null;
119
+
67
120
  // Original broadcast (saved before wrapping)
68
121
  this._originalBroadcast = null;
122
+
123
+ /**
124
+ * Pending generated images, keyed by agentId. The imageTool emits
125
+ * its own `imageGenerated` broadcast independently of the agent's
126
+ * text completion; we buffer those URLs here and flush them in
127
+ * `_relayAgentResponse` so the image reaches Telegram even if the
128
+ * agent's final reply text doesn't embed the image markdown.
129
+ *
130
+ * Shape: Map<agentId, Array<{ imageUrl, prompt, addedAt }>>
131
+ *
132
+ * Entries older than IMAGE_PENDING_TTL_MS are pruned at flush time
133
+ * — guards against a stranded image surfacing later in an unrelated
134
+ * conversation turn. The TTL is long enough for slow generations
135
+ * (DALL-E 3, video previews) but short enough to keep the buffer
136
+ * from accumulating across hours of idle.
137
+ */
138
+ this._pendingImagesByAgent = new Map();
69
139
  }
70
140
 
71
- // --- Dependency Injection ---
141
+ // ── Dependency Injection ───────────────────────────────────────────
72
142
 
73
143
  setOrchestrator(orchestrator) { this.orchestrator = orchestrator; }
74
144
  setAgentPool(agentPool) { this.agentPool = agentPool; }
@@ -77,8 +147,20 @@ class TelegramService {
77
147
  this._interceptBroadcasts(wsManager);
78
148
  }
79
149
  setFlowExecutor(flowExecutor) { this.flowExecutor = flowExecutor; }
150
+ setBackendBaseUrl(url) { if (url) this.backendBaseUrl = String(url).replace(/\/$/, ''); }
151
+ /** Accept a string OR a () => string|null getter. The getter form is
152
+ * preferred so a sign-in / sign-out cycle after boot is reflected
153
+ * without re-wiring. */
154
+ setPlatformApiKey(keyOrGetter) { this._platformApiKey = keyOrGetter || null; }
155
+ _resolvePlatformKey() {
156
+ const k = this._platformApiKey;
157
+ if (typeof k === 'function') {
158
+ try { return k() || null; } catch { return null; }
159
+ }
160
+ return k || null;
161
+ }
80
162
 
81
- // --- Config Persistence ---
163
+ // ── Config Persistence ─────────────────────────────────────────────
82
164
 
83
165
  async _ensureDataDir() {
84
166
  if (!this.dataDir) {
@@ -95,22 +177,58 @@ class TelegramService {
95
177
  try {
96
178
  const data = await fs.readFile(this.configPath, 'utf8');
97
179
  this.config = JSON.parse(data);
98
- this.chatId = this.config.chatId || null;
99
- this.watchEnabled = this.config.watchEnabled || false;
100
180
  } catch {
101
181
  this.config = {};
102
182
  }
183
+
184
+ // Migration: legacy `{ chatId, watchEnabled }` → `{ chats: [...] }`.
185
+ // We intentionally do NOT delete the legacy field from this.config
186
+ // until the next save so a hand-edited file roundtrips cleanly.
187
+ if (this.config.chatId && !Array.isArray(this.config.chats)) {
188
+ this.config.chats = [{
189
+ chatId: String(this.config.chatId),
190
+ type: 'private',
191
+ title: null,
192
+ addedAt: this.config.updatedAt || new Date().toISOString(),
193
+ watchEnabled: !!this.config.watchEnabled,
194
+ lastAgentId: null,
195
+ }];
196
+ }
197
+
198
+ this.chats = new Map();
199
+ for (const entry of (this.config.chats || [])) {
200
+ if (!entry?.chatId) continue;
201
+ this.chats.set(String(entry.chatId), {
202
+ type: entry.type || 'private',
203
+ title: entry.title || null,
204
+ addedAt: entry.addedAt || new Date().toISOString(),
205
+ watchEnabled: !!entry.watchEnabled,
206
+ lastAgentId: entry.lastAgentId || null,
207
+ activeAgentIds: new Set(entry.activeAgentIds || []),
208
+ thinkingByAgentId: new Map(),
209
+ });
210
+ }
103
211
  }
104
212
 
105
213
  async _saveConfig() {
106
214
  await this._ensureDataDir();
107
- this.config.chatId = this.chatId;
108
- this.config.watchEnabled = this.watchEnabled;
215
+ // Re-shape in-memory state for persistence.
216
+ this.config.chats = [...this.chats.entries()].map(([chatId, s]) => ({
217
+ chatId,
218
+ type: s.type,
219
+ title: s.title,
220
+ addedAt: s.addedAt,
221
+ watchEnabled: s.watchEnabled,
222
+ lastAgentId: s.lastAgentId,
223
+ activeAgentIds: [...s.activeAgentIds],
224
+ }));
225
+ delete this.config.chatId;
226
+ delete this.config.watchEnabled;
109
227
  this.config.updatedAt = new Date().toISOString();
110
228
  await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2), 'utf8');
111
229
  }
112
230
 
113
- // --- Lifecycle ---
231
+ // ── Lifecycle ──────────────────────────────────────────────────────
114
232
 
115
233
  async autoConnect() {
116
234
  await this._loadConfig();
@@ -127,7 +245,6 @@ class TelegramService {
127
245
  if (this.status === TELEGRAM_STATUS.CONNECTED) {
128
246
  await this.disconnect();
129
247
  }
130
-
131
248
  this.status = TELEGRAM_STATUS.CONNECTING;
132
249
  this.logger?.info('[TelegramService] Connecting...');
133
250
 
@@ -135,18 +252,18 @@ class TelegramService {
135
252
  const TelegramBot = (await import('node-telegram-bot-api')).default;
136
253
  this.bot = new TelegramBot(botToken, { polling: true });
137
254
 
138
- // Verify token by getting bot info
139
255
  const me = await this.bot.getMe();
140
- this.logger?.info('[TelegramService] Connected', { botName: me.username });
256
+ this.botUsername = me.username || null;
257
+ this.logger?.info('[TelegramService] Connected', { botName: this.botUsername });
141
258
 
142
259
  this.config.botToken = botToken;
143
- this.config.botUsername = me.username;
260
+ this.config.botUsername = this.botUsername;
144
261
  await this._saveConfig();
145
262
 
146
263
  this._setupHandlers();
147
264
  this.status = TELEGRAM_STATUS.CONNECTED;
148
265
 
149
- return { username: me.username, id: me.id };
266
+ return { username: this.botUsername, id: me.id };
150
267
  } catch (error) {
151
268
  this.status = TELEGRAM_STATUS.FAILED;
152
269
  this.logger?.error('[TelegramService] Connection failed', { error: error.message });
@@ -156,11 +273,13 @@ class TelegramService {
156
273
 
157
274
  async disconnect() {
158
275
  if (this.bot) {
159
- try { await this.bot.stopPolling(); } catch {}
276
+ try { await this.bot.stopPolling(); } catch { /* ignore */ }
160
277
  this.bot = null;
161
278
  }
162
279
  this.status = TELEGRAM_STATUS.DISCONNECTED;
163
- this._clearNotificationTimer();
280
+ for (const t of this.notificationTimers.values()) clearTimeout(t);
281
+ this.notificationTimers.clear();
282
+ this.notificationQueue.clear();
164
283
  this.logger?.info('[TelegramService] Disconnected');
165
284
  }
166
285
 
@@ -168,130 +287,303 @@ class TelegramService {
168
287
  return {
169
288
  status: this.status,
170
289
  connected: this.status === TELEGRAM_STATUS.CONNECTED,
171
- chatId: this.chatId,
172
- botUsername: this.config.botUsername || null,
173
- watchEnabled: this.watchEnabled
290
+ botUsername: this.botUsername,
291
+ chatCount: this.chats.size,
292
+ // Surface the legacy `chatId` as the first chat for UI back-compat;
293
+ // any new UI should switch to `chats`.
294
+ chatId: this.chats.size > 0 ? [...this.chats.keys()][0] : null,
295
+ chats: [...this.chats.entries()].map(([chatId, s]) => ({
296
+ chatId,
297
+ type: s.type,
298
+ title: s.title,
299
+ watchEnabled: s.watchEnabled,
300
+ })),
301
+ watchEnabled: [...this.chats.values()].some(s => s.watchEnabled),
174
302
  };
175
303
  }
176
304
 
177
- // --- Command & Message Handlers ---
305
+ // ── Command & Message Handlers ─────────────────────────────────────
178
306
 
179
307
  _setupHandlers() {
180
308
  if (!this.bot) return;
181
309
 
182
- this.bot.onText(/\/start/, (msg) => this._cmdStart(msg));
183
- this.bot.onText(/\/help/, (msg) => this._cmdHelp(msg));
184
- this.bot.onText(/\/status/, (msg) => this._cmdStatus(msg));
185
- this.bot.onText(/\/agents/, (msg) => this._cmdAgents(msg));
186
- this.bot.onText(/\/agent (.+)/, (msg, match) => this._cmdAgentDetail(msg, match[1].trim()));
187
- this.bot.onText(/\/flows/, (msg) => this._cmdFlows(msg));
188
- this.bot.onText(/\/run (.+)/, (msg, match) => this._cmdRunFlow(msg, match[1].trim()));
189
- this.bot.onText(/\/stop (.+)/, (msg, match) => this._cmdStopAgent(msg, match[1].trim()));
190
- this.bot.onText(/\/following/, (msg) => this._cmdFollowing(msg));
191
- this.bot.onText(/\/unfollow (.+)/, (msg, match) => this._cmdUnfollow(msg, match[1].trim()));
192
- this.bot.onText(/\/watch/, (msg) => this._cmdWatch(msg));
193
- this.bot.onText(/\/unwatch/, (msg) => this._cmdUnwatch(msg));
194
- this.bot.onText(/\/watching/, (msg) => this._cmdWatching(msg));
195
-
196
- // Handle non-command text (agent messages)
197
- this.bot.on('message', (msg) => {
198
- if (msg.text && !msg.text.startsWith('/')) {
199
- this._handleTextMessage(msg);
200
- }
201
- });
310
+ this.bot.onText(/^\/start(@\w+)?(\s|$)/, (msg) => this._cmdStart(msg));
311
+ this.bot.onText(/^\/help(@\w+)?(\s|$)/, (msg) => this._cmdHelp(msg));
312
+ this.bot.onText(/^\/status(@\w+)?(\s|$)/, (msg) => this._cmdStatus(msg));
313
+ this.bot.onText(/^\/agents(@\w+)?(\s|$)/, (msg) => this._cmdAgents(msg, 0));
314
+ this.bot.onText(/^\/agent(@\w+)?\s+(.+)/, (msg, m) => this._cmdAgentDetail(msg, m[2].trim()));
315
+ this.bot.onText(/^\/flows(@\w+)?(\s|$)/, (msg) => this._cmdFlows(msg, 0));
316
+ this.bot.onText(/^\/run(@\w+)?\s+(.+)/, (msg, m) => this._cmdRunFlow(msg, m[2].trim()));
317
+ this.bot.onText(/^\/stop(@\w+)?\s+(.+)/, (msg, m) => this._cmdStopAgent(msg, m[2].trim()));
318
+ this.bot.onText(/^\/following(@\w+)?(\s|$)/, (msg) => this._cmdFollowing(msg));
319
+ this.bot.onText(/^\/unfollow(@\w+)?\s+(.+)/, (msg, m) => this._cmdUnfollow(msg, m[2].trim()));
320
+ this.bot.onText(/^\/watch(@\w+)?(\s|$)/, (msg) => this._cmdWatch(msg));
321
+ this.bot.onText(/^\/unwatch(@\w+)?(\s|$)/, (msg) => this._cmdUnwatch(msg));
322
+ this.bot.onText(/^\/watching(@\w+)?(\s|$)/, (msg) => this._cmdWatching(msg));
323
+ this.bot.onText(/^\/reset(@\w+)?(\s|$)/, (msg) => this._cmdReset(msg));
324
+ this.bot.onText(/^\/new(@\w+)?\s+(.+)/, (msg, m) => this._cmdNew(msg, m[2].trim()));
325
+ this.bot.onText(/^\/logout(@\w+)?(\s|$)/, (msg) => this._cmdLogout(msg));
326
+
327
+ // Generic message handler — branches on type.
328
+ this.bot.on('message', (msg) => this._onMessage(msg));
329
+
330
+ // Inline-keyboard callbacks.
331
+ this.bot.on('callback_query', (q) => this._handleCallbackQuery(q));
332
+ }
202
333
 
203
- // Handle inline keyboard callbacks
204
- this.bot.on('callback_query', (query) => this._handleCallbackQuery(query));
334
+ async _onMessage(msg) {
335
+ // Skip slash commands; their handlers fire via onText.
336
+ if (typeof msg.text === 'string' && msg.text.startsWith('/')) return;
337
+
338
+ if (msg.voice) {
339
+ await this._handleVoiceMessage(msg);
340
+ return;
341
+ }
342
+ if (msg.text) {
343
+ await this._handleTextMessage(msg);
344
+ return;
345
+ }
346
+ // Other media types (photos, documents, stickers) are intentionally
347
+ // ignored for now — agents can't yet consume them. Future work:
348
+ // forward image_url to a vision-capable agent.
205
349
  }
206
350
 
207
351
  _isAuthorized(msg) {
208
- return this.chatId && String(msg.chat.id) === String(this.chatId);
352
+ const chatId = String(msg.chat.id);
353
+ return this.chats.has(chatId);
354
+ }
355
+
356
+ /**
357
+ * Parse `@agent-name ...` (or just `agent-name ...`) at the start of
358
+ * a message, doing longest-prefix matching against the live agent
359
+ * list so names with spaces work without quoting.
360
+ *
361
+ * Accepts:
362
+ * "@Cody hello" → { agentName: 'Cody', matchedAgent: <agent>, messageText: 'hello' }
363
+ * "@John Smith report" → { agentName: 'John Smith', matchedAgent: <agent>, messageText: 'report' }
364
+ * "@John_Smith report" → same — underscore + dash are accepted as a
365
+ * space-substitute for slug-style addressing
366
+ * "no prefix" → { agentName: null, matchedAgent: null, messageText: 'no prefix' }
367
+ * "@Unknown msg" → { agentName: 'Unknown', matchedAgent: null, messageText: 'msg' }
368
+ * (caller emits a friendly "not found" reply)
369
+ *
370
+ * Algorithm:
371
+ * 1. Strip leading `@` if present. Remember whether one was there.
372
+ * 2. For each agent, see if the remaining text starts with the
373
+ * agent's name (case-insensitive). Try TWO normalisations:
374
+ * a) literal name with all spaces
375
+ * b) name with spaces replaced by [ _\-]+ (slug form)
376
+ * Treat a match as valid only when followed by whitespace or
377
+ * end-of-string (so "@John" doesn't accidentally match "Johnson").
378
+ * 3. Among valid matches, pick the LONGEST agent name — handles
379
+ * the "@John Smith" vs "@John" ambiguity correctly.
380
+ * 4. If no agent name matched but there WAS an `@` prefix, fall
381
+ * back to the legacy single-token capture so the caller can
382
+ * surface a "not found" error with the user-typed text.
383
+ *
384
+ * @param {string} text
385
+ * @param {Array<{id:string, name:string}>} agents
386
+ * @returns {{ agentName: string|null, matchedAgent: object|null, messageText: string }}
387
+ */
388
+ _resolveAtPrefix(text, agents) {
389
+ if (typeof text !== 'string' || text.length === 0) {
390
+ return { agentName: null, matchedAgent: null, messageText: text || '' };
391
+ }
392
+
393
+ const hasAt = text.startsWith('@');
394
+ const body = hasAt ? text.slice(1) : text;
395
+
396
+ // Try to find the longest agent name that the body starts with.
397
+ let best = null; // { agent, matchedRaw, restStart }
398
+ for (const agent of agents || []) {
399
+ const name = String(agent?.name || '');
400
+ if (!name) continue;
401
+ // Build a regex that matches either the literal name or its slug
402
+ // variant (spaces → any of `[ _\-]+`). Case-insensitive. Must be
403
+ // followed by whitespace or end-of-string so partial-word
404
+ // collisions don't false-match.
405
+ const slugPattern = name
406
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex metas
407
+ .replace(/\s+/g, '[ _\\-]+'); // any space substitute
408
+ const re = new RegExp(`^(${slugPattern})(?:\\s|$)`, 'i');
409
+ const m = body.match(re);
410
+ if (m) {
411
+ if (!best || m[1].length > best.matchedRaw.length) {
412
+ best = { agent, matchedRaw: m[1], restStart: m[1].length };
413
+ }
414
+ }
415
+ }
416
+
417
+ if (best) {
418
+ const rest = body.slice(best.restStart).replace(/^\s+/, '');
419
+ return {
420
+ agentName: best.agent.name,
421
+ matchedAgent: best.agent,
422
+ messageText: rest,
423
+ };
424
+ }
425
+
426
+ if (!hasAt) {
427
+ return { agentName: null, matchedAgent: null, messageText: text };
428
+ }
429
+
430
+ // No agent matched but the user clearly intended an @-prefix
431
+ // (typo, agent renamed, etc.). Fall back to the legacy "first
432
+ // whitespace-free token after @" capture so the caller can quote
433
+ // the user-typed name in the "not found" reply.
434
+ const legacy = body.match(/^(\S+)(?:\s+([\s\S]+))?/);
435
+ if (legacy) {
436
+ return {
437
+ agentName: legacy[1],
438
+ matchedAgent: null,
439
+ messageText: (legacy[2] || '').trim(),
440
+ };
441
+ }
442
+ return { agentName: null, matchedAgent: null, messageText: text };
443
+ }
444
+
445
+ /**
446
+ * In a group chat, the bot should only respond to a text message
447
+ * when @-mentioned. This prevents bot spam in shared channels.
448
+ * Returns the message text with the @mention stripped when matched.
449
+ * In private chats, the text is returned unchanged.
450
+ */
451
+ _stripMentionOrSkip(msg) {
452
+ const isGroup = msg.chat.type === 'group' || msg.chat.type === 'supergroup';
453
+ const text = (msg.text || msg.caption || '').trim();
454
+ if (!isGroup) return { addressed: true, text };
455
+ if (!this.botUsername) return { addressed: false, text };
456
+
457
+ const mention = `@${this.botUsername.toLowerCase()}`;
458
+ const lower = text.toLowerCase();
459
+ if (lower.includes(mention)) {
460
+ const stripped = text.replace(new RegExp(`@${this.botUsername}\\b`, 'gi'), '').replace(/\s+/g, ' ').trim();
461
+ return { addressed: true, text: stripped };
462
+ }
463
+ return { addressed: false, text };
209
464
  }
210
465
 
466
+ // ── Commands ───────────────────────────────────────────────────────
467
+
211
468
  async _cmdStart(msg) {
212
469
  const chatId = String(msg.chat.id);
213
-
214
- if (!this.chatId) {
215
- this.chatId = chatId;
470
+ const type = msg.chat.type || 'private';
471
+ const title = msg.chat.title || null;
472
+
473
+ if (!this.chats.has(chatId)) {
474
+ this.chats.set(chatId, {
475
+ type, title,
476
+ addedAt: new Date().toISOString(),
477
+ watchEnabled: false,
478
+ lastAgentId: null,
479
+ activeAgentIds: new Set(),
480
+ thinkingByAgentId: new Map(),
481
+ });
216
482
  await this._saveConfig();
217
- await this._send(chatId, this._escapeMarkdown('*Loxia Autopilot connected!* 🚀\n\nThis chat is now linked. Use /help to see available commands.\n\nAddress agents with @agent-name your message.'));
218
- } else if (chatId === this.chatId) {
219
- await this._send(chatId, this._escapeMarkdown('Already connected. Use /help for commands.'));
483
+ const greeting = type === 'private'
484
+ ? this._md`*Loxia Autopilot connected\\!* 🚀\n\nThis chat is now linked\\. Use /help for commands\\.\n\nAddress agents with \`@agent\\-name your message\` or just start typing once you’ve picked one\\.`
485
+ : this._md`*Loxia Autopilot connected\\!* 🚀\n\nGroup registered: *${title || chatId}*\\. Mention me with @${this.botUsername || 'loxia\\_bot'} to address an agent\\.`;
486
+ await this._send(chatId, greeting);
220
487
  } else {
221
- await this._send(chatId, this._escapeMarkdown('⛔ Another chat is already registered. Disconnect from the web UI first.'));
488
+ await this._send(chatId, this._md`Already connected\\. Use /help for commands\\.`);
222
489
  }
223
490
  }
224
491
 
225
492
  async _cmdHelp(msg) {
226
493
  if (!this._isAuthorized(msg)) return;
227
- const help = [
228
- '*Loxia Autopilot Telegram Remote*\n',
229
- '*Chat with agents:*',
230
- '`@agent-name your message` send to specific agent',
231
- 'Type without prefix sends to last used agent\n',
232
- '*Commands:*',
233
- '/agentslist all agents',
234
- '/agent <name> — agent detail',
235
- '/status system overview',
236
- '/followingagents you\'re following',
237
- '/unfollow <name> stop following an agent',
238
- '/flowslist flows',
239
- '/run <flow> — start a flow',
240
- '/stop <agent> — stop agent execution',
241
- '/watchsubscribe to notifications',
242
- '/unwatchunsubscribe',
243
- '/watchingnotification status',
244
- '/help — this message'
245
- ];
246
- await this._send(msg.chat.id, this._escapeMarkdown(help.join('\n')));
494
+ // Hand-written MarkdownV2. Special chars (`.`, `-`, `!`, `(`, `)`,
495
+ // `>`) MUST be escaped per Telegram's MarkdownV2 spec. The
496
+ // _escapeMarkdown wrapper that used to wrap this whole thing
497
+ // turned every `*` and backtick into literal text — that's why
498
+ // the help message displayed asterisks instead of bold.
499
+ const help =
500
+ `*Loxia Autopilot Telegram*
501
+
502
+ *Chat with agents:*
503
+ \`@agent\\-name message\` send to a specific agent
504
+ Names with spaces work too: \`@John Smith hello\` or \`@John\\_Smith hello\`
505
+ Type without prefix sends to your last\\-used agent
506
+
507
+ *Commands:*
508
+ /agentslist all agents \\(paginated\\)
509
+ /agent <name> agent detail card
510
+ /statussystem overview
511
+ /followingagents you have spoken with from this chat
512
+ /unfollow <name> — stop receiving replies from an agent
513
+ /flows list flows \\(paginated\\)
514
+ /run <flow> — start a flow
515
+ /stop <agent> — halt agent execution
516
+ /reset — clear the conversation with your active agent
517
+ /new <agent> — switch to a different agent with fresh context
518
+ /watch — subscribe to event notifications
519
+ /unwatch — unsubscribe
520
+ /watching — show notification status
521
+ /logout — disconnect this chat from Loxia
522
+ /help — this message
523
+
524
+ _Voice notes are transcribed automatically\\._`;
525
+ await this._send(msg.chat.id, help);
247
526
  }
248
527
 
249
528
  async _cmdStatus(msg) {
250
529
  if (!this._isAuthorized(msg)) return;
251
-
252
530
  try {
253
531
  const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
254
532
  const active = agents.filter(a => a.status === 'active' || a.mode === 'auto');
255
533
  const idle = agents.filter(a => a.mode === 'chat' || a.status === 'idle');
256
-
257
- let text = `*System Status*\n\n`;
258
- text += `Agents: ${agents.length} total, ${active.length} active, ${idle.length} idle\n`;
259
- text += `Notifications: ${this.watchEnabled ? '🔔 On' : '🔕 Off'}`;
260
-
534
+ const state = this._chatState(msg);
535
+ const watch = state?.watchEnabled ? '🔔 On' : '🔕 Off';
536
+
537
+ const text = [
538
+ '*System Status*',
539
+ '',
540
+ `Agents: ${agents.length} total, ${active.length} active, ${idle.length} idle`,
541
+ `Notifications: ${watch}`,
542
+ `Linked chats: ${this.chats.size}`,
543
+ ].join('\n');
261
544
  await this._send(msg.chat.id, this._escapeMarkdown(text));
262
545
  } catch (error) {
263
546
  await this._send(msg.chat.id, this._escapeMarkdown(`❌ Error: ${error.message}`));
264
547
  }
265
548
  }
266
549
 
267
- async _cmdAgents(msg) {
550
+ async _cmdAgents(msg, page = 0) {
268
551
  if (!this._isAuthorized(msg)) return;
269
-
270
552
  try {
271
553
  const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
272
554
  if (agents.length === 0) {
273
555
  await this._send(msg.chat.id, this._escapeMarkdown('No agents loaded.'));
274
556
  return;
275
557
  }
276
-
277
- let text = `*Agents (${agents.length}):*\n\n`;
278
- const buttons = [];
279
-
280
- for (const agent of agents) {
281
- const status = agent.mode === 'auto' ? '🟢' : agent.status === 'active' ? '🟡' : '⚪';
282
- const mode = agent.mode === 'auto' ? 'autonomous' : 'chat';
283
- text += `${status} *${this._escapeMarkdown(agent.name)}* — ${mode}\n`;
284
- buttons.push([{ text: agent.name, callback_data: `agent_detail:${agent.id}` }]);
285
- }
286
-
287
- await this._send(msg.chat.id, text, {
288
- reply_markup: { inline_keyboard: buttons }
289
- });
558
+ await this._renderAgentsPage(msg.chat.id, agents, page);
290
559
  } catch (error) {
291
560
  await this._send(msg.chat.id, this._escapeMarkdown(`❌ Error: ${error.message}`));
292
561
  }
293
562
  }
294
563
 
564
+ async _renderAgentsPage(chatId, agents, page) {
565
+ const pages = Math.max(1, Math.ceil(agents.length / PAGE_SIZE));
566
+ const safePage = Math.min(Math.max(0, page), pages - 1);
567
+ const slice = agents.slice(safePage * PAGE_SIZE, (safePage + 1) * PAGE_SIZE);
568
+
569
+ let text = `*Agents (${agents.length})* — page ${safePage + 1}/${pages}\n\n`;
570
+ const buttons = [];
571
+ for (const agent of slice) {
572
+ const dot = agent.mode === 'auto' ? '🟢' : agent.status === 'active' ? '🟡' : '⚪';
573
+ const mode = agent.mode === 'auto' ? 'autonomous' : 'chat';
574
+ text += `${dot} *${this._escapeMarkdown(agent.name)}* — ${mode}\n`;
575
+ buttons.push([{ text: agent.name, callback_data: `agent_detail:${agent.id}` }]);
576
+ }
577
+ if (pages > 1) {
578
+ const nav = [];
579
+ if (safePage > 0) nav.push({ text: '‹ Prev', callback_data: `agents_page:${safePage - 1}` });
580
+ nav.push({ text: `${safePage + 1}/${pages}`, callback_data: 'noop' });
581
+ if (safePage < pages - 1) nav.push({ text: 'Next ›', callback_data: `agents_page:${safePage + 1}` });
582
+ buttons.push(nav);
583
+ }
584
+ await this._send(chatId, text, { reply_markup: { inline_keyboard: buttons } });
585
+ }
586
+
295
587
  async _cmdAgentDetail(msg, agentName) {
296
588
  if (!this._isAuthorized(msg)) return;
297
589
  await this._showAgentDetail(msg.chat.id, agentName);
@@ -304,12 +596,10 @@ class TelegramService {
304
596
  a.name.toLowerCase() === agentNameOrId.toLowerCase() ||
305
597
  a.id === agentNameOrId
306
598
  );
307
-
308
599
  if (!agent) {
309
600
  await this._send(chatId, this._escapeMarkdown(`Agent "${agentNameOrId}" not found.`));
310
601
  return;
311
602
  }
312
-
313
603
  const status = agent.mode === 'auto' ? '🟢 Autonomous' : '🟡 Chat';
314
604
  let text = `*${this._escapeMarkdown(agent.name)}*\n\n`;
315
605
  text += `Status: ${status}\n`;
@@ -317,52 +607,56 @@ class TelegramService {
317
607
  if (agent.lastActivity) {
318
608
  text += `Last active: ${new Date(agent.lastActivity).toLocaleTimeString()}\n`;
319
609
  }
320
-
321
- const buttons = [
322
- [
323
- { text: '💬 Send Message', callback_data: `msg_agent:${agent.id}` },
324
- { text: '⏹ Stop', callback_data: `stop_agent:${agent.id}` }
325
- ]
326
- ];
327
-
610
+ const buttons = [[
611
+ { text: '💬 Send Message', callback_data: `msg_agent:${agent.id}` },
612
+ { text: '⏹ Stop', callback_data: `stop_agent:${agent.id}` },
613
+ ]];
328
614
  await this._send(chatId, text, { reply_markup: { inline_keyboard: buttons } });
329
615
  } catch (error) {
330
616
  await this._send(chatId, this._escapeMarkdown(`❌ Error: ${error.message}`));
331
617
  }
332
618
  }
333
619
 
334
- async _cmdFlows(msg) {
620
+ async _cmdFlows(msg, page = 0) {
335
621
  if (!this._isAuthorized(msg)) return;
336
-
337
622
  try {
338
623
  if (!this.orchestrator?.stateManager) {
339
624
  await this._send(msg.chat.id, this._escapeMarkdown('Flows not available.'));
340
625
  return;
341
626
  }
342
-
343
627
  const projectDir = this.orchestrator.config?.project?.directory || process.cwd();
344
628
  const flowIndex = await this.orchestrator.stateManager.loadFlowIndex?.(projectDir) || {};
345
629
  const flows = Object.entries(flowIndex);
346
-
347
630
  if (flows.length === 0) {
348
631
  await this._send(msg.chat.id, this._escapeMarkdown('No flows defined.'));
349
632
  return;
350
633
  }
351
-
352
- let text = `*Flows (${flows.length}):*\n\n`;
353
- const buttons = [];
354
-
355
- for (const [id, flow] of flows) {
356
- text += `📋 *${this._escapeMarkdown(flow.name || id)}*\n`;
357
- buttons.push([{ text: `▶️ Run ${flow.name || id}`, callback_data: `run_flow:${id}` }]);
358
- }
359
-
360
- await this._send(msg.chat.id, text, { reply_markup: { inline_keyboard: buttons } });
634
+ await this._renderFlowsPage(msg.chat.id, flows, page);
361
635
  } catch (error) {
362
636
  await this._send(msg.chat.id, this._escapeMarkdown(`❌ Error: ${error.message}`));
363
637
  }
364
638
  }
365
639
 
640
+ async _renderFlowsPage(chatId, flows, page) {
641
+ const pages = Math.max(1, Math.ceil(flows.length / PAGE_SIZE));
642
+ const safePage = Math.min(Math.max(0, page), pages - 1);
643
+ const slice = flows.slice(safePage * PAGE_SIZE, (safePage + 1) * PAGE_SIZE);
644
+ let text = `*Flows (${flows.length})* — page ${safePage + 1}/${pages}\n\n`;
645
+ const buttons = [];
646
+ for (const [id, flow] of slice) {
647
+ text += `📋 *${this._escapeMarkdown(flow.name || id)}*\n`;
648
+ buttons.push([{ text: `▶️ Run ${flow.name || id}`, callback_data: `run_flow:${id}` }]);
649
+ }
650
+ if (pages > 1) {
651
+ const nav = [];
652
+ if (safePage > 0) nav.push({ text: '‹ Prev', callback_data: `flows_page:${safePage - 1}` });
653
+ nav.push({ text: `${safePage + 1}/${pages}`, callback_data: 'noop' });
654
+ if (safePage < pages - 1) nav.push({ text: 'Next ›', callback_data: `flows_page:${safePage + 1}` });
655
+ buttons.push(nav);
656
+ }
657
+ await this._send(chatId, text, { reply_markup: { inline_keyboard: buttons } });
658
+ }
659
+
366
660
  async _cmdRunFlow(msg, flowName) {
367
661
  if (!this._isAuthorized(msg)) return;
368
662
  await this._runFlow(msg.chat.id, flowName);
@@ -374,9 +668,7 @@ class TelegramService {
374
668
  await this._send(chatId, this._escapeMarkdown('Flow executor not available.'));
375
669
  return;
376
670
  }
377
-
378
671
  await this._send(chatId, this._escapeMarkdown(`▶️ Starting flow: ${flowNameOrId}...`));
379
- // Flow execution is async — completion will be captured by broadcast listener
380
672
  const projectDir = this.orchestrator?.config?.project?.directory || process.cwd();
381
673
  await this.flowExecutor.executeFlow(flowNameOrId, { projectDir });
382
674
  } catch (error) {
@@ -386,7 +678,6 @@ class TelegramService {
386
678
 
387
679
  async _cmdStopAgent(msg, agentName) {
388
680
  if (!this._isAuthorized(msg)) return;
389
-
390
681
  try {
391
682
  const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
392
683
  const agent = agents.find(a => a.name.toLowerCase() === agentName.toLowerCase());
@@ -394,11 +685,9 @@ class TelegramService {
394
685
  await this._send(msg.chat.id, this._escapeMarkdown(`Agent "${agentName}" not found.`));
395
686
  return;
396
687
  }
397
-
398
688
  if (this.orchestrator?.messageProcessor) {
399
689
  await this.orchestrator.messageProcessor.stopAutonomousExecution(agent.id);
400
690
  }
401
-
402
691
  await this._send(msg.chat.id, this._escapeMarkdown(`⏹ Stopped ${agent.name}`));
403
692
  } catch (error) {
404
693
  await this._send(msg.chat.id, this._escapeMarkdown(`❌ Error: ${error.message}`));
@@ -407,106 +696,241 @@ class TelegramService {
407
696
 
408
697
  async _cmdFollowing(msg) {
409
698
  if (!this._isAuthorized(msg)) return;
410
-
411
- if (this.activeAgentIds.size === 0) {
412
- await this._send(msg.chat.id, this._escapeMarkdown('Not following any agents. Send @agent-name to start chatting.'));
699
+ const state = this._chatState(msg);
700
+ if (!state || state.activeAgentIds.size === 0) {
701
+ await this._send(msg.chat.id, this._escapeMarkdown('Not following any agents in this chat. Send @agent-name to start.'));
413
702
  return;
414
703
  }
415
-
416
704
  const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
417
- let text = `*Following ${this.activeAgentIds.size} agent(s):*\n\n`;
705
+ let text = `*Following ${state.activeAgentIds.size} agent\\(s\\):*\n\n`;
418
706
  const buttons = [];
419
-
420
- for (const id of this.activeAgentIds) {
707
+ for (const id of state.activeAgentIds) {
421
708
  const agent = agents.find(a => a.id === id);
422
709
  const name = agent?.name || id;
423
- const isLast = id === this.lastAgentId;
710
+ const isLast = id === state.lastAgentId;
424
711
  text += `${isLast ? '💬' : '👁'} *${this._escapeMarkdown(name)}*${isLast ? ' \\(active\\)' : ''}\n`;
425
712
  buttons.push([{ text: `❌ Unfollow ${name}`, callback_data: `unfollow:${id}` }]);
426
713
  }
427
-
428
714
  text += '\n_Active = default for messages without @prefix_';
429
715
  await this._send(msg.chat.id, text, { reply_markup: { inline_keyboard: buttons } });
430
716
  }
431
717
 
432
718
  async _cmdUnfollow(msg, agentName) {
433
719
  if (!this._isAuthorized(msg)) return;
434
-
720
+ const state = this._chatState(msg);
435
721
  const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
436
722
  const agent = agents.find(a => a.name.toLowerCase() === agentName.toLowerCase());
437
- if (!agent || !this.activeAgentIds.has(agent.id)) {
723
+ if (!agent || !state.activeAgentIds.has(agent.id)) {
438
724
  await this._send(msg.chat.id, this._escapeMarkdown(`Not following "${agentName}".`));
439
725
  return;
440
726
  }
441
-
442
- this.activeAgentIds.delete(agent.id);
443
- if (this.lastAgentId === agent.id) {
444
- // Switch lastAgentId to another active agent, or null
445
- this.lastAgentId = this.activeAgentIds.size > 0 ? [...this.activeAgentIds][this.activeAgentIds.size - 1] : null;
727
+ state.activeAgentIds.delete(agent.id);
728
+ if (state.lastAgentId === agent.id) {
729
+ state.lastAgentId = state.activeAgentIds.size > 0
730
+ ? [...state.activeAgentIds][state.activeAgentIds.size - 1]
731
+ : null;
446
732
  }
447
- await this._send(msg.chat.id, this._escapeMarkdown(`Unfollowed ${agent.name}. Responses will no longer be relayed.`));
733
+ await this._saveConfig();
734
+ await this._send(msg.chat.id, this._escapeMarkdown(`Unfollowed ${agent.name}.`));
448
735
  }
449
736
 
450
737
  async _cmdWatch(msg) {
451
738
  if (!this._isAuthorized(msg)) return;
452
- this.watchEnabled = true;
739
+ const state = this._chatState(msg);
740
+ state.watchEnabled = true;
453
741
  await this._saveConfig();
454
- await this._send(msg.chat.id, this._escapeMarkdown('🔔 Notifications enabled. You\'ll receive alerts for errors, completions and prompts.'));
742
+ await this._send(msg.chat.id, this._escapeMarkdown('🔔 Notifications enabled. You\'ll receive alerts for completions and prompts.'));
455
743
  }
456
744
 
457
745
  async _cmdUnwatch(msg) {
458
746
  if (!this._isAuthorized(msg)) return;
459
- this.watchEnabled = false;
460
- this._clearNotificationTimer();
747
+ const state = this._chatState(msg);
748
+ state.watchEnabled = false;
461
749
  await this._saveConfig();
462
- await this._send(msg.chat.id, this._escapeMarkdown('🔕 Notifications disabled.'));
750
+ await this._send(msg.chat.id, this._escapeMarkdown('🔕 Notifications disabled. (Errors and timeouts are still relayed.)'));
463
751
  }
464
752
 
465
753
  async _cmdWatching(msg) {
466
754
  if (!this._isAuthorized(msg)) return;
467
- await this._send(msg.chat.id, this._escapeMarkdown(this.watchEnabled
755
+ const state = this._chatState(msg);
756
+ await this._send(msg.chat.id, this._escapeMarkdown(state.watchEnabled
468
757
  ? '🔔 Notifications are ON. Use /unwatch to disable.'
469
758
  : '🔕 Notifications are OFF. Use /watch to enable.'
470
759
  ));
471
760
  }
472
761
 
473
- // --- Agent Conversation ---
762
+ async _cmdReset(msg) {
763
+ if (!this._isAuthorized(msg)) return;
764
+ const state = this._chatState(msg);
765
+ if (!state.lastAgentId) {
766
+ await this._send(msg.chat.id, this._escapeMarkdown('No active agent to reset. Use /agents to pick one.'));
767
+ return;
768
+ }
769
+ const agentId = state.lastAgentId;
770
+ const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
771
+ const name = agent?.name || agentId;
772
+ try {
773
+ // Reset agent conversation if the orchestrator exposes it.
774
+ if (this.orchestrator?.messageProcessor?.resetAgentConversation) {
775
+ await this.orchestrator.messageProcessor.resetAgentConversation(agentId);
776
+ } else if (this.agentPool?.clearAgentMessages) {
777
+ await this.agentPool.clearAgentMessages(agentId);
778
+ }
779
+ await this._send(msg.chat.id, this._escapeMarkdown(`🔄 Cleared conversation with ${name}.`));
780
+ } catch (error) {
781
+ await this._send(msg.chat.id, this._escapeMarkdown(`❌ Reset failed: ${error.message}`));
782
+ }
783
+ }
784
+
785
+ async _cmdNew(msg, agentName) {
786
+ if (!this._isAuthorized(msg)) return;
787
+ const state = this._chatState(msg);
788
+ const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
789
+ const agent = agents.find(a => a.name.toLowerCase() === agentName.toLowerCase());
790
+ if (!agent) {
791
+ await this._send(msg.chat.id, this._escapeMarkdown(`Agent "${agentName}" not found.`));
792
+ return;
793
+ }
794
+ state.lastAgentId = agent.id;
795
+ state.activeAgentIds.add(agent.id);
796
+ // Reset their context.
797
+ try {
798
+ if (this.orchestrator?.messageProcessor?.resetAgentConversation) {
799
+ await this.orchestrator.messageProcessor.resetAgentConversation(agent.id);
800
+ } else if (this.agentPool?.clearAgentMessages) {
801
+ await this.agentPool.clearAgentMessages(agent.id);
802
+ }
803
+ } catch { /* non-fatal */ }
804
+ await this._saveConfig();
805
+ await this._send(msg.chat.id, this._escapeMarkdown(`✨ Switched to ${agent.name} with a fresh context.`));
806
+ }
807
+
808
+ async _cmdLogout(msg) {
809
+ const chatId = String(msg.chat.id);
810
+ if (!this.chats.has(chatId)) {
811
+ await this._send(chatId, this._escapeMarkdown('Not registered.'));
812
+ return;
813
+ }
814
+ this.chats.delete(chatId);
815
+ await this._saveConfig();
816
+ await this._send(chatId, this._escapeMarkdown('👋 This chat has been disconnected from Loxia. /start to re-link.'));
817
+ }
818
+
819
+ _chatState(msg) {
820
+ return this.chats.get(String(msg?.chat?.id));
821
+ }
822
+
823
+ // ── Agent Conversation ─────────────────────────────────────────────
474
824
 
475
825
  async _handleTextMessage(msg) {
476
826
  if (!this._isAuthorized(msg)) return;
477
- const text = msg.text?.trim();
478
- if (!text) return;
827
+ const state = this._chatState(msg);
828
+ if (!state) return;
479
829
 
480
- // Check if this is a reply to a pending prompt relay
481
- if (this.replyContext) {
830
+ // Pending prompt reply check uses chat-local context.
831
+ if (this.replyContext.has(String(msg.chat.id))) {
482
832
  await this._handlePromptReply(msg);
483
833
  return;
484
834
  }
485
835
 
486
- // Parse @agent-name prefix
487
- let agentName = null;
488
- let messageText = text;
836
+ // Group chats: require @mention.
837
+ const { addressed, text } = this._stripMentionOrSkip(msg);
838
+ if (!addressed) return;
839
+ if (!text) return;
840
+
841
+ await this._dispatchToAgent(msg, text);
842
+ }
843
+
844
+ async _handleVoiceMessage(msg) {
845
+ if (!this._isAuthorized(msg)) return;
846
+ const state = this._chatState(msg);
847
+ if (!state) return;
848
+
849
+ // Groups: skip voice unless the chat has had at least one @mention
850
+ // (we can't transcribe-and-then-check; too costly to transcribe
851
+ // every voice). For now, ignore voice in groups entirely.
852
+ if (msg.chat.type !== 'private') return;
489
853
 
490
- const atMatch = text.match(/^@(\S+)\s+([\s\S]+)/);
491
- if (atMatch) {
492
- agentName = atMatch[1];
493
- messageText = atMatch[2].trim();
854
+ try {
855
+ await this.bot.sendChatAction(msg.chat.id, 'typing');
856
+ } catch { /* ignore */ }
857
+
858
+ let transcribed;
859
+ try {
860
+ transcribed = await this._transcribeVoice(msg.voice, msg);
861
+ } catch (err) {
862
+ // Log at ERROR level (not warn) so operators see the failure
863
+ // in the local server console. Then surface a friendly but
864
+ // diagnostic message to the user — include the upstream status
865
+ // / code when present so the cause is visible without server
866
+ // log access.
867
+ this.logger?.error?.('[TelegramService] voice transcription failed', {
868
+ error: err.message, code: err.code, status: err.status,
869
+ });
870
+ let userHint;
871
+ if (err.code === 'not_configured') {
872
+ userHint = 'Voice transcription needs an Azure speech-to-text deployment (e.g. `gpt-4o-mini-transcribe`). Please type your message for now.';
873
+ } else if (err.status === 401 || err.status === 403) {
874
+ userHint = 'Your account isn\'t authorised to call the transcription endpoint. Sign out and back in, then try again.';
875
+ } else if (err.status === 502 || err.status === 503) {
876
+ userHint = 'The transcription service is temporarily unavailable. Please try again in a moment.';
877
+ } else {
878
+ // Surface the upstream message itself so subtle bugs (audio
879
+ // format, file size, etc.) self-diagnose. Truncated to keep
880
+ // the Telegram bubble readable.
881
+ const tail = (err.message || 'unknown error').slice(0, 200);
882
+ userHint = `Please try again, or type your message. (debug: ${tail})`;
883
+ }
884
+ await this._send(msg.chat.id, this._escapeMarkdown(`🎤 Couldn't transcribe that voice note. ${userHint}`));
885
+ return;
494
886
  }
495
887
 
496
- // Resolve agent
888
+ if (!transcribed) {
889
+ await this._send(msg.chat.id, this._escapeMarkdown('🎤 Voice note was empty. Please try again or type your message.'));
890
+ return;
891
+ }
892
+
893
+ // Show the user what we heard, then act on it as if they had typed.
894
+ // `_Heard:_` is MarkdownV2 italic markup — must stay UNESCAPED, or
895
+ // the user just sees literal underscores. Only the transcribed
896
+ // body gets escaped, since it can contain arbitrary characters
897
+ // that would otherwise break the parse_mode.
898
+ await this._send(msg.chat.id, `🎤 _Heard:_ ${this._escapeMarkdown(transcribed)}`);
899
+ await this._dispatchToAgent(msg, transcribed);
900
+ }
901
+
902
+ /**
903
+ * Common path: resolve target agent and submit to the orchestrator.
904
+ * Emits a typing indicator + a "🔄 Thinking…" placeholder which the
905
+ * broadcast handler will edit with the real reply.
906
+ */
907
+ async _dispatchToAgent(msg, text) {
908
+ const state = this._chatState(msg);
909
+
910
+ // Parse @agent-name prefix. Supports names with spaces by doing
911
+ // longest-prefix matching against the live agent list — see
912
+ // _resolveAtPrefix() for the contract.
913
+ const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
914
+ const parsed = this._resolveAtPrefix(text, agents);
915
+ const agentName = parsed.agentName; // null when message has no @prefix
916
+ const messageText = parsed.messageText;
917
+
918
+ // Resolve agent.
497
919
  let targetAgent = null;
498
920
  if (agentName) {
499
- const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
500
- targetAgent = agents.find(a => a.name.toLowerCase() === agentName.toLowerCase());
921
+ targetAgent = parsed.matchedAgent || null;
501
922
  if (!targetAgent) {
502
- await this._send(msg.chat.id, this._escapeMarkdown(`❌ Agent "${agentName}" not found. Use /agents to see available agents.`));
923
+ await this._send(msg.chat.id, this._escapeMarkdown(
924
+ `❌ Agent "${agentName}" not found. Use /agents to see available agents. ` +
925
+ 'Tip: names with spaces can be written either as `@John Smith` or `@John_Smith`.'
926
+ ));
503
927
  return;
504
928
  }
505
- this.lastAgentId = targetAgent.id;
506
- this.activeAgentIds.add(targetAgent.id);
507
- } else if (this.lastAgentId) {
508
- // Sticky session
509
- targetAgent = this.agentPool ? await this.agentPool.getAgent(this.lastAgentId) : null;
929
+ state.lastAgentId = targetAgent.id;
930
+ state.activeAgentIds.add(targetAgent.id);
931
+ await this._saveConfig();
932
+ } else if (state.lastAgentId) {
933
+ targetAgent = this.agentPool ? await this.agentPool.getAgent(state.lastAgentId) : null;
510
934
  if (!targetAgent) {
511
935
  await this._send(msg.chat.id, this._escapeMarkdown('No agent selected. Use @agent-name message to address one.'));
512
936
  return;
@@ -516,15 +940,24 @@ class TelegramService {
516
940
  return;
517
941
  }
518
942
 
519
- // Send to agent
943
+ // Begin typing + thinking placeholder. Saved so the broadcast
944
+ // handler can edit it with the agent's actual reply.
945
+ try { await this.bot.sendChatAction(msg.chat.id, 'typing'); } catch { /* ignore */ }
520
946
  try {
521
- await this._send(msg.chat.id, `📨 *${this._escapeMarkdown(targetAgent.name)}*`);
947
+ const placeholder = await this.bot.sendMessage(
948
+ msg.chat.id,
949
+ `🔄 *${this._escapeMarkdown(targetAgent.name)}* is thinking…`,
950
+ { parse_mode: 'MarkdownV2' }
951
+ );
952
+ state.thinkingByAgentId.set(targetAgent.id, placeholder.message_id);
953
+ } catch (err) {
954
+ this.logger?.debug?.('[TelegramService] thinking placeholder send failed', { error: err.message });
955
+ }
522
956
 
957
+ // Dispatch to orchestrator.
958
+ try {
523
959
  if (this.orchestrator) {
524
- const sessionId = `telegram-${this.chatId}`;
525
- // Capture source at ingress — see services/messageSource.js. The
526
- // source rides the payload into the orchestrator and becomes the
527
- // `(Message by alice from Telegram > …)` line the agent sees.
960
+ const sessionId = `telegram-${msg.chat.id}`;
528
961
  const source = createTelegramSource(msg);
529
962
  await this.orchestrator.processRequest({
530
963
  interface: 'telegram',
@@ -536,83 +969,196 @@ class TelegramService {
536
969
  streamingEnabled: false,
537
970
  source,
538
971
  },
539
- projectDir: this.orchestrator.config?.project?.directory || process.cwd()
972
+ projectDir: this.orchestrator.config?.project?.directory || process.cwd(),
540
973
  });
541
974
  }
542
975
  } catch (error) {
976
+ // Clear the placeholder, surface the failure.
977
+ await this._clearThinking(msg.chat.id, targetAgent.id);
543
978
  await this._send(msg.chat.id, this._escapeMarkdown(`❌ Failed to send: ${error.message}`));
544
979
  }
545
980
  }
546
981
 
547
- // --- Callback Query Handler (Inline Keyboards) ---
982
+ async _clearThinking(chatId, agentId) {
983
+ const state = this.chats.get(String(chatId));
984
+ if (!state) return;
985
+ const messageId = state.thinkingByAgentId.get(agentId);
986
+ if (!messageId) return;
987
+ state.thinkingByAgentId.delete(agentId);
988
+ try {
989
+ await this.bot.deleteMessage(chatId, messageId);
990
+ } catch { /* if user deleted it, that's fine */ }
991
+ }
992
+
993
+ // ── Voice transcription (via backend) ──────────────────────────────
994
+
995
+ async _transcribeVoice(voice, msg = null) {
996
+ if (!voice?.file_id) throw Object.assign(new Error('no file_id'), { code: 'bad_request' });
997
+ const platformKey = this._resolvePlatformKey();
998
+ if (!platformKey) {
999
+ throw Object.assign(new Error('platform key not set'), { code: 'not_configured' });
1000
+ }
1001
+ // 1) Get the file URL from Telegram + download bytes.
1002
+ const fileLink = await this.bot.getFileLink(voice.file_id);
1003
+ const audioRes = await fetch(fileLink);
1004
+ if (!audioRes.ok) throw new Error(`audio download failed: ${audioRes.status}`);
1005
+ const audioBuf = Buffer.from(await audioRes.arrayBuffer());
1006
+ const audioBase64 = audioBuf.toString('base64');
1007
+
1008
+ // 2) POST to backend /llm/transcribe. Telegram knows the audio
1009
+ // duration upfront (voice.duration); pass it so the backend can
1010
+ // price the call correctly even when the model's response shape
1011
+ // doesn't include duration (gpt-4o-*-transcribe with json format).
1012
+ // The Telegram message id is a natural idempotency key — a retry
1013
+ // of the same voice note dedups at the recorder layer.
1014
+ const url = `${this.backendBaseUrl}/llm/transcribe`;
1015
+ const apiRes = await fetch(url, {
1016
+ method: 'POST',
1017
+ headers: {
1018
+ 'Authorization': `Bearer ${platformKey}`,
1019
+ 'Content-Type': 'application/json',
1020
+ },
1021
+ body: JSON.stringify({
1022
+ audioBase64,
1023
+ mimeType: voice.mime_type || 'audio/ogg',
1024
+ filename: `voice_${Date.now()}.ogg`,
1025
+ durationSeconds: typeof voice.duration === 'number' ? voice.duration : undefined,
1026
+ idempotencyKey: msg?.message_id
1027
+ ? `tg-${msg.chat?.id}-${msg.message_id}`
1028
+ : undefined,
1029
+ }),
1030
+ });
1031
+ if (apiRes.status === 503) {
1032
+ throw Object.assign(new Error('not configured'), { code: 'not_configured', status: 503 });
1033
+ }
1034
+ if (!apiRes.ok) {
1035
+ const body = await apiRes.text().catch(() => '');
1036
+ let parsedMsg = '';
1037
+ try { parsedMsg = JSON.parse(body)?.error || ''; } catch { /* not JSON */ }
1038
+ const detail = parsedMsg || body.slice(0, 200);
1039
+ throw Object.assign(
1040
+ new Error(`backend transcribe ${apiRes.status}: ${detail}`),
1041
+ { status: apiRes.status }
1042
+ );
1043
+ }
1044
+ const data = await apiRes.json();
1045
+ return (data?.text || '').trim();
1046
+ }
1047
+
1048
+ // ── Callback Query Handler ─────────────────────────────────────────
548
1049
 
549
1050
  async _handleCallbackQuery(query) {
550
- if (!this.chatId || String(query.message.chat.id) !== String(this.chatId)) return;
1051
+ const chatId = String(query.message.chat.id);
1052
+ if (!this.chats.has(chatId)) return;
1053
+ const state = this.chats.get(chatId);
551
1054
 
552
- const data = query.data;
553
- try {
554
- await this.bot.answerCallbackQuery(query.id);
555
- } catch {}
1055
+ const data = query.data || '';
1056
+ try { await this.bot.answerCallbackQuery(query.id); } catch { /* ignore */ }
556
1057
 
1058
+ if (data === 'noop') return;
1059
+
1060
+ if (data.startsWith('agents_page:')) {
1061
+ const page = parseInt(data.split(':')[1], 10) || 0;
1062
+ const agents = this.agentPool ? await this.agentPool.getAllAgents() : [];
1063
+ await this._renderAgentsPage(chatId, agents, page);
1064
+ return;
1065
+ }
1066
+ if (data.startsWith('flows_page:')) {
1067
+ const page = parseInt(data.split(':')[1], 10) || 0;
1068
+ if (!this.orchestrator?.stateManager) return;
1069
+ const projectDir = this.orchestrator.config?.project?.directory || process.cwd();
1070
+ const flowIndex = await this.orchestrator.stateManager.loadFlowIndex?.(projectDir) || {};
1071
+ await this._renderFlowsPage(chatId, Object.entries(flowIndex), page);
1072
+ return;
1073
+ }
557
1074
  if (data.startsWith('agent_detail:')) {
558
- const agentId = data.replace('agent_detail:', '');
559
- await this._showAgentDetail(query.message.chat.id, agentId);
560
- } else if (data.startsWith('msg_agent:')) {
561
- const agentId = data.replace('msg_agent:', '');
562
- this.lastAgentId = agentId;
563
- this.activeAgentIds.add(agentId);
1075
+ await this._showAgentDetail(chatId, data.slice('agent_detail:'.length));
1076
+ return;
1077
+ }
1078
+ if (data.startsWith('msg_agent:')) {
1079
+ const agentId = data.slice('msg_agent:'.length);
1080
+ state.lastAgentId = agentId;
1081
+ state.activeAgentIds.add(agentId);
1082
+ await this._saveConfig();
564
1083
  const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
565
- const name = agent?.name || agentId;
566
- await this._send(query.message.chat.id, this._escapeMarkdown(`💬 Now chatting with ${name}. Type your message.`));
567
- } else if (data.startsWith('stop_agent:')) {
568
- const agentId = data.replace('stop_agent:', '');
1084
+ await this._send(chatId, this._escapeMarkdown(`💬 Now chatting with ${agent?.name || agentId}. Type your message.`));
1085
+ return;
1086
+ }
1087
+ if (data.startsWith('stop_agent:')) {
1088
+ const agentId = data.slice('stop_agent:'.length);
569
1089
  if (this.orchestrator?.messageProcessor) {
570
1090
  await this.orchestrator.messageProcessor.stopAutonomousExecution(agentId);
571
1091
  }
572
- await this._send(query.message.chat.id, this._escapeMarkdown('⏹ Agent stopped.'));
573
- } else if (data.startsWith('run_flow:')) {
574
- const flowId = data.replace('run_flow:', '');
575
- await this._runFlow(query.message.chat.id, flowId);
576
- } else if (data.startsWith('unfollow:')) {
577
- const agentId = data.replace('unfollow:', '');
578
- this.activeAgentIds.delete(agentId);
579
- if (this.lastAgentId === agentId) {
580
- this.lastAgentId = this.activeAgentIds.size > 0 ? [...this.activeAgentIds][this.activeAgentIds.size - 1] : null;
1092
+ await this._send(chatId, this._escapeMarkdown('⏹ Agent stopped.'));
1093
+ return;
1094
+ }
1095
+ if (data.startsWith('run_flow:')) {
1096
+ await this._runFlow(chatId, data.slice('run_flow:'.length));
1097
+ return;
1098
+ }
1099
+ if (data.startsWith('unfollow:')) {
1100
+ const agentId = data.slice('unfollow:'.length);
1101
+ state.activeAgentIds.delete(agentId);
1102
+ if (state.lastAgentId === agentId) {
1103
+ state.lastAgentId = state.activeAgentIds.size > 0 ? [...state.activeAgentIds][0] : null;
581
1104
  }
1105
+ await this._saveConfig();
582
1106
  const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
583
- await this._send(query.message.chat.id, this._escapeMarkdown(`Unfollowed ${agent?.name || agentId}.`));
584
- } else if (data.startsWith('prompt_reply:')) {
585
- // Handle prompt option selection
586
- const [, requestId, answerIndex] = data.match(/prompt_reply:(.+):(\d+)/) || [];
587
- if (requestId && this.pendingRelays.has(requestId)) {
588
- await this._submitPromptReply(requestId, answerIndex);
1107
+ await this._send(chatId, this._escapeMarkdown(`Unfollowed ${agent?.name || agentId}.`));
1108
+ return;
1109
+ }
1110
+ if (data.startsWith('prompt_reply:')) {
1111
+ const m = data.match(/prompt_reply:(.+):(\d+)/);
1112
+ if (m && this.pendingRelays.has(m[1])) {
1113
+ await this._submitPromptReply(m[1], m[2]);
589
1114
  }
1115
+ return;
590
1116
  }
1117
+ if (data.startsWith('act:')) {
1118
+ // <actions> button tapped — treat value as a user message to the
1119
+ // sticky agent. Format: `act:<agentId>:<value>` where value may
1120
+ // contain colons; split on first two.
1121
+ const idx1 = data.indexOf(':');
1122
+ const idx2 = data.indexOf(':', idx1 + 1);
1123
+ const agentId = data.slice(idx1 + 1, idx2);
1124
+ const value = data.slice(idx2 + 1);
1125
+ if (!agentId || !value) return;
1126
+
1127
+ // Synthesize a message that mirrors what a real user input would
1128
+ // look like in the same handler. Forwarding the tap as a
1129
+ // synthetic user turn keeps the agent's transcript honest.
1130
+ const synthetic = {
1131
+ chat: query.message.chat,
1132
+ from: query.from,
1133
+ message_id: query.message.message_id,
1134
+ text: `@${(await this._agentName(agentId))} ${value}`,
1135
+ };
1136
+ await this._dispatchToAgent(synthetic, synthetic.text);
1137
+ }
1138
+ }
1139
+
1140
+ async _agentName(agentId) {
1141
+ const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
1142
+ return agent?.name || agentId;
591
1143
  }
592
1144
 
593
- // --- Broadcast Interceptor ---
1145
+ // ── Broadcast Interceptor ──────────────────────────────────────────
594
1146
 
595
1147
  _interceptBroadcasts(wsManager) {
596
1148
  if (!wsManager || this._originalBroadcast) return;
597
-
598
1149
  const originalBroadcast = wsManager.broadcastToSession.bind(wsManager);
599
1150
  this._originalBroadcast = originalBroadcast;
600
-
601
1151
  wsManager.broadcastToSession = (sessionId, message) => {
602
- // Call original
603
1152
  originalBroadcast(sessionId, message);
604
- // Forward to Telegram
605
1153
  this._handleBroadcastEvent(message);
606
1154
  };
607
1155
  }
608
1156
 
609
1157
  async _handleBroadcastEvent(message) {
610
- if (!this.bot || !this.chatId || this.status !== TELEGRAM_STATUS.CONNECTED) return;
611
-
1158
+ if (!this.bot || this.chats.size === 0 || this.status !== TELEGRAM_STATUS.CONNECTED) return;
612
1159
  const type = message?.type;
613
1160
  if (!type) return;
614
1161
 
615
- // Always relay prompt/credential requests (they block agent progress)
616
1162
  if (type === 'user_prompt_request') {
617
1163
  await this._relayPromptRequest(message);
618
1164
  return;
@@ -621,37 +1167,56 @@ class TelegramService {
621
1167
  await this._relayCredentialRequest(message);
622
1168
  return;
623
1169
  }
624
-
625
- // Agent responses — only relay stream_complete (message_added is a duplicate of the same content)
626
1170
  if (type === 'stream_complete') {
627
1171
  await this._relayAgentResponse(message);
628
1172
  return;
629
1173
  }
1174
+ if (type === 'imageGenerated') {
1175
+ // Buffer the URL keyed by agentId. The actual send happens when
1176
+ // the matching stream_complete arrives (typical case — agent
1177
+ // says "here's your image: ..." with the image markdown), or as
1178
+ // a standalone send if no text reply ever lands (image-only
1179
+ // turn, e.g. an automated render flow).
1180
+ this._bufferGeneratedImage(message);
1181
+ return;
1182
+ }
630
1183
 
631
- // Notifications only if watching
632
- if (!this.watchEnabled) return;
633
-
634
- const notificationTypes = {
635
- 'agent_error': '⚠️ *Agent Error*',
636
- 'flow_run_failed': '❌ *Flow Failed*',
637
- 'agent_timeout': ' *Agent Timeout*',
638
- 'execution_stopped': ' *Agent Finished*',
639
- 'flow_run_completed': ' *Flow Completed*',
640
- 'criticalError': '🔴 *Critical Error*'
1184
+ // Errors and timeouts ALWAYS reach the user. Other completions
1185
+ // require /watch.
1186
+ const ALWAYS_RELAY = new Set(['agent_error', 'agent_timeout', 'criticalError', 'flow_run_failed']);
1187
+ const WATCH_GATED = new Set(['execution_stopped', 'flow_run_completed']);
1188
+
1189
+ const headers = {
1190
+ agent_error: '⚠️ *Agent Error*',
1191
+ agent_timeout: ' *Agent Timeout*',
1192
+ criticalError: '🔴 *Critical Error*',
1193
+ flow_run_failed: ' *Flow Failed*',
1194
+ execution_stopped: '✅ *Agent Finished*',
1195
+ flow_run_completed: '✅ *Flow Completed*',
641
1196
  };
1197
+ if (!headers[type]) return;
642
1198
 
643
- if (notificationTypes[type]) {
644
- const header = notificationTypes[type];
645
- let text = `${header}\n`;
1199
+ let text = `${headers[type]}\n`;
1200
+ if (message.agentName || message.data?.agentName) {
1201
+ text += `Agent: \`${message.agentName || message.data?.agentName}\`\n`;
1202
+ }
1203
+ if (message.message || message.data?.message || message.error) {
1204
+ text += `${message.message || message.data?.message || message.error}\n`;
1205
+ }
646
1206
 
647
- if (message.agentName || message.data?.agentName) {
648
- text += `Agent: \`${message.agentName || message.data?.agentName}\`\n`;
649
- }
650
- if (message.message || message.data?.message || message.error) {
651
- text += `${message.message || message.data?.message || message.error}\n`;
1207
+ if (ALWAYS_RELAY.has(type)) {
1208
+ this._fanoutNotification(text);
1209
+ } else if (WATCH_GATED.has(type)) {
1210
+ for (const [chatId, state] of this.chats.entries()) {
1211
+ if (state.watchEnabled) this._queueNotification(chatId, text);
652
1212
  }
1213
+ }
1214
+ }
653
1215
 
654
- this._queueNotification(text);
1216
+ /** Send `text` to every linked chat regardless of /watch state. */
1217
+ async _fanoutNotification(text) {
1218
+ for (const chatId of this.chats.keys()) {
1219
+ try { await this._send(chatId, text); } catch { /* per-chat failures shouldn't block others */ }
655
1220
  }
656
1221
  }
657
1222
 
@@ -659,188 +1224,415 @@ class TelegramService {
659
1224
  const agentId = message.agentId || message.data?.agentId;
660
1225
  if (!agentId) return;
661
1226
 
662
- // Only relay responses for agents the user has addressed from Telegram
663
- if (!this.activeAgentIds.has(agentId)) return;
664
-
665
1227
  const content = message.content || message.data?.content ||
666
1228
  message.message?.content || message.data?.message?.content;
667
- if (!content) return;
668
1229
 
669
- // Skip tool-result messages and internal messages
1230
+ // Skip user/tool roles.
670
1231
  const role = message.role || message.data?.role || message.message?.role;
671
1232
  if (role === 'user' || role === 'tool') return;
672
1233
 
673
- // Parse every <external>…</external> block; content inside is relayed
674
- // verbatim (see ./channelFilter.js). The local operator's web UI
675
- // continues to see the full raw broadcast this filter only decides
676
- // what leaves the process.
677
- const { blocks } = filterContentForExternalRelay(content);
1234
+ // Drain any image buffered by the imageTool. We do this even when
1235
+ // there's no text content image-only completions still need to
1236
+ // reach Telegram. We do it for every active chat with this agent.
1237
+ const pendingImages = this._consumePendingImages(agentId);
1238
+
1239
+ const { blocks } = content ? filterContentForExternalRelay(content) : { blocks: [] };
1240
+
678
1241
  if (blocks.length === 0) {
679
- this.logger?.debug?.('[TelegramService] no <external> blocks in agent response nothing relayed', { agentId });
1242
+ // No <external> text block. Clear thinking placeholders so the
1243
+ // user isn't left looking at "🔄 …", and — critically — still
1244
+ // ship any buffered images for this agent so "draw me a sunset"
1245
+ // through Telegram doesn't go silent when the agent's text
1246
+ // reply omits the image markdown.
1247
+ for (const [chatId, state] of this.chats.entries()) {
1248
+ await this._clearThinking(chatId, agentId);
1249
+ if (pendingImages.length > 0 && state.activeAgentIds.has(agentId)) {
1250
+ for (const img of pendingImages) {
1251
+ await this._sendMedia(chatId, { kind: 'image', src: img.imageUrl, caption: img.prompt || undefined });
1252
+ }
1253
+ }
1254
+ }
680
1255
  return;
681
1256
  }
682
1257
 
683
- const bridged = this.getBridgedChannels(agentId);
684
- if (bridged.length === 0) return;
685
- const ownedAliases = bridged.map(c => c.alias);
686
-
687
1258
  const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
688
1259
  const agentName = agent?.name || agentId;
689
1260
 
690
- for (const block of blocks) {
691
- const targets = resolveBlockTargets(block, ownedAliases);
692
- if (targets.length === 0) continue;
693
- // Today there is a single Telegram chat per agent, so any match
694
- // ultimately sends to `this.chatId`. Loop defensively in case the
695
- // service ever grows multi-chat support.
696
- for (const alias of targets) {
1261
+ // For each chat where this agent is active, send the block(s).
1262
+ for (const [chatId, state] of this.chats.entries()) {
1263
+ if (!state.activeAgentIds.has(agentId)) continue;
1264
+
1265
+ const ownedAliases = this._aliasesForChat(chatId);
1266
+ let firstBlock = true;
1267
+
1268
+ for (const block of blocks) {
1269
+ const targets = resolveBlockTargets(block, ownedAliases);
1270
+ if (targets.length === 0) continue;
1271
+
697
1272
  try {
698
- await this._sendFormattedResponse(this.chatId, agentName, block.text);
1273
+ const parsed = parseTelegramBlock(block.text);
1274
+
1275
+ // Edit the thinking placeholder on the first block; subsequent
1276
+ // blocks go as new messages so users still see them as separate
1277
+ // turns when an agent emits multiple <external> blocks.
1278
+ if (firstBlock) {
1279
+ await this._replaceThinkingWithReply(chatId, agentId, agentName, parsed);
1280
+ firstBlock = false;
1281
+ } else {
1282
+ await this._sendParsedPayload(chatId, agentName, parsed);
1283
+ }
1284
+
1285
+ // Media is sent after the text body in either path.
1286
+ for (const media of parsed.media) {
1287
+ await this._sendMedia(chatId, media);
1288
+ }
699
1289
  } catch (error) {
700
- this.logger?.warn?.('[TelegramService] Failed to relay block', {
701
- alias, error: error.message
1290
+ this.logger?.warn?.('[TelegramService] block relay failed', {
1291
+ chatId, agentId, error: error.message,
702
1292
  });
703
1293
  }
704
1294
  }
1295
+
1296
+ // After the text blocks land, ship any imageTool-generated
1297
+ // images that aren't already embedded in a parsed block. We
1298
+ // dedupe by URL against the block parser's media list so we
1299
+ // don't double-post an image the agent already referenced
1300
+ // inline.
1301
+ if (pendingImages.length > 0) {
1302
+ const embeddedUrls = new Set();
1303
+ for (const block of blocks) {
1304
+ try {
1305
+ for (const m of parseTelegramBlock(block.text).media) {
1306
+ if (m?.src) embeddedUrls.add(m.src);
1307
+ }
1308
+ } catch { /* parser failures already logged above */ }
1309
+ }
1310
+ for (const img of pendingImages) {
1311
+ if (embeddedUrls.has(img.imageUrl)) continue;
1312
+ await this._sendMedia(chatId, { kind: 'image', src: img.imageUrl, caption: img.prompt || undefined });
1313
+ }
1314
+ }
705
1315
  }
706
1316
  }
707
1317
 
708
1318
  /**
709
- * Single-element alias list for this agent's Telegram bridge. The
710
- * service currently tracks one chat, so the alias is a stable
711
- * `telegram` handle; expanding to multi-chat later is additive (return
712
- * one entry per chat).
713
- *
714
- * @param {string} agentId
715
- * @returns {Array<{alias: string, label: string}>}
1319
+ * Stash an `imageGenerated` broadcast for later flush by
1320
+ * `_relayAgentResponse`. Prunes stale entries (>5min) on insert so
1321
+ * the buffer can't grow unboundedly if no stream_complete ever
1322
+ * arrives for that agent.
716
1323
  */
717
- getBridgedChannels(agentId) {
718
- if (!this.isAgentBridged(agentId)) return [];
719
- return [{ alias: 'telegram', label: 'Telegram chat' }];
1324
+ _bufferGeneratedImage(message) {
1325
+ const agentId = message.agentId || message.data?.agentId;
1326
+ const imageUrl = message.imageUrl || message.data?.imageUrl;
1327
+ if (!agentId || !imageUrl) return;
1328
+ const prompt = message.prompt || message.data?.prompt || null;
1329
+
1330
+ const arr = this._pendingImagesByAgent.get(agentId) || [];
1331
+ const now = Date.now();
1332
+ // Drop stale entries while we're touching this list.
1333
+ const fresh = arr.filter(e => now - e.addedAt < IMAGE_PENDING_TTL_MS);
1334
+ fresh.push({ imageUrl, prompt, addedAt: now });
1335
+ this._pendingImagesByAgent.set(agentId, fresh);
1336
+ }
1337
+
1338
+ /** Atomically read-and-clear pending images for `agentId`. */
1339
+ _consumePendingImages(agentId) {
1340
+ const arr = this._pendingImagesByAgent.get(agentId);
1341
+ if (!arr || arr.length === 0) return [];
1342
+ this._pendingImagesByAgent.delete(agentId);
1343
+ const now = Date.now();
1344
+ return arr.filter(e => now - e.addedAt < IMAGE_PENDING_TTL_MS);
1345
+ }
1346
+
1347
+ _aliasesForChat(chatId) {
1348
+ // Today we advertise a single chat-scoped alias; expanding later
1349
+ // to per-channel aliases is a one-line change.
1350
+ return ['telegram', `telegram:chat-${chatId}`];
720
1351
  }
721
1352
 
722
1353
  /**
723
- * True when the agent is currently addressable from Telegram the
724
- * scheduler uses this to decide whether to inject the `<external>`
725
- * prompt guidance for this turn.
726
- *
727
- * @param {string} agentId
728
- * @returns {boolean}
1354
+ * Edit the in-flight thinking placeholder to show the agent's real
1355
+ * reply. Falls back to a fresh message if editing fails (e.g. the
1356
+ * placeholder was deleted by the user).
729
1357
  */
730
- isAgentBridged(agentId) {
731
- if (!agentId || this.status !== TELEGRAM_STATUS.CONNECTED) return false;
732
- return this.activeAgentIds.has(agentId);
733
- }
1358
+ async _replaceThinkingWithReply(chatId, agentId, agentName, parsed) {
1359
+ const state = this.chats.get(String(chatId));
1360
+ const placeholderId = state?.thinkingByAgentId.get(agentId);
1361
+ state?.thinkingByAgentId.delete(agentId);
1362
+
1363
+ const header = `*${this._escapeMarkdown(agentName)}:*\n\n`;
1364
+ const formatted = this._formatAgentBody(parsed.text);
1365
+ const replyMarkup = this._buildActionKeyboard(agentId, parsed.actions);
1366
+
1367
+ if (formatted.kind === 'document') {
1368
+ // Code-heavy reply: drop the placeholder, send the document.
1369
+ if (placeholderId) {
1370
+ try { await this.bot.deleteMessage(chatId, placeholderId); } catch {}
1371
+ }
1372
+ await this._sendCodeDocument(chatId, agentName, formatted.body);
1373
+ return;
1374
+ }
1375
+
1376
+ const fullText = header + formatted.body;
1377
+ const sendOpts = { parse_mode: formatted.parseMode };
1378
+ if (replyMarkup) sendOpts.reply_markup = replyMarkup;
734
1379
 
735
- // --- Smart Response Formatting ---
1380
+ if (placeholderId) {
1381
+ try {
1382
+ await this.bot.editMessageText(fullText, {
1383
+ chat_id: chatId,
1384
+ message_id: placeholderId,
1385
+ ...sendOpts,
1386
+ });
1387
+ return;
1388
+ } catch (err) {
1389
+ this.logger?.debug?.('[TelegramService] edit placeholder failed, sending fresh', { error: err.message });
1390
+ }
1391
+ }
1392
+ await this._sendRaw(chatId, fullText, sendOpts);
1393
+ }
736
1394
 
737
- async _sendFormattedResponse(chatId, agentName, content) {
1395
+ /** Send a parsed payload (additional blocks after the first). */
1396
+ async _sendParsedPayload(chatId, agentName, parsed) {
738
1397
  const header = `*${this._escapeMarkdown(agentName)}:*\n\n`;
1398
+ const formatted = this._formatAgentBody(parsed.text);
1399
+ if (formatted.kind === 'document') {
1400
+ await this._sendCodeDocument(chatId, agentName, formatted.body);
1401
+ return;
1402
+ }
1403
+ const replyMarkup = this._buildActionKeyboard(null, parsed.actions);
1404
+ const opts = { parse_mode: formatted.parseMode };
1405
+ if (replyMarkup) opts.reply_markup = replyMarkup;
1406
+ await this._sendRaw(chatId, header + formatted.body, opts);
1407
+ }
739
1408
 
740
- // Check for code blocks
741
- const hasCode = content.includes('```');
1409
+ _buildActionKeyboard(agentId, actions) {
1410
+ if (!Array.isArray(actions) || actions.length === 0) return null;
1411
+ if (!agentId) return null; // can't dispatch without an agent
1412
+ return {
1413
+ inline_keyboard: actions.map(a => ([{
1414
+ text: a.label,
1415
+ // Telegram caps callback_data at 64 bytes. The shape is
1416
+ // `act:<agentId>:<value>` so the value is what's first to be
1417
+ // truncated. agentId is typically a UUID (36 chars) + 'act:'
1418
+ // prefix (4 chars) leaves 24 chars for the value.
1419
+ callback_data: `act:${agentId}:${a.value}`.slice(0, 64),
1420
+ }])),
1421
+ };
1422
+ }
742
1423
 
743
- if (content.length + header.length <= MAX_MESSAGE_LENGTH) {
744
- if (hasCode) {
745
- // Send as-is with code blocks — Telegram handles ``` natively
746
- await this._send(chatId, header + this._escapeMarkdownPreserveCode(content));
747
- } else {
748
- await this._send(chatId, header + this._escapeMarkdown(content));
1424
+ // ── Text formatting: HTML for code blocks, MarkdownV2 otherwise ────
1425
+
1426
+ _formatAgentBody(body) {
1427
+ if (!body || typeof body !== 'string' || body.trim() === '') {
1428
+ return { kind: 'text', parseMode: 'MarkdownV2', body: this._escapeMarkdown('(empty response)') };
1429
+ }
1430
+
1431
+ const fenceCount = (body.match(/```/g) || []).length;
1432
+ const hasFencedCode = fenceCount >= 2;
1433
+ const isCodeHeavy = hasFencedCode && body.length > CODE_AS_FILE_THRESHOLD;
1434
+
1435
+ if (isCodeHeavy) {
1436
+ // Send as document — guarantees no markdown mangling for big payloads.
1437
+ return { kind: 'document', body };
1438
+ }
1439
+
1440
+ if (hasFencedCode) {
1441
+ // HTML mode is far more forgiving than MarkdownV2 for mixed-code text.
1442
+ // Convert fenced blocks to <pre><code>…</code></pre>.
1443
+ const htmlBody = body.replace(/```(\w+)?\n([\s\S]*?)```/g, (_full, _lang, code) => {
1444
+ const escaped = String(code)
1445
+ .replace(/&/g, '&amp;')
1446
+ .replace(/</g, '&lt;')
1447
+ .replace(/>/g, '&gt;');
1448
+ return `<pre>${escaped}</pre>`;
1449
+ });
1450
+ // Inline-code backticks too.
1451
+ const withInline = htmlBody.replace(/`([^`\n]+)`/g, (_m, c) => {
1452
+ const esc = String(c).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1453
+ return `<code>${esc}</code>`;
1454
+ });
1455
+ // Escape everything else that HTML mode cares about (only <, >, &
1456
+ // outside the tags). Easiest: split by <pre>/<code> tags and
1457
+ // escape non-code parts.
1458
+ const safe = this._escapeHtmlPreservingTags(withInline);
1459
+ const truncated = safe.length > MAX_MESSAGE_LENGTH
1460
+ ? safe.slice(0, MAX_MESSAGE_LENGTH - 100) + '\n\n… (truncated)'
1461
+ : safe;
1462
+ return { kind: 'text', parseMode: 'HTML', body: truncated };
1463
+ }
1464
+
1465
+ // Plain text: full MarkdownV2 escape.
1466
+ const escaped = this._escapeMarkdown(body);
1467
+ const truncated = escaped.length > MAX_MESSAGE_LENGTH
1468
+ ? escaped.slice(0, MAX_MESSAGE_LENGTH - 100) + '\n\n… (truncated)'
1469
+ : escaped;
1470
+ return { kind: 'text', parseMode: 'MarkdownV2', body: truncated };
1471
+ }
1472
+
1473
+ _escapeHtmlPreservingTags(text) {
1474
+ // Split into segments alternating between tag-segments and text.
1475
+ // We only allow <pre>/<code>; anything else gets escaped.
1476
+ const parts = text.split(/(<\/?(?:pre|code)>)/g);
1477
+ return parts.map((p) => {
1478
+ if (/^<\/?(pre|code)>$/i.test(p)) return p;
1479
+ return p.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1480
+ }).join('');
1481
+ }
1482
+
1483
+ async _sendCodeDocument(chatId, agentName, body) {
1484
+ if (!this.bot) return;
1485
+ try {
1486
+ const filename = `${(agentName || 'reply').replace(/[^A-Za-z0-9-]/g, '_')}-${Date.now()}.txt`;
1487
+ // node-telegram-bot-api accepts a Buffer for sendDocument with
1488
+ // options.filename for the displayed name.
1489
+ await this.bot.sendDocument(
1490
+ chatId,
1491
+ Buffer.from(body, 'utf8'),
1492
+ { caption: `*${this._escapeMarkdown(agentName)}* — full reply (sent as a file because it\\\'s code\\-heavy).`, parse_mode: 'MarkdownV2' },
1493
+ { filename, contentType: 'text/plain' }
1494
+ );
1495
+ } catch (error) {
1496
+ this.logger?.warn?.('[TelegramService] code-document send failed, falling back to truncated text', { error: error.message });
1497
+ const fallback = body.slice(0, MAX_MESSAGE_LENGTH - 200) + '\n\n… (truncated; see local web UI for full reply)';
1498
+ await this._sendRaw(chatId, `*${this._escapeMarkdown(agentName)}:*\n\n` + this._escapeMarkdown(fallback), { parse_mode: 'MarkdownV2' });
1499
+ }
1500
+ }
1501
+
1502
+ async _sendMedia(chatId, media) {
1503
+ if (!this.bot) return;
1504
+ try {
1505
+ const captionMd = media.caption ? this._escapeMarkdown(media.caption) : undefined;
1506
+ if (media.kind === 'image') {
1507
+ await this.bot.sendPhoto(chatId, media.src, {
1508
+ caption: captionMd, parse_mode: 'MarkdownV2',
1509
+ });
1510
+ } else if (media.kind === 'document') {
1511
+ await this.bot.sendDocument(chatId, media.src, {
1512
+ caption: captionMd, parse_mode: 'MarkdownV2',
1513
+ });
749
1514
  }
750
- } else {
751
- // Split long messages
752
- await this._send(chatId, header + this._escapeMarkdown(content.slice(0, MAX_MESSAGE_LENGTH - 100) + '\n\n… (truncated)'));
1515
+ } catch (error) {
1516
+ this.logger?.warn?.('[TelegramService] media send failed', { error: error.message, kind: media.kind });
753
1517
  }
754
1518
  }
755
1519
 
756
- // --- Prompt Relay ---
1520
+ // ── Prompt Relay ───────────────────────────────────────────────────
757
1521
 
758
1522
  async _relayPromptRequest(message) {
759
- if (!this.chatId) return;
760
-
1523
+ if (this.chats.size === 0) return;
761
1524
  const data = message.data || message;
762
1525
  const requestId = data.requestId;
763
1526
  const agentId = data.agentId;
764
1527
  const questions = data.questions || [];
765
-
766
1528
  if (!requestId || questions.length === 0) return;
767
1529
 
768
1530
  const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
769
1531
  const agentName = agent?.name || agentId;
770
1532
 
771
- this.pendingRelays.set(requestId, { type: 'user_prompt', agentId, questions, timestamp: Date.now() });
772
-
773
- for (const q of questions) {
774
- let text = `🔔 *${this._escapeMarkdown(agentName)}* needs your input:\n\n`;
775
- text += this._escapeMarkdown(q.question) + '\n';
776
-
777
- const options = q.options || [];
778
- if (options.length > 0) {
779
- const buttons = options.map((opt, i) => ([{
780
- text: opt.label,
781
- callback_data: `prompt_reply:${requestId}:${i}`
782
- }]));
783
- await this._send(this.chatId, text, { reply_markup: { inline_keyboard: buttons } });
784
- } else {
785
- text += '\n_Type your reply:_';
786
- this.replyContext = { type: 'user_prompt', requestId, agentId };
787
- await this._send(this.chatId, text);
1533
+ // Fan out to every chat that has this agent active.
1534
+ for (const [chatId, state] of this.chats.entries()) {
1535
+ if (!state.activeAgentIds.has(agentId)) continue;
1536
+
1537
+ this.pendingRelays.set(requestId, { type: 'user_prompt', agentId, chatId, questions, timestamp: Date.now() });
1538
+
1539
+ for (const q of questions) {
1540
+ let text = `🔔 *${this._escapeMarkdown(agentName)}* needs your input:\n\n`;
1541
+ text += this._escapeMarkdown(q.question) + '\n';
1542
+ const options = q.options || [];
1543
+ if (options.length > 0) {
1544
+ const buttons = options.map((opt, i) => ([{
1545
+ text: opt.label,
1546
+ callback_data: `prompt_reply:${requestId}:${i}`,
1547
+ }]));
1548
+ await this._send(chatId, text, { reply_markup: { inline_keyboard: buttons } });
1549
+ } else {
1550
+ text += '\n_Type your reply:_';
1551
+ this.replyContext.set(chatId, { type: 'user_prompt', requestId, agentId });
1552
+ await this._send(chatId, text);
1553
+ }
788
1554
  }
789
- }
790
1555
 
791
- // Set timeout reminder
792
- setTimeout(() => {
793
- if (this.pendingRelays.has(requestId)) {
794
- this._send(this.chatId, this._escapeMarkdown(`⏰ Reminder: ${agentName} is still waiting for your input.`)).catch(() => {});
1556
+ // Two timers per request:
1557
+ // - A 3-min reminder ("still waiting")
1558
+ // - A 10-min hard timeout ("expired — please re-trigger the action")
1559
+ const reminderId = setTimeout(() => {
1560
+ if (this.pendingRelays.has(requestId)) {
1561
+ this._send(chatId, this._escapeMarkdown(`⏰ Reminder: ${agentName} is still waiting for your input.`)).catch(() => {});
1562
+ }
1563
+ }, PROMPT_REMINDER_MS);
1564
+ const timeoutId = setTimeout(() => {
1565
+ if (this.pendingRelays.has(requestId)) {
1566
+ this.pendingRelays.delete(requestId);
1567
+ this.replyContext.delete(chatId);
1568
+ this._send(chatId, this._escapeMarkdown(`⏱ Prompt from ${agentName} expired without a reply. Re-run the action if you still need it.`)).catch(() => {});
1569
+ }
1570
+ }, PROMPT_TIMEOUT_MS);
1571
+ const existing = this.pendingRelays.get(requestId);
1572
+ if (existing) {
1573
+ existing.reminderId = reminderId;
1574
+ existing.timeoutId = timeoutId;
795
1575
  }
796
- }, PROMPT_REMINDER_MS);
1576
+ }
797
1577
  }
798
1578
 
799
1579
  async _relayCredentialRequest(message) {
800
- if (!this.chatId) return;
801
-
1580
+ if (this.chats.size === 0) return;
802
1581
  const data = message.data || message;
803
1582
  const requestId = data.requestId;
804
1583
  const agentId = data.agentId;
805
-
806
1584
  const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
807
1585
  const agentName = agent?.name || agentId;
808
1586
 
809
- this.pendingRelays.set(requestId, { type: 'credential', agentId, timestamp: Date.now() });
810
- this.replyContext = { type: 'credential', requestId, agentId };
811
-
812
- let text = `🔐 *${this._escapeMarkdown(agentName)}* needs credentials:\n\n`;
813
- text += this._escapeMarkdown(data.message || 'Please provide the requested credentials.');
814
- text += '\n\n_Type your reply:_';
815
-
816
- await this._send(this.chatId, text);
1587
+ for (const [chatId, state] of this.chats.entries()) {
1588
+ if (!state.activeAgentIds.has(agentId)) continue;
1589
+ this.pendingRelays.set(requestId, { type: 'credential', agentId, chatId, timestamp: Date.now() });
1590
+ this.replyContext.set(chatId, { type: 'credential', requestId, agentId });
1591
+ const text = `🔐 *${this._escapeMarkdown(agentName)}* needs credentials:\n\n` +
1592
+ this._escapeMarkdown(data.message || 'Please provide the requested credentials.') +
1593
+ '\n\n_Type your reply:_';
1594
+ await this._send(chatId, text);
1595
+
1596
+ // Timeout for credential requests too.
1597
+ const timeoutId = setTimeout(() => {
1598
+ if (this.pendingRelays.has(requestId)) {
1599
+ this.pendingRelays.delete(requestId);
1600
+ this.replyContext.delete(chatId);
1601
+ this._send(chatId, this._escapeMarkdown(`⏱ Credential request from ${agentName} expired. Re-trigger if needed.`)).catch(() => {});
1602
+ }
1603
+ }, PROMPT_TIMEOUT_MS);
1604
+ const existing = this.pendingRelays.get(requestId);
1605
+ if (existing) existing.timeoutId = timeoutId;
1606
+ }
817
1607
  }
818
1608
 
819
1609
  async _handlePromptReply(msg) {
820
- if (!this.replyContext) return;
1610
+ const chatId = String(msg.chat.id);
1611
+ const ctx = this.replyContext.get(chatId);
1612
+ if (!ctx) return;
821
1613
 
822
- const { type, requestId, agentId } = this.replyContext;
1614
+ const { type, requestId } = ctx;
823
1615
  const text = msg.text?.trim();
824
1616
  if (!text) return;
825
1617
 
826
- this.replyContext = null;
1618
+ this.replyContext.delete(chatId);
1619
+ const relay = this.pendingRelays.get(requestId);
1620
+ if (relay) {
1621
+ if (relay.reminderId) clearTimeout(relay.reminderId);
1622
+ if (relay.timeoutId) clearTimeout(relay.timeoutId);
1623
+ }
827
1624
  this.pendingRelays.delete(requestId);
828
1625
 
829
1626
  try {
830
1627
  if (type === 'user_prompt' && this.webSocketManager) {
831
- // Submit prompt response via the same path the web UI uses
832
1628
  this.webSocketManager._handleUserPromptResult?.({
833
- requestId,
834
- answers: { default: text }
1629
+ requestId, answers: { default: text },
835
1630
  });
836
1631
  } else if (type === 'credential' && this.webSocketManager) {
837
1632
  this.webSocketManager._handleCredentialResponse?.({
838
- requestId,
839
- credentials: { value: text },
840
- saveForFuture: false
1633
+ requestId, credentials: { value: text }, saveForFuture: false,
841
1634
  });
842
1635
  }
843
-
844
1636
  await this._send(msg.chat.id, this._escapeMarkdown('✅ Response submitted.'));
845
1637
  } catch (error) {
846
1638
  await this._send(msg.chat.id, this._escapeMarkdown(`❌ Failed to submit: ${error.message}`));
@@ -850,122 +1642,167 @@ class TelegramService {
850
1642
  async _submitPromptReply(requestId, answerIndex) {
851
1643
  const relay = this.pendingRelays.get(requestId);
852
1644
  if (!relay) return;
853
-
1645
+ if (relay.reminderId) clearTimeout(relay.reminderId);
1646
+ if (relay.timeoutId) clearTimeout(relay.timeoutId);
854
1647
  this.pendingRelays.delete(requestId);
855
- this.replyContext = null;
1648
+ this.replyContext.delete(String(relay.chatId));
856
1649
 
857
1650
  try {
858
- const question = relay.questions[0];
1651
+ const question = relay.questions?.[0];
859
1652
  const option = question?.options?.[parseInt(answerIndex)];
860
1653
  const answer = option?.label || String(answerIndex);
861
-
862
1654
  if (this.webSocketManager?._handleUserPromptResult) {
863
1655
  this.webSocketManager._handleUserPromptResult({
864
- requestId,
865
- answers: { [question.question]: answer }
1656
+ requestId, answers: { [question.question]: answer },
866
1657
  });
867
1658
  }
868
-
869
- await this._send(this.chatId, this._escapeMarkdown(`✅ Selected: ${answer}`));
1659
+ await this._send(relay.chatId, this._escapeMarkdown(`✅ Selected: ${answer}`));
870
1660
  } catch (error) {
871
- await this._send(this.chatId, this._escapeMarkdown(`❌ Failed: ${error.message}`));
1661
+ await this._send(relay.chatId, this._escapeMarkdown(`❌ Failed: ${error.message}`));
872
1662
  }
873
1663
  }
874
1664
 
875
- // --- Notification Batching ---
1665
+ // ── Notification Batching ──────────────────────────────────────────
876
1666
 
877
- _queueNotification(text) {
878
- this.notificationQueue.push(text);
879
- if (!this.notificationTimer) {
880
- this.notificationTimer = setTimeout(() => this._flushNotifications(), NOTIFICATION_BATCH_INTERVAL_MS);
1667
+ _queueNotification(chatId, text) {
1668
+ let q = this.notificationQueue.get(chatId);
1669
+ if (!q) { q = []; this.notificationQueue.set(chatId, q); }
1670
+ q.push(text);
1671
+ if (!this.notificationTimers.has(chatId)) {
1672
+ this.notificationTimers.set(chatId, setTimeout(() => this._flushNotifications(chatId), NOTIFICATION_BATCH_INTERVAL_MS));
881
1673
  }
882
1674
  }
883
1675
 
884
- async _flushNotifications() {
885
- this.notificationTimer = null;
886
- if (this.notificationQueue.length === 0 || !this.chatId) return;
887
-
888
- const messages = this.notificationQueue.splice(0);
889
- const combined = messages.join('\n\n');
890
-
1676
+ async _flushNotifications(chatId) {
1677
+ this.notificationTimers.delete(chatId);
1678
+ const q = this.notificationQueue.get(chatId) || [];
1679
+ this.notificationQueue.set(chatId, []);
1680
+ if (q.length === 0) return;
1681
+ const combined = q.join('\n\n');
891
1682
  if (combined.length <= MAX_MESSAGE_LENGTH) {
892
- await this._send(this.chatId, combined);
1683
+ await this._send(chatId, combined);
893
1684
  } else {
894
- await this._send(this.chatId, messages[0] + (messages.length > 1
895
- ? `\n\n_…and ${messages.length - 1} more events_`
896
- : ''));
897
- }
898
- }
899
-
900
- _clearNotificationTimer() {
901
- if (this.notificationTimer) {
902
- clearTimeout(this.notificationTimer);
903
- this.notificationTimer = null;
1685
+ await this._send(chatId, q[0] + `\n\n_…and ${q.length - 1} more events_`);
904
1686
  }
905
- this.notificationQueue = [];
906
1687
  }
907
1688
 
908
- // --- Telegram API Helpers ---
1689
+ // ── Telegram API Helpers ───────────────────────────────────────────
909
1690
 
910
1691
  async _send(chatId, text, options = {}) {
1692
+ return this._sendRaw(chatId, text, { parse_mode: 'MarkdownV2', ...options });
1693
+ }
1694
+
1695
+ async _sendRaw(chatId, text, options) {
911
1696
  if (!this.bot || !chatId) return;
912
1697
  try {
913
- return await this.bot.sendMessage(chatId, text, {
914
- parse_mode: 'MarkdownV2',
915
- ...options
916
- });
1698
+ return await this.bot.sendMessage(chatId, text, options);
917
1699
  } catch (error) {
918
- // Fallback: send without formatting if markdown fails
919
- this.logger?.warn('[TelegramService] Markdown send failed, retrying plain', { error: error.message });
1700
+ // Fallback path: strip formatting and retry plain. This catches
1701
+ // MarkdownV2-escape gaps the regex doesn't cover.
1702
+ this.logger?.warn('[TelegramService] formatted send failed, retrying plain', { error: error.message });
920
1703
  try {
921
- return await this.bot.sendMessage(chatId, text.replace(/[\\*_`\[\]()~>#+\-=|{}.!]/g, ''), options);
1704
+ const plainOpts = { ...options };
1705
+ delete plainOpts.parse_mode;
1706
+ return await this.bot.sendMessage(
1707
+ chatId,
1708
+ String(text).replace(/[\\*_`\[\]()~>#+\-=|{}.!]/g, ''),
1709
+ plainOpts
1710
+ );
922
1711
  } catch (e2) {
923
- this.logger?.error('[TelegramService] Send failed', { error: e2.message });
1712
+ this.logger?.error('[TelegramService] send failed', { error: e2.message });
924
1713
  }
925
1714
  }
926
1715
  }
927
1716
 
928
1717
  async sendPhoto(chatId, photoPath, caption = '') {
929
1718
  if (!this.bot || !chatId) return;
930
- try {
931
- return await this.bot.sendPhoto(chatId, photoPath, { caption });
932
- } catch (error) {
933
- this.logger?.error('[TelegramService] Send photo failed', { error: error.message });
934
- }
1719
+ try { return await this.bot.sendPhoto(chatId, photoPath, { caption }); }
1720
+ catch (error) { this.logger?.error('[TelegramService] sendPhoto failed', { error: error.message }); }
935
1721
  }
936
1722
 
937
1723
  async sendDocument(chatId, docPath, caption = '') {
938
1724
  if (!this.bot || !chatId) return;
939
- try {
940
- return await this.bot.sendDocument(chatId, docPath, { caption });
941
- } catch (error) {
942
- this.logger?.error('[TelegramService] Send document failed', { error: error.message });
943
- }
1725
+ try { return await this.bot.sendDocument(chatId, docPath, { caption }); }
1726
+ catch (error) { this.logger?.error('[TelegramService] sendDocument failed', { error: error.message }); }
944
1727
  }
945
1728
 
946
1729
  async sendTestMessage() {
947
- if (!this.chatId) throw new Error('No chat registered. Send /start from Telegram first.');
948
- await this._send(this.chatId, this._escapeMarkdown(' Loxia Autopilot test message received!'));
1730
+ if (this.chats.size === 0) {
1731
+ throw new Error('No chats registered. Send /start from Telegram first.');
1732
+ }
1733
+ for (const chatId of this.chats.keys()) {
1734
+ await this._send(chatId, this._escapeMarkdown('✅ Loxia Autopilot — test message received!'));
1735
+ }
949
1736
  }
950
1737
 
951
- // --- Markdown Escaping ---
1738
+ // ── Markdown Escaping ──────────────────────────────────────────────
952
1739
 
953
1740
  _escapeMarkdown(text) {
954
1741
  if (!text) return '';
955
- // Escape MarkdownV2 special chars
956
1742
  return text.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, '\\$1');
957
1743
  }
958
1744
 
959
- _escapeMarkdownPreserveCode(text) {
960
- if (!text) return '';
961
- // Split by code blocks, escape non-code parts
962
- const parts = text.split(/(```[\s\S]*?```)/g);
963
- return parts.map(part => {
964
- if (part.startsWith('```')) {
965
- return part; // Don't escape code blocks
1745
+ /**
1746
+ * Tagged-template helper for MarkdownV2 messages with safe interpolation.
1747
+ *
1748
+ * The LITERAL parts of the template (the bits between `${...}`) are
1749
+ * passed through unchanged — author writes raw MarkdownV2 there
1750
+ * (e.g. `*bold*`, `` `code` ``, `_italic_`, with `\.` / `\!` etc.
1751
+ * spelled out where Telegram requires).
1752
+ *
1753
+ * The INTERPOLATED parts (`${value}`) are escaped automatically so a
1754
+ * variable containing `_` or `*` can't accidentally break formatting
1755
+ * or inject markup.
1756
+ *
1757
+ * The original `_escapeMarkdown(entireString)` pattern that wrapped
1758
+ * a hand-written MarkdownV2 string in escapes is the bug we keep
1759
+ * hitting — every intended `*bold*` ended up as literal `*bold*`.
1760
+ *
1761
+ * @example
1762
+ * const text = this._md`*Welcome* ${userName}\\. Use /help for commands\\.`;
1763
+ *
1764
+ * @returns {string} ready to pass to this._send() with MarkdownV2 parse_mode.
1765
+ */
1766
+ _md(strings, ...values) {
1767
+ let out = '';
1768
+ for (let i = 0; i < strings.length; i++) {
1769
+ out += strings[i];
1770
+ if (i < values.length) out += this._escapeMarkdown(String(values[i] ?? ''));
1771
+ }
1772
+ return out;
1773
+ }
1774
+
1775
+ // ── Public bridge introspection ────────────────────────────────────
1776
+
1777
+ /**
1778
+ * Bridged channels for an agent: one alias per chat the agent has
1779
+ * been addressed from. The scheduler injects the `<external to="…">`
1780
+ * prompt guidance based on this list.
1781
+ */
1782
+ getBridgedChannels(agentId) {
1783
+ const out = [];
1784
+ for (const [chatId, state] of this.chats.entries()) {
1785
+ if (state.activeAgentIds.has(agentId)) {
1786
+ out.push({
1787
+ alias: `telegram:chat-${chatId}`,
1788
+ label: state.title ? `Telegram > ${state.title}` : 'Telegram chat',
1789
+ });
966
1790
  }
967
- return this._escapeMarkdown(part);
968
- }).join('');
1791
+ }
1792
+ if (out.length > 0) {
1793
+ // Also expose the bare `telegram` alias for backward compat
1794
+ // with agents that don't know about per-chat addressing.
1795
+ out.push({ alias: 'telegram', label: 'Telegram (any chat)' });
1796
+ }
1797
+ return out;
1798
+ }
1799
+
1800
+ isAgentBridged(agentId) {
1801
+ if (!agentId || this.status !== TELEGRAM_STATUS.CONNECTED) return false;
1802
+ for (const state of this.chats.values()) {
1803
+ if (state.activeAgentIds.has(agentId)) return true;
1804
+ }
1805
+ return false;
969
1806
  }
970
1807
  }
971
1808
 
@@ -979,5 +1816,10 @@ export function getTelegramService(logger = null) {
979
1816
  return instance;
980
1817
  }
981
1818
 
1819
+ // Tests need to drop the singleton between cases.
1820
+ export function _resetTelegramSingletonForTests() {
1821
+ instance = null;
1822
+ }
1823
+
982
1824
  export { TelegramService, TELEGRAM_STATUS };
983
1825
  export default TelegramService;