@poncho-ai/cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (516) hide show
  1. package/.turbo/turbo-build.log +19 -0
  2. package/.turbo/turbo-test.log +389 -0
  3. package/CHANGELOG.md +17 -0
  4. package/LICENSE +21 -0
  5. package/dist/chunk-22OMLQUR.js +1249 -0
  6. package/dist/chunk-24JFN5RM.js +1887 -0
  7. package/dist/chunk-24TAT3US.js +2137 -0
  8. package/dist/chunk-26YBLT7G.js +997 -0
  9. package/dist/chunk-2EJIC6UW.js +1893 -0
  10. package/dist/chunk-2JNCF37R.js +1156 -0
  11. package/dist/chunk-2LVMUHJX.js +1874 -0
  12. package/dist/chunk-2OVSD65B.js +1269 -0
  13. package/dist/chunk-2RQ45LI6.js +1251 -0
  14. package/dist/chunk-2SMIRDLI.js +1854 -0
  15. package/dist/chunk-2UXPHBFI.js +1862 -0
  16. package/dist/chunk-2VFM7SSZ.js +1135 -0
  17. package/dist/chunk-2ZAUADNG.js +1456 -0
  18. package/dist/chunk-2ZNUT5WA.js +1862 -0
  19. package/dist/chunk-33ZQ7WTP.js +1834 -0
  20. package/dist/chunk-34ARQX3O.js +1156 -0
  21. package/dist/chunk-3BEWSRFW.js +1893 -0
  22. package/dist/chunk-3DVE5AG6.js +1862 -0
  23. package/dist/chunk-3KE6MHO6.js +1608 -0
  24. package/dist/chunk-3MOLPB7Z.js +997 -0
  25. package/dist/chunk-3OZZOYAZ.js +1884 -0
  26. package/dist/chunk-3VJYNZEF.js +2181 -0
  27. package/dist/chunk-3W27LOUH.js +997 -0
  28. package/dist/chunk-3WMAW74D.js +1163 -0
  29. package/dist/chunk-3Z4AHBPF.js +1569 -0
  30. package/dist/chunk-43NK6MB4.js +1977 -0
  31. package/dist/chunk-4E5M2IGA.js +1300 -0
  32. package/dist/chunk-4FVI4LVI.js +1862 -0
  33. package/dist/chunk-4GNQQJUP.js +1156 -0
  34. package/dist/chunk-4PGZFTVC.js +1234 -0
  35. package/dist/chunk-4QE2HDNC.js +1355 -0
  36. package/dist/chunk-4S2EL4ED.js +1135 -0
  37. package/dist/chunk-536SSOJ3.js +1797 -0
  38. package/dist/chunk-5CNEGIC5.js +997 -0
  39. package/dist/chunk-5CWN43YL.js +1147 -0
  40. package/dist/chunk-5HZCYTUZ.js +997 -0
  41. package/dist/chunk-5ICNG6RX.js +1885 -0
  42. package/dist/chunk-5OUIRXMN.js +997 -0
  43. package/dist/chunk-5T34JOWH.js +1460 -0
  44. package/dist/chunk-5XBAIQX3.js +1862 -0
  45. package/dist/chunk-65AIX3CS.js +1441 -0
  46. package/dist/chunk-67NBW4NG.js +1355 -0
  47. package/dist/chunk-6AFIL35M.js +1414 -0
  48. package/dist/chunk-6B3XMBKA.js +1716 -0
  49. package/dist/chunk-6CEJO4OM.js +1242 -0
  50. package/dist/chunk-6DQZUP3B.js +1460 -0
  51. package/dist/chunk-6ET624OE.js +1441 -0
  52. package/dist/chunk-6I7WFMAU.js +1862 -0
  53. package/dist/chunk-6MOKAYCL.js +1449 -0
  54. package/dist/chunk-6RFUALWB.js +1845 -0
  55. package/dist/chunk-73E57JUS.js +1231 -0
  56. package/dist/chunk-73SU7GT4.js +816 -0
  57. package/dist/chunk-74IRETUF.js +997 -0
  58. package/dist/chunk-77BYFMUN.js +1924 -0
  59. package/dist/chunk-7DAC2XE5.js +1460 -0
  60. package/dist/chunk-7GBQ4YSB.js +1024 -0
  61. package/dist/chunk-7TOJGUQ5.js +1803 -0
  62. package/dist/chunk-7W7KPLEG.js +1163 -0
  63. package/dist/chunk-7Y7ZXEN2.js +817 -0
  64. package/dist/chunk-A5WKH7H2.js +852 -0
  65. package/dist/chunk-A775UYQB.js +1886 -0
  66. package/dist/chunk-AC4OGTSK.js +1313 -0
  67. package/dist/chunk-ACCRUQ6J.js +1271 -0
  68. package/dist/chunk-AEAZZFTT.js +1886 -0
  69. package/dist/chunk-AIAC5Z55.js +1147 -0
  70. package/dist/chunk-AN34PI2R.js +1238 -0
  71. package/dist/chunk-APIA7MHJ.js +1355 -0
  72. package/dist/chunk-AQGIIT7R.js +1347 -0
  73. package/dist/chunk-ATXKV2NH.js +2111 -0
  74. package/dist/chunk-AVBKQZYR.js +1010 -0
  75. package/dist/chunk-AWIXDCZF.js +1329 -0
  76. package/dist/chunk-AXFHQBKT.js +2128 -0
  77. package/dist/chunk-AYDSTU4P.js +1156 -0
  78. package/dist/chunk-AZ35PK7E.js +1271 -0
  79. package/dist/chunk-BE6HB4IO.js +2155 -0
  80. package/dist/chunk-BIX2FI3S.js +1625 -0
  81. package/dist/chunk-BPPM5YPG.js +997 -0
  82. package/dist/chunk-BRCIVYKE.js +1420 -0
  83. package/dist/chunk-BRK2KKTF.js +1236 -0
  84. package/dist/chunk-BU7R2TVT.js +1862 -0
  85. package/dist/chunk-BXJQ4G5F.js +1901 -0
  86. package/dist/chunk-C3KTCROR.js +1128 -0
  87. package/dist/chunk-C42IGDJW.js +1032 -0
  88. package/dist/chunk-CBRPO2FE.js +1886 -0
  89. package/dist/chunk-CDNITKC6.js +1163 -0
  90. package/dist/chunk-CDVWUDFM.js +1389 -0
  91. package/dist/chunk-CH2IE453.js +1147 -0
  92. package/dist/chunk-CIHC46FS.js +1010 -0
  93. package/dist/chunk-CIYO754K.js +1687 -0
  94. package/dist/chunk-CJFNJ7U3.js +918 -0
  95. package/dist/chunk-CJN66CJY.js +1024 -0
  96. package/dist/chunk-CLJFTDJQ.js +1667 -0
  97. package/dist/chunk-CLNCOQNI.js +997 -0
  98. package/dist/chunk-CN4AUQL4.js +1460 -0
  99. package/dist/chunk-CQYGUBY6.js +1854 -0
  100. package/dist/chunk-CRJUMKVB.js +1862 -0
  101. package/dist/chunk-CZHUYI2J.js +997 -0
  102. package/dist/chunk-DIBSIWSR.js +1256 -0
  103. package/dist/chunk-DJGC3R4O.js +1836 -0
  104. package/dist/chunk-DJR2PEAQ.js +1884 -0
  105. package/dist/chunk-DKE7NWBK.js +1862 -0
  106. package/dist/chunk-DXYDN2OS.js +1147 -0
  107. package/dist/chunk-DZ3FEUJ7.js +1147 -0
  108. package/dist/chunk-EC47SFY3.js +1113 -0
  109. package/dist/chunk-ECAALEAK.js +1239 -0
  110. package/dist/chunk-ECEYIAQZ.js +997 -0
  111. package/dist/chunk-EKX7AV7O.js +1024 -0
  112. package/dist/chunk-EN6CTYUN.js +634 -0
  113. package/dist/chunk-EXCH47WX.js +1460 -0
  114. package/dist/chunk-EY2JOCTM.js +1862 -0
  115. package/dist/chunk-EYHB3LTH.js +1818 -0
  116. package/dist/chunk-F2AC5PKU.js +1446 -0
  117. package/dist/chunk-F5RCUJ62.js +1156 -0
  118. package/dist/chunk-F6OM65VA.js +1460 -0
  119. package/dist/chunk-FAEJ5CQU.js +997 -0
  120. package/dist/chunk-FB7X4KBF.js +1460 -0
  121. package/dist/chunk-FBSEEW3H.js +1862 -0
  122. package/dist/chunk-FBYY3TE5.js +1862 -0
  123. package/dist/chunk-FEA3GBGG.js +997 -0
  124. package/dist/chunk-FHPRGTOJ.js +1131 -0
  125. package/dist/chunk-FLAY6YWY.js +1389 -0
  126. package/dist/chunk-FNXIVJ3B.js +997 -0
  127. package/dist/chunk-FWRVG7RM.js +2160 -0
  128. package/dist/chunk-G47UW452.js +916 -0
  129. package/dist/chunk-G6V5O5AV.js +997 -0
  130. package/dist/chunk-GACQBIOO.js +1613 -0
  131. package/dist/chunk-GBFHLBWT.js +881 -0
  132. package/dist/chunk-GLJLTQMZ.js +997 -0
  133. package/dist/chunk-GN7DDBAT.js +1872 -0
  134. package/dist/chunk-GPTI42MM.js +1355 -0
  135. package/dist/chunk-GPXGCPVY.js +1834 -0
  136. package/dist/chunk-GRASQSCU.js +1886 -0
  137. package/dist/chunk-GU2WWG5C.js +1314 -0
  138. package/dist/chunk-GW3SAYT3.js +1679 -0
  139. package/dist/chunk-GZ4F2VI5.js +1447 -0
  140. package/dist/chunk-GZYXND4U.js +1322 -0
  141. package/dist/chunk-H27BRPVI.js +1862 -0
  142. package/dist/chunk-H4JRTOW7.js +1862 -0
  143. package/dist/chunk-HDS72SRU.js +1138 -0
  144. package/dist/chunk-HEUGSUL5.js +757 -0
  145. package/dist/chunk-HMZN5GPS.js +1024 -0
  146. package/dist/chunk-HN5SVGQO.js +1163 -0
  147. package/dist/chunk-HN6VQ5FI.js +242 -0
  148. package/dist/chunk-HNTQ66EL.js +2054 -0
  149. package/dist/chunk-HSDL3YK5.js +1134 -0
  150. package/dist/chunk-HVMIMERW.js +997 -0
  151. package/dist/chunk-HVWCQS2B.js +1834 -0
  152. package/dist/chunk-HVYMSOXQ.js +2156 -0
  153. package/dist/chunk-HYQITTK3.js +1862 -0
  154. package/dist/chunk-I4CCYPAI.js +1239 -0
  155. package/dist/chunk-IDG3X4UL.js +1229 -0
  156. package/dist/chunk-IDVRTQJ3.js +1514 -0
  157. package/dist/chunk-IEER23NN.js +1862 -0
  158. package/dist/chunk-IKLRYA4W.js +785 -0
  159. package/dist/chunk-IQBUSFBY.js +1270 -0
  160. package/dist/chunk-IUXQDZIR.js +1862 -0
  161. package/dist/chunk-IV74RJFV.js +1862 -0
  162. package/dist/chunk-IVEMIXXO.js +1854 -0
  163. package/dist/chunk-J4PNCGSP.js +1460 -0
  164. package/dist/chunk-J7ULLJUI.js +1862 -0
  165. package/dist/chunk-J7WHTBOM.js +983 -0
  166. package/dist/chunk-JBN6D7EG.js +1757 -0
  167. package/dist/chunk-JDKSD54C.js +1366 -0
  168. package/dist/chunk-JGWTOS7A.js +1507 -0
  169. package/dist/chunk-JHJ6FMSI.js +1804 -0
  170. package/dist/chunk-JJSHCP32.js +1282 -0
  171. package/dist/chunk-JLMAMC3V.js +1625 -0
  172. package/dist/chunk-JMTF7VV5.js +1919 -0
  173. package/dist/chunk-JO56GKTV.js +1133 -0
  174. package/dist/chunk-JRN3P6CS.js +1441 -0
  175. package/dist/chunk-JTKPRRSM.js +1024 -0
  176. package/dist/chunk-JVTFCTGO.js +1868 -0
  177. package/dist/chunk-JXH452LK.js +956 -0
  178. package/dist/chunk-K4SW5SP4.js +1147 -0
  179. package/dist/chunk-KANVHOQK.js +1880 -0
  180. package/dist/chunk-KBJZJWPZ.js +1143 -0
  181. package/dist/chunk-KFC4ZVRH.js +1798 -0
  182. package/dist/chunk-KHG6MSLS.js +1885 -0
  183. package/dist/chunk-KJJE6V5N.js +1886 -0
  184. package/dist/chunk-KKLEILZP.js +1868 -0
  185. package/dist/chunk-KW5QXXEN.js +1641 -0
  186. package/dist/chunk-KWMTK4N7.js +1147 -0
  187. package/dist/chunk-KYOW6LIV.js +1231 -0
  188. package/dist/chunk-KYWIUH3M.js +1355 -0
  189. package/dist/chunk-KZEA3HJL.js +1833 -0
  190. package/dist/chunk-L3KONBJE.js +1355 -0
  191. package/dist/chunk-L47B3OMM.js +1156 -0
  192. package/dist/chunk-LCQFWI6S.js +997 -0
  193. package/dist/chunk-LFIUZUI5.js +1138 -0
  194. package/dist/chunk-LHSLPQR4.js +1854 -0
  195. package/dist/chunk-LJGVAOFP.js +647 -0
  196. package/dist/chunk-LKJDYQPA.js +1687 -0
  197. package/dist/chunk-LOY6PWTR.js +1336 -0
  198. package/dist/chunk-LPFN3GNV.js +1147 -0
  199. package/dist/chunk-LPM7AN7S.js +1147 -0
  200. package/dist/chunk-LU6ES63L.js +1249 -0
  201. package/dist/chunk-M73DE7AU.js +1389 -0
  202. package/dist/chunk-M7OJ52WK.js +1448 -0
  203. package/dist/chunk-MCSM3DAE.js +1024 -0
  204. package/dist/chunk-MEP7OYUL.js +1417 -0
  205. package/dist/chunk-MFH6MVWX.js +1441 -0
  206. package/dist/chunk-MIHU3TFJ.js +1156 -0
  207. package/dist/chunk-MIU5FMSV.js +1329 -0
  208. package/dist/chunk-MJQMHH7Z.js +1976 -0
  209. package/dist/chunk-MLR2HUUY.js +1255 -0
  210. package/dist/chunk-MSBZHMUV.js +997 -0
  211. package/dist/chunk-MV4DZQRB.js +1163 -0
  212. package/dist/chunk-MWBLZDYK.js +1854 -0
  213. package/dist/chunk-N2MEPDSA.js +675 -0
  214. package/dist/chunk-N36ATUZM.js +1863 -0
  215. package/dist/chunk-NEHLM4WN.js +1610 -0
  216. package/dist/chunk-NHOJZ7FZ.js +1138 -0
  217. package/dist/chunk-NLPQBHHH.js +1282 -0
  218. package/dist/chunk-NMY3FWV7.js +1236 -0
  219. package/dist/chunk-NN2WDNCO.js +1138 -0
  220. package/dist/chunk-NNC7LH2Y.js +1258 -0
  221. package/dist/chunk-NQRWXPJ5.js +1275 -0
  222. package/dist/chunk-NR3G3D6Q.js +1238 -0
  223. package/dist/chunk-NRUAFOL3.js +2121 -0
  224. package/dist/chunk-NSCG7F6H.js +1862 -0
  225. package/dist/chunk-NXHVG7ZI.js +1113 -0
  226. package/dist/chunk-NYKPTBXA.js +1258 -0
  227. package/dist/chunk-O4AE4MFX.js +920 -0
  228. package/dist/chunk-O7GTMG3C.js +1467 -0
  229. package/dist/chunk-OBUP5UIM.js +997 -0
  230. package/dist/chunk-ONI2DTTL.js +1156 -0
  231. package/dist/chunk-OQHLTSVD.js +1776 -0
  232. package/dist/chunk-OVV6SHTA.js +997 -0
  233. package/dist/chunk-PAPAMVNI.js +1156 -0
  234. package/dist/chunk-PH7OXFMJ.js +997 -0
  235. package/dist/chunk-PHFLPSZU.js +1608 -0
  236. package/dist/chunk-PTVSK5DV.js +1854 -0
  237. package/dist/chunk-PUHWX6PD.js +1156 -0
  238. package/dist/chunk-PYDU2HN2.js +1803 -0
  239. package/dist/chunk-Q2AMIXBY.js +1250 -0
  240. package/dist/chunk-Q2EARVB7.js +1414 -0
  241. package/dist/chunk-Q4HFSYSN.js +1024 -0
  242. package/dist/chunk-Q65PNALY.js +1024 -0
  243. package/dist/chunk-QBQNHCYH.js +1791 -0
  244. package/dist/chunk-QGI55HK3.js +1433 -0
  245. package/dist/chunk-QIAODEAT.js +1862 -0
  246. package/dist/chunk-QLRJ2X3B.js +1800 -0
  247. package/dist/chunk-QTLVBIBL.js +1147 -0
  248. package/dist/chunk-QUPLQ7O4.js +1862 -0
  249. package/dist/chunk-QZGUSWLN.js +1415 -0
  250. package/dist/chunk-R7N3T7YR.js +1130 -0
  251. package/dist/chunk-RFS3GC46.js +239 -0
  252. package/dist/chunk-RJA7HIQO.js +1823 -0
  253. package/dist/chunk-RJLU3F7G.js +1156 -0
  254. package/dist/chunk-RLBXW5AY.js +1156 -0
  255. package/dist/chunk-RMKHLIMU.js +1147 -0
  256. package/dist/chunk-RS7F3BLO.js +1255 -0
  257. package/dist/chunk-RYLDHQQT.js +997 -0
  258. package/dist/chunk-RYYTEAQ7.js +1147 -0
  259. package/dist/chunk-S2KNKYQJ.js +1815 -0
  260. package/dist/chunk-S5R5IZJH.js +2056 -0
  261. package/dist/chunk-S5S7OBDZ.js +1355 -0
  262. package/dist/chunk-S6YKNJQG.js +1868 -0
  263. package/dist/chunk-SBDVWSWM.js +1251 -0
  264. package/dist/chunk-SBXSREFV.js +1237 -0
  265. package/dist/chunk-SDZJV47M.js +1238 -0
  266. package/dist/chunk-SMBILO75.js +1024 -0
  267. package/dist/chunk-SUSLSP3P.js +1456 -0
  268. package/dist/chunk-T4GPZ2AG.js +1128 -0
  269. package/dist/chunk-TA7EDJXT.js +1460 -0
  270. package/dist/chunk-TAW2ISST.js +1024 -0
  271. package/dist/chunk-TB6KKWS2.js +1138 -0
  272. package/dist/chunk-TBZCEX5O.js +1355 -0
  273. package/dist/chunk-TDOPKFNW.js +756 -0
  274. package/dist/chunk-TGY3JSDQ.js +1460 -0
  275. package/dist/chunk-THOIGUSY.js +666 -0
  276. package/dist/chunk-TLSGQKLO.js +1836 -0
  277. package/dist/chunk-TQ2QEU3G.js +1460 -0
  278. package/dist/chunk-TRJODOZM.js +1232 -0
  279. package/dist/chunk-TSBMRYLQ.js +1717 -0
  280. package/dist/chunk-TUL3R6KB.js +1024 -0
  281. package/dist/chunk-TWCD6YPP.js +2129 -0
  282. package/dist/chunk-TWQDGVLI.js +1323 -0
  283. package/dist/chunk-TZQLWQTW.js +769 -0
  284. package/dist/chunk-U22K555L.js +1803 -0
  285. package/dist/chunk-U3VXLQTW.js +1707 -0
  286. package/dist/chunk-U5QM4SRB.js +2054 -0
  287. package/dist/chunk-UCZLOOAW.js +997 -0
  288. package/dist/chunk-UDA2OZLK.js +1242 -0
  289. package/dist/chunk-UDASJ4IC.js +1355 -0
  290. package/dist/chunk-UGU2KSOQ.js +1113 -0
  291. package/dist/chunk-UJHB3CLA.js +1130 -0
  292. package/dist/chunk-UNUTAECX.js +1238 -0
  293. package/dist/chunk-UUUJVWXA.js +1806 -0
  294. package/dist/chunk-UV4WM7Q5.js +2039 -0
  295. package/dist/chunk-UXPGPOQ3.js +2113 -0
  296. package/dist/chunk-UYVWNYFB.js +1434 -0
  297. package/dist/chunk-UYWI4MPU.js +1460 -0
  298. package/dist/chunk-V2FNBN3P.js +997 -0
  299. package/dist/chunk-V6CF5XXG.js +1862 -0
  300. package/dist/chunk-V6UUV2SZ.js +1156 -0
  301. package/dist/chunk-VARTDMWQ.js +1862 -0
  302. package/dist/chunk-VC7ZYKMP.js +1156 -0
  303. package/dist/chunk-VCJNX77B.js +2038 -0
  304. package/dist/chunk-VDE2I72J.js +650 -0
  305. package/dist/chunk-VEO7FKEL.js +1156 -0
  306. package/dist/chunk-VIJYIU7E.js +2124 -0
  307. package/dist/chunk-VJX4WETG.js +1136 -0
  308. package/dist/chunk-VO3QDFU2.js +1276 -0
  309. package/dist/chunk-VOX2Q2V2.js +1933 -0
  310. package/dist/chunk-W7J5XM2X.js +1862 -0
  311. package/dist/chunk-WONM6P4N.js +1862 -0
  312. package/dist/chunk-WW576PYD.js +1862 -0
  313. package/dist/chunk-XCZCCA2D.js +997 -0
  314. package/dist/chunk-XHQOG4X6.js +1871 -0
  315. package/dist/chunk-XIYLHBWA.js +1163 -0
  316. package/dist/chunk-XKZ6XWSE.js +1907 -0
  317. package/dist/chunk-XLHKOBSF.js +1815 -0
  318. package/dist/chunk-XMMFUBB5.js +1270 -0
  319. package/dist/chunk-XQLK777K.js +1442 -0
  320. package/dist/chunk-XRN47M65.js +997 -0
  321. package/dist/chunk-XVBKUEXA.js +1441 -0
  322. package/dist/chunk-XY4ISIAV.js +1639 -0
  323. package/dist/chunk-Y2SOII6F.js +1156 -0
  324. package/dist/chunk-Y5TJU6YZ.js +1163 -0
  325. package/dist/chunk-YBPCMSUU.js +1147 -0
  326. package/dist/chunk-YDAZ3YZT.js +1004 -0
  327. package/dist/chunk-YH2QPUWO.js +1621 -0
  328. package/dist/chunk-YJX4O5CY.js +1355 -0
  329. package/dist/chunk-YNJMS3VK.js +997 -0
  330. package/dist/chunk-YNRZMOC3.js +997 -0
  331. package/dist/chunk-YNUF5JNP.js +1163 -0
  332. package/dist/chunk-YO7TJ6SG.js +2135 -0
  333. package/dist/chunk-YTUUFYVS.js +1842 -0
  334. package/dist/chunk-YXCOG54V.js +997 -0
  335. package/dist/chunk-Z7V254BA.js +1432 -0
  336. package/dist/chunk-ZBHRR3RS.js +1256 -0
  337. package/dist/chunk-ZM47X5PT.js +1236 -0
  338. package/dist/chunk-ZO6JUCLC.js +917 -0
  339. package/dist/chunk-ZOF4ERNI.js +2039 -0
  340. package/dist/chunk-ZOTRZN3T.js +1238 -0
  341. package/dist/chunk-ZPBA4JGE.js +1234 -0
  342. package/dist/chunk-ZTKGRHNV.js +1138 -0
  343. package/dist/chunk-ZW2JM2OY.js +997 -0
  344. package/dist/chunk-ZXYHUC7C.js +1722 -0
  345. package/dist/cli.d.ts +1 -0
  346. package/dist/cli.js +8 -0
  347. package/dist/index.d.ts +47 -0
  348. package/dist/index.js +37 -0
  349. package/dist/run-interactive-ink-2CDKFV6C.js +783 -0
  350. package/dist/run-interactive-ink-2JULLCIS.js +461 -0
  351. package/dist/run-interactive-ink-2KIHEGXT.js +451 -0
  352. package/dist/run-interactive-ink-2LX2NZRL.js +737 -0
  353. package/dist/run-interactive-ink-2OH6AV3C.js +756 -0
  354. package/dist/run-interactive-ink-2PLJ5XST.js +423 -0
  355. package/dist/run-interactive-ink-2ZI75VMK.js +462 -0
  356. package/dist/run-interactive-ink-3BHGPZ4B.js +423 -0
  357. package/dist/run-interactive-ink-3IUV3456.js +777 -0
  358. package/dist/run-interactive-ink-3QZK5PWY.js +168 -0
  359. package/dist/run-interactive-ink-3VLL4FNN.js +423 -0
  360. package/dist/run-interactive-ink-47Y2KAFZ.js +668 -0
  361. package/dist/run-interactive-ink-4CBTS636.js +423 -0
  362. package/dist/run-interactive-ink-4CHDJRAK.js +423 -0
  363. package/dist/run-interactive-ink-4E6GM5ST.js +423 -0
  364. package/dist/run-interactive-ink-4EKHIHGU.js +462 -0
  365. package/dist/run-interactive-ink-4NO7O233.js +744 -0
  366. package/dist/run-interactive-ink-55IZU2EZ.js +741 -0
  367. package/dist/run-interactive-ink-5A7BER6J.js +423 -0
  368. package/dist/run-interactive-ink-5NEFKF3R.js +423 -0
  369. package/dist/run-interactive-ink-5TCBH3Z4.js +462 -0
  370. package/dist/run-interactive-ink-5WOLWMGH.js +747 -0
  371. package/dist/run-interactive-ink-66JFIG2P.js +462 -0
  372. package/dist/run-interactive-ink-6R77DKEV.js +744 -0
  373. package/dist/run-interactive-ink-7B6HI7XT.js +679 -0
  374. package/dist/run-interactive-ink-7ICZH4E3.js +423 -0
  375. package/dist/run-interactive-ink-7LWBJYQE.js +423 -0
  376. package/dist/run-interactive-ink-7Y7MVKPA.js +438 -0
  377. package/dist/run-interactive-ink-A264MR35.js +423 -0
  378. package/dist/run-interactive-ink-A3NCKFFM.js +423 -0
  379. package/dist/run-interactive-ink-A4FBMTZ5.js +563 -0
  380. package/dist/run-interactive-ink-A7YPXRXR.js +462 -0
  381. package/dist/run-interactive-ink-AAER2EXR.js +451 -0
  382. package/dist/run-interactive-ink-ADVQJ2GF.js +423 -0
  383. package/dist/run-interactive-ink-ALV34HZF.js +451 -0
  384. package/dist/run-interactive-ink-ARDK3CO6.js +462 -0
  385. package/dist/run-interactive-ink-AVMVDBQK.js +462 -0
  386. package/dist/run-interactive-ink-AWV7ZOTC.js +423 -0
  387. package/dist/run-interactive-ink-AWZLJJLH.js +423 -0
  388. package/dist/run-interactive-ink-B25V52JO.js +462 -0
  389. package/dist/run-interactive-ink-BC6RGDCH.js +451 -0
  390. package/dist/run-interactive-ink-BMWLPUEU.js +451 -0
  391. package/dist/run-interactive-ink-BRT2MMN6.js +423 -0
  392. package/dist/run-interactive-ink-CB42OWV4.js +572 -0
  393. package/dist/run-interactive-ink-COZSBQND.js +777 -0
  394. package/dist/run-interactive-ink-CQFV44HM.js +451 -0
  395. package/dist/run-interactive-ink-CQMG45KQ.js +462 -0
  396. package/dist/run-interactive-ink-CQP3B7JM.js +669 -0
  397. package/dist/run-interactive-ink-CUCLNJCF.js +451 -0
  398. package/dist/run-interactive-ink-DEEZYQK5.js +423 -0
  399. package/dist/run-interactive-ink-DF5P6WZX.js +423 -0
  400. package/dist/run-interactive-ink-DFITKRY4.js +423 -0
  401. package/dist/run-interactive-ink-DG6TTEQQ.js +462 -0
  402. package/dist/run-interactive-ink-DH7ECECB.js +438 -0
  403. package/dist/run-interactive-ink-DMTUJHP6.js +423 -0
  404. package/dist/run-interactive-ink-DNSBSWLT.js +451 -0
  405. package/dist/run-interactive-ink-DV3TZEM3.js +742 -0
  406. package/dist/run-interactive-ink-E4AYCUDK.js +451 -0
  407. package/dist/run-interactive-ink-E4GPBTSL.js +462 -0
  408. package/dist/run-interactive-ink-ECVTPOIE.js +462 -0
  409. package/dist/run-interactive-ink-ELPCCGT3.js +423 -0
  410. package/dist/run-interactive-ink-ELWVRJZS.js +451 -0
  411. package/dist/run-interactive-ink-ENBMPAV7.js +462 -0
  412. package/dist/run-interactive-ink-EOVWUC3C.js +451 -0
  413. package/dist/run-interactive-ink-EVHWEXM4.js +451 -0
  414. package/dist/run-interactive-ink-F7SBCWE3.js +423 -0
  415. package/dist/run-interactive-ink-FCHXZ3JW.js +423 -0
  416. package/dist/run-interactive-ink-G27WWB5V.js +423 -0
  417. package/dist/run-interactive-ink-GDFTNYRC.js +462 -0
  418. package/dist/run-interactive-ink-GHGZAYSM.js +533 -0
  419. package/dist/run-interactive-ink-GK4IVHVT.js +684 -0
  420. package/dist/run-interactive-ink-GNCZNR6W.js +423 -0
  421. package/dist/run-interactive-ink-GQ53M5SW.js +605 -0
  422. package/dist/run-interactive-ink-GT7R7X2P.js +762 -0
  423. package/dist/run-interactive-ink-GWZTEIEZ.js +462 -0
  424. package/dist/run-interactive-ink-HA45VNUD.js +703 -0
  425. package/dist/run-interactive-ink-HB44RGFJ.js +423 -0
  426. package/dist/run-interactive-ink-HCVGKG23.js +462 -0
  427. package/dist/run-interactive-ink-HYKJ4PZ3.js +462 -0
  428. package/dist/run-interactive-ink-IGEBXARA.js +423 -0
  429. package/dist/run-interactive-ink-IGU7UVL5.js +462 -0
  430. package/dist/run-interactive-ink-JNVKOJRV.js +462 -0
  431. package/dist/run-interactive-ink-JYON5JQQ.js +461 -0
  432. package/dist/run-interactive-ink-K47CRELE.js +423 -0
  433. package/dist/run-interactive-ink-KCHMEHVH.js +547 -0
  434. package/dist/run-interactive-ink-KEB6ENSZ.js +423 -0
  435. package/dist/run-interactive-ink-KEWSKPTE.js +451 -0
  436. package/dist/run-interactive-ink-KJUHMADH.js +423 -0
  437. package/dist/run-interactive-ink-KW5NPJ32.js +423 -0
  438. package/dist/run-interactive-ink-L3EDWKF6.js +687 -0
  439. package/dist/run-interactive-ink-L4BCC6WG.js +462 -0
  440. package/dist/run-interactive-ink-LQPEZ6PR.js +520 -0
  441. package/dist/run-interactive-ink-LQXS5GMO.js +253 -0
  442. package/dist/run-interactive-ink-M4SOBC5E.js +817 -0
  443. package/dist/run-interactive-ink-MJGAQA2R.js +423 -0
  444. package/dist/run-interactive-ink-MPJB6PCJ.js +451 -0
  445. package/dist/run-interactive-ink-MXWZBG3F.js +461 -0
  446. package/dist/run-interactive-ink-N4XIVCWV.js +562 -0
  447. package/dist/run-interactive-ink-ND3PWHDU.js +462 -0
  448. package/dist/run-interactive-ink-NJTAWS3L.js +462 -0
  449. package/dist/run-interactive-ink-OBWWSIZ5.js +423 -0
  450. package/dist/run-interactive-ink-OD7YJBYI.js +423 -0
  451. package/dist/run-interactive-ink-OE23JGIN.js +451 -0
  452. package/dist/run-interactive-ink-OJ4EUMR5.js +462 -0
  453. package/dist/run-interactive-ink-OOC74RCY.js +423 -0
  454. package/dist/run-interactive-ink-P2PHTOX6.js +462 -0
  455. package/dist/run-interactive-ink-PE3XWCVU.js +423 -0
  456. package/dist/run-interactive-ink-PJBWWQF3.js +423 -0
  457. package/dist/run-interactive-ink-PLLZW6BV.js +678 -0
  458. package/dist/run-interactive-ink-PTXNQZ57.js +451 -0
  459. package/dist/run-interactive-ink-PWN5Q6T6.js +423 -0
  460. package/dist/run-interactive-ink-PZK4RD77.js +423 -0
  461. package/dist/run-interactive-ink-QBSXVTCG.js +462 -0
  462. package/dist/run-interactive-ink-QHOQB55Q.js +727 -0
  463. package/dist/run-interactive-ink-QIAC6ZMT.js +451 -0
  464. package/dist/run-interactive-ink-QLLGPIUL.js +462 -0
  465. package/dist/run-interactive-ink-R5W3ZEMY.js +818 -0
  466. package/dist/run-interactive-ink-RHDW3EHS.js +462 -0
  467. package/dist/run-interactive-ink-RLRKPNTS.js +668 -0
  468. package/dist/run-interactive-ink-RORQKBWV.js +425 -0
  469. package/dist/run-interactive-ink-RS6OZ66I.js +423 -0
  470. package/dist/run-interactive-ink-RVGRYBNQ.js +684 -0
  471. package/dist/run-interactive-ink-S35BKUZB.js +423 -0
  472. package/dist/run-interactive-ink-SS6RAQDE.js +423 -0
  473. package/dist/run-interactive-ink-SVP37E33.js +451 -0
  474. package/dist/run-interactive-ink-T53KH7FU.js +423 -0
  475. package/dist/run-interactive-ink-TCQUCJVS.js +423 -0
  476. package/dist/run-interactive-ink-TLUBKTTN.js +423 -0
  477. package/dist/run-interactive-ink-U2BAAHUU.js +438 -0
  478. package/dist/run-interactive-ink-U2KXGJ5S.js +451 -0
  479. package/dist/run-interactive-ink-U73PEMAO.js +423 -0
  480. package/dist/run-interactive-ink-UFTOTXIX.js +669 -0
  481. package/dist/run-interactive-ink-UJNQ54ZU.js +451 -0
  482. package/dist/run-interactive-ink-V63D5IV5.js +423 -0
  483. package/dist/run-interactive-ink-VLBITT4H.js +451 -0
  484. package/dist/run-interactive-ink-VSAX3XFR.js +462 -0
  485. package/dist/run-interactive-ink-WBJOY622.js +729 -0
  486. package/dist/run-interactive-ink-WERR64KP.js +451 -0
  487. package/dist/run-interactive-ink-WHTQ5OV6.js +732 -0
  488. package/dist/run-interactive-ink-WJBK4XIO.js +423 -0
  489. package/dist/run-interactive-ink-WQLCJ34D.js +462 -0
  490. package/dist/run-interactive-ink-WRKQJIAG.js +733 -0
  491. package/dist/run-interactive-ink-XG3P25DM.js +423 -0
  492. package/dist/run-interactive-ink-XPVJ22HP.js +462 -0
  493. package/dist/run-interactive-ink-XS5I2CGI.js +423 -0
  494. package/dist/run-interactive-ink-XVK7DXPB.js +785 -0
  495. package/dist/run-interactive-ink-YB3USTSB.js +706 -0
  496. package/dist/run-interactive-ink-YCBRQCG2.js +423 -0
  497. package/dist/run-interactive-ink-YOPSMTYJ.js +423 -0
  498. package/dist/run-interactive-ink-YYPCL65X.js +665 -0
  499. package/dist/run-interactive-ink-YYZT5L4Z.js +462 -0
  500. package/dist/run-interactive-ink-ZBPYRTJK.js +462 -0
  501. package/dist/run-interactive-ink-ZCCKFR2A.js +451 -0
  502. package/dist/run-interactive-ink-ZKYQ4CJW.js +423 -0
  503. package/dist/run-interactive-ink-ZMO2352Q.js +685 -0
  504. package/dist/run-interactive-ink-ZTDQ773P.js +423 -0
  505. package/dist/run-interactive-ink-ZWH74XDY.js +674 -0
  506. package/package.json +50 -0
  507. package/src/cli.ts +4 -0
  508. package/src/index.ts +1800 -0
  509. package/src/init-feature-context.ts +153 -0
  510. package/src/init-onboarding.ts +529 -0
  511. package/src/interactive-ink.tsx +5 -0
  512. package/src/run-interactive-ink.ts +618 -0
  513. package/src/web-ui.ts +1975 -0
  514. package/test/cli.test.ts +587 -0
  515. package/test/init-onboarding.contract.test.ts +48 -0
  516. package/tsconfig.json +9 -0
