@aoagents/ao-web 0.2.3 → 0.2.5

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 (265) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +228 -221
  3. package/.next/app-path-routes-manifest.json +7 -6
  4. package/.next/build-manifest.json +14 -14
  5. package/.next/next-minimal-server.js.nft.json +1 -1
  6. package/.next/next-server.js.nft.json +1 -1
  7. package/.next/prerender-manifest.json +31 -31
  8. package/.next/react-loadable-manifest.json +14 -14
  9. package/.next/required-server-files.json +7 -4
  10. package/.next/server/app/_not-found/page.js +2 -2
  11. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/_not-found.html +1 -1
  14. package/.next/server/app/_not-found.rsc +15 -15
  15. package/.next/server/app/api/backlog/route.js +1 -1
  16. package/.next/server/app/api/backlog/route.js.nft.json +1 -1
  17. package/.next/server/app/api/backlog/route_client-reference-manifest.js +1 -1
  18. package/.next/server/app/api/events/route.js +2 -2
  19. package/.next/server/app/api/events/route.js.nft.json +1 -1
  20. package/.next/server/app/api/events/route_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/issues/route.js +1 -1
  22. package/.next/server/app/api/issues/route.js.nft.json +1 -1
  23. package/.next/server/app/api/issues/route_client-reference-manifest.js +1 -1
  24. package/.next/server/app/api/observability/route.js +1 -1
  25. package/.next/server/app/api/observability/route.js.nft.json +1 -1
  26. package/.next/server/app/api/observability/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/orchestrators/route.js +1 -1
  28. package/.next/server/app/api/orchestrators/route.js.nft.json +1 -1
  29. package/.next/server/app/api/orchestrators/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/projects/route.js +1 -1
  31. package/.next/server/app/api/projects/route.js.nft.json +1 -1
  32. package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  33. package/.next/server/app/api/prs/[id]/merge/route.js +1 -1
  34. package/.next/server/app/api/prs/[id]/merge/route.js.nft.json +1 -1
  35. package/.next/server/app/api/prs/[id]/merge/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/runtime/terminal/route.js +1 -1
  37. package/.next/server/app/api/runtime/terminal/route.js.nft.json +1 -1
  38. package/.next/server/app/api/runtime/terminal/route_client-reference-manifest.js +1 -1
  39. package/.next/server/app/api/sessions/[id]/kill/route.js +1 -1
  40. package/.next/server/app/api/sessions/[id]/kill/route.js.nft.json +1 -1
  41. package/.next/server/app/api/sessions/[id]/kill/route_client-reference-manifest.js +1 -1
  42. package/.next/server/app/api/sessions/[id]/message/route.js +1 -1
  43. package/.next/server/app/api/sessions/[id]/message/route.js.nft.json +1 -1
  44. package/.next/server/app/api/sessions/[id]/message/route_client-reference-manifest.js +1 -1
  45. package/.next/server/app/api/sessions/[id]/remap/route.js +1 -1
  46. package/.next/server/app/api/sessions/[id]/remap/route.js.nft.json +1 -1
  47. package/.next/server/app/api/sessions/[id]/remap/route_client-reference-manifest.js +1 -1
  48. package/.next/server/app/api/sessions/[id]/restore/route.js +1 -1
  49. package/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -1
  50. package/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +1 -1
  51. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  52. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  53. package/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +1 -1
  54. package/.next/server/app/api/sessions/[id]/send/route.js +1 -1
  55. package/.next/server/app/api/sessions/[id]/send/route.js.nft.json +1 -1
  56. package/.next/server/app/api/sessions/[id]/send/route_client-reference-manifest.js +1 -1
  57. package/.next/server/app/api/sessions/patches/route.js +1 -0
  58. package/.next/server/app/api/sessions/patches/route.js.nft.json +1 -0
  59. package/.next/server/app/api/sessions/patches/route_client-reference-manifest.js +1 -0
  60. package/.next/server/app/api/sessions/route.js +1 -1
  61. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  62. package/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  63. package/.next/server/app/api/setup-labels/route.js +1 -1
  64. package/.next/server/app/api/setup-labels/route.js.nft.json +1 -1
  65. package/.next/server/app/api/setup-labels/route_client-reference-manifest.js +1 -1
  66. package/.next/server/app/api/spawn/route.js +1 -1
  67. package/.next/server/app/api/spawn/route.js.nft.json +1 -1
  68. package/.next/server/app/api/spawn/route_client-reference-manifest.js +1 -1
  69. package/.next/server/app/api/verify/route.js +1 -1
  70. package/.next/server/app/api/verify/route.js.nft.json +1 -1
  71. package/.next/server/app/api/verify/route_client-reference-manifest.js +1 -1
  72. package/.next/server/app/api/webhooks/[...slug]/route.js +1 -1
  73. package/.next/server/app/api/webhooks/[...slug]/route.js.nft.json +1 -1
  74. package/.next/server/app/api/webhooks/[...slug]/route_client-reference-manifest.js +1 -1
  75. package/.next/server/app/apple-icon/route.js +1 -1
  76. package/.next/server/app/apple-icon/route.js.nft.json +1 -1
  77. package/.next/server/app/apple-icon/route_client-reference-manifest.js +1 -1
  78. package/.next/server/app/dev/terminal-test/page.js +3 -3
  79. package/.next/server/app/dev/terminal-test/page.js.nft.json +1 -1
  80. package/.next/server/app/dev/terminal-test/page_client-reference-manifest.js +1 -1
  81. package/.next/server/app/dev/terminal-test.html +1 -1
  82. package/.next/server/app/dev/terminal-test.rsc +17 -17
  83. package/.next/server/app/icon/route.js +1 -1
  84. package/.next/server/app/icon/route.js.nft.json +1 -1
  85. package/.next/server/app/icon/route_client-reference-manifest.js +1 -1
  86. package/.next/server/app/icon-192/route.js +1 -1
  87. package/.next/server/app/icon-192/route.js.nft.json +1 -1
  88. package/.next/server/app/icon-192/route_client-reference-manifest.js +1 -1
  89. package/.next/server/app/icon-512/route.js +1 -1
  90. package/.next/server/app/icon-512/route.js.nft.json +1 -1
  91. package/.next/server/app/icon-512/route_client-reference-manifest.js +1 -1
  92. package/.next/server/app/manifest.webmanifest/route.js +2 -2
  93. package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -1
  94. package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
  95. package/.next/server/app/manifest.webmanifest.body +1 -1
  96. package/.next/server/app/orchestrators/page.js +2 -2
  97. package/.next/server/app/orchestrators/page.js.nft.json +1 -1
  98. package/.next/server/app/orchestrators/page_client-reference-manifest.js +1 -1
  99. package/.next/server/app/page.js +2 -2
  100. package/.next/server/app/page.js.nft.json +1 -1
  101. package/.next/server/app/page_client-reference-manifest.js +1 -1
  102. package/.next/server/app/prs/page.js +2 -2
  103. package/.next/server/app/prs/page.js.nft.json +1 -1
  104. package/.next/server/app/prs/page_client-reference-manifest.js +1 -1
  105. package/.next/server/app/sessions/[id]/page.js +5 -5
  106. package/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  107. package/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  108. package/.next/server/app/test-direct/page.js +2 -2
  109. package/.next/server/app/test-direct/page.js.nft.json +1 -1
  110. package/.next/server/app/test-direct/page_client-reference-manifest.js +1 -1
  111. package/.next/server/app/test-direct.html +1 -1
  112. package/.next/server/app/test-direct.rsc +17 -17
  113. package/.next/server/app-paths-manifest.json +7 -6
  114. package/.next/server/chunks/100.js +1 -0
  115. package/.next/server/chunks/106.js +1 -0
  116. package/.next/server/chunks/172.js +9 -0
  117. package/.next/server/chunks/180.js +25 -0
  118. package/.next/server/chunks/333.js +1 -0
  119. package/.next/server/chunks/367.js +3 -0
  120. package/.next/server/chunks/561.js +22 -0
  121. package/.next/server/chunks/627.js +380 -363
  122. package/.next/server/chunks/803.js +6 -0
  123. package/.next/server/chunks/886.js +384 -0
  124. package/.next/server/chunks/907.js +1 -0
  125. package/.next/server/middleware-build-manifest.js +1 -1
  126. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  127. package/.next/server/next-font-manifest.js +1 -1
  128. package/.next/server/next-font-manifest.json +1 -1
  129. package/.next/server/pages/404.html +1 -1
  130. package/.next/server/pages/500.html +1 -1
  131. package/.next/server/pages/_app.js +1 -1
  132. package/.next/server/pages/_app.js.nft.json +1 -1
  133. package/.next/server/pages/_document.js +1 -1
  134. package/.next/server/pages/_document.js.nft.json +1 -1
  135. package/.next/server/pages/_error.js +3 -3
  136. package/.next/server/pages/_error.js.nft.json +1 -1
  137. package/.next/server/server-reference-manifest.json +1 -1
  138. package/.next/static/OyzSw2fQpoJHXyoBWoZ2X/_buildManifest.js +1 -0
  139. package/.next/static/chunks/1461-af7c54935f21d56d.js +1 -0
  140. package/.next/static/chunks/{9393.acf1934a190d793b.js → 2529.32352c1ce5253e3e.js} +1 -1
  141. package/.next/static/chunks/{7411.ecda44797fb514a0.js → 5491.fd98884d48631149.js} +1 -1
  142. package/.next/static/chunks/6923-ef8434dfec6f2a3c.js +1 -0
  143. package/.next/static/chunks/7097.fc904e5f313c4994.js +1 -0
  144. package/.next/static/chunks/7317.685aa5231218e8d3.js +1 -0
  145. package/.next/static/chunks/8713-d3d663f55dc00e48.js +1 -0
  146. package/.next/static/chunks/8785-6b4b3ff260617997.js +1 -0
  147. package/.next/static/chunks/88a6fc35-f836b4b72df5eafa.js +1 -0
  148. package/.next/static/chunks/9478.656c9c263bc55893.js +1 -0
  149. package/.next/static/chunks/app/_not-found/page-019391920005654f.js +1 -0
  150. package/.next/static/chunks/app/api/backlog/route-019391920005654f.js +1 -0
  151. package/.next/static/chunks/app/api/events/route-019391920005654f.js +1 -0
  152. package/.next/static/chunks/app/api/issues/route-019391920005654f.js +1 -0
  153. package/.next/static/chunks/app/api/observability/route-019391920005654f.js +1 -0
  154. package/.next/static/chunks/app/api/orchestrators/route-019391920005654f.js +1 -0
  155. package/.next/static/chunks/app/api/projects/route-019391920005654f.js +1 -0
  156. package/.next/static/chunks/app/api/prs/[id]/merge/route-019391920005654f.js +1 -0
  157. package/.next/static/chunks/app/api/runtime/terminal/route-019391920005654f.js +1 -0
  158. package/.next/static/chunks/app/api/sessions/[id]/kill/route-019391920005654f.js +1 -0
  159. package/.next/static/chunks/app/api/sessions/[id]/message/route-019391920005654f.js +1 -0
  160. package/.next/static/chunks/app/api/sessions/[id]/remap/route-019391920005654f.js +1 -0
  161. package/.next/static/chunks/app/api/sessions/[id]/restore/route-019391920005654f.js +1 -0
  162. package/.next/static/chunks/app/api/sessions/[id]/route-019391920005654f.js +1 -0
  163. package/.next/static/chunks/app/api/sessions/[id]/send/route-019391920005654f.js +1 -0
  164. package/.next/static/chunks/app/api/sessions/patches/route-019391920005654f.js +1 -0
  165. package/.next/static/chunks/app/api/sessions/route-019391920005654f.js +1 -0
  166. package/.next/static/chunks/app/api/setup-labels/route-019391920005654f.js +1 -0
  167. package/.next/static/chunks/app/api/spawn/route-019391920005654f.js +1 -0
  168. package/.next/static/chunks/app/api/verify/route-019391920005654f.js +1 -0
  169. package/.next/static/chunks/app/api/webhooks/[...slug]/route-019391920005654f.js +1 -0
  170. package/.next/static/chunks/app/apple-icon/route-019391920005654f.js +1 -0
  171. package/.next/static/chunks/app/dev/terminal-test/page-4912fa9e6459f124.js +1 -0
  172. package/.next/static/chunks/app/error-670f1d8bf2b6859c.js +1 -0
  173. package/.next/static/chunks/app/{global-error-7b5c8ae45329c659.js → global-error-ca06d2b1be2d4ae0.js} +1 -1
  174. package/.next/static/chunks/app/icon/route-019391920005654f.js +1 -0
  175. package/.next/static/chunks/app/icon-192/route-019391920005654f.js +1 -0
  176. package/.next/static/chunks/app/icon-512/route-019391920005654f.js +1 -0
  177. package/.next/static/chunks/app/layout-ddf79478d9a673bb.js +1 -0
  178. package/.next/static/chunks/app/loading-019391920005654f.js +1 -0
  179. package/.next/static/chunks/app/manifest.webmanifest/route-019391920005654f.js +1 -0
  180. package/.next/static/chunks/app/{not-found-3772c2e09c29d80f.js → not-found-824d5d3c6e296eeb.js} +1 -1
  181. package/.next/static/chunks/app/orchestrators/page-4da04720a09483d2.js +1 -0
  182. package/.next/static/chunks/app/page-abf3e3c98cac072f.js +1 -0
  183. package/.next/static/chunks/app/prs/page-46455c406183ffbf.js +1 -0
  184. package/.next/static/chunks/app/sessions/[id]/error-eb0973907da68a37.js +1 -0
  185. package/.next/static/chunks/app/sessions/[id]/loading-019391920005654f.js +1 -0
  186. package/.next/static/chunks/app/sessions/[id]/{not-found-3772c2e09c29d80f.js → not-found-824d5d3c6e296eeb.js} +1 -1
  187. package/.next/static/chunks/app/sessions/[id]/page-7eb67fd32f39b15f.js +1 -0
  188. package/.next/static/chunks/app/test-direct/page-1fd63edb9a9ca8be.js +1 -0
  189. package/.next/static/chunks/{df4ed4d4.6a752eba3933e9a8.js → df4ed4d4.b6997d8b8ce9d79b.js} +1 -1
  190. package/.next/static/chunks/framework-7060e2ac4971c604.js +1 -0
  191. package/.next/static/chunks/main-app-690acf9d5d2050c9.js +1 -0
  192. package/.next/static/chunks/main-ed1610689fbd6f0d.js +1 -0
  193. package/.next/static/chunks/pages/_app-f4baf4dbe88f6f54.js +1 -0
  194. package/.next/static/chunks/pages/_error-a7f6723f42093f29.js +1 -0
  195. package/.next/static/chunks/{webpack-e12ceebeb7a1cc7e.js → webpack-ab6c08c78ffc8113.js} +1 -1
  196. package/.next/static/css/a5398483e6b75ad3.css +1 -0
  197. package/LICENSE +21 -0
  198. package/dist-server/direct-terminal-ws.js +33 -236
  199. package/dist-server/mux-websocket.js +516 -0
  200. package/dist-server/start-all.js +0 -2
  201. package/next.config.js +1 -0
  202. package/package.json +27 -27
  203. package/.next/server/chunks/27.js +0 -438
  204. package/.next/server/chunks/377.js +0 -1
  205. package/.next/server/chunks/393.js +0 -1
  206. package/.next/server/chunks/639.js +0 -6
  207. package/.next/server/chunks/693.js +0 -1
  208. package/.next/server/chunks/705.js +0 -22
  209. package/.next/server/chunks/787.js +0 -29
  210. package/.next/server/chunks/796.js +0 -1
  211. package/.next/server/chunks/934.js +0 -1
  212. package/.next/server/chunks/956.js +0 -9
  213. package/.next/static/chunks/1250-e7cf6b069fbb03ed.js +0 -1
  214. package/.next/static/chunks/2205.498806f73783aa54.js +0 -1
  215. package/.next/static/chunks/3698-9c12c45b8184022c.js +0 -1
  216. package/.next/static/chunks/6381.1541d5695a727108.js +0 -1
  217. package/.next/static/chunks/7505-2d2422d31862995f.js +0 -1
  218. package/.next/static/chunks/8597-1385f90ec1cebf47.js +0 -1
  219. package/.next/static/chunks/8762.f3d526855363db16.js +0 -1
  220. package/.next/static/chunks/a51c26f2-a21e680a5df6764e.js +0 -1
  221. package/.next/static/chunks/app/_not-found/page-2224bc1d3dce1b3e.js +0 -1
  222. package/.next/static/chunks/app/api/backlog/route-2224bc1d3dce1b3e.js +0 -1
  223. package/.next/static/chunks/app/api/events/route-2224bc1d3dce1b3e.js +0 -1
  224. package/.next/static/chunks/app/api/issues/route-2224bc1d3dce1b3e.js +0 -1
  225. package/.next/static/chunks/app/api/observability/route-2224bc1d3dce1b3e.js +0 -1
  226. package/.next/static/chunks/app/api/orchestrators/route-2224bc1d3dce1b3e.js +0 -1
  227. package/.next/static/chunks/app/api/projects/route-2224bc1d3dce1b3e.js +0 -1
  228. package/.next/static/chunks/app/api/prs/[id]/merge/route-2224bc1d3dce1b3e.js +0 -1
  229. package/.next/static/chunks/app/api/runtime/terminal/route-2224bc1d3dce1b3e.js +0 -1
  230. package/.next/static/chunks/app/api/sessions/[id]/kill/route-2224bc1d3dce1b3e.js +0 -1
  231. package/.next/static/chunks/app/api/sessions/[id]/message/route-2224bc1d3dce1b3e.js +0 -1
  232. package/.next/static/chunks/app/api/sessions/[id]/remap/route-2224bc1d3dce1b3e.js +0 -1
  233. package/.next/static/chunks/app/api/sessions/[id]/restore/route-2224bc1d3dce1b3e.js +0 -1
  234. package/.next/static/chunks/app/api/sessions/[id]/route-2224bc1d3dce1b3e.js +0 -1
  235. package/.next/static/chunks/app/api/sessions/[id]/send/route-2224bc1d3dce1b3e.js +0 -1
  236. package/.next/static/chunks/app/api/sessions/route-2224bc1d3dce1b3e.js +0 -1
  237. package/.next/static/chunks/app/api/setup-labels/route-2224bc1d3dce1b3e.js +0 -1
  238. package/.next/static/chunks/app/api/spawn/route-2224bc1d3dce1b3e.js +0 -1
  239. package/.next/static/chunks/app/api/verify/route-2224bc1d3dce1b3e.js +0 -1
  240. package/.next/static/chunks/app/api/webhooks/[...slug]/route-2224bc1d3dce1b3e.js +0 -1
  241. package/.next/static/chunks/app/apple-icon/route-2224bc1d3dce1b3e.js +0 -1
  242. package/.next/static/chunks/app/dev/terminal-test/page-ac0ce5b046fcad82.js +0 -1
  243. package/.next/static/chunks/app/error-4896c9d3b7681a80.js +0 -1
  244. package/.next/static/chunks/app/icon/route-2224bc1d3dce1b3e.js +0 -1
  245. package/.next/static/chunks/app/icon-192/route-2224bc1d3dce1b3e.js +0 -1
  246. package/.next/static/chunks/app/icon-512/route-2224bc1d3dce1b3e.js +0 -1
  247. package/.next/static/chunks/app/layout-023f2083204e4f68.js +0 -1
  248. package/.next/static/chunks/app/loading-2224bc1d3dce1b3e.js +0 -1
  249. package/.next/static/chunks/app/manifest.webmanifest/route-2224bc1d3dce1b3e.js +0 -1
  250. package/.next/static/chunks/app/orchestrators/page-df85236df674fc5a.js +0 -1
  251. package/.next/static/chunks/app/page-e97da633b7ef25f2.js +0 -1
  252. package/.next/static/chunks/app/prs/page-8cc0fce584d238c5.js +0 -1
  253. package/.next/static/chunks/app/sessions/[id]/error-90bc99b777fecabf.js +0 -1
  254. package/.next/static/chunks/app/sessions/[id]/loading-2224bc1d3dce1b3e.js +0 -1
  255. package/.next/static/chunks/app/sessions/[id]/page-c9c95a8604786cff.js +0 -1
  256. package/.next/static/chunks/app/test-direct/page-16ceeb9f7664394a.js +0 -1
  257. package/.next/static/chunks/framework-f8b8ba0f71d38056.js +0 -1
  258. package/.next/static/chunks/main-2bc85c765bf1fc49.js +0 -1
  259. package/.next/static/chunks/main-app-93fb36c3bd1a6739.js +0 -1
  260. package/.next/static/chunks/pages/_app-a0b975794f15bd68.js +0 -1
  261. package/.next/static/chunks/pages/_error-5f4e3b5eea57917d.js +0 -1
  262. package/.next/static/css/908f93fdd7ffba42.css +0 -1
  263. package/.next/static/oB_TdR1wCu7ot1D0AG_cw/_buildManifest.js +0 -1
  264. package/dist-server/terminal-websocket.js +0 -375
  265. /package/.next/static/{oB_TdR1wCu7ot1D0AG_cw → OyzSw2fQpoJHXyoBWoZ2X}/_ssgManifest.js +0 -0