package/src/web-ui.ts ADDED
@@ -0,0 +1,1975 @@
1
+ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, resolve } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import type { IncomingMessage, ServerResponse } from "node:http";
6
+ import type { Message } from "@poncho-ai/sdk";
7
+
8
+ export interface WebUiConversation {
9
+ conversationId: string;
10
+ title: string;
11
+ messages: Message[];
12
+ runtimeRunId?: string;
13
+ ownerId: string;
14
+ tenantId: string | null;
15
+ createdAt: number;
16
+ updatedAt: number;
17
+ }
18
+
19
+ type ConversationStoreFile = {
20
+ conversations: WebUiConversation[];
21
+ };
22
+
23
+ const DEFAULT_OWNER = "local-owner";
24
+
25
+ const getStateDirectory = (): string => {
26
+ const cwd = process.cwd();
27
+ const home = homedir();
28
+ // On serverless platforms (Vercel, AWS Lambda), only /tmp is writable
29
+ const isServerless =
30
+ process.env.VERCEL === "1" ||
31
+ process.env.VERCEL_ENV !== undefined ||
32
+ process.env.VERCEL_URL !== undefined ||
33
+ process.env.AWS_LAMBDA_FUNCTION_NAME !== undefined ||
34
+ process.env.AWS_EXECUTION_ENV?.includes("AWS_Lambda") === true ||
35
+ process.env.LAMBDA_TASK_ROOT !== undefined ||
36
+ process.env.NOW_REGION !== undefined ||
37
+ cwd.startsWith("/var/task") ||
38
+ home.startsWith("/var/task") ||
39
+ process.env.SERVERLESS === "1";
40
+ if (isServerless) {
41
+ return "/tmp/.poncho/state";
42
+ }
43
+ return resolve(homedir(), ".poncho", "state");
44
+ };
45
+
46
+ export class FileConversationStore {
47
+ private readonly filePath: string;
48
+ private readonly conversations = new Map<string, WebUiConversation>();
49
+ private loaded = false;
50
+ private writing = Promise.resolve();
51
+
52
+ constructor(workingDir: string) {
53
+ const projectName = basename(workingDir).replace(/[^a-zA-Z0-9_-]+/g, "-") || "project";
54
+ const projectHash = createHash("sha256")
55
+ .update(workingDir)
56
+ .digest("hex")
57
+ .slice(0, 12);
58
+ this.filePath = resolve(
59
+ getStateDirectory(),
60
+ `${projectName}-${projectHash}-web-ui-state.json`,
61
+ );
62
+ }
63
+
64
+ private async ensureLoaded(): Promise<void> {
65
+ if (this.loaded) {
66
+ return;
67
+ }
68
+ this.loaded = true;
69
+ try {
70
+ const content = await readFile(this.filePath, "utf8");
71
+ const parsed = JSON.parse(content) as ConversationStoreFile;
72
+ for (const conversation of parsed.conversations ?? []) {
73
+ this.conversations.set(conversation.conversationId, conversation);
74
+ }
75
+ } catch {
76
+ // File does not exist yet or contains invalid JSON.
77
+ }
78
+ }
79
+
80
+ private async persist(): Promise<void> {
81
+ const payload: ConversationStoreFile = {
82
+ conversations: Array.from(this.conversations.values()),
83
+ };
84
+ this.writing = this.writing.then(async () => {
85
+ await mkdir(dirname(this.filePath), { recursive: true });
86
+ await writeFile(this.filePath, JSON.stringify(payload, null, 2), "utf8");
87
+ });
88
+ await this.writing;
89
+ }
90
+
91
+ async list(ownerId = DEFAULT_OWNER): Promise<WebUiConversation[]> {
92
+ await this.ensureLoaded();
93
+ return Array.from(this.conversations.values())
94
+ .filter((conversation) => conversation.ownerId === ownerId)
95
+ .sort((a, b) => b.updatedAt - a.updatedAt);
96
+ }
97
+
98
+ async get(conversationId: string): Promise<WebUiConversation | undefined> {
99
+ await this.ensureLoaded();
100
+ return this.conversations.get(conversationId);
101
+ }
102
+
103
+ async create(ownerId = DEFAULT_OWNER, title?: string): Promise<WebUiConversation> {
104
+ await this.ensureLoaded();
105
+ const now = Date.now();
106
+ const conversation: WebUiConversation = {
107
+ conversationId: randomUUID(),
108
+ title: title && title.trim().length > 0 ? title.trim() : "New conversation",
109
+ messages: [],
110
+ ownerId,
111
+ tenantId: null,
112
+ createdAt: now,
113
+ updatedAt: now,
114
+ };
115
+ this.conversations.set(conversation.conversationId, conversation);
116
+ await this.persist();
117
+ return conversation;
118
+ }
119
+
120
+ async update(conversation: WebUiConversation): Promise<void> {
121
+ await this.ensureLoaded();
122
+ this.conversations.set(conversation.conversationId, {
123
+ ...conversation,
124
+ updatedAt: Date.now(),
125
+ });
126
+ await this.persist();
127
+ }
128
+
129
+ async rename(conversationId: string, title: string): Promise<WebUiConversation | undefined> {
130
+ await this.ensureLoaded();
131
+ const existing = this.conversations.get(conversationId);
132
+ if (!existing) {
133
+ return undefined;
134
+ }
135
+ const updated = {
136
+ ...existing,
137
+ title: title.trim().length > 0 ? title.trim() : existing.title,
138
+ updatedAt: Date.now(),
139
+ };
140
+ this.conversations.set(conversationId, updated);
141
+ await this.persist();
142
+ return updated;
143
+ }
144
+
145
+ async delete(conversationId: string): Promise<boolean> {
146
+ await this.ensureLoaded();
147
+ const removed = this.conversations.delete(conversationId);
148
+ if (removed) {
149
+ await this.persist();
150
+ }
151
+ return removed;
152
+ }
153
+ }
154
+
155
+ type SessionRecord = {
156
+ sessionId: string;
157
+ ownerId: string;
158
+ csrfToken: string;
159
+ createdAt: number;
160
+ expiresAt: number;
161
+ lastSeenAt: number;
162
+ };
163
+
164
+ export class SessionStore {
165
+ private readonly sessions = new Map<string, SessionRecord>();
166
+ private readonly ttlMs: number;
167
+
168
+ constructor(ttlMs = 1000 * 60 * 60 * 8) {
169
+ this.ttlMs = ttlMs;
170
+ }
171
+
172
+ create(ownerId = DEFAULT_OWNER): SessionRecord {
173
+ const now = Date.now();
174
+ const session: SessionRecord = {
175
+ sessionId: randomUUID(),
176
+ ownerId,
177
+ csrfToken: randomUUID(),
178
+ createdAt: now,
179
+ expiresAt: now + this.ttlMs,
180
+ lastSeenAt: now,
181
+ };
182
+ this.sessions.set(session.sessionId, session);
183
+ return session;
184
+ }
185
+
186
+ get(sessionId: string): SessionRecord | undefined {
187
+ const session = this.sessions.get(sessionId);
188
+ if (!session) {
189
+ return undefined;
190
+ }
191
+ if (Date.now() > session.expiresAt) {
192
+ this.sessions.delete(sessionId);
193
+ return undefined;
194
+ }
195
+ session.lastSeenAt = Date.now();
196
+ return session;
197
+ }
198
+
199
+ delete(sessionId: string): void {
200
+ this.sessions.delete(sessionId);
201
+ }
202
+ }
203
+
204
+ type LoginAttemptState = {
205
+ count: number;
206
+ firstFailureAt: number;
207
+ lockedUntil?: number;
208
+ };
209
+
210
+ export class LoginRateLimiter {
211
+ private readonly attempts = new Map<string, LoginAttemptState>();
212
+
213
+ constructor(
214
+ private readonly maxAttempts = 5,
215
+ private readonly windowMs = 1000 * 60 * 5,
216
+ private readonly lockoutMs = 1000 * 60 * 10,
217
+ ) {}
218
+
219
+ canAttempt(key: string): { allowed: boolean; retryAfterSeconds?: number } {
220
+ const current = this.attempts.get(key);
221
+ if (!current) {
222
+ return { allowed: true };
223
+ }
224
+ if (current.lockedUntil && Date.now() < current.lockedUntil) {
225
+ return {
226
+ allowed: false,
227
+ retryAfterSeconds: Math.ceil((current.lockedUntil - Date.now()) / 1000),
228
+ };
229
+ }
230
+ return { allowed: true };
231
+ }
232
+
233
+ registerFailure(key: string): { locked: boolean; retryAfterSeconds?: number } {
234
+ const now = Date.now();
235
+ const current = this.attempts.get(key);
236
+ if (!current || now - current.firstFailureAt > this.windowMs) {
237
+ this.attempts.set(key, { count: 1, firstFailureAt: now });
238
+ return { locked: false };
239
+ }
240
+ const count = current.count + 1;
241
+ const next: LoginAttemptState = {
242
+ ...current,
243
+ count,
244
+ };
245
+ if (count >= this.maxAttempts) {
246
+ next.lockedUntil = now + this.lockoutMs;
247
+ this.attempts.set(key, next);
248
+ return { locked: true, retryAfterSeconds: Math.ceil(this.lockoutMs / 1000) };
249
+ }
250
+ this.attempts.set(key, next);
251
+ return { locked: false };
252
+ }
253
+
254
+ registerSuccess(key: string): void {
255
+ this.attempts.delete(key);
256
+ }
257
+ }
258
+
259
+ export const parseCookies = (request: IncomingMessage): Record<string, string> => {
260
+ const cookieHeader = request.headers.cookie ?? "";
261
+ const pairs = cookieHeader
262
+ .split(";")
263
+ .map((part) => part.trim())
264
+ .filter(Boolean);
265
+ const cookies: Record<string, string> = {};
266
+ for (const pair of pairs) {
267
+ const index = pair.indexOf("=");
268
+ if (index <= 0) {
269
+ continue;
270
+ }
271
+ const key = pair.slice(0, index);
272
+ const value = pair.slice(index + 1);
273
+ try {
274
+ cookies[key] = decodeURIComponent(value);
275
+ } catch {
276
+ // Ignore malformed cookie encoding instead of throwing.
277
+ cookies[key] = value;
278
+ }
279
+ }
280
+ return cookies;
281
+ };
282
+
283
+ export const setCookie = (
284
+ response: ServerResponse,
285
+ name: string,
286
+ value: string,
287
+ options: {
288
+ httpOnly?: boolean;
289
+ secure?: boolean;
290
+ sameSite?: "Lax" | "Strict" | "None";
291
+ path?: string;
292
+ maxAge?: number;
293
+ },
294
+ ): void => {
295
+ const segments = [`${name}=${encodeURIComponent(value)}`];
296
+ segments.push(`Path=${options.path ?? "/"}`);
297
+ if (typeof options.maxAge === "number") {
298
+ segments.push(`Max-Age=${Math.max(0, Math.floor(options.maxAge))}`);
299
+ }
300
+ if (options.httpOnly) {
301
+ segments.push("HttpOnly");
302
+ }
303
+ if (options.secure) {
304
+ segments.push("Secure");
305
+ }
306
+ if (options.sameSite) {
307
+ segments.push(`SameSite=${options.sameSite}`);
308
+ }
309
+ const previous = response.getHeader("Set-Cookie");
310
+ const serialized = segments.join("; ");
311
+ if (!previous) {
312
+ response.setHeader("Set-Cookie", serialized);
313
+ return;
314
+ }
315
+ if (Array.isArray(previous)) {
316
+ response.setHeader("Set-Cookie", [...previous, serialized]);
317
+ return;
318
+ }
319
+ response.setHeader("Set-Cookie", [String(previous), serialized]);
320
+ };
321
+
322
+ export const verifyPassphrase = (provided: string, expected: string): boolean => {
323
+ const providedBuffer = Buffer.from(provided);
324
+ const expectedBuffer = Buffer.from(expected);
325
+ if (providedBuffer.length !== expectedBuffer.length) {
326
+ const zero = Buffer.alloc(expectedBuffer.length);
327
+ return timingSafeEqual(expectedBuffer, zero) && false;
328
+ }
329
+ return timingSafeEqual(providedBuffer, expectedBuffer);
330
+ };
331
+
332
+ export const getRequestIp = (request: IncomingMessage): string => {
333
+ // Trust direct socket peer by default to avoid spoofable forwarded headers.
334
+ // Reverse-proxy deployments can map trusted client IPs before this layer.
335
+ return request.socket.remoteAddress ?? "unknown";
336
+ };
337
+
338
+ export const inferConversationTitle = (text: string): string => {
339
+ const normalized = text.trim().replace(/\s+/g, " ");
340
+ if (!normalized) {
341
+ return "New conversation";
342
+ }
343
+ return normalized.length <= 48 ? normalized : `${normalized.slice(0, 48)}...`;
344
+ };
345
+
346
+ // ---------------------------------------------------------------------------
347
+ // PWA assets
348
+ // ---------------------------------------------------------------------------
349
+
350
+ export const renderManifest = (options?: { agentName?: string }): string => {
351
+ const name = options?.agentName ?? "Agent";
352
+ return JSON.stringify({
353
+ name,
354
+ short_name: name,
355
+ description: `${name} — AI agent powered by Poncho`,
356
+ start_url: "/",
357
+ display: "standalone",
358
+ background_color: "#000000",
359
+ theme_color: "#000000",
360
+ icons: [
361
+ { src: "/icon.svg", sizes: "any", type: "image/svg+xml" },
362
+ { src: "/icon-192.png", sizes: "192x192", type: "image/png" },
363
+ { src: "/icon-512.png", sizes: "512x512", type: "image/png" },
364
+ ],
365
+ });
366
+ };
367
+
368
+ export const renderIconSvg = (options?: { agentName?: string }): string => {
369
+ const letter = (options?.agentName ?? "A").charAt(0).toUpperCase();
370
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
371
+ <rect width="512" height="512" rx="96" fill="#000"/>
372
+ <text x="256" y="256" dy=".35em" text-anchor="middle"
373
+ font-family="-apple-system,BlinkMacSystemFont,sans-serif"
374
+ font-size="280" font-weight="700" fill="#fff">${letter}</text>
375
+ </svg>`;
376
+ };
377
+
378
+ export const renderServiceWorker = (): string => `
379
+ const CACHE_NAME = "poncho-shell-v1";
380
+ const SHELL_URLS = ["/"];
381
+
382
+ self.addEventListener("install", (event) => {
383
+ event.waitUntil(
384
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_URLS))
385
+ );
386
+ self.skipWaiting();
387
+ });
388
+
389
+ self.addEventListener("activate", (event) => {
390
+ event.waitUntil(
391
+ caches.keys().then((keys) =>
392
+ Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
393
+ )
394
+ );
395
+ self.clients.claim();
396
+ });
397
+
398
+ self.addEventListener("fetch", (event) => {
399
+ const url = new URL(event.request.url);
400
+ // Only cache GET requests for the app shell; let API calls pass through
401
+ if (event.request.method !== "GET" || url.pathname.startsWith("/api/")) {
402
+ return;
403
+ }
404
+ event.respondWith(
405
+ fetch(event.request)
406
+ .then((response) => {
407
+ const clone = response.clone();
408
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
409
+ return response;
410
+ })
411
+ .catch(() => caches.match(event.request))
412
+ );
413
+ });
414
+ `;
415
+
416
+ export const renderWebUiHtml = (options?: { agentName?: string }): string => {
417
+ const agentInitial = (options?.agentName ?? "A").charAt(0).toUpperCase();
418
+ const agentName = options?.agentName ?? "Agent";
419
+ return `<!doctype html>
420
+ <html lang="en">
421
+ <head>
422
+ <meta charset="utf-8">
423
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
424
+ <meta name="theme-color" content="#000000">
425
+ <meta name="apple-mobile-web-app-capable" content="yes">
426
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
427
+ <meta name="apple-mobile-web-app-title" content="${agentName}">
428
+ <link rel="manifest" href="/manifest.json">
429
+ <link rel="icon" href="/icon.svg" type="image/svg+xml">
430
+ <link rel="apple-touch-icon" href="/icon-192.png">
431
+ <title>${agentName}</title>
432
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inconsolata:400,700">
433
+ <style>
434
+ * { box-sizing: border-box; margin: 0; padding: 0; }
435
+ html, body { height: 100vh; overflow: hidden; overscroll-behavior: none; touch-action: pan-y; }
436
+ body {
437
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", sans-serif;
438
+ background: #000;
439
+ color: #ededed;
440
+ font-size: 14px;
441
+ line-height: 1.5;
442
+ -webkit-font-smoothing: antialiased;
443
+ -moz-osx-font-smoothing: grayscale;
444
+ }
445
+ button, input, textarea { font: inherit; color: inherit; }
446
+ .hidden { display: none !important; }
447
+ a { color: #ededed; }
448
+
449
+ /* Auth */
450
+ .auth {
451
+ min-height: 100vh;
452
+ display: grid;
453
+ place-items: center;
454
+ padding: 20px;
455
+ background: #000;
456
+ }
457
+ .auth-card {
458
+ width: min(380px, 90vw);
459
+ background: #0a0a0a;
460
+ border: 1px solid rgba(255,255,255,0.08);
461
+ border-radius: 12px;
462
+ padding: 32px;
463
+ display: grid;
464
+ gap: 20px;
465
+ }
466
+ .auth-brand {
467
+ display: flex;
468
+ align-items: center;
469
+ gap: 8px;
470
+ }
471
+ .auth-brand svg { width: 20px; height: 20px; }
472
+ .auth-title {
473
+ font-size: 16px;
474
+ font-weight: 500;
475
+ letter-spacing: -0.01em;
476
+ }
477
+ .auth-text { color: #666; font-size: 13px; line-height: 1.5; }
478
+ .auth-input {
479
+ width: 100%;
480
+ background: #000;
481
+ border: 1px solid rgba(255,255,255,0.12);
482
+ border-radius: 6px;
483
+ color: #ededed;
484
+ padding: 10px 12px;
485
+ font-size: 14px;
486
+ outline: none;
487
+ transition: border-color 0.15s;
488
+ }
489
+ .auth-input:focus { border-color: rgba(255,255,255,0.3); }
490
+ .auth-input::placeholder { color: #555; }
491
+ .auth-submit {
492
+ background: #ededed;
493
+ color: #000;
494
+ border: 0;
495
+ border-radius: 6px;
496
+ padding: 10px 16px;
497
+ font-size: 14px;
498
+ font-weight: 500;
499
+ cursor: pointer;
500
+ transition: background 0.15s;
501
+ }
502
+ .auth-submit:hover { background: #fff; }
503
+ .error { color: #ff4444; font-size: 13px; min-height: 16px; }
504
+ .message-error {
505
+ background: rgba(255,68,68,0.08);
506
+ border: 1px solid rgba(255,68,68,0.25);
507
+ border-radius: 10px;
508
+ color: #ff6b6b;
509
+ padding: 12px 16px;
510
+ font-size: 13px;
511
+ line-height: 1.5;
512
+ max-width: 600px;
513
+ }
514
+ .message-error strong { color: #ff4444; }
515
+
516
+ /* Layout - use fixed positioning with explicit dimensions */
517
+ .shell {
518
+ position: fixed;
519
+ top: 0;
520
+ left: 0;
521
+ width: 100vw;
522
+ height: 100vh;
523
+ height: 100dvh; /* Dynamic viewport height for normal browsers */
524
+ display: flex;
525
+ overflow: hidden;
526
+ }
527
+ /* PWA standalone mode: use 100vh which works correctly */
528
+ @media (display-mode: standalone) {
529
+ .shell {
530
+ height: 100vh;
531
+ }
532
+ }
533
+
534
+ /* Edge swipe blocker - invisible touch target to intercept right edge gestures */
535
+ .edge-blocker-right {
536
+ position: fixed;
537
+ top: 0;
538
+ bottom: 0;
539
+ right: 0;
540
+ width: 20px;
541
+ z-index: 9999;
542
+ touch-action: none;
543
+ }
544
+ .sidebar {
545
+ width: 260px;
546
+ background: #000;
547
+ border-right: 1px solid rgba(255,255,255,0.06);
548
+ display: flex;
549
+ flex-direction: column;
550
+ padding: 12px 8px;
551
+ }
552
+ .new-chat-btn {
553
+ background: transparent;
554
+ border: 0;
555
+ color: #888;
556
+ border-radius: 12px;
557
+ height: 36px;
558
+ padding: 0 10px;
559
+ display: flex;
560
+ align-items: center;
561
+ gap: 8px;
562
+ font-size: 13px;
563
+ cursor: pointer;
564
+ transition: background 0.15s, color 0.15s;
565
+ }
566
+ .new-chat-btn:hover { color: #ededed; }
567
+ .new-chat-btn svg { width: 16px; height: 16px; }
568
+ .conversation-list {
569
+ flex: 1;
570
+ overflow-y: auto;
571
+ margin-top: 12px;
572
+ display: flex;
573
+ flex-direction: column;
574
+ gap: 2px;
575
+ }
576
+ .conversation-item {
577
+ padding: 7px 28px 7px 10px;
578
+ border-radius: 12px;
579
+ cursor: pointer;
580
+ font-size: 13px;
581
+ color: #555;
582
+ white-space: nowrap;
583
+ overflow: hidden;
584
+ text-overflow: ellipsis;
585
+ position: relative;
586
+ transition: color 0.15s;
587
+ }
588
+ .conversation-item:hover { color: #999; }
589
+ .conversation-item.active {
590
+ color: #ededed;
591
+ }
592
+ .conversation-item .delete-btn {
593
+ position: absolute;
594
+ right: 0;
595
+ top: 0;
596
+ bottom: 0;
597
+ opacity: 0;
598
+ background: #000;
599
+ border: 0;
600
+ color: #444;
601
+ padding: 0 8px;
602
+ border-radius: 0 4px 4px 0;
603
+ cursor: pointer;
604
+ font-size: 16px;
605
+ line-height: 1;
606
+ display: grid;
607
+ place-items: center;
608
+ transition: opacity 0.15s, color 0.15s;
609
+ }
610
+ .conversation-item:hover .delete-btn { opacity: 1; }
611
+ .conversation-item.active .delete-btn { background: rgba(0,0,0,1); }
612
+ .conversation-item .delete-btn::before {
613
+ content: "";
614
+ position: absolute;
615
+ right: 100%;
616
+ top: 0;
617
+ bottom: 0;
618
+ width: 24px;
619
+ background: linear-gradient(to right, transparent, #000);
620
+ pointer-events: none;
621
+ }
622
+ .conversation-item.active .delete-btn::before {
623
+ background: linear-gradient(to right, transparent, rgba(0,0,0,1));
624
+ }
625
+ .conversation-item .delete-btn:hover { color: #888; }
626
+ .conversation-item .delete-btn.confirming {
627
+ opacity: 1;
628
+ width: auto;
629
+ padding: 0 8px;
630
+ font-size: 11px;
631
+ color: #ff4444;
632
+ border-radius: 3px;
633
+ }
634
+ .conversation-item .delete-btn.confirming:hover {
635
+ color: #ff6666;
636
+ }
637
+ .sidebar-footer {
638
+ margin-top: auto;
639
+ padding-top: 8px;
640
+ }
641
+ .logout-btn {
642
+ background: transparent;
643
+ border: 0;
644
+ color: #555;
645
+ width: 100%;
646
+ padding: 8px 10px;
647
+ text-align: left;
648
+ border-radius: 6px;
649
+ cursor: pointer;
650
+ font-size: 13px;
651
+ transition: color 0.15s, background 0.15s;
652
+ }
653
+ .logout-btn:hover { color: #888; }
654
+
655
+ /* Main */
656
+ .main { flex: 1; display: flex; flex-direction: column; min-width: 0; max-width: 100%; background: #000; overflow: hidden; }
657
+ .topbar {
658
+ height: calc(52px + env(safe-area-inset-top, 0px));
659
+ padding-top: env(safe-area-inset-top, 0px);
660
+ display: flex;
661
+ align-items: center;
662
+ justify-content: center;
663
+ font-size: 13px;
664
+ font-weight: 500;
665
+ color: #888;
666
+ border-bottom: 1px solid rgba(255,255,255,0.06);
667
+ position: relative;
668
+ flex-shrink: 0;
669
+ }
670
+ .topbar-title {
671
+ max-width: calc(100% - 100px);
672
+ overflow: hidden;
673
+ text-overflow: ellipsis;
674
+ white-space: nowrap;
675
+ letter-spacing: -0.01em;
676
+ padding: 0 50px;
677
+ }
678
+ .sidebar-toggle {
679
+ display: none;
680
+ position: absolute;
681
+ left: 12px;
682
+ bottom: 4px; /* Position from bottom of topbar content area */
683
+ background: transparent;
684
+ border: 0;
685
+ color: #666;
686
+ width: 44px;
687
+ height: 44px;
688
+ border-radius: 6px;
689
+ cursor: pointer;
690
+ transition: color 0.15s, background 0.15s;
691
+ font-size: 18px;
692
+ z-index: 10;
693
+ -webkit-tap-highlight-color: transparent;
694
+ }
695
+ .sidebar-toggle:hover { color: #ededed; }
696
+ .topbar-new-chat {
697
+ display: none;
698
+ position: absolute;
699
+ right: 12px;
700
+ bottom: 4px;
701
+ background: transparent;
702
+ border: 0;
703
+ color: #666;
704
+ width: 44px;
705
+ height: 44px;
706
+ border-radius: 6px;
707
+ cursor: pointer;
708
+ transition: color 0.15s, background 0.15s;
709
+ z-index: 10;
710
+ -webkit-tap-highlight-color: transparent;
711
+ }
712
+ .topbar-new-chat:hover { color: #ededed; }
713
+ .topbar-new-chat svg { width: 16px; height: 16px; }
714
+
715
+ /* Messages */
716
+ .messages { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 24px 24px; }
717
+ .messages-column { max-width: 680px; margin: 0 auto; }
718
+ .message-row { margin-bottom: 24px; display: flex; max-width: 100%; }
719
+ .message-row.user { justify-content: flex-end; }
720
+ .assistant-wrap { display: flex; gap: 12px; max-width: 100%; min-width: 0; }
721
+ .assistant-avatar {
722
+ width: 24px;
723
+ height: 24px;
724
+ background: #ededed;
725
+ color: #000;
726
+ border-radius: 6px;
727
+ display: grid;
728
+ place-items: center;
729
+ font-size: 11px;
730
+ font-weight: 600;
731
+ flex-shrink: 0;
732
+ margin-top: 2px;
733
+ }
734
+ .assistant-content {
735
+ line-height: 1.65;
736
+ color: #ededed;
737
+ font-size: 14px;
738
+ min-width: 0;
739
+ max-width: 100%;
740
+ overflow-wrap: break-word;
741
+ word-break: break-word;
742
+ margin-top: 2px;
743
+ }
744
+ .assistant-content p { margin: 0 0 12px; }
745
+ .assistant-content p:last-child { margin-bottom: 0; }
746
+ .assistant-content ul, .assistant-content ol { margin: 8px 0; padding-left: 20px; }
747
+ .assistant-content li { margin: 4px 0; }
748
+ .assistant-content strong { font-weight: 600; color: #fff; }
749
+ .assistant-content h2 {
750
+ font-size: 16px;
751
+ font-weight: 600;
752
+ letter-spacing: -0.02em;
753
+ margin: 20px 0 8px;
754
+ color: #fff;
755
+ }
756
+ .assistant-content h3 {
757
+ font-size: 14px;
758
+ font-weight: 600;
759
+ letter-spacing: -0.01em;
760
+ margin: 16px 0 6px;
761
+ color: #fff;
762
+ }
763
+ .assistant-content code {
764
+ background: rgba(255,255,255,0.06);
765
+ border: 1px solid rgba(255,255,255,0.06);
766
+ padding: 2px 5px;
767
+ border-radius: 4px;
768
+ font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
769
+ font-size: 0.88em;
770
+ }
771
+ .assistant-content pre {
772
+ background: #0a0a0a;
773
+ border: 1px solid rgba(255,255,255,0.06);
774
+ padding: 14px 16px;
775
+ border-radius: 8px;
776
+ overflow-x: auto;
777
+ margin: 14px 0;
778
+ }
779
+ .assistant-content pre code {
780
+ background: none;
781
+ border: 0;
782
+ padding: 0;
783
+ font-size: 13px;
784
+ line-height: 1.5;
785
+ }
786
+ .tool-activity {
787
+ margin-top: 12px;
788
+ border: 1px solid rgba(255,255,255,0.08);
789
+ background: rgba(255,255,255,0.03);
790
+ border-radius: 10px;
791
+ font-size: 12px;
792
+ line-height: 1.45;
793
+ color: #bcbcbc;
794
+ max-width: 300px;
795
+ }
796
+ .tool-activity-disclosure {
797
+ display: block;
798
+ }
799
+ .tool-activity-summary {
800
+ list-style: none;
801
+ display: flex;
802
+ align-items: center;
803
+ gap: 8px;
804
+ cursor: pointer;
805
+ padding: 10px 12px;
806
+ user-select: none;
807
+ }
808
+ .tool-activity-summary::-webkit-details-marker {
809
+ display: none;
810
+ }
811
+ .tool-activity-label {
812
+ font-size: 11px;
813
+ text-transform: uppercase;
814
+ letter-spacing: 0.06em;
815
+ color: #8a8a8a;
816
+ font-weight: 600;
817
+ }
818
+ .tool-activity-caret {
819
+ margin-left: auto;
820
+ color: #8a8a8a;
821
+ display: inline-flex;
822
+ align-items: center;
823
+ justify-content: center;
824
+ transition: transform 120ms ease;
825
+ transform: rotate(0deg);
826
+ }
827
+ .tool-activity-caret svg {
828
+ width: 14px;
829
+ height: 14px;
830
+ display: block;
831
+ }
832
+ .tool-activity-disclosure[open] .tool-activity-caret {
833
+ transform: rotate(90deg);
834
+ }
835
+ .tool-activity-list {
836
+ display: grid;
837
+ gap: 6px;
838
+ padding: 0 12px 10px;
839
+ }
840
+ .tool-activity-item {
841
+ font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
842
+ background: rgba(255,255,255,0.04);
843
+ border-radius: 6px;
844
+ padding: 4px 7px;
845
+ color: #d6d6d6;
846
+ }
847
+ .user-bubble {
848
+ background: #111;
849
+ border: 1px solid rgba(255,255,255,0.08);
850
+ padding: 10px 16px;
851
+ border-radius: 18px;
852
+ max-width: 70%;
853
+ font-size: 14px;
854
+ line-height: 1.5;
855
+ overflow-wrap: break-word;
856
+ word-break: break-word;
857
+ }
858
+ .empty-state {
859
+ display: flex;
860
+ flex-direction: column;
861
+ align-items: center;
862
+ justify-content: center;
863
+ height: 100%;
864
+ gap: 16px;
865
+ color: #555;
866
+ }
867
+ .empty-state .assistant-avatar {
868
+ width: 36px;
869
+ height: 36px;
870
+ font-size: 14px;
871
+ border-radius: 8px;
872
+ }
873
+ .empty-state-text {
874
+ font-size: 14px;
875
+ color: #555;
876
+ }
877
+ .thinking-indicator {
878
+ display: inline-block;
879
+ font-family: Inconsolata, monospace;
880
+ font-size: 20px;
881
+ line-height: 1;
882
+ vertical-align: middle;
883
+ color: #ededed;
884
+ opacity: 0.5;
885
+ }
886
+
887
+ /* Composer */
888
+ .composer {
889
+ padding: 12px 24px 24px;
890
+ position: relative;
891
+ }
892
+ /* PWA standalone mode - extra bottom padding */
893
+ @media (display-mode: standalone), (-webkit-touch-callout: none) {
894
+ .composer {
895
+ padding-bottom: 32px;
896
+ }
897
+ }
898
+ @supports (-webkit-touch-callout: none) {
899
+ /* iOS Safari standalone check via JS class */
900
+ .standalone .composer {
901
+ padding-bottom: 32px;
902
+ }
903
+ }
904
+ .composer::before {
905
+ content: "";
906
+ position: absolute;
907
+ left: 0;
908
+ right: 0;
909
+ bottom: 100%;
910
+ height: 48px;
911
+ background: linear-gradient(to top, #000 0%, transparent 100%);
912
+ pointer-events: none;
913
+ }
914
+ .composer-inner { max-width: 680px; margin: 0 auto; }
915
+ .composer-shell {
916
+ background: #0a0a0a;
917
+ border: 1px solid rgba(255,255,255,0.1);
918
+ border-radius: 9999px;
919
+ display: flex;
920
+ align-items: center;
921
+ padding: 4px 6px 4px 18px;
922
+ transition: border-color 0.15s;
923
+ }
924
+ .composer-shell:focus-within { border-color: rgba(255,255,255,0.2); }
925
+ .composer-input {
926
+ flex: 1;
927
+ background: transparent;
928
+ border: 0;
929
+ outline: none;
930
+ color: #ededed;
931
+ min-height: 40px;
932
+ max-height: 200px;
933
+ resize: none;
934
+ padding: 10px 0 8px;
935
+ font-size: 14px;
936
+ line-height: 1.5;
937
+ }
938
+ .composer-input::placeholder { color: #444; }
939
+ .send-btn {
940
+ width: 32px;
941
+ height: 32px;
942
+ background: #ededed;
943
+ border: 0;
944
+ border-radius: 50%;
945
+ color: #000;
946
+ cursor: pointer;
947
+ display: grid;
948
+ place-items: center;
949
+ flex-shrink: 0;
950
+ margin-bottom: 2px;
951
+ transition: background 0.15s, opacity 0.15s;
952
+ }
953
+ .send-btn:hover { background: #fff; }
954
+ .send-btn:disabled { opacity: 0.2; cursor: default; }
955
+ .send-btn:disabled:hover { background: #ededed; }
956
+ .disclaimer {
957
+ text-align: center;
958
+ color: #333;
959
+ font-size: 12px;
960
+ margin-top: 10px;
961
+ }
962
+
963
+ /* Scrollbar */
964
+ ::-webkit-scrollbar { width: 6px; }
965
+ ::-webkit-scrollbar-track { background: transparent; }
966
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
967
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.16); }
968
+
969
+ /* Mobile */
970
+ @media (max-width: 768px) {
971
+ .sidebar {
972
+ position: fixed;
973
+ inset: 0 auto 0 0;
974
+ z-index: 100;
975
+ transform: translateX(-100%);
976
+ padding-top: calc(env(safe-area-inset-top, 0px) + 12px);
977
+ will-change: transform;
978
+ }
979
+ .sidebar.dragging { transition: none; }
980
+ .sidebar:not(.dragging) { transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); }
981
+ .shell.sidebar-open .sidebar { transform: translateX(0); }
982
+ .sidebar-toggle { display: grid; place-items: center; }
983
+ .topbar-new-chat { display: grid; place-items: center; }
984
+ .sidebar-backdrop {
985
+ position: fixed;
986
+ inset: 0;
987
+ background: rgba(0,0,0,0.6);
988
+ z-index: 50;
989
+ backdrop-filter: blur(2px);
990
+ -webkit-backdrop-filter: blur(2px);
991
+ opacity: 0;
992
+ pointer-events: none;
993
+ will-change: opacity;
994
+ }
995
+ .sidebar-backdrop:not(.dragging) { transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1); }
996
+ .sidebar-backdrop.dragging { transition: none; }
997
+ .shell.sidebar-open .sidebar-backdrop { opacity: 1; pointer-events: auto; }
998
+ .messages { padding: 16px; }
999
+ .composer { padding: 8px 16px 16px; }
1000
+ /* Always show delete button on mobile (no hover) */
1001
+ .conversation-item .delete-btn { opacity: 1; }
1002
+ }
1003
+
1004
+ /* Reduced motion */
1005
+ @media (prefers-reduced-motion: reduce) {
1006
+ *, *::before, *::after {
1007
+ animation-duration: 0.01ms !important;
1008
+ transition-duration: 0.01ms !important;
1009
+ }
1010
+ }
1011
+ </style>
1012
+ </head>
1013
+ <body data-agent-initial="${agentInitial}" data-agent-name="${agentName}">
1014
+ <div class="edge-blocker-right"></div>
1015
+ <div id="auth" class="auth hidden">
1016
+ <form id="login-form" class="auth-card">
1017
+ <div class="auth-brand">
1018
+ <svg viewBox="0 0 24 24" fill="none"><path d="M12 2L2 19.5h20L12 2z" fill="currentColor"/></svg>
1019
+ <h2 class="auth-title">Poncho</h2>
1020
+ </div>
1021
+ <p class="auth-text">Enter the passphrase to continue.</p>
1022
+ <input id="passphrase" class="auth-input" type="password" placeholder="Passphrase" required>
1023
+ <button class="auth-submit" type="submit">Continue</button>
1024
+ <div id="login-error" class="error"></div>
1025
+ </form>
1026
+ </div>
1027
+
1028
+ <div id="app" class="shell hidden">
1029
+ <aside class="sidebar">
1030
+ <button id="new-chat" class="new-chat-btn">
1031
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
1032
+ </button>
1033
+ <div id="conversation-list" class="conversation-list"></div>
1034
+ <div class="sidebar-footer">
1035
+ <button id="logout" class="logout-btn">Log out</button>
1036
+ </div>
1037
+ </aside>
1038
+ <div id="sidebar-backdrop" class="sidebar-backdrop"></div>
1039
+ <main class="main">
1040
+ <div class="topbar">
1041
+ <button id="sidebar-toggle" class="sidebar-toggle">&#9776;</button>
1042
+ <div id="chat-title" class="topbar-title"></div>
1043
+ <button id="topbar-new-chat" class="topbar-new-chat">
1044
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
1045
+ </button>
1046
+ </div>
1047
+ <div id="messages" class="messages">
1048
+ <div class="empty-state">
1049
+ <div class="assistant-avatar">${agentInitial}</div>
1050
+ <div class="empty-state-text">How can I help you today?</div>
1051
+ </div>
1052
+ </div>
1053
+ <form id="composer" class="composer">
1054
+ <div class="composer-inner">
1055
+ <div class="composer-shell">
1056
+ <textarea id="prompt" class="composer-input" placeholder="Send a message..." rows="1"></textarea>
1057
+ <button id="send" class="send-btn" type="submit">
1058
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 12V4M4 7l4-4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
1059
+ </button>
1060
+ </div>
1061
+ </div>
1062
+ </form>
1063
+ </main>
1064
+ </div>
1065
+
1066
+ <script>
1067
+ const state = {
1068
+ csrfToken: "",
1069
+ conversations: [],
1070
+ activeConversationId: null,
1071
+ activeMessages: [],
1072
+ isStreaming: false,
1073
+ confirmDeleteId: null
1074
+ };
1075
+
1076
+ const agentInitial = document.body.dataset.agentInitial || "A";
1077
+ const $ = (id) => document.getElementById(id);
1078
+ const elements = {
1079
+ auth: $("auth"),
1080
+ app: $("app"),
1081
+ loginForm: $("login-form"),
1082
+ passphrase: $("passphrase"),
1083
+ loginError: $("login-error"),
1084
+ list: $("conversation-list"),
1085
+ newChat: $("new-chat"),
1086
+ topbarNewChat: $("topbar-new-chat"),
1087
+ messages: $("messages"),
1088
+ chatTitle: $("chat-title"),
1089
+ logout: $("logout"),
1090
+ composer: $("composer"),
1091
+ prompt: $("prompt"),
1092
+ send: $("send"),
1093
+ shell: $("app"),
1094
+ sidebarToggle: $("sidebar-toggle"),
1095
+ sidebarBackdrop: $("sidebar-backdrop")
1096
+ };
1097
+
1098
+ const pushConversationUrl = (conversationId) => {
1099
+ const target = conversationId ? "/c/" + encodeURIComponent(conversationId) : "/";
1100
+ if (window.location.pathname !== target) {
1101
+ history.pushState({ conversationId: conversationId || null }, "", target);
1102
+ }
1103
+ };
1104
+
1105
+ const replaceConversationUrl = (conversationId) => {
1106
+ const target = conversationId ? "/c/" + encodeURIComponent(conversationId) : "/";
1107
+ if (window.location.pathname !== target) {
1108
+ history.replaceState({ conversationId: conversationId || null }, "", target);
1109
+ }
1110
+ };
1111
+
1112
+ const getConversationIdFromUrl = () => {
1113
+ const match = window.location.pathname.match(/^\\/c\\/([^\\/]+)/);
1114
+ return match ? decodeURIComponent(match[1]) : null;
1115
+ };
1116
+
1117
+ const mutatingMethods = new Set(["POST", "PATCH", "PUT", "DELETE"]);
1118
+
1119
+ const api = async (path, options = {}) => {
1120
+ const method = (options.method || "GET").toUpperCase();
1121
+ const headers = { ...(options.headers || {}) };
1122
+ if (mutatingMethods.has(method) && state.csrfToken) {
1123
+ headers["x-csrf-token"] = state.csrfToken;
1124
+ }
1125
+ if (options.body && !headers["Content-Type"]) {
1126
+ headers["Content-Type"] = "application/json";
1127
+ }
1128
+ const response = await fetch(path, { credentials: "include", ...options, method, headers });
1129
+ if (!response.ok) {
1130
+ let payload = {};
1131
+ try { payload = await response.json(); } catch {}
1132
+ const error = new Error(payload.message || ("Request failed: " + response.status));
1133
+ error.status = response.status;
1134
+ error.payload = payload;
1135
+ throw error;
1136
+ }
1137
+ const contentType = response.headers.get("content-type") || "";
1138
+ if (contentType.includes("application/json")) {
1139
+ return await response.json();
1140
+ }
1141
+ return await response.text();
1142
+ };
1143
+
1144
+ const escapeHtml = (value) =>
1145
+ String(value || "")
1146
+ .replace(/&/g, "&amp;")
1147
+ .replace(/</g, "&lt;")
1148
+ .replace(/>/g, "&gt;")
1149
+ .replace(/"/g, "&quot;")
1150
+ .replace(/'/g, "&#39;");
1151
+
1152
+ const renderInlineMarkdown = (value) => {
1153
+ let html = escapeHtml(value);
1154
+ html = html.replace(/\\*\\*([^*]+)\\*\\*/g, "<strong>$1</strong>");
1155
+ html = html.replace(/\\x60([^\\x60]+)\\x60/g, "<code>$1</code>");
1156
+ return html;
1157
+ };
1158
+
1159
+ const renderMarkdownBlock = (value) => {
1160
+ const lines = String(value || "").split("\\n");
1161
+ let html = "";
1162
+ let inList = false;
1163
+
1164
+ for (const rawLine of lines) {
1165
+ const line = rawLine.trimEnd();
1166
+ const trimmed = line.trim();
1167
+ const headingMatch = trimmed.match(/^(#{1,3})\\s+(.+)$/);
1168
+
1169
+ if (headingMatch) {
1170
+ if (inList) {
1171
+ html += "</ul>";
1172
+ inList = false;
1173
+ }
1174
+ const level = Math.min(3, headingMatch[1].length);
1175
+ const tag = level === 1 ? "h2" : level === 2 ? "h3" : "p";
1176
+ html += "<" + tag + ">" + renderInlineMarkdown(headingMatch[2]) + "</" + tag + ">";
1177
+ continue;
1178
+ }
1179
+
1180
+ if (/^\\s*-\\s+/.test(line)) {
1181
+ if (!inList) {
1182
+ html += "<ul>";
1183
+ inList = true;
1184
+ }
1185
+ html += "<li>" + renderInlineMarkdown(line.replace(/^\\s*-\\s+/, "")) + "</li>";
1186
+ continue;
1187
+ }
1188
+ if (inList) {
1189
+ html += "</ul>";
1190
+ inList = false;
1191
+ }
1192
+ if (trimmed.length === 0) {
1193
+ continue;
1194
+ }
1195
+ html += "<p>" + renderInlineMarkdown(line) + "</p>";
1196
+ }
1197
+
1198
+ if (inList) {
1199
+ html += "</ul>";
1200
+ }
1201
+ return html;
1202
+ };
1203
+
1204
+ const renderAssistantMarkdown = (value) => {
1205
+ const source = String(value || "");
1206
+ const fenceRegex = /\\x60\\x60\\x60([\\s\\S]*?)\\x60\\x60\\x60/g;
1207
+ let html = "";
1208
+ let lastIndex = 0;
1209
+ let match;
1210
+
1211
+ while ((match = fenceRegex.exec(source))) {
1212
+ const before = source.slice(lastIndex, match.index);
1213
+ html += renderMarkdownBlock(before);
1214
+ const codeText = String(match[1] || "").replace(/^\\n+|\\n+$/g, "");
1215
+ html += "<pre><code>" + escapeHtml(codeText) + "</code></pre>";
1216
+ lastIndex = match.index + match[0].length;
1217
+ }
1218
+
1219
+ html += renderMarkdownBlock(source.slice(lastIndex));
1220
+ return html || "<p></p>";
1221
+ };
1222
+
1223
+ const extractToolActivity = (value) => {
1224
+ const source = String(value || "");
1225
+ let markerIndex = source.lastIndexOf("\\n### Tool activity\\n");
1226
+ if (markerIndex < 0 && source.startsWith("### Tool activity\\n")) {
1227
+ markerIndex = 0;
1228
+ }
1229
+ if (markerIndex < 0) {
1230
+ return { content: source, activities: [] };
1231
+ }
1232
+ const content = markerIndex === 0 ? "" : source.slice(0, markerIndex).trimEnd();
1233
+ const rawSection = markerIndex === 0 ? source : source.slice(markerIndex + 1);
1234
+ const afterHeading = rawSection.replace(/^### Tool activity\\s*\\n?/, "");
1235
+ const activities = afterHeading
1236
+ .split("\\n")
1237
+ .map((line) => line.trim())
1238
+ .filter((line) => line.startsWith("- "))
1239
+ .map((line) => line.slice(2).trim())
1240
+ .filter(Boolean);
1241
+ return { content, activities };
1242
+ };
1243
+
1244
+ const renderToolActivity = (items) => {
1245
+ if (!items || !items.length) {
1246
+ return "";
1247
+ }
1248
+ const chips = items
1249
+ .map((item) => '<div class="tool-activity-item">' + escapeHtml(item) + "</div>")
1250
+ .join("");
1251
+ return (
1252
+ '<div class="tool-activity">' +
1253
+ '<details class="tool-activity-disclosure">' +
1254
+ '<summary class="tool-activity-summary">' +
1255
+ '<span class="tool-activity-label">Tool activity</span>' +
1256
+ '<span class="tool-activity-caret" aria-hidden="true"><svg viewBox="0 0 12 12" fill="none"><path d="M4.5 2.75L8 6L4.5 9.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg></span>' +
1257
+ "</summary>" +
1258
+ '<div class="tool-activity-list">' +
1259
+ chips +
1260
+ "</div>" +
1261
+ "</details>" +
1262
+ "</div>"
1263
+ );
1264
+ };
1265
+
1266
+ const formatDate = (epoch) => {
1267
+ try {
1268
+ const date = new Date(epoch);
1269
+ const now = new Date();
1270
+ const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
1271
+ const startOfDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
1272
+ const dayDiff = Math.floor((startOfToday - startOfDate) / 86400000);
1273
+ if (dayDiff === 0) {
1274
+ return "Today";
1275
+ }
1276
+ if (dayDiff === 1) {
1277
+ return "Yesterday";
1278
+ }
1279
+ if (dayDiff < 7 && dayDiff > 1) {
1280
+ return date.toLocaleDateString(undefined, { weekday: "short" });
1281
+ }
1282
+ return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
1283
+ } catch {
1284
+ return "";
1285
+ }
1286
+ };
1287
+
1288
+ const isMobile = () => window.matchMedia("(max-width: 900px)").matches;
1289
+
1290
+ const setSidebarOpen = (open) => {
1291
+ if (!isMobile()) {
1292
+ elements.shell.classList.remove("sidebar-open");
1293
+ return;
1294
+ }
1295
+ elements.shell.classList.toggle("sidebar-open", open);
1296
+ };
1297
+
1298
+ const renderConversationList = () => {
1299
+ elements.list.innerHTML = "";
1300
+ for (const c of state.conversations) {
1301
+ const item = document.createElement("div");
1302
+ item.className = "conversation-item" + (c.conversationId === state.activeConversationId ? " active" : "");
1303
+ item.textContent = c.title;
1304
+
1305
+ const isConfirming = state.confirmDeleteId === c.conversationId;
1306
+ const deleteBtn = document.createElement("button");
1307
+ deleteBtn.className = "delete-btn" + (isConfirming ? " confirming" : "");
1308
+ deleteBtn.textContent = isConfirming ? "sure?" : "\\u00d7";
1309
+ deleteBtn.onclick = async (e) => {
1310
+ e.stopPropagation();
1311
+ if (!isConfirming) {
1312
+ state.confirmDeleteId = c.conversationId;
1313
+ renderConversationList();
1314
+ return;
1315
+ }
1316
+ await api("/api/conversations/" + c.conversationId, { method: "DELETE" });
1317
+ if (state.activeConversationId === c.conversationId) {
1318
+ state.activeConversationId = null;
1319
+ state.activeMessages = [];
1320
+ pushConversationUrl(null);
1321
+ elements.chatTitle.textContent = "";
1322
+ renderMessages([]);
1323
+ }
1324
+ state.confirmDeleteId = null;
1325
+ await loadConversations();
1326
+ };
1327
+ item.appendChild(deleteBtn);
1328
+
1329
+ item.onclick = async () => {
1330
+ // Clear any delete confirmation, but still navigate
1331
+ if (state.confirmDeleteId) {
1332
+ state.confirmDeleteId = null;
1333
+ }
1334
+ state.activeConversationId = c.conversationId;
1335
+ pushConversationUrl(c.conversationId);
1336
+ renderConversationList();
1337
+ await loadConversation(c.conversationId);
1338
+ if (isMobile()) setSidebarOpen(false);
1339
+ };
1340
+
1341
+ elements.list.appendChild(item);
1342
+ }
1343
+ };
1344
+
1345
+ const renderMessages = (messages, isStreaming = false) => {
1346
+ elements.messages.innerHTML = "";
1347
+ if (!messages || !messages.length) {
1348
+ elements.messages.innerHTML = '<div class="empty-state"><div class="assistant-avatar">' + agentInitial + '</div><div>How can I help you today?</div></div>';
1349
+ return;
1350
+ }
1351
+ const col = document.createElement("div");
1352
+ col.className = "messages-column";
1353
+ messages.forEach((m, i) => {
1354
+ const row = document.createElement("div");
1355
+ row.className = "message-row " + m.role;
1356
+ if (m.role === "assistant") {
1357
+ const wrap = document.createElement("div");
1358
+ wrap.className = "assistant-wrap";
1359
+ wrap.innerHTML = '<div class="assistant-avatar">' + agentInitial + '</div>';
1360
+ const content = document.createElement("div");
1361
+ content.className = "assistant-content";
1362
+ const text = String(m.content || "");
1363
+ const parsed = extractToolActivity(text);
1364
+ const metadataToolActivity =
1365
+ m.metadata && Array.isArray(m.metadata.toolActivity)
1366
+ ? m.metadata.toolActivity
1367
+ : [];
1368
+ const toolActivity =
1369
+ Array.isArray(m._toolActivity) && m._toolActivity.length > 0
1370
+ ? m._toolActivity
1371
+ : metadataToolActivity.length > 0
1372
+ ? metadataToolActivity
1373
+ : parsed.activities;
1374
+ if (m._error) {
1375
+ const errorEl = document.createElement("div");
1376
+ errorEl.className = "message-error";
1377
+ errorEl.innerHTML = "<strong>Error</strong><br>" + escapeHtml(m._error);
1378
+ content.appendChild(errorEl);
1379
+ } else if (isStreaming && i === messages.length - 1 && !parsed.content) {
1380
+ const spinner = document.createElement("span");
1381
+ spinner.className = "thinking-indicator";
1382
+ const starFrames = ["✶","✸","✹","✺","✹","✷"];
1383
+ let frame = 0;
1384
+ spinner.textContent = starFrames[0];
1385
+ spinner._interval = setInterval(() => { frame = (frame + 1) % starFrames.length; spinner.textContent = starFrames[frame]; }, 70);
1386
+ content.appendChild(spinner);
1387
+ } else {
1388
+ content.innerHTML = renderAssistantMarkdown(parsed.content);
1389
+ }
1390
+ if (toolActivity.length > 0) {
1391
+ content.insertAdjacentHTML("beforeend", renderToolActivity(toolActivity));
1392
+ }
1393
+ wrap.appendChild(content);
1394
+ row.appendChild(wrap);
1395
+ } else {
1396
+ row.innerHTML = '<div class="user-bubble">' + escapeHtml(m.content) + '</div>';
1397
+ }
1398
+ col.appendChild(row);
1399
+ });
1400
+ elements.messages.appendChild(col);
1401
+ elements.messages.scrollTop = elements.messages.scrollHeight;
1402
+ };
1403
+
1404
+ const loadConversations = async () => {
1405
+ const payload = await api("/api/conversations");
1406
+ state.conversations = payload.conversations || [];
1407
+ renderConversationList();
1408
+ };
1409
+
1410
+ const loadConversation = async (conversationId) => {
1411
+ const payload = await api("/api/conversations/" + encodeURIComponent(conversationId));
1412
+ elements.chatTitle.textContent = payload.conversation.title;
1413
+ state.activeMessages = payload.conversation.messages || [];
1414
+ renderMessages(state.activeMessages);
1415
+ elements.prompt.focus();
1416
+ };
1417
+
1418
+ const createConversation = async (title, options = {}) => {
1419
+ const shouldLoadConversation = options.loadConversation !== false;
1420
+ const payload = await api("/api/conversations", {
1421
+ method: "POST",
1422
+ body: JSON.stringify(title ? { title } : {})
1423
+ });
1424
+ state.activeConversationId = payload.conversation.conversationId;
1425
+ state.confirmDeleteId = null;
1426
+ pushConversationUrl(state.activeConversationId);
1427
+ await loadConversations();
1428
+ if (shouldLoadConversation) {
1429
+ await loadConversation(state.activeConversationId);
1430
+ } else {
1431
+ elements.chatTitle.textContent = payload.conversation.title || "New conversation";
1432
+ }
1433
+ return state.activeConversationId;
1434
+ };
1435
+
1436
+ const parseSseChunk = (buffer, onEvent) => {
1437
+ let rest = buffer;
1438
+ while (true) {
1439
+ const index = rest.indexOf("\\n\\n");
1440
+ if (index < 0) {
1441
+ return rest;
1442
+ }
1443
+ const raw = rest.slice(0, index);
1444
+ rest = rest.slice(index + 2);
1445
+ const lines = raw.split("\\n");
1446
+ let eventName = "message";
1447
+ let data = "";
1448
+ for (const line of lines) {
1449
+ if (line.startsWith("event:")) {
1450
+ eventName = line.slice(6).trim();
1451
+ } else if (line.startsWith("data:")) {
1452
+ data += line.slice(5).trim();
1453
+ }
1454
+ }
1455
+ if (data) {
1456
+ try {
1457
+ onEvent(eventName, JSON.parse(data));
1458
+ } catch {}
1459
+ }
1460
+ }
1461
+ };
1462
+
1463
+ const setStreaming = (value) => {
1464
+ state.isStreaming = value;
1465
+ elements.send.disabled = value;
1466
+ };
1467
+
1468
+ const pushToolActivity = (assistantMessage, line) => {
1469
+ if (!line) {
1470
+ return;
1471
+ }
1472
+ if (
1473
+ !assistantMessage.metadata ||
1474
+ !Array.isArray(assistantMessage.metadata.toolActivity)
1475
+ ) {
1476
+ assistantMessage.metadata = {
1477
+ ...(assistantMessage.metadata || {}),
1478
+ toolActivity: [],
1479
+ };
1480
+ }
1481
+ assistantMessage.metadata.toolActivity.push(line);
1482
+ };
1483
+
1484
+ const autoResizePrompt = () => {
1485
+ const el = elements.prompt;
1486
+ el.style.height = "auto";
1487
+ const scrollHeight = el.scrollHeight;
1488
+ const nextHeight = Math.min(scrollHeight, 200);
1489
+ el.style.height = nextHeight + "px";
1490
+ el.style.overflowY = scrollHeight > 200 ? "auto" : "hidden";
1491
+ };
1492
+
1493
+ const sendMessage = async (text) => {
1494
+ const messageText = (text || "").trim();
1495
+ if (!messageText || state.isStreaming) {
1496
+ return;
1497
+ }
1498
+ const localMessages = [...(state.activeMessages || []), { role: "user", content: messageText }];
1499
+ let assistantMessage = { role: "assistant", content: "", metadata: { toolActivity: [] } };
1500
+ localMessages.push(assistantMessage);
1501
+ state.activeMessages = localMessages;
1502
+ renderMessages(localMessages, true);
1503
+ setStreaming(true);
1504
+ let conversationId = state.activeConversationId;
1505
+ try {
1506
+ if (!conversationId) {
1507
+ conversationId = await createConversation(messageText, { loadConversation: false });
1508
+ }
1509
+ const response = await fetch("/api/conversations/" + encodeURIComponent(conversationId) + "/messages", {
1510
+ method: "POST",
1511
+ credentials: "include",
1512
+ headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
1513
+ body: JSON.stringify({ message: messageText })
1514
+ });
1515
+ if (!response.ok || !response.body) {
1516
+ throw new Error("Failed to stream response");
1517
+ }
1518
+ const reader = response.body.getReader();
1519
+ const decoder = new TextDecoder();
1520
+ let buffer = "";
1521
+ while (true) {
1522
+ const { value, done } = await reader.read();
1523
+ if (done) {
1524
+ break;
1525
+ }
1526
+ buffer += decoder.decode(value, { stream: true });
1527
+ buffer = parseSseChunk(buffer, (eventName, payload) => {
1528
+ if (eventName === "model:chunk") {
1529
+ assistantMessage.content += String(payload.content || "");
1530
+ renderMessages(localMessages, true);
1531
+ }
1532
+ if (eventName === "tool:started") {
1533
+ pushToolActivity(assistantMessage, "start " + (payload.tool || "tool"));
1534
+ renderMessages(localMessages, true);
1535
+ }
1536
+ if (eventName === "tool:completed") {
1537
+ const duration = typeof payload.duration === "number" ? payload.duration : null;
1538
+ pushToolActivity(
1539
+ assistantMessage,
1540
+ "done " +
1541
+ (payload.tool || "tool") +
1542
+ (duration !== null ? " (" + duration + "ms)" : ""),
1543
+ );
1544
+ renderMessages(localMessages, true);
1545
+ }
1546
+ if (eventName === "tool:error") {
1547
+ pushToolActivity(
1548
+ assistantMessage,
1549
+ "error " + (payload.tool || "tool") + ": " + (payload.error || "unknown error"),
1550
+ );
1551
+ renderMessages(localMessages, true);
1552
+ }
1553
+ if (eventName === "tool:approval:required") {
1554
+ pushToolActivity(assistantMessage, "approval required for " + (payload.tool || "tool"));
1555
+ renderMessages(localMessages, true);
1556
+ }
1557
+ if (eventName === "tool:approval:granted") {
1558
+ pushToolActivity(assistantMessage, "approval granted");
1559
+ renderMessages(localMessages, true);
1560
+ }
1561
+ if (eventName === "tool:approval:denied") {
1562
+ pushToolActivity(assistantMessage, "approval denied");
1563
+ renderMessages(localMessages, true);
1564
+ }
1565
+ if (eventName === "run:completed" && (!assistantMessage.content || assistantMessage.content.length === 0)) {
1566
+ assistantMessage.content = String(payload.result?.response || "");
1567
+ renderMessages(localMessages, false);
1568
+ }
1569
+ if (eventName === "run:error") {
1570
+ const errMsg = payload.error?.message || "Something went wrong";
1571
+ assistantMessage.content = "";
1572
+ assistantMessage._error = errMsg;
1573
+ renderMessages(localMessages, false);
1574
+ }
1575
+ });
1576
+ }
1577
+ await loadConversations();
1578
+ await loadConversation(conversationId);
1579
+ } finally {
1580
+ setStreaming(false);
1581
+ elements.prompt.focus();
1582
+ }
1583
+ };
1584
+
1585
+ const requireAuth = async () => {
1586
+ try {
1587
+ const session = await api("/api/auth/session");
1588
+ if (!session.authenticated) {
1589
+ elements.auth.classList.remove("hidden");
1590
+ elements.app.classList.add("hidden");
1591
+ return false;
1592
+ }
1593
+ state.csrfToken = session.csrfToken || "";
1594
+ elements.auth.classList.add("hidden");
1595
+ elements.app.classList.remove("hidden");
1596
+ return true;
1597
+ } catch {
1598
+ elements.auth.classList.remove("hidden");
1599
+ elements.app.classList.add("hidden");
1600
+ return false;
1601
+ }
1602
+ };
1603
+
1604
+ elements.loginForm.addEventListener("submit", async (event) => {
1605
+ event.preventDefault();
1606
+ elements.loginError.textContent = "";
1607
+ try {
1608
+ const result = await api("/api/auth/login", {
1609
+ method: "POST",
1610
+ body: JSON.stringify({ passphrase: elements.passphrase.value || "" })
1611
+ });
1612
+ state.csrfToken = result.csrfToken || "";
1613
+ elements.passphrase.value = "";
1614
+ elements.auth.classList.add("hidden");
1615
+ elements.app.classList.remove("hidden");
1616
+ await loadConversations();
1617
+ const urlConversationId = getConversationIdFromUrl();
1618
+ if (urlConversationId) {
1619
+ state.activeConversationId = urlConversationId;
1620
+ renderConversationList();
1621
+ try {
1622
+ await loadConversation(urlConversationId);
1623
+ } catch {
1624
+ state.activeConversationId = null;
1625
+ state.activeMessages = [];
1626
+ replaceConversationUrl(null);
1627
+ renderMessages([]);
1628
+ renderConversationList();
1629
+ }
1630
+ }
1631
+ } catch (error) {
1632
+ elements.loginError.textContent = error.message || "Login failed";
1633
+ }
1634
+ });
1635
+
1636
+ const startNewChat = () => {
1637
+ state.activeConversationId = null;
1638
+ state.activeMessages = [];
1639
+ state.confirmDeleteId = null;
1640
+ pushConversationUrl(null);
1641
+ elements.chatTitle.textContent = "";
1642
+ renderMessages([]);
1643
+ renderConversationList();
1644
+ elements.prompt.focus();
1645
+ if (isMobile()) {
1646
+ setSidebarOpen(false);
1647
+ }
1648
+ };
1649
+
1650
+ elements.newChat.addEventListener("click", startNewChat);
1651
+ elements.topbarNewChat.addEventListener("click", startNewChat);
1652
+
1653
+ elements.prompt.addEventListener("input", () => {
1654
+ autoResizePrompt();
1655
+ });
1656
+
1657
+ elements.prompt.addEventListener("keydown", (event) => {
1658
+ if (event.key === "Enter" && !event.shiftKey) {
1659
+ event.preventDefault();
1660
+ elements.composer.requestSubmit();
1661
+ }
1662
+ });
1663
+
1664
+ elements.sidebarToggle.addEventListener("click", () => {
1665
+ if (isMobile()) setSidebarOpen(!elements.shell.classList.contains("sidebar-open"));
1666
+ });
1667
+
1668
+ elements.sidebarBackdrop.addEventListener("click", () => setSidebarOpen(false));
1669
+
1670
+ elements.logout.addEventListener("click", async () => {
1671
+ await api("/api/auth/logout", { method: "POST" });
1672
+ state.activeConversationId = null;
1673
+ state.activeMessages = [];
1674
+ state.confirmDeleteId = null;
1675
+ state.conversations = [];
1676
+ state.csrfToken = "";
1677
+ await requireAuth();
1678
+ });
1679
+
1680
+ elements.composer.addEventListener("submit", async (event) => {
1681
+ event.preventDefault();
1682
+ const value = elements.prompt.value;
1683
+ elements.prompt.value = "";
1684
+ autoResizePrompt();
1685
+ await sendMessage(value);
1686
+ });
1687
+
1688
+ document.addEventListener("click", (event) => {
1689
+ if (!(event.target instanceof Node)) {
1690
+ return;
1691
+ }
1692
+ if (!event.target.closest(".conversation-item") && state.confirmDeleteId) {
1693
+ state.confirmDeleteId = null;
1694
+ renderConversationList();
1695
+ }
1696
+ });
1697
+
1698
+ window.addEventListener("resize", () => {
1699
+ setSidebarOpen(false);
1700
+ });
1701
+
1702
+ const navigateToConversation = async (conversationId) => {
1703
+ if (conversationId) {
1704
+ state.activeConversationId = conversationId;
1705
+ renderConversationList();
1706
+ try {
1707
+ await loadConversation(conversationId);
1708
+ } catch {
1709
+ // Conversation not found – fall back to empty state
1710
+ state.activeConversationId = null;
1711
+ state.activeMessages = [];
1712
+ replaceConversationUrl(null);
1713
+ elements.chatTitle.textContent = "";
1714
+ renderMessages([]);
1715
+ renderConversationList();
1716
+ }
1717
+ } else {
1718
+ state.activeConversationId = null;
1719
+ state.activeMessages = [];
1720
+ elements.chatTitle.textContent = "";
1721
+ renderMessages([]);
1722
+ renderConversationList();
1723
+ }
1724
+ };
1725
+
1726
+ window.addEventListener("popstate", async () => {
1727
+ if (state.isStreaming) return;
1728
+ const conversationId = getConversationIdFromUrl();
1729
+ await navigateToConversation(conversationId);
1730
+ });
1731
+
1732
+ (async () => {
1733
+ const authenticated = await requireAuth();
1734
+ if (!authenticated) {
1735
+ return;
1736
+ }
1737
+ await loadConversations();
1738
+ const urlConversationId = getConversationIdFromUrl();
1739
+ if (urlConversationId) {
1740
+ state.activeConversationId = urlConversationId;
1741
+ replaceConversationUrl(urlConversationId);
1742
+ renderConversationList();
1743
+ try {
1744
+ await loadConversation(urlConversationId);
1745
+ } catch {
1746
+ // URL pointed to a conversation that no longer exists
1747
+ state.activeConversationId = null;
1748
+ state.activeMessages = [];
1749
+ replaceConversationUrl(null);
1750
+ elements.chatTitle.textContent = "";
1751
+ renderMessages([]);
1752
+ renderConversationList();
1753
+ if (state.conversations.length === 0) {
1754
+ await createConversation();
1755
+ }
1756
+ }
1757
+ } else if (state.conversations.length === 0) {
1758
+ await createConversation();
1759
+ }
1760
+ autoResizePrompt();
1761
+ elements.prompt.focus();
1762
+ })();
1763
+
1764
+ if ("serviceWorker" in navigator) {
1765
+ navigator.serviceWorker.register("/sw.js").catch(() => {});
1766
+ }
1767
+
1768
+ // Detect iOS standalone mode and add class for CSS targeting
1769
+ if (window.navigator.standalone === true || window.matchMedia("(display-mode: standalone)").matches) {
1770
+ document.documentElement.classList.add("standalone");
1771
+ }
1772
+
1773
+ // iOS viewport and keyboard handling
1774
+ (function() {
1775
+ var shell = document.querySelector(".shell");
1776
+ var pinScroll = function() { if (window.scrollY !== 0) window.scrollTo(0, 0); };
1777
+
1778
+ // Track the "full" height when keyboard is not open
1779
+ var fullHeight = window.innerHeight;
1780
+
1781
+ // Resize shell when iOS keyboard opens/closes
1782
+ var resizeForKeyboard = function() {
1783
+ if (!shell || !window.visualViewport) return;
1784
+ var vvHeight = window.visualViewport.height;
1785
+
1786
+ // Update fullHeight if viewport grew (keyboard closed)
1787
+ if (vvHeight > fullHeight) {
1788
+ fullHeight = vvHeight;
1789
+ }
1790
+
1791
+ // Only apply height override if keyboard appears to be open
1792
+ // (viewport significantly smaller than full height)
1793
+ if (vvHeight < fullHeight - 100) {
1794
+ shell.style.height = vvHeight + "px";
1795
+ } else {
1796
+ // Keyboard closed - remove override, let CSS handle it
1797
+ shell.style.height = "";
1798
+ }
1799
+ pinScroll();
1800
+ };
1801
+
1802
+ if (window.visualViewport) {
1803
+ window.visualViewport.addEventListener("scroll", pinScroll);
1804
+ window.visualViewport.addEventListener("resize", resizeForKeyboard);
1805
+ }
1806
+ document.addEventListener("scroll", pinScroll);
1807
+
1808
+ // Draggable sidebar from left edge (mobile only)
1809
+ (function() {
1810
+ var sidebar = document.querySelector(".sidebar");
1811
+ var backdrop = document.querySelector(".sidebar-backdrop");
1812
+ var shell = document.querySelector(".shell");
1813
+ if (!sidebar || !backdrop || !shell) return;
1814
+
1815
+ var sidebarWidth = 260;
1816
+ var edgeThreshold = 200; // px from left edge to start drag
1817
+ var velocityThreshold = 0.3; // px/ms to trigger open/close
1818
+
1819
+ var dragging = false;
1820
+ var startX = 0;
1821
+ var startY = 0;
1822
+ var currentX = 0;
1823
+ var startTime = 0;
1824
+ var isOpen = false;
1825
+ var directionLocked = false;
1826
+ var isHorizontal = false;
1827
+
1828
+ function getProgress() {
1829
+ // Returns 0 (closed) to 1 (open)
1830
+ if (isOpen) {
1831
+ return Math.max(0, Math.min(1, 1 + currentX / sidebarWidth));
1832
+ } else {
1833
+ return Math.max(0, Math.min(1, currentX / sidebarWidth));
1834
+ }
1835
+ }
1836
+
1837
+ function updatePosition(progress) {
1838
+ var offset = (progress - 1) * sidebarWidth;
1839
+ sidebar.style.transform = "translateX(" + offset + "px)";
1840
+ backdrop.style.opacity = progress;
1841
+ if (progress > 0) {
1842
+ backdrop.style.pointerEvents = "auto";
1843
+ } else {
1844
+ backdrop.style.pointerEvents = "none";
1845
+ }
1846
+ }
1847
+
1848
+ function onTouchStart(e) {
1849
+ if (window.innerWidth > 768) return;
1850
+
1851
+ // Don't intercept touches on interactive elements
1852
+ var target = e.target;
1853
+ if (target.closest("button") || target.closest("a") || target.closest("input") || target.closest("textarea")) {
1854
+ return;
1855
+ }
1856
+
1857
+ var touch = e.touches[0];
1858
+ isOpen = shell.classList.contains("sidebar-open");
1859
+
1860
+ // When sidebar is closed: only respond to edge swipes
1861
+ // When sidebar is open: only respond to backdrop touches (not sidebar content)
1862
+ var fromEdge = touch.clientX < edgeThreshold;
1863
+ var onBackdrop = e.target === backdrop;
1864
+
1865
+ if (!isOpen && !fromEdge) return;
1866
+ if (isOpen && !onBackdrop) return;
1867
+
1868
+ // Prevent Safari back gesture when starting from edge
1869
+ if (fromEdge) {
1870
+ e.preventDefault();
1871
+ }
1872
+
1873
+ startX = touch.clientX;
1874
+ startY = touch.clientY;
1875
+ currentX = 0;
1876
+ startTime = Date.now();
1877
+ directionLocked = false;
1878
+ isHorizontal = false;
1879
+ dragging = true;
1880
+ sidebar.classList.add("dragging");
1881
+ backdrop.classList.add("dragging");
1882
+ }
1883
+
1884
+ function onTouchMove(e) {
1885
+ if (!dragging) return;
1886
+ var touch = e.touches[0];
1887
+ var dx = touch.clientX - startX;
1888
+ var dy = touch.clientY - startY;
1889
+
1890
+ // Lock direction after some movement
1891
+ if (!directionLocked && (Math.abs(dx) > 10 || Math.abs(dy) > 10)) {
1892
+ directionLocked = true;
1893
+ isHorizontal = Math.abs(dx) > Math.abs(dy);
1894
+ if (!isHorizontal) {
1895
+ // Vertical scroll, cancel drag
1896
+ dragging = false;
1897
+ sidebar.classList.remove("dragging");
1898
+ backdrop.classList.remove("dragging");
1899
+ return;
1900
+ }
1901
+ }
1902
+
1903
+ if (!directionLocked) return;
1904
+
1905
+ // Prevent scrolling while dragging sidebar
1906
+ e.preventDefault();
1907
+
1908
+ currentX = dx;
1909
+ updatePosition(getProgress());
1910
+ }
1911
+
1912
+ function onTouchEnd(e) {
1913
+ if (!dragging) return;
1914
+ dragging = false;
1915
+ sidebar.classList.remove("dragging");
1916
+ backdrop.classList.remove("dragging");
1917
+
1918
+ var touch = e.changedTouches[0];
1919
+ var dx = touch.clientX - startX;
1920
+ var dt = Date.now() - startTime;
1921
+ var velocity = dx / dt; // px/ms
1922
+
1923
+ var progress = getProgress();
1924
+ var shouldOpen;
1925
+
1926
+ // Use velocity if fast enough, otherwise use position threshold
1927
+ if (Math.abs(velocity) > velocityThreshold) {
1928
+ shouldOpen = velocity > 0;
1929
+ } else {
1930
+ shouldOpen = progress > 0.5;
1931
+ }
1932
+
1933
+ // Reset inline styles and let CSS handle the animation
1934
+ sidebar.style.transform = "";
1935
+ backdrop.style.opacity = "";
1936
+ backdrop.style.pointerEvents = "";
1937
+
1938
+ if (shouldOpen) {
1939
+ shell.classList.add("sidebar-open");
1940
+ } else {
1941
+ shell.classList.remove("sidebar-open");
1942
+ }
1943
+ }
1944
+
1945
+ document.addEventListener("touchstart", onTouchStart, { passive: false });
1946
+ document.addEventListener("touchmove", onTouchMove, { passive: false });
1947
+ document.addEventListener("touchend", onTouchEnd, { passive: true });
1948
+ document.addEventListener("touchcancel", onTouchEnd, { passive: true });
1949
+ })();
1950
+
1951
+ // Prevent Safari back/forward navigation by manipulating history
1952
+ // This doesn't stop the gesture animation but prevents actual navigation
1953
+ if (window.navigator.standalone || window.matchMedia("(display-mode: standalone)").matches) {
1954
+ history.pushState(null, "", location.href);
1955
+ window.addEventListener("popstate", function() {
1956
+ history.pushState(null, "", location.href);
1957
+ });
1958
+ }
1959
+
1960
+ // Right edge blocker - intercept touch events to prevent forward navigation
1961
+ var rightBlocker = document.querySelector(".edge-blocker-right");
1962
+ if (rightBlocker) {
1963
+ rightBlocker.addEventListener("touchstart", function(e) {
1964
+ e.preventDefault();
1965
+ }, { passive: false });
1966
+ rightBlocker.addEventListener("touchmove", function(e) {
1967
+ e.preventDefault();
1968
+ }, { passive: false });
1969
+ }
1970
+ })();
1971
+
1972
+ </script>
1973
+ </body>
1974
+ </html>`;
1975
+ };