@@ -0,0 +1,516 @@
1
+ /**
2
+ * Multiplexed WebSocket server for terminal multiplexing.
3
+ * Manages multiple terminal connections over a single persistent WebSocket.
4
+ *
5
+ * Session updates are delivered via a single shared SSE connection from this
6
+ * process to Next.js /api/events, then broadcast to all subscribed clients.
7
+ * This replaces per-client HTTP polling and makes session updates event-driven.
8
+ */
9
+ import { WebSocketServer, WebSocket } from "ws";
10
+ import { homedir, userInfo } from "node:os";
11
+ import { spawn } from "node:child_process";
12
+ import { findTmux, resolveTmuxSession, validateSessionId } from "./tmux-utils.js";
13
+ /**
14
+ * Manages a single shared SSE connection to Next.js /api/events.
15
+ * Broadcasts session patches to all subscribed callbacks.
16
+ * Lazily connects on first subscriber, disconnects when the last one leaves.
17
+ */
18
+ export class SessionBroadcaster {
19
+ subscribers = new Set();
20
+ abortController = null;
21
+ reconnectTimer = null;
22
+ baseUrl;
23
+ constructor(nextPort) {
24
+ this.baseUrl = `http://localhost:${nextPort}`;
25
+ }
26
+ /**
27
+ * Subscribe to session patches. Returns an unsubscribe function.
28
+ * Sends an immediate snapshot to the new subscriber, then live SSE pushes.
29
+ */
30
+ subscribe(callback) {
31
+ const wasEmpty = this.subscribers.size === 0;
32
+ this.subscribers.add(callback);
33
+ // Immediately send a one-off snapshot to just this new subscriber
34
+ void this.fetchSnapshot().then((sessions) => {
35
+ if (sessions && this.subscribers.has(callback)) {
36
+ callback(sessions);
37
+ }
38
+ });
39
+ // Start the shared SSE connection if this is the first subscriber
40
+ if (wasEmpty) {
41
+ void this.connect();
42
+ }
43
+ return () => {
44
+ this.subscribers.delete(callback);
45
+ if (this.subscribers.size === 0) {
46
+ this.disconnect();
47
+ }
48
+ };
49
+ }
50
+ broadcast(sessions) {
51
+ for (const callback of this.subscribers) {
52
+ try {
53
+ callback(sessions);
54
+ }
55
+ catch (err) {
56
+ console.error("[MuxServer] Session broadcast subscriber threw:", err);
57
+ }
58
+ }
59
+ }
60
+ /** One-shot HTTP fetch of the current session list for immediate delivery. */
61
+ async fetchSnapshot() {
62
+ try {
63
+ const controller = new AbortController();
64
+ const timeoutId = setTimeout(() => controller.abort(), 4000);
65
+ try {
66
+ const res = await fetch(`${this.baseUrl}/api/sessions/patches`, {
67
+ signal: controller.signal,
68
+ });
69
+ clearTimeout(timeoutId);
70
+ if (!res.ok)
71
+ return null;
72
+ const data = (await res.json());
73
+ return data.sessions ?? null;
74
+ }
75
+ catch {
76
+ clearTimeout(timeoutId);
77
+ return null;
78
+ }
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ /** Open a persistent SSE connection and stream events to all subscribers. */
85
+ async connect() {
86
+ if (this.abortController)
87
+ return;
88
+ const controller = new AbortController();
89
+ this.abortController = controller;
90
+ const { signal } = controller;
91
+ try {
92
+ const res = await fetch(`${this.baseUrl}/api/events`, {
93
+ signal,
94
+ headers: { Accept: "text/event-stream" },
95
+ });
96
+ if (!res.ok || !res.body) {
97
+ throw new Error(`SSE connect failed: ${res.status}`);
98
+ }
99
+ const reader = res.body.getReader();
100
+ const decoder = new TextDecoder();
101
+ let buffer = "";
102
+ while (!signal.aborted) {
103
+ const { done, value } = await reader.read();
104
+ if (done)
105
+ break;
106
+ buffer += decoder.decode(value, { stream: true });
107
+ const lines = buffer.split("\n");
108
+ buffer = lines.pop() ?? "";
109
+ for (const line of lines) {
110
+ if (!line.startsWith("data: "))
111
+ continue;
112
+ try {
113
+ const event = JSON.parse(line.slice(6));
114
+ if (event.type === "snapshot" && event.sessions) {
115
+ this.broadcast(event.sessions);
116
+ }
117
+ }
118
+ catch {
119
+ // ignore malformed events
120
+ }
121
+ }
122
+ }
123
+ }
124
+ catch (err) {
125
+ if (signal.aborted)
126
+ return; // intentional disconnect, not an error
127
+ console.warn("[MuxServer] SSE connection lost:", err instanceof Error ? err.message : err);
128
+ }
129
+ finally {
130
+ // Only clear our own controller — a concurrent connect() may have already
131
+ // set a new one (e.g. disconnect() → subscribe() → connect() in the same tick).
132
+ if (this.abortController === controller) {
133
+ this.abortController = null;
134
+ }
135
+ }
136
+ // Reconnect with backoff if there are still subscribers
137
+ if (this.subscribers.size > 0) {
138
+ console.log("[MuxServer] SSE reconnecting in 5s");
139
+ this.reconnectTimer = setTimeout(() => {
140
+ this.reconnectTimer = null;
141
+ if (this.subscribers.size > 0)
142
+ void this.connect();
143
+ }, 5000);
144
+ }
145
+ }
146
+ disconnect() {
147
+ if (this.reconnectTimer) {
148
+ clearTimeout(this.reconnectTimer);
149
+ this.reconnectTimer = null;
150
+ }
151
+ this.abortController?.abort();
152
+ this.abortController = null;
153
+ }
154
+ }
155
+ let ptySpawn;
156
+ /* eslint-enable @typescript-eslint/consistent-type-imports */
157
+ try {
158
+ const nodePty = await import("node-pty");
159
+ ptySpawn = nodePty.spawn;
160
+ }
161
+ catch (err) {
162
+ console.warn("[MuxServer] node-pty not available — mux server will be disabled.", err);
163
+ }
164
+ const RING_BUFFER_MAX = 50 * 1024; // 50KB max per terminal
165
+ const MAX_REATTACH_ATTEMPTS = 3;
166
+ /**
167
+ * TerminalManager manages PTY processes independently of WebSocket connections.
168
+ * A single manager instance is shared across all mux connections.
169
+ */
170
+ class TerminalManager {
171
+ terminals = new Map();
172
+ TMUX;
173
+ constructor(tmuxPath) {
174
+ this.TMUX = tmuxPath ?? findTmux();
175
+ }
176
+ /**
177
+ * Open/attach to a terminal. If already open, just return.
178
+ * If has subscribers but PTY crashed, re-attach.
179
+ */
180
+ open(id) {
181
+ // Validate and resolve
182
+ if (!validateSessionId(id)) {
183
+ throw new Error(`Invalid session ID: ${id}`);
184
+ }
185
+ const tmuxSessionId = resolveTmuxSession(id, this.TMUX);
186
+ if (!tmuxSessionId) {
187
+ throw new Error(`Session not found: ${id}`);
188
+ }
189
+ // Get or create terminal entry
190
+ let terminal = this.terminals.get(id);
191
+ if (!terminal) {
192
+ terminal = {
193
+ id,
194
+ tmuxSessionId,
195
+ pty: null,
196
+ subscribers: new Set(),
197
+ exitCallbacks: new Set(),
198
+ buffer: [],
199
+ bufferBytes: 0,
200
+ reattachAttempts: 0,
201
+ };
202
+ this.terminals.set(id, terminal);
203
+ }
204
+ // If PTY is already attached, we're done
205
+ if (terminal.pty) {
206
+ return tmuxSessionId;
207
+ }
208
+ // Enable mouse mode
209
+ const mouseProc = spawn(this.TMUX, ["set-option", "-t", tmuxSessionId, "mouse", "on"]);
210
+ mouseProc.on("error", (err) => {
211
+ console.error(`[MuxServer] Failed to set mouse mode for ${tmuxSessionId}:`, err.message);
212
+ });
213
+ // Hide the status bar
214
+ const statusProc = spawn(this.TMUX, ["set-option", "-t", tmuxSessionId, "status", "off"]);
215
+ statusProc.on("error", (err) => {
216
+ console.error(`[MuxServer] Failed to hide status bar for ${tmuxSessionId}:`, err.message);
217
+ });
218
+ // Build environment
219
+ const homeDir = process.env.HOME || homedir();
220
+ const currentUser = process.env.USER || userInfo().username;
221
+ const env = {
222
+ HOME: homeDir,
223
+ SHELL: process.env.SHELL || "/bin/bash",
224
+ USER: currentUser,
225
+ PATH: process.env.PATH || "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin",
226
+ TERM: "xterm-256color",
227
+ LANG: process.env.LANG || "en_US.UTF-8",
228
+ TMPDIR: process.env.TMPDIR || "/tmp",
229
+ };
230
+ if (!ptySpawn) {
231
+ throw new Error("node-pty not available");
232
+ }
233
+ // Spawn PTY
234
+ const pty = ptySpawn(this.TMUX, ["attach-session", "-t", tmuxSessionId], {
235
+ name: "xterm-256color",
236
+ cols: 80,
237
+ rows: 24,
238
+ cwd: homeDir,
239
+ env,
240
+ });
241
+ terminal.pty = pty;
242
+ // Wire up data events
243
+ pty.onData((data) => {
244
+ // Push to all subscribers — isolate each callback so a throw in one
245
+ // (e.g. a closed ws.send) doesn't abort the loop or skip the buffer.
246
+ for (const callback of terminal.subscribers) {
247
+ try {
248
+ callback(data);
249
+ }
250
+ catch (err) {
251
+ console.error("[MuxServer] Subscriber callback threw:", err);
252
+ }
253
+ }
254
+ // Append to ring buffer
255
+ terminal.buffer.push(data);
256
+ terminal.bufferBytes += Buffer.byteLength(data, "utf8");
257
+ // Trim buffer if over limit
258
+ while (terminal.bufferBytes > RING_BUFFER_MAX && terminal.buffer.length > 0) {
259
+ const removed = terminal.buffer.shift() ?? "";
260
+ terminal.bufferBytes -= Buffer.byteLength(removed, "utf8");
261
+ }
262
+ });
263
+ // Handle PTY exit
264
+ pty.onExit(({ exitCode }) => {
265
+ console.log(`[MuxServer] PTY exited for ${id} with code ${exitCode}`);
266
+ terminal.pty = null;
267
+ // Re-attach if subscribers are still present, up to MAX_REATTACH_ATTEMPTS.
268
+ // The cap prevents an unbounded respawn loop when the PTY crashes immediately
269
+ // after every attach (e.g. resource exhaustion or a broken tmux session).
270
+ if (terminal.subscribers.size > 0 && terminal.reattachAttempts < MAX_REATTACH_ATTEMPTS) {
271
+ terminal.reattachAttempts += 1;
272
+ console.log(`[MuxServer] Re-attaching to ${id} (attempt ${terminal.reattachAttempts}/${MAX_REATTACH_ATTEMPTS})`);
273
+ try {
274
+ this.open(id);
275
+ terminal.reattachAttempts = 0; // reset on successful attach
276
+ return; // re-attached — don't notify exit
277
+ }
278
+ catch (err) {
279
+ console.error(`[MuxServer] Failed to re-attach ${id}:`, err);
280
+ }
281
+ }
282
+ else if (terminal.reattachAttempts >= MAX_REATTACH_ATTEMPTS) {
283
+ console.error(`[MuxServer] Max re-attach attempts reached for ${id}, giving up`);
284
+ }
285
+ // Notify subscribers that the terminal has exited (re-attach failed or no subscribers)
286
+ for (const cb of terminal.exitCallbacks) {
287
+ cb(exitCode);
288
+ }
289
+ });
290
+ console.log(`[MuxServer] Opened terminal ${id} (tmux: ${tmuxSessionId})`);
291
+ return tmuxSessionId;
292
+ }
293
+ /**
294
+ * Write data to the PTY if attached
295
+ */
296
+ write(id, data) {
297
+ const terminal = this.terminals.get(id);
298
+ if (terminal?.pty) {
299
+ terminal.pty.write(data);
300
+ }
301
+ }
302
+ /**
303
+ * Resize the PTY if attached
304
+ */
305
+ resize(id, cols, rows) {
306
+ const terminal = this.terminals.get(id);
307
+ if (terminal?.pty) {
308
+ terminal.pty.resize(cols, rows);
309
+ }
310
+ }
311
+ /**
312
+ * Subscribe to terminal data. Returns unsubscribe function.
313
+ * Automatically opens the terminal if needed.
314
+ * @param onExit - called when the PTY exits and cannot be re-attached
315
+ */
316
+ subscribe(id, callback, onExit) {
317
+ // Ensure terminal is open
318
+ this.open(id);
319
+ const terminal = this.terminals.get(id);
320
+ if (!terminal) {
321
+ throw new Error(`Failed to open terminal: ${id}`);
322
+ }
323
+ // Add subscriber
324
+ terminal.subscribers.add(callback);
325
+ if (onExit)
326
+ terminal.exitCallbacks.add(onExit);
327
+ // Return unsubscribe function
328
+ return () => {
329
+ terminal.subscribers.delete(callback);
330
+ if (onExit)
331
+ terminal.exitCallbacks.delete(onExit);
332
+ // Kill PTY and clean up when the last subscriber leaves
333
+ if (terminal.subscribers.size === 0) {
334
+ if (terminal.pty) {
335
+ terminal.pty.kill();
336
+ terminal.pty = null;
337
+ }
338
+ this.terminals.delete(id);
339
+ }
340
+ };
341
+ }
342
+ /**
343
+ * Get buffered data for a terminal
344
+ */
345
+ getBuffer(id) {
346
+ const terminal = this.terminals.get(id);
347
+ if (!terminal)
348
+ return "";
349
+ return terminal.buffer.join("");
350
+ }
351
+ }
352
+ /**
353
+ * Create a mux WebSocket server (noServer mode).
354
+ * Returns the WebSocketServer instance for manual upgrade routing.
355
+ */
356
+ export function createMuxWebSocket(tmuxPath) {
357
+ if (!ptySpawn) {
358
+ console.warn("[MuxServer] node-pty not available — mux WebSocket will be disabled");
359
+ return null;
360
+ }
361
+ const terminalManager = new TerminalManager(tmuxPath);
362
+ const nextPort = process.env.PORT || "3000";
363
+ const broadcaster = new SessionBroadcaster(nextPort);
364
+ const wss = new WebSocketServer({ noServer: true });
365
+ wss.on("connection", (ws) => {
366
+ console.log("[MuxServer] New mux connection");
367
+ const subscriptions = new Map();
368
+ let sessionUnsubscribe = null;
369
+ let missedPongs = 0;
370
+ const MAX_MISSED_PONGS = 3;
371
+ // Heartbeat: send native WebSocket ping every 15s.
372
+ // Browsers automatically respond to native pings with pong frames —
373
+ // no application-level code is needed on the client side.
374
+ const heartbeatInterval = setInterval(() => {
375
+ if (ws.readyState === WebSocket.OPEN) {
376
+ // Send the ping first so it counts as a sent-but-unanswered probe
377
+ ws.ping();
378
+ missedPongs += 1;
379
+ if (missedPongs >= MAX_MISSED_PONGS) {
380
+ console.log("[MuxServer] Too many missed pongs, terminating connection");
381
+ ws.terminate();
382
+ }
383
+ }
384
+ }, 15_000);
385
+ // Native pong resets the missed counter
386
+ ws.on("pong", () => {
387
+ missedPongs = 0;
388
+ });
389
+ /**
390
+ * Handle incoming messages
391
+ */
392
+ ws.on("message", (data) => {
393
+ try {
394
+ const msg = JSON.parse(data.toString("utf8"));
395
+ if (msg.ch === "system") {
396
+ if (msg.type === "ping") {
397
+ const pong = { ch: "system", type: "pong" };
398
+ ws.send(JSON.stringify(pong));
399
+ }
400
+ }
401
+ else if (msg.ch === "terminal") {
402
+ const { id, type } = msg;
403
+ try {
404
+ if (type === "open") {
405
+ // Validate session exists
406
+ terminalManager.open(id);
407
+ // Send opened confirmation (idempotent — safe to send on re-open)
408
+ const openedMsg = { ch: "terminal", id, type: "opened" };
409
+ ws.send(JSON.stringify(openedMsg));
410
+ // Subscribe and send history buffer only for new subscribers.
411
+ // Skipping the buffer on re-open prevents duplicate output when
412
+ // MuxProvider re-sends open for all terminals on reconnect.
413
+ if (!subscriptions.has(id)) {
414
+ // Send buffered history to catch up the new subscriber
415
+ const buffer = terminalManager.getBuffer(id);
416
+ if (buffer) {
417
+ const bufferMsg = {
418
+ ch: "terminal",
419
+ id,
420
+ type: "data",
421
+ data: buffer,
422
+ };
423
+ ws.send(JSON.stringify(bufferMsg));
424
+ }
425
+ const unsub = terminalManager.subscribe(id, (data) => {
426
+ const dataMsg = {
427
+ ch: "terminal",
428
+ id,
429
+ type: "data",
430
+ data,
431
+ };
432
+ if (ws.readyState === WebSocket.OPEN) {
433
+ ws.send(JSON.stringify(dataMsg));
434
+ }
435
+ }, (exitCode) => {
436
+ const exitedMsg = { ch: "terminal", id, type: "exited", code: exitCode };
437
+ if (ws.readyState === WebSocket.OPEN) {
438
+ ws.send(JSON.stringify(exitedMsg));
439
+ }
440
+ });
441
+ subscriptions.set(id, unsub);
442
+ }
443
+ }
444
+ else if (type === "data" && "data" in msg) {
445
+ terminalManager.write(id, msg.data);
446
+ }
447
+ else if (type === "resize" && "cols" in msg && "rows" in msg) {
448
+ terminalManager.resize(id, msg.cols, msg.rows);
449
+ }
450
+ else if (type === "close") {
451
+ // Unsubscribe this client only — TerminalManager is shared across
452
+ // all mux connections so we must not kill the PTY here.
453
+ const unsub = subscriptions.get(id);
454
+ if (unsub) {
455
+ unsub();
456
+ subscriptions.delete(id);
457
+ }
458
+ }
459
+ }
460
+ catch (err) {
461
+ if (ws.readyState === WebSocket.OPEN) {
462
+ const errorMsg = {
463
+ ch: "terminal",
464
+ id,
465
+ type: "error",
466
+ message: err instanceof Error ? err.message : String(err),
467
+ };
468
+ ws.send(JSON.stringify(errorMsg));
469
+ }
470
+ }
471
+ }
472
+ else if (msg.ch === "subscribe") {
473
+ if (msg.topics.includes("sessions") && !sessionUnsubscribe) {
474
+ sessionUnsubscribe = broadcaster.subscribe((sessions) => {
475
+ if (ws.readyState === WebSocket.OPEN) {
476
+ const snapMsg = { ch: "sessions", type: "snapshot", sessions };
477
+ ws.send(JSON.stringify(snapMsg));
478
+ }
479
+ });
480
+ }
481
+ }
482
+ }
483
+ catch (err) {
484
+ console.error("[MuxServer] Failed to parse message:", err);
485
+ const errorMsg = {
486
+ ch: "system",
487
+ type: "error",
488
+ message: "Invalid message format",
489
+ };
490
+ if (ws.readyState === WebSocket.OPEN) {
491
+ ws.send(JSON.stringify(errorMsg));
492
+ }
493
+ }
494
+ });
495
+ /**
496
+ * Handle connection close
497
+ */
498
+ ws.on("close", () => {
499
+ console.log("[MuxServer] Mux connection closed");
500
+ clearInterval(heartbeatInterval);
501
+ sessionUnsubscribe?.();
502
+ sessionUnsubscribe = null;
503
+ for (const unsub of subscriptions.values()) {
504
+ unsub();
505
+ }
506
+ subscriptions.clear();
507
+ });
508
+ // In the ws library, "error" is always followed by "close", so the close
509
+ // handler below handles all cleanup. Log the error here and nothing more.
510
+ ws.on("error", (err) => {
511
+ console.error("[MuxServer] WebSocket error:", err.message);
512
+ });
513
+ });
514
+ console.log("[MuxServer] Mux WebSocket server created (noServer mode)");
515
+ return wss;
516
+ }
@@ -77,8 +77,6 @@ function resolveNextBin() {
77
77
  // Start Next.js production server
78
78
  const port = process.env["PORT"] || "3000";
79
79
  spawnProcess("next", resolveNextBin(), ["start", "-p", port]);
80
- // Start terminal WebSocket server (auto-restart on crash)
81
- spawnProcess("terminal", "node", [resolve(__dirname, "terminal-websocket.js")], { restart: true });
82
80
  // Start direct terminal WebSocket server (auto-restart on crash)
83
81
  spawnProcess("direct-terminal", "node", [resolve(__dirname, "direct-terminal-ws.js")], { restart: true });
84
82
  // Graceful shutdown — send SIGTERM to children and wait for them to exit
package/next.config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /** @type {import('next').NextConfig} */
2
2
  const nextConfig = {
3
+ serverExternalPackages: ["@composio/core"],
3
4
  transpilePackages: [
4
5
  "@aoagents/ao-core",
5
6
  "@aoagents/ao-plugin-agent-claude-code",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aoagents/ao-web",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Web dashboard for agent-orchestrator",
5
5
  "type": "module",
6
6
  "files": [
@@ -11,31 +11,7 @@
11
11
  "dist-server",
12
12
  "next.config.js"
13
13
  ],
14
- "scripts": {
15
- "dev": "concurrently \"npm:dev:next\" \"npm:dev:terminal\" \"npm:dev:direct-terminal\"",
16
- "dev:next": "next dev -p ${PORT:-3000}",
17
- "dev:terminal": "tsx watch server/terminal-websocket.ts",
18
- "dev:direct-terminal": "tsx watch server/direct-terminal-ws.ts",
19
- "build": "next build && tsc -p tsconfig.server.json",
20
- "start": "next start",
21
- "start:all": "node dist-server/start-all.js",
22
- "dev:optimized": "next build && tsc -p tsconfig.server.json && node dist-server/start-all.js",
23
- "typecheck": "tsc --noEmit",
24
- "test": "vitest run",
25
- "test:watch": "vitest",
26
- "clean": "rm -rf .next dist-server",
27
- "screenshot": "tsx e2e/screenshot.ts",
28
- "screenshot:install": "npx playwright install chromium"
29
- },
30
14
  "dependencies": {
31
- "@aoagents/ao-core": "workspace:*",
32
- "@aoagents/ao-plugin-agent-claude-code": "workspace:*",
33
- "@aoagents/ao-plugin-agent-opencode": "workspace:*",
34
- "@aoagents/ao-plugin-runtime-tmux": "workspace:*",
35
- "@aoagents/ao-plugin-scm-github": "workspace:*",
36
- "@aoagents/ao-plugin-tracker-github": "workspace:*",
37
- "@aoagents/ao-plugin-tracker-linear": "workspace:*",
38
- "@aoagents/ao-plugin-workspace-worktree": "workspace:*",
39
15
  "@xterm/addon-fit": "^0.11.0",
40
16
  "@xterm/addon-web-links": "^0.12.0",
41
17
  "next": "^15.1.0",
@@ -44,7 +20,16 @@
44
20
  "react-dom": "^19.0.0",
45
21
  "server-only": "^0.0.1",
46
22
  "ws": "^8.19.0",
47
- "xterm": "^5.3.0"
23
+ "xterm": "^5.3.0",
24
+ "@aoagents/ao-plugin-agent-claude-code": "0.2.5",
25
+ "@aoagents/ao-core": "0.2.5",
26
+ "@aoagents/ao-plugin-agent-cursor": "0.1.0",
27
+ "@aoagents/ao-plugin-agent-opencode": "0.2.5",
28
+ "@aoagents/ao-plugin-scm-github": "0.2.5",
29
+ "@aoagents/ao-plugin-tracker-linear": "0.2.5",
30
+ "@aoagents/ao-plugin-runtime-tmux": "0.2.5",
31
+ "@aoagents/ao-plugin-tracker-github": "0.2.5",
32
+ "@aoagents/ao-plugin-workspace-worktree": "0.2.5"
48
33
  },
49
34
  "optionalDependencies": {
50
35
  "node-pty": "^1.1.0"
@@ -67,5 +52,20 @@
67
52
  "tsx": "^4.19.0",
68
53
  "typescript": "^5.7.0",
69
54
  "vitest": "^2.1.0"
55
+ },
56
+ "scripts": {
57
+ "dev": "concurrently \"npm:dev:next\" \"npm:dev:direct-terminal\"",
58
+ "dev:next": "next dev -p ${PORT:-3000}",
59
+ "dev:direct-terminal": "tsx watch server/direct-terminal-ws.ts",
60
+ "build": "next build && tsc -p tsconfig.server.json",
61
+ "start": "next start",
62
+ "start:all": "node dist-server/start-all.js",
63
+ "dev:optimized": "next build && tsc -p tsconfig.server.json && node dist-server/start-all.js",
64
+ "typecheck": "tsc --noEmit",
65
+ "test": "vitest run",
66
+ "test:watch": "vitest",
67
+ "clean": "rm -rf .next dist-server",
68
+ "screenshot": "tsx e2e/screenshot.ts",
69
+ "screenshot:install": "npx playwright install chromium"
70
70
  }
71
- }
71
+ }