onbuzz 4.9.13 → 4.10.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 (451) hide show
  1. package/node_modules/glob/README.md +31 -5
  2. package/node_modules/glob/dist/commonjs/glob.d.ts +8 -0
  3. package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -1
  4. package/node_modules/glob/dist/commonjs/glob.js +2 -1
  5. package/node_modules/glob/dist/commonjs/glob.js.map +1 -1
  6. package/node_modules/glob/dist/commonjs/index.min.js +3 -3
  7. package/node_modules/glob/dist/commonjs/index.min.js.map +4 -4
  8. package/node_modules/glob/dist/commonjs/pattern.d.ts +3 -0
  9. package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -1
  10. package/node_modules/glob/dist/commonjs/pattern.js +4 -0
  11. package/node_modules/glob/dist/commonjs/pattern.js.map +1 -1
  12. package/node_modules/glob/dist/esm/glob.d.ts +8 -0
  13. package/node_modules/glob/dist/esm/glob.d.ts.map +1 -1
  14. package/node_modules/glob/dist/esm/glob.js +2 -1
  15. package/node_modules/glob/dist/esm/glob.js.map +1 -1
  16. package/node_modules/glob/dist/esm/index.min.js +3 -3
  17. package/node_modules/glob/dist/esm/index.min.js.map +4 -4
  18. package/node_modules/glob/dist/esm/pattern.d.ts +3 -0
  19. package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -1
  20. package/node_modules/glob/dist/esm/pattern.js +4 -0
  21. package/node_modules/glob/dist/esm/pattern.js.map +1 -1
  22. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/README.md +7 -10
  23. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/package.json +7 -18
  24. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/README.md +3 -6
  25. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.js +6 -4
  26. package/node_modules/glob/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -0
  27. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.js +6 -4
  28. package/node_modules/glob/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
  29. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/package.json +11 -7
  30. package/node_modules/glob/node_modules/minimatch/README.md +76 -1
  31. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +1 -1
  32. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -1
  33. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -1
  34. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +4 -2
  35. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -1
  36. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +309 -55
  37. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -1
  38. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -1
  39. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +2 -4
  40. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -1
  41. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +1 -1
  42. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -1
  43. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +4 -4
  44. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -1
  45. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +81 -1
  46. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -1
  47. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +232 -134
  48. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -1
  49. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +1 -1
  50. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -1
  51. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +8 -8
  52. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -1
  53. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +1 -1
  54. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -1
  55. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
  56. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +4 -2
  57. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -1
  58. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +309 -55
  59. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -1
  60. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -1
  61. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +2 -4
  62. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
  63. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +1 -1
  64. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -1
  65. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +4 -4
  66. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -1
  67. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +81 -1
  68. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -1
  69. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +232 -134
  70. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -1
  71. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +1 -1
  72. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -1
  73. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +8 -8
  74. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
  75. package/node_modules/glob/node_modules/minimatch/package.json +17 -11
  76. package/node_modules/glob/package.json +10 -13
  77. package/node_modules/minipass/LICENSE.md +55 -0
  78. package/node_modules/minipass/dist/commonjs/index.d.ts +12 -16
  79. package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -1
  80. package/node_modules/minipass/dist/commonjs/index.js +13 -3
  81. package/node_modules/minipass/dist/commonjs/index.js.map +1 -1
  82. package/node_modules/minipass/dist/esm/index.d.ts +12 -16
  83. package/node_modules/minipass/dist/esm/index.d.ts.map +1 -1
  84. package/node_modules/minipass/dist/esm/index.js +3 -1
  85. package/node_modules/minipass/dist/esm/index.js.map +1 -1
  86. package/node_modules/minipass/package.json +9 -14
  87. package/node_modules/path-scurry/node_modules/lru-cache/README.md +96 -10
  88. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.d.ts.map +1 -0
  89. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.js.map +1 -0
  90. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.d.ts +5 -0
  91. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
  92. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts +1400 -0
  93. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts.map +1 -0
  94. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
  95. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js.map +1 -0
  96. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
  97. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js.map +7 -0
  98. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts +12 -0
  99. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts.map +1 -0
  100. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
  101. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js.map +1 -0
  102. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.cjs.map +1 -0
  103. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
  104. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.d.ts +5 -0
  105. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
  106. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +109 -32
  107. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -1
  108. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +334 -197
  109. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -1
  110. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +1 -1
  111. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +4 -4
  112. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.d.ts.map +1 -0
  113. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.js.map +1 -0
  114. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.d.ts +5 -0
  115. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
  116. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts +1400 -0
  117. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts.map +1 -0
  118. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
  119. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js.map +1 -0
  120. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
  121. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js.map +7 -0
  122. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts +12 -0
  123. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts.map +1 -0
  124. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
  125. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js.map +1 -0
  126. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts +12 -0
  127. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts.map +1 -0
  128. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
  129. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js.map +1 -0
  130. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.d.ts.map +1 -0
  131. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.js.map +1 -0
  132. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.d.ts +5 -0
  133. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
  134. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts +1400 -0
  135. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts.map +1 -0
  136. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
  137. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js.map +1 -0
  138. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
  139. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js.map +7 -0
  140. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts +12 -0
  141. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts.map +1 -0
  142. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
  143. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js.map +1 -0
  144. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
  145. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.mjs.map +1 -0
  146. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.d.ts +5 -0
  147. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
  148. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +109 -32
  149. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -1
  150. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +333 -196
  151. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -1
  152. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +1 -1
  153. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +4 -4
  154. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.d.ts.map +1 -0
  155. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.js.map +1 -0
  156. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.d.ts +5 -0
  157. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
  158. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts +1400 -0
  159. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts.map +1 -0
  160. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
  161. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js.map +1 -0
  162. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
  163. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js.map +7 -0
  164. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts +12 -0
  165. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts.map +1 -0
  166. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
  167. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js.map +1 -0
  168. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts +12 -0
  169. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts.map +1 -0
  170. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
  171. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js.map +1 -0
  172. package/node_modules/path-scurry/node_modules/lru-cache/package.json +71 -18
  173. package/node_modules/path-scurry/package.json +8 -24
  174. package/package.json +1 -1
  175. package/scripts/debug-balance-probe.mjs +35 -35
  176. package/scripts/push-image.sh +43 -43
  177. package/scripts/setup-acr.sh +65 -65
  178. package/scripts/verify-optional-deps.js +96 -1
  179. package/src/__tests__/composioCliFlags.test.js +239 -239
  180. package/src/analyzers/CSSAnalyzer.js +298 -297
  181. package/src/analyzers/ConfigValidator.js +691 -690
  182. package/src/analyzers/ESLintAnalyzer.js +320 -320
  183. package/src/analyzers/JavaScriptAnalyzer.js +260 -261
  184. package/src/analyzers/PrettierFormatter.js +246 -247
  185. package/src/analyzers/PythonAnalyzer.js +283 -283
  186. package/src/analyzers/SecurityAnalyzer.js +729 -729
  187. package/src/analyzers/SparrowAnalyzer.js +341 -341
  188. package/src/analyzers/TypeScriptAnalyzer.js +247 -247
  189. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -41
  190. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -362
  191. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -40
  192. package/src/analyzers/__tests__/PythonAnalyzer.test.js +205 -208
  193. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -303
  194. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -187
  195. package/src/analyzers/codeCloneDetector/analyzer.js +344 -344
  196. package/src/analyzers/codeCloneDetector/detector.js +250 -250
  197. package/src/analyzers/codeCloneDetector/index.js +194 -192
  198. package/src/analyzers/codeCloneDetector/parser.js +199 -199
  199. package/src/core/__tests__/agentPool.test.js +866 -866
  200. package/src/core/__tests__/agentPoolAutoResume.test.js +209 -209
  201. package/src/core/__tests__/agentPoolWakeOnMessage.test.js +315 -315
  202. package/src/core/__tests__/agentScheduler.emptyResponseChatStall.test.js +213 -213
  203. package/src/core/__tests__/agentScheduler.errorCategorisation.test.js +246 -246
  204. package/src/core/__tests__/agentScheduler.firstChunkTimeout.test.js +138 -138
  205. package/src/core/__tests__/agentScheduler.modeTransitions.test.js +233 -233
  206. package/src/core/__tests__/agentScheduler.nativePromptPick.test.js +319 -319
  207. package/src/core/__tests__/agentScheduler.taskLifecycleInstruction.test.js +78 -78
  208. package/src/core/__tests__/agentScheduler.visualizer.test.js +258 -258
  209. package/src/core/__tests__/flowCheckpointStore.test.js +140 -140
  210. package/src/core/__tests__/flowEndToEnd.test.js +565 -565
  211. package/src/core/__tests__/flowFieldMapping.test.js +188 -189
  212. package/src/core/__tests__/flowLintClientMirror.test.js +96 -98
  213. package/src/core/__tests__/flowSavePayload.test.js +170 -169
  214. package/src/core/__tests__/flowTemplates.test.js +311 -311
  215. package/src/core/__tests__/flowVersionStore.test.js +123 -123
  216. package/src/core/__tests__/messageProcessor.test.js +669 -669
  217. package/src/core/__tests__/stateManager.test.js +0 -1
  218. package/src/core/agentPool.js +2474 -2475
  219. package/src/core/agentScheduler.js +1 -4
  220. package/src/core/contextManager.js +708 -708
  221. package/src/core/flowExecutor.js +1510 -1510
  222. package/src/core/flowFieldMapping.js +136 -138
  223. package/src/core/messageProcessor.js +953 -954
  224. package/src/core/orchestrator.js +593 -595
  225. package/src/core/stateManager.js +1765 -1752
  226. package/src/index.js +1221 -1221
  227. package/src/interfaces/__tests__/archivedAgentDelete.test.js +207 -207
  228. package/src/interfaces/__tests__/bulkAgentRoute.test.js +361 -361
  229. package/src/interfaces/__tests__/imageServing.test.js +228 -228
  230. package/src/interfaces/__tests__/remoteSessionAuth.test.js +308 -308
  231. package/src/interfaces/__tests__/videoJobsRoutes.test.js +178 -179
  232. package/src/interfaces/__tests__/webServer.marketplace.test.js +629 -629
  233. package/src/interfaces/schedulerRoutes.js +50 -50
  234. package/src/interfaces/terminal/__tests__/smoke/connection.test.js +341 -350
  235. package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -156
  236. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +325 -330
  237. package/src/interfaces/terminal/__tests__/smoke/tools.test.js +385 -388
  238. package/src/interfaces/terminal/api/session.js +265 -266
  239. package/src/interfaces/terminal/api/websocket.js +496 -497
  240. package/src/interfaces/terminal/components/AgentCreator.js +691 -705
  241. package/src/interfaces/terminal/components/AgentEditor.js +676 -678
  242. package/src/interfaces/terminal/components/AgentSwitcher.js +331 -330
  243. package/src/interfaces/terminal/components/ErrorPanel.js +263 -264
  244. package/src/interfaces/terminal/components/Header.js +28 -28
  245. package/src/interfaces/terminal/components/Layout.js +598 -603
  246. package/src/interfaces/terminal/components/MessageList.js +280 -281
  247. package/src/interfaces/terminal/components/SettingsPanel.js +410 -415
  248. package/src/interfaces/terminal/components/StatusBar.js +2 -0
  249. package/src/interfaces/terminal/index.js +168 -168
  250. package/src/interfaces/terminal/state/useAgentControl.js +496 -496
  251. package/src/interfaces/terminal/state/useAgents.js +537 -537
  252. package/src/interfaces/terminal/state/useMessages.js +629 -630
  253. package/src/interfaces/terminal/state/useTools.js +554 -554
  254. package/src/interfaces/terminal/utils/debugLogger.js +44 -44
  255. package/src/interfaces/terminal/utils/settingsStorage.js +232 -232
  256. package/src/interfaces/webServer.js +7578 -7579
  257. package/src/interfaces/webServer.js.bak +7046 -7046
  258. package/src/modules/fileExplorer/__tests__/zipDownload.test.js +237 -237
  259. package/src/modules/fileExplorer/controller.js +470 -469
  260. package/src/modules/fileExplorer/routes.js +285 -286
  261. package/src/modules/widget/__tests__/isDisabled.test.js +41 -41
  262. package/src/modules/widget/__tests__/routes.test.js +677 -678
  263. package/src/modules/widget/__tests__/runtime.test.js +401 -401
  264. package/src/modules/widget/__tests__/versioning.test.js +309 -309
  265. package/src/modules/widget/__tests__/webComponentRuntime.test.js +565 -565
  266. package/src/modules/widget/__tests__/widgetTool.test.js +316 -316
  267. package/src/modules/widget/routes.js +435 -435
  268. package/src/modules/widget/runtime/bundle.js +640 -640
  269. package/src/modules/widget/runtime/webComponentBundle.js +470 -470
  270. package/src/modules/widget/schema.js +182 -181
  271. package/src/modules/widget/widgetTool.js +1389 -1389
  272. package/src/services/__tests__/agentActivityService.test.js +401 -402
  273. package/src/services/__tests__/benchmarkService.test.js +184 -184
  274. package/src/services/__tests__/contextInjectionService.test.js +246 -246
  275. package/src/services/__tests__/conversationQuery.test.js +721 -723
  276. package/src/services/__tests__/credentialVault.test.js +469 -469
  277. package/src/services/__tests__/discordService.integration.test.js +638 -639
  278. package/src/services/__tests__/flowContextService.test.js +590 -590
  279. package/src/services/__tests__/memoryService.test.js +1 -1
  280. package/src/services/__tests__/messageSource.test.js +380 -380
  281. package/src/services/__tests__/modelRouterNaming.test.js +111 -111
  282. package/src/services/__tests__/projectDetector.test.js +34 -34
  283. package/src/services/__tests__/promptService.test.js +242 -242
  284. package/src/services/__tests__/telegramService.test.js +941 -941
  285. package/src/services/__tests__/tokenCountingService.test.js +48 -48
  286. package/src/services/agentActivityService.js +419 -420
  287. package/src/services/aiService.js +2997 -3001
  288. package/src/services/apiKeyManager.js +359 -359
  289. package/src/services/benchmarkService.js +196 -196
  290. package/src/services/codebaseKnowledgeService.js +2 -2
  291. package/src/services/composioService.js +738 -738
  292. package/src/services/conversationCompactionService.js +1258 -1257
  293. package/src/services/credentialVault.js +685 -685
  294. package/src/services/discordService.js +792 -793
  295. package/src/services/embeddings/__tests__/azureCustomProvider.test.js +232 -232
  296. package/src/services/embeddings/__tests__/embeddingService.test.js +417 -417
  297. package/src/services/embeddings/__tests__/localProvider.test.js +263 -263
  298. package/src/services/embeddings/autoRecall.js +218 -219
  299. package/src/services/embeddings/indexers/__tests__/agentIndexer.test.js +232 -232
  300. package/src/services/embeddings/indexers/__tests__/memoryIndexer.test.js +418 -418
  301. package/src/services/embeddings/indexers/__tests__/reminisceIndexer.test.js +356 -357
  302. package/src/services/embeddings/indexers/__tests__/skillsIndexer.test.js +145 -145
  303. package/src/services/embeddings/indexers/__tests__/taskIndexer.test.js +146 -146
  304. package/src/services/embeddings/indexers/composioIndexer.js +279 -279
  305. package/src/services/embeddings/providerInterface.js +206 -206
  306. package/src/services/embeddings/providers/localProvider.js +11 -7
  307. package/src/services/embeddings/providers/openaiProvider.js +101 -101
  308. package/src/services/embeddings/vectorStore/inMemoryJsonStore.js +356 -356
  309. package/src/services/errorHandler.js +809 -809
  310. package/src/services/flowContextService.js +586 -586
  311. package/src/services/grounding/MockAdapter.js +125 -125
  312. package/src/services/modelRouterService.js +26 -31
  313. package/src/services/modelsService.js +322 -322
  314. package/src/services/ollamaService.js +452 -452
  315. package/src/services/projectDetector.js +403 -404
  316. package/src/services/promptService.js +418 -418
  317. package/src/services/qualityInspector.js +795 -795
  318. package/src/services/scheduleService.js +726 -726
  319. package/src/services/serviceRegistry.js +386 -386
  320. package/src/services/telegrafBot.js +174 -174
  321. package/src/services/telegramService.js +1972 -1972
  322. package/src/services/visualEditorBridge.js +1033 -1033
  323. package/src/services/visualEditorServer.js +1769 -1774
  324. package/src/services/whatsappService.js +667 -668
  325. package/src/tools/__tests__/agentCommunicationTool.findAgent.test.js +226 -226
  326. package/src/tools/__tests__/agentCommunicationTool.test.js +3 -3
  327. package/src/tools/__tests__/agentDelayTool.test.js +342 -342
  328. package/src/tools/__tests__/baseTool.test.js +3 -3
  329. package/src/tools/__tests__/codeMapTool.test.js +915 -915
  330. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -309
  331. package/src/tools/__tests__/fileTreeTool.test.js +274 -274
  332. package/src/tools/__tests__/filesystemTool.test.js +815 -815
  333. package/src/tools/__tests__/foundryWebSearchTool.test.js +252 -252
  334. package/src/tools/__tests__/imageTool.validator.test.js +194 -194
  335. package/src/tools/__tests__/jobDoneTool.test.js +580 -581
  336. package/src/tools/__tests__/memoryTool.forgetStale.test.js +272 -272
  337. package/src/tools/__tests__/memoryTool.reminisce.test.js +2 -2
  338. package/src/tools/__tests__/memoryTool.reminisceSemanticSearch.test.js +301 -301
  339. package/src/tools/__tests__/memoryTool.semanticSearch.test.js +405 -405
  340. package/src/tools/__tests__/memoryTool.teamPool.test.js +293 -293
  341. package/src/tools/__tests__/memoryTool.test.js +1 -1
  342. package/src/tools/__tests__/seekTool.test.js +282 -282
  343. package/src/tools/__tests__/skillsTool.search.test.js +164 -164
  344. package/src/tools/__tests__/skillsTool.test.js +226 -226
  345. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -509
  346. package/src/tools/__tests__/taskManagerTool.discipline.test.js +137 -137
  347. package/src/tools/__tests__/taskManagerTool.search.test.js +143 -143
  348. package/src/tools/__tests__/taskManagerTool.test.js +866 -866
  349. package/src/tools/__tests__/terminalTool.test.js +448 -448
  350. package/src/tools/__tests__/toolShapeForgiveness.test.js +259 -260
  351. package/src/tools/__tests__/userPromptTool.test.js +297 -297
  352. package/src/tools/__tests__/videoTool.jobs.test.js +147 -147
  353. package/src/tools/__tests__/webTool.e2e.test.js +609 -603
  354. package/src/tools/__tests__/webTool.unit.test.js +195 -195
  355. package/src/tools/__tests__/webTool.visionModel.test.js +75 -75
  356. package/src/tools/agentCommunicationTool.js +8 -10
  357. package/src/tools/agentDelayTool.js +496 -497
  358. package/src/tools/asyncToolManager.js +602 -603
  359. package/src/tools/baseTool.js +12 -11
  360. package/src/tools/cloneDetectionTool.js +576 -581
  361. package/src/tools/codeMapTool.js +0 -6
  362. package/src/tools/composioTool.js +617 -617
  363. package/src/tools/dependencyResolverTool.js +1211 -1212
  364. package/src/tools/desktop/DesktopTool.js +629 -638
  365. package/src/tools/desktop/__tests__/DesktopTool.e2e.test.js +306 -306
  366. package/src/tools/desktop/__tests__/DesktopTool.test.js +507 -507
  367. package/src/tools/desktop/__tests__/osController.test.js +364 -364
  368. package/src/tools/desktop/osController.js +491 -491
  369. package/src/tools/docxTool.js +623 -623
  370. package/src/tools/excelTool.js +636 -636
  371. package/src/tools/fileContentReplaceTool.js +5 -7
  372. package/src/tools/fileSystemTool.js +12 -19
  373. package/src/tools/fileTreeTool.js +840 -840
  374. package/src/tools/foundryWebSearchTool.js +273 -273
  375. package/src/tools/helpTool.js +198 -198
  376. package/src/tools/imageTool.js +1397 -1397
  377. package/src/tools/importAnalyzerTool.js +1056 -1056
  378. package/src/tools/jobDoneTool.js +495 -495
  379. package/src/tools/memoryTool.js +1 -1
  380. package/src/tools/office/pres/__tests__/presSystem.test.js +365 -365
  381. package/src/tools/office/pres/archetypes/agenda.js +61 -61
  382. package/src/tools/office/pres/archetypes/bentoGrid.js +218 -219
  383. package/src/tools/office/pres/archetypes/bigStat.js +140 -142
  384. package/src/tools/office/pres/archetypes/closing.js +70 -70
  385. package/src/tools/office/pres/archetypes/hero.js +70 -70
  386. package/src/tools/office/pres/archetypes/productHero.js +93 -94
  387. package/src/tools/office/pres/archetypes/table.js +73 -74
  388. package/src/tools/office/pres/backgrounds/orb.js +66 -66
  389. package/src/tools/office/pres/components.js +422 -423
  390. package/src/tools/officeTool.js +441 -441
  391. package/src/tools/pdfTool.js +625 -627
  392. package/src/tools/platformControlTool.js +1081 -1081
  393. package/src/tools/seekTool.js +917 -918
  394. package/src/tools/skillsTool.js +1 -1
  395. package/src/tools/staticAnalysisTool.js +2143 -2146
  396. package/src/tools/taskManagerTool.js +3324 -3324
  397. package/src/tools/terminalTool.js +2615 -2618
  398. package/src/tools/videoTool.js +1303 -1303
  399. package/src/tools/visionTool.js +508 -508
  400. package/src/tools/visualEditorTool.js +1289 -1290
  401. package/src/tools/webTool.js +3368 -3368
  402. package/src/tools/whatsappTool.js +464 -464
  403. package/src/types/__tests__/agent.test.js +499 -499
  404. package/src/types/__tests__/contextReference.test.js +606 -606
  405. package/src/types/__tests__/conversation.test.js +555 -555
  406. package/src/types/__tests__/toolCommand.test.js +584 -584
  407. package/src/types/contextReference.js +974 -971
  408. package/src/types/conversation.js +729 -729
  409. package/src/types/toolCommand.js +746 -746
  410. package/src/utilities/__tests__/attachmentValidator.test.js +80 -80
  411. package/src/utilities/__tests__/auditReport.test.js +328 -328
  412. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -388
  413. package/src/utilities/__tests__/jsonRepair.test.js +103 -104
  414. package/src/utilities/__tests__/modeTransitionReasons.test.js +105 -105
  415. package/src/utilities/__tests__/platformUtils.test.js +80 -87
  416. package/src/utilities/__tests__/structuredFileValidator.test.js +261 -263
  417. package/src/utilities/__tests__/toolConstants.test.js +92 -94
  418. package/src/utilities/__tests__/useIsTouchDevice.detect.test.js +114 -114
  419. package/src/utilities/__tests__/webUiUtilSync.test.js +117 -117
  420. package/src/utilities/attachmentValidator.js +284 -288
  421. package/src/utilities/authCache.js.backup-1779570472481 +121 -121
  422. package/src/utilities/browserStealth.js +631 -630
  423. package/src/utilities/configManager.js +616 -617
  424. package/src/utilities/directoryAccessManager.js +564 -565
  425. package/src/utilities/fileProcessor.js +308 -307
  426. package/src/utilities/humanBehavior.js +454 -453
  427. package/src/utilities/logger.js +479 -479
  428. package/src/utilities/structuredFileValidator.js +696 -699
  429. package/src/utilities/tagParser.js +5 -10
  430. package/src/utilities/userDataDir.js +308 -308
  431. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +0 -1
  432. package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +0 -1
  433. package/node_modules/minipass/LICENSE +0 -15
  434. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/LICENSE.md +0 -0
  435. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts +0 -0
  436. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts.map +0 -0
  437. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js +0 -0
  438. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js.map +0 -0
  439. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/package.json +0 -0
  440. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts +0 -0
  441. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts.map +0 -0
  442. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js +0 -0
  443. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js.map +0 -0
  444. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/package.json +0 -0
  445. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/LICENSE +0 -0
  446. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts +0 -0
  447. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts.map +0 -0
  448. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/package.json +0 -0
  449. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts +0 -0
  450. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts.map +0 -0
  451. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/package.json +0 -0
@@ -1,1774 +1,1769 @@
1
- /**
2
- * Visual Editor Server
3
- *
4
- * Runs on port 4000 and provides:
5
- * - Health check endpoint
6
- * - Editor HTML page for iframe embedding
7
- * - Proxy to user's running app
8
- * - WebSocket for backend bridge communication
9
- * - Element picker overlay injection
10
- */
11
-
12
- import express from 'express';
13
- import { createProxyMiddleware } from 'http-proxy-middleware';
14
- import { WebSocketServer } from 'ws';
15
- import path from 'path';
16
- import { resolveModuleFilename } from '../utilities/esmCjsPath.js';
17
- import { Transform } from 'stream';
18
- import https from 'https';
19
- import http from 'http';
20
- import zlib from 'zlib';
21
-
22
- // Import service registry for port allocation and registration
23
- import registry, { findFreePort } from './serviceRegistry.js';
24
-
25
- // Lazy getter for bridge to avoid circular dependency
26
- // (visualEditorBridge imports from this file)
27
- let bridgeGetter = null;
28
- function setBridgeGetter(getter) {
29
- bridgeGetter = getter;
30
- }
31
- function getBridge() {
32
- if (!bridgeGetter) {
33
- // Fallback: try dynamic import (async, only for initialization)
34
- return null;
35
- }
36
- return bridgeGetter();
37
- }
38
-
39
- const __filename = resolveModuleFilename(import.meta.url);
40
- const __dirname = path.dirname(__filename);
41
-
42
- // Service name for registry
43
- const SERVICE_NAME = 'visualEditor';
44
-
45
- // Config manager reference (set when initialized from main app)
46
- let configManagerRef = null;
47
-
48
- /**
49
- * Set the config manager reference for reading configuration
50
- * @param {ConfigManager} configManager - The config manager instance
51
- */
52
- export function setConfigManager(configManager) {
53
- configManagerRef = configManager;
54
- }
55
-
56
- /**
57
- * Get configuration value with fallback chain:
58
- * 1. Config manager (from config file)
59
- * 2. Environment variable
60
- * 3. Default value
61
- */
62
- function getConfigValue(configPath, envVar, defaultValue) {
63
- // Try config manager first
64
- if (configManagerRef) {
65
- const configValue = configManagerRef.get(configPath);
66
- if (configValue !== undefined) {
67
- return configValue;
68
- }
69
- }
70
-
71
- // Try environment variable
72
- const envValue = process.env[envVar];
73
- if (envValue !== undefined) {
74
- // Parse numbers
75
- if (typeof defaultValue === 'number') {
76
- const parsed = parseInt(envValue, 10);
77
- if (!isNaN(parsed)) return parsed;
78
- }
79
- return envValue;
80
- }
81
-
82
- // Return default
83
- return defaultValue;
84
- }
85
-
86
- // Hard-coded fallback defaults (used only when nothing else is configured)
87
- const FALLBACK_PORT = 4000;
88
- const FALLBACK_APP_URL = 'http://localhost:3000';
89
-
90
- /**
91
- * Get the configured port (evaluated at runtime, not module load)
92
- * @returns {number}
93
- */
94
- function getDefaultPort() {
95
- return getConfigValue('visualEditor.port', 'LOXIA_VISUAL_EDITOR_PORT', FALLBACK_PORT);
96
- }
97
-
98
- /**
99
- * Get the configured default app URL (evaluated at runtime)
100
- * @returns {string}
101
- */
102
- function getDefaultAppUrl() {
103
- return getConfigValue('visualEditor.defaultAppUrl', 'LOXIA_DEFAULT_APP_URL', FALLBACK_APP_URL);
104
- }
105
-
106
- /**
107
- * Visual Editor Server class
108
- */
109
- class VisualEditorServer {
110
- /**
111
- * @param {Object} config - Configuration options
112
- * @param {number} config.port - Server port (default: 4000)
113
- * @param {Object} config.logger - Logger instance
114
- */
115
- constructor(config = {}) {
116
- this.port = config.port || getDefaultPort();
117
- this.logger = config.logger || console;
118
- this.server = null;
119
- this.wss = null;
120
- this.app = null;
121
- this.isRunning = false;
122
-
123
- // Track active connections
124
- this.wsConnections = new Map(); // agentId -> WebSocket
125
- this.activeAppUrls = new Map(); // agentId -> appUrl
126
- this.staticDirs = new Map(); // agentId -> directory path for static serving
127
- }
128
-
129
- /**
130
- * Register a static directory to serve for an agent
131
- * @param {string} agentId - Agent identifier
132
- * @param {string} directory - Directory path to serve
133
- */
134
- registerStaticDir(agentId, directory) {
135
- this.staticDirs.set(agentId, directory);
136
- this.logger.info?.(`[VisualEditorServer] Registered static dir for ${agentId}: ${directory}`) ||
137
- console.log(`[VisualEditorServer] Registered static dir for ${agentId}: ${directory}`);
138
- }
139
-
140
- /**
141
- * Unregister a static directory
142
- * @param {string} agentId - Agent identifier
143
- */
144
- unregisterStaticDir(agentId) {
145
- this.staticDirs.delete(agentId);
146
- }
147
-
148
- /**
149
- * Start the Visual Editor Server
150
- * Uses findFreePort to handle port conflicts and registers with service registry
151
- * @returns {Promise<Object>} Start result
152
- */
153
- async start() {
154
- if (this.isRunning) {
155
- return { success: true, port: this.port, message: 'Already running' };
156
- }
157
-
158
- // Find a free port by checking on 0.0.0.0 (matching the actual bind address)
159
- const preferredPort = this.port;
160
- try {
161
- const actualPort = await findFreePort(preferredPort, 100, '0.0.0.0');
162
-
163
- if (actualPort !== preferredPort) {
164
- this.logger.info?.(`[VisualEditorServer] Port ${preferredPort} taken, using ${actualPort}`) ||
165
- console.log(`[VisualEditorServer] Port ${preferredPort} taken, using ${actualPort}`);
166
- this.port = actualPort;
167
- }
168
- } catch (err) {
169
- this.logger.error?.(`[VisualEditorServer] Could not find free port: ${err.message}`);
170
- throw err;
171
- }
172
-
173
- return this._tryListen(this.port, 10);
174
- }
175
-
176
- /**
177
- * Try to listen on a port, retrying on EADDRINUSE up to maxRetries times.
178
- * Handles the TOCTOU race between findFreePort and actual listen().
179
- */
180
- async _tryListen(port, maxRetries) {
181
- this.port = port;
182
- this.app = express();
183
- this._setupMiddleware();
184
- this._setupRoutes();
185
-
186
- return new Promise((resolve, reject) => {
187
- this.server = this.app.listen(this.port, '0.0.0.0', () => {
188
- this.isRunning = true;
189
- this._setupWebSocketServer();
190
-
191
- // Register with service registry
192
- registry.register(SERVICE_NAME, {
193
- port: this.port,
194
- host: 'localhost',
195
- protocol: 'http',
196
- metadata: {
197
- wsPath: '/ws',
198
- startedAt: Date.now()
199
- }
200
- });
201
-
202
- this.logger.info?.(`[VisualEditorServer] Running on port ${this.port}`) ||
203
- console.log(`[VisualEditorServer] Running on port ${this.port}`);
204
-
205
- resolve({ success: true, port: this.port });
206
- });
207
-
208
- this.server.on('error', (err) => {
209
- if (err.code === 'EADDRINUSE' && maxRetries > 0) {
210
- const nextPort = this.port + 1;
211
- this.logger.info?.(`[VisualEditorServer] Port ${this.port} in use, trying ${nextPort}...`) ||
212
- console.log(`[VisualEditorServer] Port ${this.port} in use, trying ${nextPort}...`);
213
- // Clean up and retry on next port
214
- try { this.server.close(); } catch {}
215
- this.app = null;
216
- this.server = null;
217
- resolve(this._tryListen(nextPort, maxRetries - 1));
218
- } else {
219
- this.logger.error?.(`[VisualEditorServer] Server error: ${err.message}`);
220
- reject(err);
221
- }
222
- });
223
- });
224
- }
225
-
226
- /**
227
- * Stop the Visual Editor Server
228
- * @returns {Promise<void>}
229
- */
230
- async stop() {
231
- if (!this.isRunning) return;
232
-
233
- // Unregister from service registry
234
- registry.unregister(SERVICE_NAME);
235
-
236
- // Close all WebSocket connections
237
- for (const [agentId, ws] of this.wsConnections.entries()) {
238
- try {
239
- ws.close(1000, 'Server shutting down');
240
- } catch (err) {
241
- // Ignore
242
- }
243
- }
244
- this.wsConnections.clear();
245
- this.activeAppUrls.clear();
246
-
247
- // Close WebSocket server
248
- if (this.wss) {
249
- this.wss.close();
250
- this.wss = null;
251
- }
252
-
253
- // Close HTTP server — force-close keep-alive connections
254
- return new Promise((resolve) => {
255
- if (this.server) {
256
- if (typeof this.server.closeAllConnections === 'function') {
257
- this.server.closeAllConnections();
258
- }
259
- this.server.close(() => {
260
- this.isRunning = false;
261
- this.server = null;
262
- this.logger.info?.('[VisualEditorServer] Stopped') ||
263
- console.log('[VisualEditorServer] Stopped');
264
- resolve();
265
- });
266
- } else {
267
- resolve();
268
- }
269
- });
270
- }
271
-
272
- /**
273
- * Get server status
274
- * @returns {Object} Status info
275
- */
276
- getStatus() {
277
- return {
278
- isRunning: this.isRunning,
279
- port: this.port,
280
- activeConnections: this.wsConnections.size,
281
- connectedAgents: Array.from(this.wsConnections.keys())
282
- };
283
- }
284
-
285
- /**
286
- * Register an app URL for an agent
287
- * @param {string} agentId - Agent identifier
288
- * @param {string} appUrl - User's app URL
289
- */
290
- registerAppUrl(agentId, appUrl) {
291
- this.activeAppUrls.set(agentId, appUrl);
292
- }
293
-
294
- /**
295
- * Unregister an agent's app URL
296
- * @param {string} agentId - Agent identifier
297
- */
298
- unregisterAppUrl(agentId) {
299
- this.activeAppUrls.delete(agentId);
300
- }
301
-
302
- /**
303
- * Send message to a specific agent's WebSocket
304
- * @param {string} agentId - Agent identifier
305
- * @param {Object} message - Message to send
306
- * @returns {boolean} Success
307
- */
308
- sendToAgent(agentId, message) {
309
- const ws = this.wsConnections.get(agentId);
310
- if (ws && ws.readyState === 1) { // WebSocket.OPEN
311
- try {
312
- ws.send(JSON.stringify(message));
313
- return true;
314
- } catch (err) {
315
- this.logger.error?.(`[VisualEditorServer] Failed to send to agent ${agentId}:`, err);
316
- }
317
- }
318
- return false;
319
- }
320
-
321
- /**
322
- * Set up Express middleware
323
- * @private
324
- */
325
- _setupMiddleware() {
326
- // Request logging for debugging
327
- this.app.use((req, res, next) => {
328
- this.logger.debug?.(`[VisualEditorServer] ${req.method} ${req.url}`);
329
- next();
330
- });
331
-
332
- // CORS for cross-origin requests
333
- this.app.use((req, res, next) => {
334
- res.header('Access-Control-Allow-Origin', '*');
335
- res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
336
- res.header('Access-Control-Allow-Headers', 'Content-Type');
337
- if (req.method === 'OPTIONS') {
338
- return res.sendStatus(200);
339
- }
340
- next();
341
- });
342
-
343
- // Parse JSON bodies
344
- this.app.use(express.json());
345
- }
346
-
347
- /**
348
- * Set up Express routes
349
- * @private
350
- */
351
- _setupRoutes() {
352
- // Health check endpoint
353
- this.app.get('/health', (req, res) => {
354
- res.json({
355
- status: 'ok',
356
- timestamp: Date.now(),
357
- connections: this.wsConnections.size
358
- });
359
- });
360
-
361
- // Test proxy connectivity endpoint (for debugging)
362
- this.app.get('/test-proxy', async (req, res) => {
363
- const targetUrl = req.query.url || 'https://httpbin.org/html';
364
- this.logger.info?.(`[VisualEditorServer] Testing connectivity to: ${targetUrl}`);
365
-
366
- try {
367
- const parsed = new URL(targetUrl);
368
- const isHttps = parsed.protocol === 'https:';
369
- const httpModule = isHttps ? https : http;
370
-
371
- const result = await new Promise((resolve, reject) => {
372
- const reqOptions = {
373
- hostname: parsed.hostname,
374
- port: parsed.port || (isHttps ? 443 : 80),
375
- path: parsed.pathname + parsed.search,
376
- method: 'HEAD', // Just check connectivity, don't download content
377
- timeout: 10000,
378
- headers: {
379
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0'
380
- },
381
- rejectUnauthorized: false // Allow self-signed certs
382
- };
383
-
384
- const request = httpModule.request(reqOptions, (response) => {
385
- resolve({
386
- success: true,
387
- url: targetUrl,
388
- status: response.statusCode,
389
- statusText: response.statusMessage,
390
- contentType: response.headers['content-type']
391
- });
392
- });
393
-
394
- request.on('timeout', () => {
395
- request.destroy();
396
- reject(new Error('Connection timed out after 10s'));
397
- });
398
-
399
- request.on('error', reject);
400
- request.end();
401
- });
402
-
403
- res.json(result);
404
- } catch (err) {
405
- this.logger.error?.(`[VisualEditorServer] Test connectivity failed:`, err.message);
406
- res.json({
407
- success: false,
408
- url: targetUrl,
409
- error: err.message,
410
- code: err.code || 'UNKNOWN'
411
- });
412
- }
413
- });
414
-
415
- // Main editor page (served in iframe)
416
- this.app.get('/', (req, res) => {
417
- const { agentId, appUrl } = req.query;
418
-
419
- if (!agentId) {
420
- return res.status(400).send('Missing agentId parameter');
421
- }
422
-
423
- const targetUrl = appUrl || this.activeAppUrls.get(agentId) || getDefaultAppUrl();
424
- const html = this._generateEditorHtml(agentId, targetUrl);
425
- res.type('html').send(html);
426
- });
427
-
428
- // Serve overlay script
429
- this.app.get('/overlay.js', (req, res) => {
430
- const overlayScript = this._getOverlayScript();
431
- res.type('application/javascript').send(overlayScript);
432
- });
433
-
434
- // Serve static files for agents (for static HTML projects)
435
- this.app.use('/static/:agentId', (req, res, next) => {
436
- const { agentId } = req.params;
437
- const staticDir = this.staticDirs.get(agentId);
438
-
439
- if (!staticDir) {
440
- return res.status(404).json({
441
- error: 'No static directory registered for this agent',
442
- agentId
443
- });
444
- }
445
-
446
- // Create static middleware for this directory
447
- const staticMiddleware = express.static(staticDir, {
448
- index: ['index.html', 'index.htm'],
449
- extensions: ['html', 'htm']
450
- });
451
-
452
- // Inject overlay script into HTML files
453
- const originalSend = res.send.bind(res);
454
- res.send = (body) => {
455
- if (typeof body === 'string' && body.includes('</body>')) {
456
- // Inject overlay script before </body>
457
- const overlayScript = `<script src="/overlay.js"></script>`;
458
- body = body.replace('</body>', `${overlayScript}</body>`);
459
- }
460
- return originalSend(body);
461
- };
462
-
463
- staticMiddleware(req, res, next);
464
- });
465
-
466
- // Proxy to user's app with overlay injection
467
- // Wrap in error handler to catch any proxy initialization errors
468
- const proxyMiddleware = this._createProxyMiddleware();
469
- this.app.use('/app', (req, res, next) => {
470
- try {
471
- proxyMiddleware(req, res, next);
472
- } catch (err) {
473
- this.logger.error?.(`[VisualEditorServer] Proxy middleware error:`, err.message);
474
- res.status(502).type('html').send(this._generateErrorHtml(
475
- req.query.target || 'unknown',
476
- `Proxy error: ${err.message}`
477
- ));
478
- }
479
- });
480
-
481
- // 404 catch-all - log and return helpful message
482
- this.app.use((req, res) => {
483
- this.logger.warn?.(`[VisualEditorServer] 404: ${req.method} ${req.url}`);
484
- res.status(404).json({
485
- error: 'Not found',
486
- path: req.url,
487
- hint: 'Use /app?target=URL to proxy to a website'
488
- });
489
- });
490
-
491
- // Express error handler - catches uncaught errors
492
- this.app.use((err, req, res, next) => {
493
- this.logger.error?.(`[VisualEditorServer] Express error:`, err.message);
494
- if (!res.headersSent) {
495
- res.status(500).type('html').send(this._generateErrorHtml(
496
- req.query?.target || req.url,
497
- `Server error: ${err.message}`
498
- ));
499
- }
500
- });
501
- }
502
-
503
- /**
504
- * Create proxy middleware for user's app
505
- * Uses router option for dynamic target selection based on query param
506
- * @private
507
- */
508
- _createProxyMiddleware() {
509
- const self = this;
510
-
511
- // Store current target URL for use in callbacks
512
- let currentTargetUrl = getDefaultAppUrl();
513
-
514
- // http-proxy-middleware v3.x uses 'on' property for event handlers
515
- return createProxyMiddleware({
516
- // Use router for dynamic target based on query parameter
517
- router: (req) => {
518
- const targetUrl = req.query.target || getDefaultAppUrl();
519
- currentTargetUrl = targetUrl; // Store for use in callbacks
520
-
521
- // Validate URL
522
- try {
523
- const parsed = new URL(targetUrl);
524
- self.logger.info?.(`[VisualEditorServer] Proxying to: ${parsed.origin}`);
525
- return parsed.origin; // Return just the origin (protocol + host + port)
526
- } catch (err) {
527
- self.logger.error?.(`[VisualEditorServer] Invalid target URL: ${targetUrl}`);
528
- return getDefaultAppUrl(); // Fallback to default
529
- }
530
- },
531
- changeOrigin: true,
532
- selfHandleResponse: true, // We'll handle response to inject script
533
- secure: false, // Don't validate SSL certificates (needed for dev servers)
534
- followRedirects: true, // Follow redirects
535
- proxyTimeout: 30000, // 30 second proxy timeout
536
- timeout: 30000, // 30 second request timeout
537
- pathRewrite: (path, req) => {
538
- // Get the path from the target URL and append request path
539
- const targetUrl = req.query.target || getDefaultAppUrl();
540
- try {
541
- const parsed = new URL(targetUrl);
542
- // Start with the path from target URL
543
- let newPath = parsed.pathname;
544
- if (newPath === '/') newPath = '';
545
-
546
- // Parse current request path and remove /app and query params
547
- const reqUrl = new URL(path, 'http://localhost');
548
- reqUrl.searchParams.delete('target');
549
- const reqPath = reqUrl.pathname.replace(/^\/app\/?/, '/');
550
-
551
- // Combine paths (avoid double slashes)
552
- const finalPath = newPath + (reqPath === '/' ? '' : reqPath) + reqUrl.search;
553
- self.logger.debug?.(`[VisualEditorServer] Path rewrite: ${path} -> ${finalPath || '/'}`);
554
- return finalPath || '/';
555
- } catch (e) {
556
- return '/';
557
- }
558
- },
559
- // v3.x event handlers using 'on' property
560
- on: {
561
- proxyReq: (proxyReq, req, res) => {
562
- const targetUrl = req.query.target || getDefaultAppUrl();
563
- self.logger.info?.(`[VisualEditorServer] Proxy request to: ${targetUrl}`);
564
-
565
- // Set browser-like headers to avoid being blocked
566
- // Wrap in try-catch because headers might already be sent on redirects
567
- try {
568
- if (!proxyReq.headersSent) {
569
- proxyReq.setHeader('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
570
- proxyReq.setHeader('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8');
571
- proxyReq.setHeader('Accept-Language', 'en-US,en;q=0.9');
572
- // IMPORTANT: Request uncompressed content - we need to read/modify HTML
573
- // If we request gzip, we'd need to decompress before injecting the overlay script
574
- proxyReq.setHeader('Accept-Encoding', 'identity');
575
- // Remove headers that might cause issues
576
- proxyReq.removeHeader('x-forwarded-for');
577
- proxyReq.removeHeader('x-forwarded-host');
578
- proxyReq.removeHeader('x-forwarded-proto');
579
- }
580
- } catch (e) {
581
- // Headers already sent (e.g., during redirect) - ignore
582
- self.logger.debug?.(`[VisualEditorServer] Could not set headers: ${e.message}`);
583
- }
584
- },
585
- proxyRes: (proxyRes, req, res) => {
586
- const targetUrl = req.query.target || currentTargetUrl;
587
- self._handleProxyResponse(proxyRes, req, res, targetUrl);
588
- },
589
- error: (err, req, res) => {
590
- try {
591
- const targetUrl = req?.query?.target || currentTargetUrl || 'unknown';
592
-
593
- // Log detailed error information
594
- self.logger.error?.('[VisualEditorServer] Proxy error:', {
595
- message: err?.message,
596
- code: err?.code,
597
- target: targetUrl,
598
- url: req?.url
599
- });
600
-
601
- // Provide more helpful error messages based on error type
602
- let errorMessage = err?.message || 'Unknown proxy error';
603
- if (err?.code === 'ECONNREFUSED') {
604
- errorMessage = `Connection refused - the server at ${targetUrl} is not running or not accepting connections`;
605
- } else if (err?.code === 'ENOTFOUND') {
606
- errorMessage = `DNS lookup failed - could not resolve hostname for ${targetUrl}`;
607
- } else if (err?.code === 'ETIMEDOUT' || err?.code === 'ESOCKETTIMEDOUT') {
608
- errorMessage = `Connection timed out - the server at ${targetUrl} took too long to respond (30s limit)`;
609
- } else if (err?.code === 'CERT_HAS_EXPIRED' || err?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
610
- errorMessage = `SSL certificate error for ${targetUrl}. This may be a self-signed certificate issue.`;
611
- } else if (err?.code === 'ECONNRESET') {
612
- errorMessage = `Connection was reset by the server at ${targetUrl}`;
613
- } else if (err?.code === 'HPE_INVALID_CONSTANT') {
614
- errorMessage = `Invalid response from ${targetUrl} - the server may not be an HTTP server`;
615
- }
616
-
617
- // Return a user-friendly HTML error page
618
- if (res && !res.headersSent) {
619
- res.writeHead(502, { 'Content-Type': 'text/html' });
620
- res.end(self._generateErrorHtml(targetUrl, errorMessage));
621
- }
622
- } catch (handlerErr) {
623
- self.logger.error?.('[VisualEditorServer] Error in error handler:', handlerErr);
624
- // Last resort - try to send a simple error
625
- try {
626
- if (res && !res.headersSent) {
627
- res.writeHead(502, { 'Content-Type': 'text/plain' });
628
- res.end('Proxy error: ' + (err?.message || 'Unknown error'));
629
- }
630
- } catch (e) {
631
- // Nothing more we can do
632
- }
633
- }
634
- }
635
- }
636
- });
637
- }
638
-
639
- /**
640
- * Generate error HTML page for proxy failures
641
- * @private
642
- */
643
- _generateErrorHtml(targetUrl, errorDetails) {
644
- return `<!DOCTYPE html>
645
- <html>
646
- <head>
647
- <title>Connection Error - Visual Editor</title>
648
- <style>
649
- * { margin: 0; padding: 0; box-sizing: border-box; }
650
- body {
651
- font-family: system-ui, -apple-system, sans-serif;
652
- background: #f3f4f6;
653
- min-height: 100vh;
654
- display: flex;
655
- align-items: center;
656
- justify-content: center;
657
- padding: 20px;
658
- }
659
- .error-container {
660
- background: white;
661
- border-radius: 12px;
662
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
663
- padding: 32px;
664
- max-width: 500px;
665
- text-align: center;
666
- }
667
- .error-icon {
668
- width: 64px;
669
- height: 64px;
670
- margin: 0 auto 16px;
671
- background: #fef2f2;
672
- border-radius: 50%;
673
- display: flex;
674
- align-items: center;
675
- justify-content: center;
676
- }
677
- .error-icon svg {
678
- width: 32px;
679
- height: 32px;
680
- color: #ef4444;
681
- }
682
- h1 {
683
- color: #1f2937;
684
- font-size: 20px;
685
- margin-bottom: 8px;
686
- }
687
- .target-url {
688
- color: #3b82f6;
689
- font-family: monospace;
690
- background: #eff6ff;
691
- padding: 8px 12px;
692
- border-radius: 6px;
693
- margin: 16px 0;
694
- word-break: break-all;
695
- }
696
- .instructions {
697
- color: #6b7280;
698
- font-size: 14px;
699
- line-height: 1.6;
700
- margin-top: 16px;
701
- }
702
- .instructions ol {
703
- text-align: left;
704
- padding-left: 20px;
705
- margin-top: 12px;
706
- }
707
- .instructions li {
708
- margin-bottom: 8px;
709
- }
710
- .retry-btn {
711
- margin-top: 20px;
712
- padding: 10px 24px;
713
- background: #3b82f6;
714
- color: white;
715
- border: none;
716
- border-radius: 6px;
717
- font-size: 14px;
718
- cursor: pointer;
719
- transition: background 0.2s;
720
- }
721
- .retry-btn:hover {
722
- background: #2563eb;
723
- }
724
- .error-details {
725
- margin-top: 16px;
726
- padding: 12px;
727
- background: #fef2f2;
728
- border-radius: 6px;
729
- color: #991b1b;
730
- font-size: 12px;
731
- font-family: monospace;
732
- }
733
- </style>
734
- </head>
735
- <body>
736
- <div class="error-container">
737
- <div class="error-icon">
738
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
739
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
740
- </svg>
741
- </div>
742
- <h1>Cannot Connect to Your App</h1>
743
- <div class="target-url">${targetUrl}</div>
744
- <div class="instructions">
745
- <p>Make sure your app is running at this address.</p>
746
- <ol>
747
- <li>Start your development server (e.g., <code>npm run dev</code>)</li>
748
- <li>Enter the correct URL in the address bar above</li>
749
- <li>Click "Go" or retry below</li>
750
- </ol>
751
- </div>
752
- <button class="retry-btn" onclick="location.reload()">Retry Connection</button>
753
- <div class="error-details">${errorDetails}</div>
754
- </div>
755
- </body>
756
- </html>`;
757
- }
758
-
759
- /**
760
- * Handle proxy response - inject overlay script into HTML
761
- * @private
762
- */
763
- _handleProxyResponse(proxyRes, req, res, targetUrl) {
764
- const contentType = proxyRes.headers['content-type'] || '';
765
- const contentEncoding = proxyRes.headers['content-encoding'] || '';
766
-
767
- // Copy headers (skip content-length and content-encoding as we'll modify content)
768
- Object.keys(proxyRes.headers).forEach(key => {
769
- const lowerKey = key.toLowerCase();
770
- if (lowerKey !== 'content-length' && lowerKey !== 'content-encoding') {
771
- res.setHeader(key, proxyRes.headers[key]);
772
- }
773
- });
774
-
775
- res.status(proxyRes.statusCode);
776
-
777
- // Only inject into HTML responses
778
- if (contentType.includes('text/html')) {
779
- const chunks = [];
780
-
781
- // Collect all data chunks
782
- proxyRes.on('data', (chunk) => {
783
- chunks.push(chunk);
784
- });
785
-
786
- proxyRes.on('end', () => {
787
- // Combine chunks into a single buffer
788
- const buffer = Buffer.concat(chunks);
789
-
790
- // Decompress if needed (some servers ignore Accept-Encoding: identity)
791
- this._decompressBuffer(buffer, contentEncoding)
792
- .then(decompressed => {
793
- const body = decompressed.toString('utf-8');
794
- // Inject overlay script before </body>
795
- const injectedHtml = this._injectOverlayScript(body, targetUrl);
796
- res.send(injectedHtml);
797
- })
798
- .catch(err => {
799
- this.logger.error?.(`[VisualEditorServer] Decompression error: ${err.message}`);
800
- // Try to send as-is (might be uncompressed despite header)
801
- try {
802
- const body = buffer.toString('utf-8');
803
- const injectedHtml = this._injectOverlayScript(body, targetUrl);
804
- res.send(injectedHtml);
805
- } catch (e) {
806
- res.status(500).send('Error processing response');
807
- }
808
- });
809
- });
810
- } else {
811
- // Pass through non-HTML responses
812
- proxyRes.pipe(res);
813
- }
814
- }
815
-
816
- /**
817
- * Decompress buffer based on content-encoding
818
- * @private
819
- */
820
- async _decompressBuffer(buffer, encoding) {
821
- if (!encoding || encoding === 'identity') {
822
- return buffer;
823
- }
824
-
825
- return new Promise((resolve, reject) => {
826
- if (encoding === 'gzip') {
827
- zlib.gunzip(buffer, (err, result) => {
828
- if (err) reject(err);
829
- else resolve(result);
830
- });
831
- } else if (encoding === 'deflate') {
832
- zlib.inflate(buffer, (err, result) => {
833
- if (err) reject(err);
834
- else resolve(result);
835
- });
836
- } else if (encoding === 'br') {
837
- zlib.brotliDecompress(buffer, (err, result) => {
838
- if (err) reject(err);
839
- else resolve(result);
840
- });
841
- } else {
842
- // Unknown encoding, try to use as-is
843
- resolve(buffer);
844
- }
845
- });
846
- }
847
-
848
- /**
849
- * Inject overlay script into HTML
850
- * @private
851
- */
852
- _injectOverlayScript(html, targetUrl) {
853
- // Use ABSOLUTE URL for overlay.js since we inject a <base> tag that would redirect relative paths
854
- const overlayUrl = `http://localhost:${this.port}/overlay.js`;
855
- const scriptTag = `
856
- <!-- Loxia Visual Editor Overlay -->
857
- <script src="${overlayUrl}"></script>
858
- `;
859
-
860
- // Add a <base> tag to make relative URLs resolve to the original site
861
- // This prevents assets (scripts, styles, images) from being requested through our server
862
- //
863
- // IMPORTANT: The base href must include the *directory* of the target URL, not just
864
- // the origin. If the target is http://host/sub/page.html and we set <base href="http://host/">,
865
- // then a relative <link href="styles.css"> resolves to http://host/styles.css (wrong) instead
866
- // of http://host/sub/styles.css (correct). We compute the directory portion of the pathname
867
- // and use that as the base href.
868
- let baseTag = '';
869
- try {
870
- const parsed = new URL(targetUrl);
871
- // Derive directory: for /sub/page.html -> /sub/, for /sub/ -> /sub/, for / -> /
872
- let dir = parsed.pathname;
873
- if (!dir.endsWith('/')) {
874
- const lastSlash = dir.lastIndexOf('/');
875
- dir = lastSlash >= 0 ? dir.substring(0, lastSlash + 1) : '/';
876
- }
877
- baseTag = `<base href="${parsed.origin}${dir}">`;
878
- } catch (e) {
879
- // Invalid URL, skip base tag
880
- }
881
-
882
- let modifiedHtml = html;
883
-
884
- // Inject base tag right after the opening <head ...> tag (case-insensitive, allows attributes).
885
- // If there is no <head> at all, synthesize one after <html ...> so we still get a base.
886
- // If a <base> already exists in the document, rewrite its href — the original would point
887
- // at the original site path and we want it pointed at our resolved directory instead.
888
- if (baseTag) {
889
- const hasBase = /<base\b[^>]*>/i.test(html);
890
- if (hasBase) {
891
- // Replace the first <base ...> with our computed one
892
- modifiedHtml = modifiedHtml.replace(/<base\b[^>]*>/i, baseTag);
893
- } else {
894
- const headOpenRe = /<head\b[^>]*>/i;
895
- if (headOpenRe.test(modifiedHtml)) {
896
- modifiedHtml = modifiedHtml.replace(headOpenRe, (match) => `${match}${baseTag}`);
897
- } else {
898
- const htmlOpenRe = /<html\b[^>]*>/i;
899
- if (htmlOpenRe.test(modifiedHtml)) {
900
- modifiedHtml = modifiedHtml.replace(htmlOpenRe, (match) => `${match}<head>${baseTag}</head>`);
901
- } else {
902
- // No <html> either — prepend a head block
903
- modifiedHtml = `<head>${baseTag}</head>${modifiedHtml}`;
904
- }
905
- }
906
- }
907
- }
908
-
909
- // Inject overlay script before </body> or at end
910
- if (modifiedHtml.includes('</body>')) {
911
- return modifiedHtml.replace('</body>', `${scriptTag}</body>`);
912
- } else if (modifiedHtml.includes('</html>')) {
913
- return modifiedHtml.replace('</html>', `${scriptTag}</html>`);
914
- } else {
915
- return modifiedHtml + scriptTag;
916
- }
917
- }
918
-
919
- /**
920
- * Set up WebSocket server for backend bridge communication
921
- * @private
922
- */
923
- _setupWebSocketServer() {
924
- this.wss = new WebSocketServer({
925
- server: this.server,
926
- path: '/ws'
927
- });
928
-
929
- this.wss.on('connection', (ws, req) => {
930
- const url = new URL(req.url, `http://localhost:${this.port}`);
931
- const agentId = url.searchParams.get('agentId');
932
-
933
- if (!agentId) {
934
- ws.close(1008, 'Missing agentId');
935
- return;
936
- }
937
-
938
- this.logger.info?.(`[VisualEditorServer] WebSocket connected: ${agentId}`);
939
-
940
- // Store connection
941
- this.wsConnections.set(agentId, ws);
942
-
943
- // Send ready message
944
- ws.send(JSON.stringify({
945
- type: 'editor-ready',
946
- agentId,
947
- timestamp: Date.now()
948
- }));
949
-
950
- // Handle incoming messages
951
- ws.on('message', (data) => {
952
- this._handleWebSocketMessage(agentId, data, ws);
953
- });
954
-
955
- // Handle close
956
- ws.on('close', (code, reason) => {
957
- this.logger.info?.(`[VisualEditorServer] WebSocket closed: ${agentId} (${code})`);
958
- this.wsConnections.delete(agentId);
959
- });
960
-
961
- // Handle errors
962
- ws.on('error', (err) => {
963
- this.logger.error?.(`[VisualEditorServer] WebSocket error for ${agentId}:`, err);
964
- });
965
- });
966
- }
967
-
968
- /**
969
- * Handle incoming WebSocket messages
970
- * @private
971
- */
972
- _handleWebSocketMessage(agentId, data, ws) {
973
- try {
974
- const message = JSON.parse(data.toString());
975
-
976
- this.logger.debug?.(`[VisualEditorServer] Message from ${agentId}:`, message.type);
977
-
978
- switch (message.type) {
979
- case 'element-selected':
980
- // Forward element selection (already handled by postMessage to web-ui)
981
- // This is for backend bridge to receive selections
982
- this._emitElementSelected(agentId, message);
983
- break;
984
-
985
- case 'ping':
986
- ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
987
- break;
988
-
989
- case 'highlight':
990
- case 'scroll-to':
991
- case 'reload':
992
- // These commands come from backend, forward to any connected editor pages
993
- // (handled via the editor page's WebSocket connection)
994
- break;
995
-
996
- case 'subscribe':
997
- // Agent subscribing to editor events
998
- if (message.appUrl) {
999
- this.activeAppUrls.set(agentId, message.appUrl);
1000
- }
1001
- break;
1002
-
1003
- case 'unsubscribe':
1004
- this.activeAppUrls.delete(agentId);
1005
- break;
1006
-
1007
- default:
1008
- this.logger.debug?.(`[VisualEditorServer] Unknown message type: ${message.type}`);
1009
- }
1010
- } catch (err) {
1011
- this.logger.error?.('[VisualEditorServer] Invalid WebSocket message:', err);
1012
- }
1013
- }
1014
-
1015
- /**
1016
- * Emit element selected event (for external listeners)
1017
- * @private
1018
- */
1019
- _emitElementSelected(agentId, message) {
1020
- const elementData = message.data || message;
1021
-
1022
- this.logger.info?.(`[VisualEditorServer] Element selected for ${agentId}:`, {
1023
- selector: elementData.selector,
1024
- component: elementData.sourceHint?.component
1025
- });
1026
-
1027
- // Forward to visualEditorBridge so context is available for message injection
1028
- const bridge = getBridge();
1029
- if (bridge && bridge.hasInstance(agentId)) {
1030
- const success = bridge.setVisualContext(agentId, {
1031
- selector: elementData.selector,
1032
- tagName: elementData.tagName,
1033
- text: elementData.text,
1034
- attributes: elementData.attributes,
1035
- boundingRect: elementData.boundingRect,
1036
- sourceHint: elementData.sourceHint,
1037
- computedStyle: elementData.computedStyle
1038
- });
1039
-
1040
- if (success) {
1041
- this.logger.info?.(`[VisualEditorServer] Visual context synced to bridge for ${agentId}`);
1042
- } else {
1043
- this.logger.warn?.(`[VisualEditorServer] Failed to sync visual context for ${agentId}`);
1044
- }
1045
- } else {
1046
- this.logger.debug?.(`[VisualEditorServer] No bridge instance for ${agentId}, context not synced`);
1047
- }
1048
- }
1049
-
1050
- /**
1051
- * Generate editor HTML page
1052
- * @private
1053
- */
1054
- _generateEditorHtml(agentId, appUrl) {
1055
- const encodedAppUrl = encodeURIComponent(appUrl);
1056
-
1057
- return `<!DOCTYPE html>
1058
- <html>
1059
- <head>
1060
- <meta charset="UTF-8">
1061
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1062
- <title>Loxia Visual Editor</title>
1063
- <style>
1064
- * { margin: 0; padding: 0; box-sizing: border-box; }
1065
- html, body { height: 100%; overflow: hidden; }
1066
- body { font-family: system-ui, -apple-system, sans-serif; }
1067
-
1068
- #app-frame {
1069
- width: 100%;
1070
- height: 100%;
1071
- border: none;
1072
- }
1073
-
1074
- #loading-overlay {
1075
- position: fixed;
1076
- top: 0;
1077
- left: 0;
1078
- right: 0;
1079
- bottom: 0;
1080
- background: #f3f4f6;
1081
- display: flex;
1082
- flex-direction: column;
1083
- align-items: center;
1084
- justify-content: center;
1085
- z-index: 1000;
1086
- transition: opacity 0.3s ease;
1087
- }
1088
-
1089
- #loading-overlay.hidden {
1090
- opacity: 0;
1091
- pointer-events: none;
1092
- }
1093
-
1094
- .spinner {
1095
- width: 40px;
1096
- height: 40px;
1097
- border: 3px solid #e5e7eb;
1098
- border-top-color: #3b82f6;
1099
- border-radius: 50%;
1100
- animation: spin 1s linear infinite;
1101
- }
1102
-
1103
- @keyframes spin {
1104
- to { transform: rotate(360deg); }
1105
- }
1106
-
1107
- .loading-text {
1108
- margin-top: 16px;
1109
- color: #6b7280;
1110
- font-size: 14px;
1111
- }
1112
-
1113
- #error-message {
1114
- display: none;
1115
- position: fixed;
1116
- top: 50%;
1117
- left: 50%;
1118
- transform: translate(-50%, -50%);
1119
- background: white;
1120
- padding: 24px;
1121
- border-radius: 8px;
1122
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
1123
- text-align: center;
1124
- max-width: 400px;
1125
- }
1126
-
1127
- #error-message h3 {
1128
- color: #ef4444;
1129
- margin-bottom: 8px;
1130
- }
1131
-
1132
- #error-message p {
1133
- color: #6b7280;
1134
- font-size: 14px;
1135
- }
1136
- </style>
1137
- </head>
1138
- <body>
1139
- <div id="loading-overlay">
1140
- <div class="spinner"></div>
1141
- <p class="loading-text">Loading preview...</p>
1142
- </div>
1143
-
1144
- <div id="error-message">
1145
- <h3>Connection Error</h3>
1146
- <p id="error-text">Could not load the preview.</p>
1147
- </div>
1148
-
1149
- <iframe
1150
- id="app-frame"
1151
- src="/app?target=${encodedAppUrl}"
1152
- sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
1153
- ></iframe>
1154
-
1155
- <script>
1156
- const agentId = '${agentId}';
1157
- const appUrl = '${appUrl}';
1158
-
1159
- // Hide loading overlay when iframe loads
1160
- const iframe = document.getElementById('app-frame');
1161
- const loadingOverlay = document.getElementById('loading-overlay');
1162
- const errorMessage = document.getElementById('error-message');
1163
- const errorText = document.getElementById('error-text');
1164
-
1165
- iframe.onload = () => {
1166
- loadingOverlay.classList.add('hidden');
1167
-
1168
- // Check if the iframe loaded our error page (proxy failure)
1169
- try {
1170
- const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
1171
- const title = iframeDoc.title || '';
1172
- if (title.includes('Connection Error') || title.includes('Cannot Connect')) {
1173
- // Proxy returned an error page - show error message
1174
- errorText.textContent = 'Could not proxy to ' + appUrl + '. See the error details in the preview.';
1175
- errorMessage.style.display = 'block';
1176
- }
1177
- } catch (e) {
1178
- // Cross-origin - can't check, but that usually means it loaded successfully
1179
- console.log('[Visual Editor] Cross-origin iframe loaded - assuming success');
1180
- }
1181
- };
1182
-
1183
- iframe.onerror = () => {
1184
- loadingOverlay.classList.add('hidden');
1185
- errorText.textContent = 'Could not connect to ' + appUrl + '. Make sure your app is running.';
1186
- errorMessage.style.display = 'block';
1187
- };
1188
-
1189
- // Forward messages from app iframe to parent (Loxia Web-UI)
1190
- window.addEventListener('message', (e) => {
1191
- // Only forward element-selected messages
1192
- if (e.data && e.data.type === 'element-selected') {
1193
- // Forward to parent window (Loxia Web-UI)
1194
- window.parent.postMessage(e.data, '*');
1195
-
1196
- // Also send via WebSocket to backend
1197
- if (window.wsConnection && window.wsConnection.readyState === 1) {
1198
- window.wsConnection.send(JSON.stringify({
1199
- ...e.data,
1200
- agentId
1201
- }));
1202
- }
1203
- }
1204
- });
1205
-
1206
- // Listen for commands from parent (Loxia Web-UI) or WebSocket
1207
- window.addEventListener('message', (e) => {
1208
- if (e.data && (e.data.type === 'highlight' || e.data.type === 'scroll-to' || e.data.type === 'toggle')) {
1209
- // Forward to app iframe (including toggle for Select/Preview mode switching)
1210
- iframe.contentWindow.postMessage(e.data, '*');
1211
- }
1212
- });
1213
-
1214
- // Connect to WebSocket for backend communication
1215
- const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
1216
- const wsUrl = wsProtocol + '//' + location.host + '/ws?agentId=' + agentId;
1217
-
1218
- function connectWebSocket() {
1219
- window.wsConnection = new WebSocket(wsUrl);
1220
-
1221
- window.wsConnection.onopen = () => {
1222
- console.log('[Visual Editor] WebSocket connected');
1223
- // Notify parent that editor is ready
1224
- window.parent.postMessage({ type: 'editor-ready' }, '*');
1225
- };
1226
-
1227
- window.wsConnection.onmessage = (e) => {
1228
- try {
1229
- const msg = JSON.parse(e.data);
1230
-
1231
- // Forward commands to app iframe
1232
- if (msg.type === 'highlight' || msg.type === 'scroll-to' || msg.type === 'reload') {
1233
- iframe.contentWindow.postMessage(msg, '*');
1234
- }
1235
- } catch (err) {
1236
- console.error('[Visual Editor] Invalid message:', err);
1237
- }
1238
- };
1239
-
1240
- window.wsConnection.onerror = (e) => {
1241
- console.error('[Visual Editor] WebSocket error');
1242
- window.parent.postMessage({
1243
- type: 'editor-error',
1244
- data: { message: 'WebSocket connection error' }
1245
- }, '*');
1246
- };
1247
-
1248
- window.wsConnection.onclose = () => {
1249
- console.log('[Visual Editor] WebSocket closed, reconnecting in 3s...');
1250
- setTimeout(connectWebSocket, 3000);
1251
- };
1252
- }
1253
-
1254
- connectWebSocket();
1255
-
1256
- // Handle iframe load timeout (35s to allow proxy's 30s timeout to report actual error)
1257
- setTimeout(() => {
1258
- if (!loadingOverlay.classList.contains('hidden')) {
1259
- loadingOverlay.classList.add('hidden');
1260
- errorText.textContent = 'Preview is taking too long to load. Check if ' + appUrl + ' is accessible and responding.';
1261
- errorMessage.style.display = 'block';
1262
- }
1263
- }, 35000);
1264
- </script>
1265
- </body>
1266
- </html>`;
1267
- }
1268
-
1269
- /**
1270
- * Get overlay script for element selection
1271
- * @private
1272
- */
1273
- _getOverlayScript() {
1274
- return `/**
1275
- * Loxia Visual Editor Overlay Script
1276
- * Injected into user's app for element selection
1277
- */
1278
- (function() {
1279
- 'use strict';
1280
-
1281
- // Prevent double injection
1282
- if (window.__LOXIA_VISUAL_EDITOR_LOADED__) return;
1283
- window.__LOXIA_VISUAL_EDITOR_LOADED__ = true;
1284
-
1285
- let isEnabled = true;
1286
- let hoveredElement = null;
1287
- let selectedElement = null;
1288
- let highlightOverlay = null;
1289
- let selectionOverlay = null;
1290
- let tooltip = null;
1291
-
1292
- // Apply cursor style based on mode
1293
- function updateCursorStyle(enabled) {
1294
- document.body.style.cursor = enabled ? 'crosshair' : '';
1295
- }
1296
- updateCursorStyle(true);
1297
-
1298
- // Create highlight overlay element (hover)
1299
- function createHighlightOverlay() {
1300
- const overlay = document.createElement('div');
1301
- overlay.id = 'loxia-highlight-overlay';
1302
- overlay.style.cssText = \`
1303
- position: fixed;
1304
- pointer-events: none;
1305
- background: rgba(59, 130, 246, 0.1);
1306
- border: 2px solid #3b82f6;
1307
- border-radius: 4px;
1308
- z-index: 999998;
1309
- transition: all 0.1s ease;
1310
- display: none;
1311
- \`;
1312
- document.body.appendChild(overlay);
1313
- return overlay;
1314
- }
1315
-
1316
- // Create selection overlay element (click)
1317
- function createSelectionOverlay() {
1318
- const overlay = document.createElement('div');
1319
- overlay.id = 'loxia-selection-overlay';
1320
- overlay.style.cssText = \`
1321
- position: fixed;
1322
- pointer-events: none;
1323
- background: rgba(34, 197, 94, 0.15);
1324
- border: 2px solid #22c55e;
1325
- border-radius: 4px;
1326
- z-index: 999999;
1327
- display: none;
1328
- \`;
1329
- document.body.appendChild(overlay);
1330
- return overlay;
1331
- }
1332
-
1333
- // Create tooltip for element info
1334
- function createTooltip() {
1335
- const tip = document.createElement('div');
1336
- tip.id = 'loxia-tooltip';
1337
- tip.style.cssText = \`
1338
- position: fixed;
1339
- background: #1f2937;
1340
- color: white;
1341
- padding: 4px 8px;
1342
- border-radius: 4px;
1343
- font-size: 12px;
1344
- font-family: ui-monospace, monospace;
1345
- z-index: 1000000;
1346
- pointer-events: none;
1347
- display: none;
1348
- max-width: 300px;
1349
- overflow: hidden;
1350
- text-overflow: ellipsis;
1351
- white-space: nowrap;
1352
- \`;
1353
- document.body.appendChild(tip);
1354
- return tip;
1355
- }
1356
-
1357
- // Generate CSS selector for element
1358
- function getSelector(el) {
1359
- if (!el || el === document.body || el === document.documentElement) {
1360
- return el ? el.tagName.toLowerCase() : '';
1361
- }
1362
-
1363
- // Try ID first
1364
- if (el.id && /^[a-zA-Z][\\w-]*$/.test(el.id)) {
1365
- return '#' + el.id;
1366
- }
1367
-
1368
- let path = [];
1369
- let current = el;
1370
-
1371
- while (current && current !== document.body && path.length < 5) {
1372
- let selector = current.tagName.toLowerCase();
1373
-
1374
- // Add id if available
1375
- if (current.id && /^[a-zA-Z][\\w-]*$/.test(current.id)) {
1376
- path.unshift('#' + current.id);
1377
- break;
1378
- }
1379
-
1380
- // Add meaningful classes (skip utility classes)
1381
- if (current.className && typeof current.className === 'string') {
1382
- const classes = current.className
1383
- .split(/\\s+/)
1384
- .filter(c => c && c.length > 2 && !c.match(/^(w-|h-|p-|m-|text-|bg-|flex|grid|block|inline)/))
1385
- .slice(0, 2);
1386
- if (classes.length) {
1387
- selector += '.' + classes.join('.');
1388
- }
1389
- }
1390
-
1391
- // Add nth-child if needed for uniqueness
1392
- const siblings = current.parentElement ?
1393
- Array.from(current.parentElement.children).filter(s => s.tagName === current.tagName) : [];
1394
- if (siblings.length > 1) {
1395
- const index = siblings.indexOf(current) + 1;
1396
- selector += ':nth-child(' + index + ')';
1397
- }
1398
-
1399
- path.unshift(selector);
1400
- current = current.parentElement;
1401
- }
1402
-
1403
- return path.join(' > ');
1404
- }
1405
-
1406
- // Try to get React component info
1407
- function getReactInfo(el) {
1408
- // Look for React fiber
1409
- const fiberKey = Object.keys(el).find(k =>
1410
- k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance')
1411
- );
1412
-
1413
- if (!fiberKey) return null;
1414
-
1415
- let fiber = el[fiberKey];
1416
- let depth = 0;
1417
- const maxDepth = 20;
1418
-
1419
- while (fiber && depth < maxDepth) {
1420
- if (fiber.type && typeof fiber.type === 'function') {
1421
- const name = fiber.type.displayName || fiber.type.name;
1422
- if (name && name !== 'Anonymous' && !name.startsWith('_')) {
1423
- return {
1424
- component: name,
1425
- source: fiber._debugSource || null
1426
- };
1427
- }
1428
- }
1429
- fiber = fiber.return;
1430
- depth++;
1431
- }
1432
-
1433
- return null;
1434
- }
1435
-
1436
- // Extract a useful relative path from absolute file path
1437
- function getRelativePath(fullPath) {
1438
- if (!fullPath) return null;
1439
-
1440
- // Common source folder markers (prioritized)
1441
- const sourceMarkers = ['/src/', '/app/', '/pages/', '/components/', '/lib/', '/utils/'];
1442
-
1443
- for (const marker of sourceMarkers) {
1444
- const index = fullPath.indexOf(marker);
1445
- if (index !== -1) {
1446
- // Return path starting from the marker (e.g., 'src/components/Button.tsx')
1447
- return fullPath.substring(index + 1);
1448
- }
1449
- }
1450
-
1451
- // Fallback: return last 3 path segments
1452
- const parts = fullPath.split('/').filter(Boolean);
1453
- if (parts.length <= 3) {
1454
- return parts.join('/');
1455
- }
1456
- return parts.slice(-3).join('/');
1457
- }
1458
-
1459
- // Get element info for selection
1460
- function getElementInfo(el) {
1461
- const rect = el.getBoundingClientRect();
1462
- const reactInfo = getReactInfo(el);
1463
- const computedStyle = window.getComputedStyle(el);
1464
-
1465
- return {
1466
- selector: getSelector(el),
1467
- tagName: el.tagName.toLowerCase(),
1468
- text: (el.textContent || '').trim().slice(0, 100),
1469
- attributes: {
1470
- id: el.id || null,
1471
- class: el.className || null,
1472
- href: el.href || null,
1473
- src: el.src || null,
1474
- type: el.type || null,
1475
- name: el.name || null
1476
- },
1477
- boundingRect: {
1478
- top: Math.round(rect.top),
1479
- left: Math.round(rect.left),
1480
- width: Math.round(rect.width),
1481
- height: Math.round(rect.height)
1482
- },
1483
- computedStyle: {
1484
- display: computedStyle.display,
1485
- position: computedStyle.position,
1486
- color: computedStyle.color,
1487
- backgroundColor: computedStyle.backgroundColor,
1488
- fontSize: computedStyle.fontSize
1489
- },
1490
- sourceHint: reactInfo ? {
1491
- component: reactInfo.component,
1492
- file: getRelativePath(reactInfo.source?.fileName),
1493
- fullPath: reactInfo.source?.fileName,
1494
- line: reactInfo.source?.lineNumber,
1495
- confidence: reactInfo.source ? 'high' : 'low'
1496
- } : null
1497
- };
1498
- }
1499
-
1500
- // Position overlay on element
1501
- function positionOverlay(overlay, el) {
1502
- const rect = el.getBoundingClientRect();
1503
- overlay.style.top = rect.top + 'px';
1504
- overlay.style.left = rect.left + 'px';
1505
- overlay.style.width = rect.width + 'px';
1506
- overlay.style.height = rect.height + 'px';
1507
- overlay.style.display = 'block';
1508
- }
1509
-
1510
- // Handle element selection
1511
- function selectElement(el, event) {
1512
- event.preventDefault();
1513
- event.stopPropagation();
1514
-
1515
- selectedElement = el;
1516
- const info = getElementInfo(el);
1517
-
1518
- // Show selection overlay
1519
- if (!selectionOverlay) selectionOverlay = createSelectionOverlay();
1520
- positionOverlay(selectionOverlay, el);
1521
-
1522
- // Send selection to parent
1523
- const message = {
1524
- type: 'element-selected',
1525
- data: info
1526
- };
1527
-
1528
- window.parent.postMessage(message, '*');
1529
-
1530
- console.log('[Loxia] Element selected:', info.selector);
1531
- }
1532
-
1533
- // Mouse move - highlight hovered element
1534
- document.addEventListener('mousemove', (e) => {
1535
- if (!isEnabled) return;
1536
-
1537
- const el = e.target;
1538
- if (el === hoveredElement) return;
1539
- if (el.id?.startsWith('loxia-')) return;
1540
-
1541
- hoveredElement = el;
1542
-
1543
- if (!highlightOverlay) highlightOverlay = createHighlightOverlay();
1544
- if (!tooltip) tooltip = createTooltip();
1545
-
1546
- positionOverlay(highlightOverlay, el);
1547
-
1548
- // Update tooltip
1549
- const tagName = el.tagName.toLowerCase();
1550
- const id = el.id ? '#' + el.id : '';
1551
- const classes = el.className && typeof el.className === 'string' ?
1552
- '.' + el.className.split(' ').slice(0, 2).join('.') : '';
1553
-
1554
- tooltip.textContent = tagName + id + classes;
1555
- tooltip.style.left = (e.clientX + 10) + 'px';
1556
- tooltip.style.top = (e.clientY + 10) + 'px';
1557
- tooltip.style.display = 'block';
1558
- }, true);
1559
-
1560
- // Mouse leave - hide highlight
1561
- document.addEventListener('mouseleave', () => {
1562
- if (highlightOverlay) highlightOverlay.style.display = 'none';
1563
- if (tooltip) tooltip.style.display = 'none';
1564
- }, true);
1565
-
1566
- // Click - select element
1567
- document.addEventListener('click', (e) => {
1568
- if (!isEnabled) return;
1569
- if (e.target.id?.startsWith('loxia-')) return;
1570
-
1571
- selectElement(e.target, e);
1572
- }, true);
1573
-
1574
- // Keyboard shortcuts
1575
- document.addEventListener('keydown', (e) => {
1576
- // Escape - toggle overlay
1577
- if (e.key === 'Escape') {
1578
- isEnabled = !isEnabled;
1579
- updateCursorStyle(isEnabled);
1580
- if (!isEnabled) {
1581
- if (highlightOverlay) highlightOverlay.style.display = 'none';
1582
- if (selectionOverlay) selectionOverlay.style.display = 'none';
1583
- if (tooltip) tooltip.style.display = 'none';
1584
- hoveredElement = null;
1585
- }
1586
- // Notify parent of mode change
1587
- window.parent.postMessage({ type: 'mode-toggled', enabled: isEnabled }, '*');
1588
- console.log('[Loxia] Visual editor ' + (isEnabled ? 'enabled' : 'disabled'));
1589
- }
1590
- });
1591
-
1592
- // Listen for commands from editor
1593
- window.addEventListener('message', (e) => {
1594
- if (!e.data || !e.data.type) return;
1595
-
1596
- switch (e.data.type) {
1597
- case 'highlight':
1598
- const highlightEl = document.querySelector(e.data.selector);
1599
- if (highlightEl) {
1600
- if (!selectionOverlay) selectionOverlay = createSelectionOverlay();
1601
- positionOverlay(selectionOverlay, highlightEl);
1602
-
1603
- // Auto-hide after duration
1604
- setTimeout(() => {
1605
- if (selectionOverlay) selectionOverlay.style.display = 'none';
1606
- }, e.data.duration || 2000);
1607
- }
1608
- break;
1609
-
1610
- case 'scroll-to':
1611
- const scrollEl = document.querySelector(e.data.selector);
1612
- if (scrollEl) {
1613
- scrollEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
1614
- }
1615
- break;
1616
-
1617
- case 'reload':
1618
- location.reload();
1619
- break;
1620
-
1621
- case 'toggle':
1622
- isEnabled = e.data.enabled !== undefined ? e.data.enabled : !isEnabled;
1623
- updateCursorStyle(isEnabled);
1624
- // Hide/show overlays based on new state
1625
- if (!isEnabled) {
1626
- if (highlightOverlay) highlightOverlay.style.display = 'none';
1627
- if (selectionOverlay) selectionOverlay.style.display = 'none';
1628
- if (tooltip) tooltip.style.display = 'none';
1629
- hoveredElement = null;
1630
- }
1631
- console.log('[Loxia] Visual editor ' + (isEnabled ? 'enabled (Select mode)' : 'disabled (Preview mode)'));
1632
- break;
1633
-
1634
- case 'set-error-reporting':
1635
- errorReportingEnabled = !!e.data.enabled;
1636
- console.log('[Loxia] Error reporting ' + (errorReportingEnabled ? 'enabled' : 'disabled'));
1637
- break;
1638
- }
1639
- });
1640
-
1641
- // === Console Error Capture ===
1642
- let errorReportingEnabled = true;
1643
- const capturedErrors = [];
1644
- const MAX_CAPTURED = 20;
1645
-
1646
- function reportError(error) {
1647
- if (!errorReportingEnabled) return;
1648
- if (capturedErrors.length >= MAX_CAPTURED) return;
1649
- // Skip Loxia's own logs
1650
- if (typeof error.message === 'string' && error.message.startsWith('[Loxia]')) return;
1651
-
1652
- const entry = {
1653
- type: error.type || 'error',
1654
- message: String(error.message || error).slice(0, 500),
1655
- source: error.source || null,
1656
- line: error.line || null,
1657
- col: error.col || null,
1658
- timestamp: Date.now()
1659
- };
1660
- capturedErrors.push(entry);
1661
-
1662
- window.parent.postMessage({
1663
- type: 'console-error',
1664
- data: entry
1665
- }, '*');
1666
- }
1667
-
1668
- // Capture unhandled errors
1669
- window.addEventListener('error', (e) => {
1670
- reportError({
1671
- type: 'runtime-error',
1672
- message: e.message,
1673
- source: e.filename,
1674
- line: e.lineno,
1675
- col: e.colno
1676
- });
1677
- });
1678
-
1679
- // Capture unhandled promise rejections
1680
- window.addEventListener('unhandledrejection', (e) => {
1681
- reportError({
1682
- type: 'unhandled-rejection',
1683
- message: e.reason ? (e.reason.message || String(e.reason)) : 'Unknown rejection'
1684
- });
1685
- });
1686
-
1687
- // Intercept console.error
1688
- const originalConsoleError = console.error;
1689
- console.error = function() {
1690
- const msg = Array.from(arguments).map(a => {
1691
- if (a instanceof Error) return a.message + (a.stack ? '\\n' + a.stack.split('\\n').slice(0, 3).join('\\n') : '');
1692
- if (typeof a === 'object') try { return JSON.stringify(a).slice(0, 300); } catch { return String(a); }
1693
- return String(a);
1694
- }).join(' ');
1695
-
1696
- reportError({ type: 'console-error', message: msg });
1697
- originalConsoleError.apply(console, arguments);
1698
- };
1699
-
1700
- // Intercept console.warn for build warnings
1701
- const originalConsoleWarn = console.warn;
1702
- console.warn = function() {
1703
- const msg = Array.from(arguments).map(a => typeof a === 'string' ? a : String(a)).join(' ');
1704
- // Only capture warnings that look like build/framework issues
1705
- if (/deprecat|warning|failed|error|cannot|invalid/i.test(msg)) {
1706
- reportError({ type: 'console-warning', message: msg });
1707
- }
1708
- originalConsoleWarn.apply(console, arguments);
1709
- };
1710
-
1711
- console.log('[Loxia] Visual Editor overlay loaded - Click elements to select, ESC to toggle');
1712
- })();`;
1713
- }
1714
- }
1715
-
1716
- // Singleton instance
1717
- let serverInstance = null;
1718
-
1719
- /**
1720
- * Get or create the Visual Editor Server singleton
1721
- * @param {Object} config - Configuration (only used on first call)
1722
- * @returns {VisualEditorServer}
1723
- */
1724
- export function getVisualEditorServer(config = {}) {
1725
- if (!serverInstance) {
1726
- serverInstance = new VisualEditorServer(config);
1727
- }
1728
- return serverInstance;
1729
- }
1730
-
1731
- /**
1732
- * Reset the singleton (for testing)
1733
- */
1734
- export async function resetVisualEditorServer() {
1735
- if (serverInstance) {
1736
- await serverInstance.stop();
1737
- serverInstance = null;
1738
- }
1739
- }
1740
-
1741
- /**
1742
- * Set the bridge getter function to enable element selection forwarding
1743
- * This avoids circular dependencies between visualEditorServer and visualEditorBridge
1744
- * @param {Function} getter - Function that returns the visualEditorBridge instance
1745
- */
1746
- export { setBridgeGetter };
1747
-
1748
- /**
1749
- * Get the Visual Editor port from service registry (source of truth)
1750
- * Falls back to server instance port or default if not registered
1751
- * @returns {number} The port number
1752
- */
1753
- export function getVisualEditorPort() {
1754
- // Check service registry first (source of truth)
1755
- const service = registry.get(SERVICE_NAME);
1756
- if (service) {
1757
- return service.port;
1758
- }
1759
-
1760
- // Fall back to server instance or default
1761
- return serverInstance?.port || getDefaultPort();
1762
- }
1763
-
1764
- /**
1765
- * Get the Visual Editor base URL from service registry
1766
- * @returns {string} The base URL (e.g., http://localhost:4000)
1767
- */
1768
- export function getVisualEditorBaseUrl() {
1769
- const port = getVisualEditorPort();
1770
- return `http://localhost:${port}`;
1771
- }
1772
-
1773
- export { FALLBACK_PORT as VISUAL_EDITOR_DEFAULT_PORT };
1774
- export default VisualEditorServer;
1
+ /**
2
+ * Visual Editor Server
3
+ *
4
+ * Runs on port 4000 and provides:
5
+ * - Health check endpoint
6
+ * - Editor HTML page for iframe embedding
7
+ * - Proxy to user's running app
8
+ * - WebSocket for backend bridge communication
9
+ * - Element picker overlay injection
10
+ */
11
+
12
+ import express from 'express';
13
+ import { createProxyMiddleware } from 'http-proxy-middleware';
14
+ import { WebSocketServer } from 'ws';
15
+ import https from 'https';
16
+ import http from 'http';
17
+ import zlib from 'zlib';
18
+
19
+ // Import service registry for port allocation and registration
20
+ import registry, { findFreePort } from './serviceRegistry.js';
21
+
22
+ // Lazy getter for bridge to avoid circular dependency
23
+ // (visualEditorBridge imports from this file)
24
+ let bridgeGetter = null;
25
+ function setBridgeGetter(getter) {
26
+ bridgeGetter = getter;
27
+ }
28
+ function getBridge() {
29
+ if (!bridgeGetter) {
30
+ // Fallback: try dynamic import (async, only for initialization)
31
+ return null;
32
+ }
33
+ return bridgeGetter();
34
+ }
35
+
36
+ // Service name for registry
37
+ const SERVICE_NAME = 'visualEditor';
38
+
39
+ // Config manager reference (set when initialized from main app)
40
+ let configManagerRef = null;
41
+
42
+ /**
43
+ * Set the config manager reference for reading configuration
44
+ * @param {ConfigManager} configManager - The config manager instance
45
+ */
46
+ export function setConfigManager(configManager) {
47
+ configManagerRef = configManager;
48
+ }
49
+
50
+ /**
51
+ * Get configuration value with fallback chain:
52
+ * 1. Config manager (from config file)
53
+ * 2. Environment variable
54
+ * 3. Default value
55
+ */
56
+ function getConfigValue(configPath, envVar, defaultValue) {
57
+ // Try config manager first
58
+ if (configManagerRef) {
59
+ const configValue = configManagerRef.get(configPath);
60
+ if (configValue !== undefined) {
61
+ return configValue;
62
+ }
63
+ }
64
+
65
+ // Try environment variable
66
+ const envValue = process.env[envVar];
67
+ if (envValue !== undefined) {
68
+ // Parse numbers
69
+ if (typeof defaultValue === 'number') {
70
+ const parsed = parseInt(envValue, 10);
71
+ if (!isNaN(parsed)) return parsed;
72
+ }
73
+ return envValue;
74
+ }
75
+
76
+ // Return default
77
+ return defaultValue;
78
+ }
79
+
80
+ // Hard-coded fallback defaults (used only when nothing else is configured)
81
+ const FALLBACK_PORT = 4000;
82
+ const FALLBACK_APP_URL = 'http://localhost:3000';
83
+
84
+ /**
85
+ * Get the configured port (evaluated at runtime, not module load)
86
+ * @returns {number}
87
+ */
88
+ function getDefaultPort() {
89
+ return getConfigValue('visualEditor.port', 'LOXIA_VISUAL_EDITOR_PORT', FALLBACK_PORT);
90
+ }
91
+
92
+ /**
93
+ * Get the configured default app URL (evaluated at runtime)
94
+ * @returns {string}
95
+ */
96
+ function getDefaultAppUrl() {
97
+ return getConfigValue('visualEditor.defaultAppUrl', 'LOXIA_DEFAULT_APP_URL', FALLBACK_APP_URL);
98
+ }
99
+
100
+ /**
101
+ * Visual Editor Server class
102
+ */
103
+ class VisualEditorServer {
104
+ /**
105
+ * @param {Object} config - Configuration options
106
+ * @param {number} config.port - Server port (default: 4000)
107
+ * @param {Object} config.logger - Logger instance
108
+ */
109
+ constructor(config = {}) {
110
+ this.port = config.port || getDefaultPort();
111
+ this.logger = config.logger || console;
112
+ this.server = null;
113
+ this.wss = null;
114
+ this.app = null;
115
+ this.isRunning = false;
116
+
117
+ // Track active connections
118
+ this.wsConnections = new Map(); // agentId -> WebSocket
119
+ this.activeAppUrls = new Map(); // agentId -> appUrl
120
+ this.staticDirs = new Map(); // agentId -> directory path for static serving
121
+ }
122
+
123
+ /**
124
+ * Register a static directory to serve for an agent
125
+ * @param {string} agentId - Agent identifier
126
+ * @param {string} directory - Directory path to serve
127
+ */
128
+ registerStaticDir(agentId, directory) {
129
+ this.staticDirs.set(agentId, directory);
130
+ this.logger.info?.(`[VisualEditorServer] Registered static dir for ${agentId}: ${directory}`) ||
131
+ console.log(`[VisualEditorServer] Registered static dir for ${agentId}: ${directory}`);
132
+ }
133
+
134
+ /**
135
+ * Unregister a static directory
136
+ * @param {string} agentId - Agent identifier
137
+ */
138
+ unregisterStaticDir(agentId) {
139
+ this.staticDirs.delete(agentId);
140
+ }
141
+
142
+ /**
143
+ * Start the Visual Editor Server
144
+ * Uses findFreePort to handle port conflicts and registers with service registry
145
+ * @returns {Promise<Object>} Start result
146
+ */
147
+ async start() {
148
+ if (this.isRunning) {
149
+ return { success: true, port: this.port, message: 'Already running' };
150
+ }
151
+
152
+ // Find a free port by checking on 0.0.0.0 (matching the actual bind address)
153
+ const preferredPort = this.port;
154
+ try {
155
+ const actualPort = await findFreePort(preferredPort, 100, '0.0.0.0');
156
+
157
+ if (actualPort !== preferredPort) {
158
+ this.logger.info?.(`[VisualEditorServer] Port ${preferredPort} taken, using ${actualPort}`) ||
159
+ console.log(`[VisualEditorServer] Port ${preferredPort} taken, using ${actualPort}`);
160
+ this.port = actualPort;
161
+ }
162
+ } catch (err) {
163
+ this.logger.error?.(`[VisualEditorServer] Could not find free port: ${err.message}`);
164
+ throw err;
165
+ }
166
+
167
+ return this._tryListen(this.port, 10);
168
+ }
169
+
170
+ /**
171
+ * Try to listen on a port, retrying on EADDRINUSE up to maxRetries times.
172
+ * Handles the TOCTOU race between findFreePort and actual listen().
173
+ */
174
+ async _tryListen(port, maxRetries) {
175
+ this.port = port;
176
+ this.app = express();
177
+ this._setupMiddleware();
178
+ this._setupRoutes();
179
+
180
+ return new Promise((resolve, reject) => {
181
+ this.server = this.app.listen(this.port, '0.0.0.0', () => {
182
+ this.isRunning = true;
183
+ this._setupWebSocketServer();
184
+
185
+ // Register with service registry
186
+ registry.register(SERVICE_NAME, {
187
+ port: this.port,
188
+ host: 'localhost',
189
+ protocol: 'http',
190
+ metadata: {
191
+ wsPath: '/ws',
192
+ startedAt: Date.now()
193
+ }
194
+ });
195
+
196
+ this.logger.info?.(`[VisualEditorServer] Running on port ${this.port}`) ||
197
+ console.log(`[VisualEditorServer] Running on port ${this.port}`);
198
+
199
+ resolve({ success: true, port: this.port });
200
+ });
201
+
202
+ this.server.on('error', (err) => {
203
+ if (err.code === 'EADDRINUSE' && maxRetries > 0) {
204
+ const nextPort = this.port + 1;
205
+ this.logger.info?.(`[VisualEditorServer] Port ${this.port} in use, trying ${nextPort}...`) ||
206
+ console.log(`[VisualEditorServer] Port ${this.port} in use, trying ${nextPort}...`);
207
+ // Clean up and retry on next port
208
+ try { this.server.close(); } catch { /* swallow */ }
209
+ this.app = null;
210
+ this.server = null;
211
+ resolve(this._tryListen(nextPort, maxRetries - 1));
212
+ } else {
213
+ this.logger.error?.(`[VisualEditorServer] Server error: ${err.message}`);
214
+ reject(err);
215
+ }
216
+ });
217
+ });
218
+ }
219
+
220
+ /**
221
+ * Stop the Visual Editor Server
222
+ * @returns {Promise<void>}
223
+ */
224
+ async stop() {
225
+ if (!this.isRunning) return;
226
+
227
+ // Unregister from service registry
228
+ registry.unregister(SERVICE_NAME);
229
+
230
+ // Close all WebSocket connections
231
+ for (const [, ws] of this.wsConnections.entries()) {
232
+ try {
233
+ ws.close(1000, 'Server shutting down');
234
+ } catch {
235
+ // Ignore
236
+ }
237
+ }
238
+ this.wsConnections.clear();
239
+ this.activeAppUrls.clear();
240
+
241
+ // Close WebSocket server
242
+ if (this.wss) {
243
+ this.wss.close();
244
+ this.wss = null;
245
+ }
246
+
247
+ // Close HTTP server — force-close keep-alive connections
248
+ return new Promise((resolve) => {
249
+ if (this.server) {
250
+ if (typeof this.server.closeAllConnections === 'function') {
251
+ this.server.closeAllConnections();
252
+ }
253
+ this.server.close(() => {
254
+ this.isRunning = false;
255
+ this.server = null;
256
+ this.logger.info?.('[VisualEditorServer] Stopped') ||
257
+ console.log('[VisualEditorServer] Stopped');
258
+ resolve();
259
+ });
260
+ } else {
261
+ resolve();
262
+ }
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Get server status
268
+ * @returns {Object} Status info
269
+ */
270
+ getStatus() {
271
+ return {
272
+ isRunning: this.isRunning,
273
+ port: this.port,
274
+ activeConnections: this.wsConnections.size,
275
+ connectedAgents: Array.from(this.wsConnections.keys())
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Register an app URL for an agent
281
+ * @param {string} agentId - Agent identifier
282
+ * @param {string} appUrl - User's app URL
283
+ */
284
+ registerAppUrl(agentId, appUrl) {
285
+ this.activeAppUrls.set(agentId, appUrl);
286
+ }
287
+
288
+ /**
289
+ * Unregister an agent's app URL
290
+ * @param {string} agentId - Agent identifier
291
+ */
292
+ unregisterAppUrl(agentId) {
293
+ this.activeAppUrls.delete(agentId);
294
+ }
295
+
296
+ /**
297
+ * Send message to a specific agent's WebSocket
298
+ * @param {string} agentId - Agent identifier
299
+ * @param {Object} message - Message to send
300
+ * @returns {boolean} Success
301
+ */
302
+ sendToAgent(agentId, message) {
303
+ const ws = this.wsConnections.get(agentId);
304
+ if (ws && ws.readyState === 1) { // WebSocket.OPEN
305
+ try {
306
+ ws.send(JSON.stringify(message));
307
+ return true;
308
+ } catch (err) {
309
+ this.logger.error?.(`[VisualEditorServer] Failed to send to agent ${agentId}:`, err);
310
+ }
311
+ }
312
+ return false;
313
+ }
314
+
315
+ /**
316
+ * Set up Express middleware
317
+ * @private
318
+ */
319
+ _setupMiddleware() {
320
+ // Request logging for debugging
321
+ this.app.use((req, res, next) => {
322
+ this.logger.debug?.(`[VisualEditorServer] ${req.method} ${req.url}`);
323
+ next();
324
+ });
325
+
326
+ // CORS for cross-origin requests
327
+ this.app.use((req, res, next) => {
328
+ res.header('Access-Control-Allow-Origin', '*');
329
+ res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
330
+ res.header('Access-Control-Allow-Headers', 'Content-Type');
331
+ if (req.method === 'OPTIONS') {
332
+ return res.sendStatus(200);
333
+ }
334
+ next();
335
+ });
336
+
337
+ // Parse JSON bodies
338
+ this.app.use(express.json());
339
+ }
340
+
341
+ /**
342
+ * Set up Express routes
343
+ * @private
344
+ */
345
+ _setupRoutes() {
346
+ // Health check endpoint
347
+ this.app.get('/health', (req, res) => {
348
+ res.json({
349
+ status: 'ok',
350
+ timestamp: Date.now(),
351
+ connections: this.wsConnections.size
352
+ });
353
+ });
354
+
355
+ // Test proxy connectivity endpoint (for debugging)
356
+ this.app.get('/test-proxy', async (req, res) => {
357
+ const targetUrl = req.query.url || 'https://httpbin.org/html';
358
+ this.logger.info?.(`[VisualEditorServer] Testing connectivity to: ${targetUrl}`);
359
+
360
+ try {
361
+ const parsed = new URL(targetUrl);
362
+ const isHttps = parsed.protocol === 'https:';
363
+ const httpModule = isHttps ? https : http;
364
+
365
+ const result = await new Promise((resolve, reject) => {
366
+ const reqOptions = {
367
+ hostname: parsed.hostname,
368
+ port: parsed.port || (isHttps ? 443 : 80),
369
+ path: parsed.pathname + parsed.search,
370
+ method: 'HEAD', // Just check connectivity, don't download content
371
+ timeout: 10000,
372
+ headers: {
373
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0'
374
+ },
375
+ rejectUnauthorized: false // Allow self-signed certs
376
+ };
377
+
378
+ const request = httpModule.request(reqOptions, (response) => {
379
+ resolve({
380
+ success: true,
381
+ url: targetUrl,
382
+ status: response.statusCode,
383
+ statusText: response.statusMessage,
384
+ contentType: response.headers['content-type']
385
+ });
386
+ });
387
+
388
+ request.on('timeout', () => {
389
+ request.destroy();
390
+ reject(new Error('Connection timed out after 10s'));
391
+ });
392
+
393
+ request.on('error', reject);
394
+ request.end();
395
+ });
396
+
397
+ res.json(result);
398
+ } catch (err) {
399
+ this.logger.error?.(`[VisualEditorServer] Test connectivity failed:`, err.message);
400
+ res.json({
401
+ success: false,
402
+ url: targetUrl,
403
+ error: err.message,
404
+ code: err.code || 'UNKNOWN'
405
+ });
406
+ }
407
+ });
408
+
409
+ // Main editor page (served in iframe)
410
+ this.app.get('/', (req, res) => {
411
+ const { agentId, appUrl } = req.query;
412
+
413
+ if (!agentId) {
414
+ return res.status(400).send('Missing agentId parameter');
415
+ }
416
+
417
+ const targetUrl = appUrl || this.activeAppUrls.get(agentId) || getDefaultAppUrl();
418
+ const html = this._generateEditorHtml(agentId, targetUrl);
419
+ res.type('html').send(html);
420
+ });
421
+
422
+ // Serve overlay script
423
+ this.app.get('/overlay.js', (req, res) => {
424
+ const overlayScript = this._getOverlayScript();
425
+ res.type('application/javascript').send(overlayScript);
426
+ });
427
+
428
+ // Serve static files for agents (for static HTML projects)
429
+ this.app.use('/static/:agentId', (req, res, next) => {
430
+ const { agentId } = req.params;
431
+ const staticDir = this.staticDirs.get(agentId);
432
+
433
+ if (!staticDir) {
434
+ return res.status(404).json({
435
+ error: 'No static directory registered for this agent',
436
+ agentId
437
+ });
438
+ }
439
+
440
+ // Create static middleware for this directory
441
+ const staticMiddleware = express.static(staticDir, {
442
+ index: ['index.html', 'index.htm'],
443
+ extensions: ['html', 'htm']
444
+ });
445
+
446
+ // Inject overlay script into HTML files
447
+ const originalSend = res.send.bind(res);
448
+ res.send = (body) => {
449
+ if (typeof body === 'string' && body.includes('</body>')) {
450
+ // Inject overlay script before </body>
451
+ const overlayScript = `<script src="/overlay.js"></script>`;
452
+ body = body.replace('</body>', `${overlayScript}</body>`);
453
+ }
454
+ return originalSend(body);
455
+ };
456
+
457
+ staticMiddleware(req, res, next);
458
+ });
459
+
460
+ // Proxy to user's app with overlay injection
461
+ // Wrap in error handler to catch any proxy initialization errors
462
+ const proxyMiddleware = this._createProxyMiddleware();
463
+ this.app.use('/app', (req, res, next) => {
464
+ try {
465
+ proxyMiddleware(req, res, next);
466
+ } catch (err) {
467
+ this.logger.error?.(`[VisualEditorServer] Proxy middleware error:`, err.message);
468
+ res.status(502).type('html').send(this._generateErrorHtml(
469
+ req.query.target || 'unknown',
470
+ `Proxy error: ${err.message}`
471
+ ));
472
+ }
473
+ });
474
+
475
+ // 404 catch-all - log and return helpful message
476
+ this.app.use((req, res) => {
477
+ this.logger.warn?.(`[VisualEditorServer] 404: ${req.method} ${req.url}`);
478
+ res.status(404).json({
479
+ error: 'Not found',
480
+ path: req.url,
481
+ hint: 'Use /app?target=URL to proxy to a website'
482
+ });
483
+ });
484
+
485
+ // Express error handler - catches uncaught errors
486
+ // eslint-disable-next-line no-unused-vars
487
+ this.app.use((err, req, res, next) => {
488
+ this.logger.error?.(`[VisualEditorServer] Express error:`, err.message);
489
+ if (!res.headersSent) {
490
+ res.status(500).type('html').send(this._generateErrorHtml(
491
+ req.query?.target || req.url,
492
+ `Server error: ${err.message}`
493
+ ));
494
+ }
495
+ });
496
+ }
497
+
498
+ /**
499
+ * Create proxy middleware for user's app
500
+ * Uses router option for dynamic target selection based on query param
501
+ * @private
502
+ */
503
+ _createProxyMiddleware() {
504
+ const self = this;
505
+
506
+ // Store current target URL for use in callbacks
507
+ let currentTargetUrl = getDefaultAppUrl();
508
+
509
+ // http-proxy-middleware v3.x uses 'on' property for event handlers
510
+ return createProxyMiddleware({
511
+ // Use router for dynamic target based on query parameter
512
+ router: (req) => {
513
+ const targetUrl = req.query.target || getDefaultAppUrl();
514
+ currentTargetUrl = targetUrl; // Store for use in callbacks
515
+
516
+ // Validate URL
517
+ try {
518
+ const parsed = new URL(targetUrl);
519
+ self.logger.info?.(`[VisualEditorServer] Proxying to: ${parsed.origin}`);
520
+ return parsed.origin; // Return just the origin (protocol + host + port)
521
+ } catch {
522
+ self.logger.error?.(`[VisualEditorServer] Invalid target URL: ${targetUrl}`);
523
+ return getDefaultAppUrl(); // Fallback to default
524
+ }
525
+ },
526
+ changeOrigin: true,
527
+ selfHandleResponse: true, // We'll handle response to inject script
528
+ secure: false, // Don't validate SSL certificates (needed for dev servers)
529
+ followRedirects: true, // Follow redirects
530
+ proxyTimeout: 30000, // 30 second proxy timeout
531
+ timeout: 30000, // 30 second request timeout
532
+ pathRewrite: (path, req) => {
533
+ // Get the path from the target URL and append request path
534
+ const targetUrl = req.query.target || getDefaultAppUrl();
535
+ try {
536
+ const parsed = new URL(targetUrl);
537
+ // Start with the path from target URL
538
+ let newPath = parsed.pathname;
539
+ if (newPath === '/') newPath = '';
540
+
541
+ // Parse current request path and remove /app and query params
542
+ const reqUrl = new URL(path, 'http://localhost');
543
+ reqUrl.searchParams.delete('target');
544
+ const reqPath = reqUrl.pathname.replace(/^\/app\/?/, '/');
545
+
546
+ // Combine paths (avoid double slashes)
547
+ const finalPath = newPath + (reqPath === '/' ? '' : reqPath) + reqUrl.search;
548
+ self.logger.debug?.(`[VisualEditorServer] Path rewrite: ${path} -> ${finalPath || '/'}`);
549
+ return finalPath || '/';
550
+ } catch {
551
+ return '/';
552
+ }
553
+ },
554
+ // v3.x event handlers using 'on' property
555
+ on: {
556
+ proxyReq: (proxyReq, req) => {
557
+ const targetUrl = req.query.target || getDefaultAppUrl();
558
+ self.logger.info?.(`[VisualEditorServer] Proxy request to: ${targetUrl}`);
559
+
560
+ // Set browser-like headers to avoid being blocked
561
+ // Wrap in try-catch because headers might already be sent on redirects
562
+ try {
563
+ if (!proxyReq.headersSent) {
564
+ proxyReq.setHeader('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
565
+ proxyReq.setHeader('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8');
566
+ proxyReq.setHeader('Accept-Language', 'en-US,en;q=0.9');
567
+ // IMPORTANT: Request uncompressed content - we need to read/modify HTML
568
+ // If we request gzip, we'd need to decompress before injecting the overlay script
569
+ proxyReq.setHeader('Accept-Encoding', 'identity');
570
+ // Remove headers that might cause issues
571
+ proxyReq.removeHeader('x-forwarded-for');
572
+ proxyReq.removeHeader('x-forwarded-host');
573
+ proxyReq.removeHeader('x-forwarded-proto');
574
+ }
575
+ } catch (e) {
576
+ // Headers already sent (e.g., during redirect) - ignore
577
+ self.logger.debug?.(`[VisualEditorServer] Could not set headers: ${e.message}`);
578
+ }
579
+ },
580
+ proxyRes: (proxyRes, req, res) => {
581
+ const targetUrl = req.query.target || currentTargetUrl;
582
+ self._handleProxyResponse(proxyRes, req, res, targetUrl);
583
+ },
584
+ error: (err, req, res) => {
585
+ try {
586
+ const targetUrl = req?.query?.target || currentTargetUrl || 'unknown';
587
+
588
+ // Log detailed error information
589
+ self.logger.error?.('[VisualEditorServer] Proxy error:', {
590
+ message: err?.message,
591
+ code: err?.code,
592
+ target: targetUrl,
593
+ url: req?.url
594
+ });
595
+
596
+ // Provide more helpful error messages based on error type
597
+ let errorMessage = err?.message || 'Unknown proxy error';
598
+ if (err?.code === 'ECONNREFUSED') {
599
+ errorMessage = `Connection refused - the server at ${targetUrl} is not running or not accepting connections`;
600
+ } else if (err?.code === 'ENOTFOUND') {
601
+ errorMessage = `DNS lookup failed - could not resolve hostname for ${targetUrl}`;
602
+ } else if (err?.code === 'ETIMEDOUT' || err?.code === 'ESOCKETTIMEDOUT') {
603
+ errorMessage = `Connection timed out - the server at ${targetUrl} took too long to respond (30s limit)`;
604
+ } else if (err?.code === 'CERT_HAS_EXPIRED' || err?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
605
+ errorMessage = `SSL certificate error for ${targetUrl}. This may be a self-signed certificate issue.`;
606
+ } else if (err?.code === 'ECONNRESET') {
607
+ errorMessage = `Connection was reset by the server at ${targetUrl}`;
608
+ } else if (err?.code === 'HPE_INVALID_CONSTANT') {
609
+ errorMessage = `Invalid response from ${targetUrl} - the server may not be an HTTP server`;
610
+ }
611
+
612
+ // Return a user-friendly HTML error page
613
+ if (res && !res.headersSent) {
614
+ res.writeHead(502, { 'Content-Type': 'text/html' });
615
+ res.end(self._generateErrorHtml(targetUrl, errorMessage));
616
+ }
617
+ } catch (handlerErr) {
618
+ self.logger.error?.('[VisualEditorServer] Error in error handler:', handlerErr);
619
+ // Last resort - try to send a simple error
620
+ try {
621
+ if (res && !res.headersSent) {
622
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
623
+ res.end('Proxy error: ' + (err?.message || 'Unknown error'));
624
+ }
625
+ } catch {
626
+ // Nothing more we can do
627
+ }
628
+ }
629
+ }
630
+ }
631
+ });
632
+ }
633
+
634
+ /**
635
+ * Generate error HTML page for proxy failures
636
+ * @private
637
+ */
638
+ _generateErrorHtml(targetUrl, errorDetails) {
639
+ return `<!DOCTYPE html>
640
+ <html>
641
+ <head>
642
+ <title>Connection Error - Visual Editor</title>
643
+ <style>
644
+ * { margin: 0; padding: 0; box-sizing: border-box; }
645
+ body {
646
+ font-family: system-ui, -apple-system, sans-serif;
647
+ background: #f3f4f6;
648
+ min-height: 100vh;
649
+ display: flex;
650
+ align-items: center;
651
+ justify-content: center;
652
+ padding: 20px;
653
+ }
654
+ .error-container {
655
+ background: white;
656
+ border-radius: 12px;
657
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
658
+ padding: 32px;
659
+ max-width: 500px;
660
+ text-align: center;
661
+ }
662
+ .error-icon {
663
+ width: 64px;
664
+ height: 64px;
665
+ margin: 0 auto 16px;
666
+ background: #fef2f2;
667
+ border-radius: 50%;
668
+ display: flex;
669
+ align-items: center;
670
+ justify-content: center;
671
+ }
672
+ .error-icon svg {
673
+ width: 32px;
674
+ height: 32px;
675
+ color: #ef4444;
676
+ }
677
+ h1 {
678
+ color: #1f2937;
679
+ font-size: 20px;
680
+ margin-bottom: 8px;
681
+ }
682
+ .target-url {
683
+ color: #3b82f6;
684
+ font-family: monospace;
685
+ background: #eff6ff;
686
+ padding: 8px 12px;
687
+ border-radius: 6px;
688
+ margin: 16px 0;
689
+ word-break: break-all;
690
+ }
691
+ .instructions {
692
+ color: #6b7280;
693
+ font-size: 14px;
694
+ line-height: 1.6;
695
+ margin-top: 16px;
696
+ }
697
+ .instructions ol {
698
+ text-align: left;
699
+ padding-left: 20px;
700
+ margin-top: 12px;
701
+ }
702
+ .instructions li {
703
+ margin-bottom: 8px;
704
+ }
705
+ .retry-btn {
706
+ margin-top: 20px;
707
+ padding: 10px 24px;
708
+ background: #3b82f6;
709
+ color: white;
710
+ border: none;
711
+ border-radius: 6px;
712
+ font-size: 14px;
713
+ cursor: pointer;
714
+ transition: background 0.2s;
715
+ }
716
+ .retry-btn:hover {
717
+ background: #2563eb;
718
+ }
719
+ .error-details {
720
+ margin-top: 16px;
721
+ padding: 12px;
722
+ background: #fef2f2;
723
+ border-radius: 6px;
724
+ color: #991b1b;
725
+ font-size: 12px;
726
+ font-family: monospace;
727
+ }
728
+ </style>
729
+ </head>
730
+ <body>
731
+ <div class="error-container">
732
+ <div class="error-icon">
733
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
734
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
735
+ </svg>
736
+ </div>
737
+ <h1>Cannot Connect to Your App</h1>
738
+ <div class="target-url">${targetUrl}</div>
739
+ <div class="instructions">
740
+ <p>Make sure your app is running at this address.</p>
741
+ <ol>
742
+ <li>Start your development server (e.g., <code>npm run dev</code>)</li>
743
+ <li>Enter the correct URL in the address bar above</li>
744
+ <li>Click "Go" or retry below</li>
745
+ </ol>
746
+ </div>
747
+ <button class="retry-btn" onclick="location.reload()">Retry Connection</button>
748
+ <div class="error-details">${errorDetails}</div>
749
+ </div>
750
+ </body>
751
+ </html>`;
752
+ }
753
+
754
+ /**
755
+ * Handle proxy response - inject overlay script into HTML
756
+ * @private
757
+ */
758
+ _handleProxyResponse(proxyRes, req, res, targetUrl) {
759
+ const contentType = proxyRes.headers['content-type'] || '';
760
+ const contentEncoding = proxyRes.headers['content-encoding'] || '';
761
+
762
+ // Copy headers (skip content-length and content-encoding as we'll modify content)
763
+ Object.keys(proxyRes.headers).forEach(key => {
764
+ const lowerKey = key.toLowerCase();
765
+ if (lowerKey !== 'content-length' && lowerKey !== 'content-encoding') {
766
+ res.setHeader(key, proxyRes.headers[key]);
767
+ }
768
+ });
769
+
770
+ res.status(proxyRes.statusCode);
771
+
772
+ // Only inject into HTML responses
773
+ if (contentType.includes('text/html')) {
774
+ const chunks = [];
775
+
776
+ // Collect all data chunks
777
+ proxyRes.on('data', (chunk) => {
778
+ chunks.push(chunk);
779
+ });
780
+
781
+ proxyRes.on('end', () => {
782
+ // Combine chunks into a single buffer
783
+ const buffer = Buffer.concat(chunks);
784
+
785
+ // Decompress if needed (some servers ignore Accept-Encoding: identity)
786
+ this._decompressBuffer(buffer, contentEncoding)
787
+ .then(decompressed => {
788
+ const body = decompressed.toString('utf-8');
789
+ // Inject overlay script before </body>
790
+ const injectedHtml = this._injectOverlayScript(body, targetUrl);
791
+ res.send(injectedHtml);
792
+ })
793
+ .catch(err => {
794
+ this.logger.error?.(`[VisualEditorServer] Decompression error: ${err.message}`);
795
+ // Try to send as-is (might be uncompressed despite header)
796
+ try {
797
+ const body = buffer.toString('utf-8');
798
+ const injectedHtml = this._injectOverlayScript(body, targetUrl);
799
+ res.send(injectedHtml);
800
+ } catch {
801
+ res.status(500).send('Error processing response');
802
+ }
803
+ });
804
+ });
805
+ } else {
806
+ // Pass through non-HTML responses
807
+ proxyRes.pipe(res);
808
+ }
809
+ }
810
+
811
+ /**
812
+ * Decompress buffer based on content-encoding
813
+ * @private
814
+ */
815
+ async _decompressBuffer(buffer, encoding) {
816
+ if (!encoding || encoding === 'identity') {
817
+ return buffer;
818
+ }
819
+
820
+ return new Promise((resolve, reject) => {
821
+ if (encoding === 'gzip') {
822
+ zlib.gunzip(buffer, (err, result) => {
823
+ if (err) reject(err);
824
+ else resolve(result);
825
+ });
826
+ } else if (encoding === 'deflate') {
827
+ zlib.inflate(buffer, (err, result) => {
828
+ if (err) reject(err);
829
+ else resolve(result);
830
+ });
831
+ } else if (encoding === 'br') {
832
+ zlib.brotliDecompress(buffer, (err, result) => {
833
+ if (err) reject(err);
834
+ else resolve(result);
835
+ });
836
+ } else {
837
+ // Unknown encoding, try to use as-is
838
+ resolve(buffer);
839
+ }
840
+ });
841
+ }
842
+
843
+ /**
844
+ * Inject overlay script into HTML
845
+ * @private
846
+ */
847
+ _injectOverlayScript(html, targetUrl) {
848
+ // Use ABSOLUTE URL for overlay.js since we inject a <base> tag that would redirect relative paths
849
+ const overlayUrl = `http://localhost:${this.port}/overlay.js`;
850
+ const scriptTag = `
851
+ <!-- Loxia Visual Editor Overlay -->
852
+ <script src="${overlayUrl}"></script>
853
+ `;
854
+
855
+ // Add a <base> tag to make relative URLs resolve to the original site
856
+ // This prevents assets (scripts, styles, images) from being requested through our server
857
+ //
858
+ // IMPORTANT: The base href must include the *directory* of the target URL, not just
859
+ // the origin. If the target is http://host/sub/page.html and we set <base href="http://host/">,
860
+ // then a relative <link href="styles.css"> resolves to http://host/styles.css (wrong) instead
861
+ // of http://host/sub/styles.css (correct). We compute the directory portion of the pathname
862
+ // and use that as the base href.
863
+ let baseTag = '';
864
+ try {
865
+ const parsed = new URL(targetUrl);
866
+ // Derive directory: for /sub/page.html -> /sub/, for /sub/ -> /sub/, for / -> /
867
+ let dir = parsed.pathname;
868
+ if (!dir.endsWith('/')) {
869
+ const lastSlash = dir.lastIndexOf('/');
870
+ dir = lastSlash >= 0 ? dir.substring(0, lastSlash + 1) : '/';
871
+ }
872
+ baseTag = `<base href="${parsed.origin}${dir}">`;
873
+ } catch {
874
+ // Invalid URL, skip base tag
875
+ }
876
+
877
+ let modifiedHtml = html;
878
+
879
+ // Inject base tag right after the opening <head ...> tag (case-insensitive, allows attributes).
880
+ // If there is no <head> at all, synthesize one after <html ...> so we still get a base.
881
+ // If a <base> already exists in the document, rewrite its href — the original would point
882
+ // at the original site path and we want it pointed at our resolved directory instead.
883
+ if (baseTag) {
884
+ const hasBase = /<base\b[^>]*>/i.test(html);
885
+ if (hasBase) {
886
+ // Replace the first <base ...> with our computed one
887
+ modifiedHtml = modifiedHtml.replace(/<base\b[^>]*>/i, baseTag);
888
+ } else {
889
+ const headOpenRe = /<head\b[^>]*>/i;
890
+ if (headOpenRe.test(modifiedHtml)) {
891
+ modifiedHtml = modifiedHtml.replace(headOpenRe, (match) => `${match}${baseTag}`);
892
+ } else {
893
+ const htmlOpenRe = /<html\b[^>]*>/i;
894
+ if (htmlOpenRe.test(modifiedHtml)) {
895
+ modifiedHtml = modifiedHtml.replace(htmlOpenRe, (match) => `${match}<head>${baseTag}</head>`);
896
+ } else {
897
+ // No <html> either — prepend a head block
898
+ modifiedHtml = `<head>${baseTag}</head>${modifiedHtml}`;
899
+ }
900
+ }
901
+ }
902
+ }
903
+
904
+ // Inject overlay script before </body> or at end
905
+ if (modifiedHtml.includes('</body>')) {
906
+ return modifiedHtml.replace('</body>', `${scriptTag}</body>`);
907
+ } else if (modifiedHtml.includes('</html>')) {
908
+ return modifiedHtml.replace('</html>', `${scriptTag}</html>`);
909
+ } else {
910
+ return modifiedHtml + scriptTag;
911
+ }
912
+ }
913
+
914
+ /**
915
+ * Set up WebSocket server for backend bridge communication
916
+ * @private
917
+ */
918
+ _setupWebSocketServer() {
919
+ this.wss = new WebSocketServer({
920
+ server: this.server,
921
+ path: '/ws'
922
+ });
923
+
924
+ this.wss.on('connection', (ws, req) => {
925
+ const url = new URL(req.url, `http://localhost:${this.port}`);
926
+ const agentId = url.searchParams.get('agentId');
927
+
928
+ if (!agentId) {
929
+ ws.close(1008, 'Missing agentId');
930
+ return;
931
+ }
932
+
933
+ this.logger.info?.(`[VisualEditorServer] WebSocket connected: ${agentId}`);
934
+
935
+ // Store connection
936
+ this.wsConnections.set(agentId, ws);
937
+
938
+ // Send ready message
939
+ ws.send(JSON.stringify({
940
+ type: 'editor-ready',
941
+ agentId,
942
+ timestamp: Date.now()
943
+ }));
944
+
945
+ // Handle incoming messages
946
+ ws.on('message', (data) => {
947
+ this._handleWebSocketMessage(agentId, data, ws);
948
+ });
949
+
950
+ // Handle close
951
+ ws.on('close', (code) => {
952
+ this.logger.info?.(`[VisualEditorServer] WebSocket closed: ${agentId} (${code})`);
953
+ this.wsConnections.delete(agentId);
954
+ });
955
+
956
+ // Handle errors
957
+ ws.on('error', (err) => {
958
+ this.logger.error?.(`[VisualEditorServer] WebSocket error for ${agentId}:`, err);
959
+ });
960
+ });
961
+ }
962
+
963
+ /**
964
+ * Handle incoming WebSocket messages
965
+ * @private
966
+ */
967
+ _handleWebSocketMessage(agentId, data, ws) {
968
+ try {
969
+ const message = JSON.parse(data.toString());
970
+
971
+ this.logger.debug?.(`[VisualEditorServer] Message from ${agentId}:`, message.type);
972
+
973
+ switch (message.type) {
974
+ case 'element-selected':
975
+ // Forward element selection (already handled by postMessage to web-ui)
976
+ // This is for backend bridge to receive selections
977
+ this._emitElementSelected(agentId, message);
978
+ break;
979
+
980
+ case 'ping':
981
+ ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
982
+ break;
983
+
984
+ case 'highlight':
985
+ case 'scroll-to':
986
+ case 'reload':
987
+ // These commands come from backend, forward to any connected editor pages
988
+ // (handled via the editor page's WebSocket connection)
989
+ break;
990
+
991
+ case 'subscribe':
992
+ // Agent subscribing to editor events
993
+ if (message.appUrl) {
994
+ this.activeAppUrls.set(agentId, message.appUrl);
995
+ }
996
+ break;
997
+
998
+ case 'unsubscribe':
999
+ this.activeAppUrls.delete(agentId);
1000
+ break;
1001
+
1002
+ default:
1003
+ this.logger.debug?.(`[VisualEditorServer] Unknown message type: ${message.type}`);
1004
+ }
1005
+ } catch (err) {
1006
+ this.logger.error?.('[VisualEditorServer] Invalid WebSocket message:', err);
1007
+ }
1008
+ }
1009
+
1010
+ /**
1011
+ * Emit element selected event (for external listeners)
1012
+ * @private
1013
+ */
1014
+ _emitElementSelected(agentId, message) {
1015
+ const elementData = message.data || message;
1016
+
1017
+ this.logger.info?.(`[VisualEditorServer] Element selected for ${agentId}:`, {
1018
+ selector: elementData.selector,
1019
+ component: elementData.sourceHint?.component
1020
+ });
1021
+
1022
+ // Forward to visualEditorBridge so context is available for message injection
1023
+ const bridge = getBridge();
1024
+ if (bridge && bridge.hasInstance(agentId)) {
1025
+ const success = bridge.setVisualContext(agentId, {
1026
+ selector: elementData.selector,
1027
+ tagName: elementData.tagName,
1028
+ text: elementData.text,
1029
+ attributes: elementData.attributes,
1030
+ boundingRect: elementData.boundingRect,
1031
+ sourceHint: elementData.sourceHint,
1032
+ computedStyle: elementData.computedStyle
1033
+ });
1034
+
1035
+ if (success) {
1036
+ this.logger.info?.(`[VisualEditorServer] Visual context synced to bridge for ${agentId}`);
1037
+ } else {
1038
+ this.logger.warn?.(`[VisualEditorServer] Failed to sync visual context for ${agentId}`);
1039
+ }
1040
+ } else {
1041
+ this.logger.debug?.(`[VisualEditorServer] No bridge instance for ${agentId}, context not synced`);
1042
+ }
1043
+ }
1044
+
1045
+ /**
1046
+ * Generate editor HTML page
1047
+ * @private
1048
+ */
1049
+ _generateEditorHtml(agentId, appUrl) {
1050
+ const encodedAppUrl = encodeURIComponent(appUrl);
1051
+
1052
+ return `<!DOCTYPE html>
1053
+ <html>
1054
+ <head>
1055
+ <meta charset="UTF-8">
1056
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1057
+ <title>Loxia Visual Editor</title>
1058
+ <style>
1059
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1060
+ html, body { height: 100%; overflow: hidden; }
1061
+ body { font-family: system-ui, -apple-system, sans-serif; }
1062
+
1063
+ #app-frame {
1064
+ width: 100%;
1065
+ height: 100%;
1066
+ border: none;
1067
+ }
1068
+
1069
+ #loading-overlay {
1070
+ position: fixed;
1071
+ top: 0;
1072
+ left: 0;
1073
+ right: 0;
1074
+ bottom: 0;
1075
+ background: #f3f4f6;
1076
+ display: flex;
1077
+ flex-direction: column;
1078
+ align-items: center;
1079
+ justify-content: center;
1080
+ z-index: 1000;
1081
+ transition: opacity 0.3s ease;
1082
+ }
1083
+
1084
+ #loading-overlay.hidden {
1085
+ opacity: 0;
1086
+ pointer-events: none;
1087
+ }
1088
+
1089
+ .spinner {
1090
+ width: 40px;
1091
+ height: 40px;
1092
+ border: 3px solid #e5e7eb;
1093
+ border-top-color: #3b82f6;
1094
+ border-radius: 50%;
1095
+ animation: spin 1s linear infinite;
1096
+ }
1097
+
1098
+ @keyframes spin {
1099
+ to { transform: rotate(360deg); }
1100
+ }
1101
+
1102
+ .loading-text {
1103
+ margin-top: 16px;
1104
+ color: #6b7280;
1105
+ font-size: 14px;
1106
+ }
1107
+
1108
+ #error-message {
1109
+ display: none;
1110
+ position: fixed;
1111
+ top: 50%;
1112
+ left: 50%;
1113
+ transform: translate(-50%, -50%);
1114
+ background: white;
1115
+ padding: 24px;
1116
+ border-radius: 8px;
1117
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
1118
+ text-align: center;
1119
+ max-width: 400px;
1120
+ }
1121
+
1122
+ #error-message h3 {
1123
+ color: #ef4444;
1124
+ margin-bottom: 8px;
1125
+ }
1126
+
1127
+ #error-message p {
1128
+ color: #6b7280;
1129
+ font-size: 14px;
1130
+ }
1131
+ </style>
1132
+ </head>
1133
+ <body>
1134
+ <div id="loading-overlay">
1135
+ <div class="spinner"></div>
1136
+ <p class="loading-text">Loading preview...</p>
1137
+ </div>
1138
+
1139
+ <div id="error-message">
1140
+ <h3>Connection Error</h3>
1141
+ <p id="error-text">Could not load the preview.</p>
1142
+ </div>
1143
+
1144
+ <iframe
1145
+ id="app-frame"
1146
+ src="/app?target=${encodedAppUrl}"
1147
+ sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
1148
+ ></iframe>
1149
+
1150
+ <script>
1151
+ const agentId = '${agentId}';
1152
+ const appUrl = '${appUrl}';
1153
+
1154
+ // Hide loading overlay when iframe loads
1155
+ const iframe = document.getElementById('app-frame');
1156
+ const loadingOverlay = document.getElementById('loading-overlay');
1157
+ const errorMessage = document.getElementById('error-message');
1158
+ const errorText = document.getElementById('error-text');
1159
+
1160
+ iframe.onload = () => {
1161
+ loadingOverlay.classList.add('hidden');
1162
+
1163
+ // Check if the iframe loaded our error page (proxy failure)
1164
+ try {
1165
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
1166
+ const title = iframeDoc.title || '';
1167
+ if (title.includes('Connection Error') || title.includes('Cannot Connect')) {
1168
+ // Proxy returned an error page - show error message
1169
+ errorText.textContent = 'Could not proxy to ' + appUrl + '. See the error details in the preview.';
1170
+ errorMessage.style.display = 'block';
1171
+ }
1172
+ } catch (e) {
1173
+ // Cross-origin - can't check, but that usually means it loaded successfully
1174
+ console.log('[Visual Editor] Cross-origin iframe loaded - assuming success');
1175
+ }
1176
+ };
1177
+
1178
+ iframe.onerror = () => {
1179
+ loadingOverlay.classList.add('hidden');
1180
+ errorText.textContent = 'Could not connect to ' + appUrl + '. Make sure your app is running.';
1181
+ errorMessage.style.display = 'block';
1182
+ };
1183
+
1184
+ // Forward messages from app iframe to parent (Loxia Web-UI)
1185
+ window.addEventListener('message', (e) => {
1186
+ // Only forward element-selected messages
1187
+ if (e.data && e.data.type === 'element-selected') {
1188
+ // Forward to parent window (Loxia Web-UI)
1189
+ window.parent.postMessage(e.data, '*');
1190
+
1191
+ // Also send via WebSocket to backend
1192
+ if (window.wsConnection && window.wsConnection.readyState === 1) {
1193
+ window.wsConnection.send(JSON.stringify({
1194
+ ...e.data,
1195
+ agentId
1196
+ }));
1197
+ }
1198
+ }
1199
+ });
1200
+
1201
+ // Listen for commands from parent (Loxia Web-UI) or WebSocket
1202
+ window.addEventListener('message', (e) => {
1203
+ if (e.data && (e.data.type === 'highlight' || e.data.type === 'scroll-to' || e.data.type === 'toggle')) {
1204
+ // Forward to app iframe (including toggle for Select/Preview mode switching)
1205
+ iframe.contentWindow.postMessage(e.data, '*');
1206
+ }
1207
+ });
1208
+
1209
+ // Connect to WebSocket for backend communication
1210
+ const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
1211
+ const wsUrl = wsProtocol + '//' + location.host + '/ws?agentId=' + agentId;
1212
+
1213
+ function connectWebSocket() {
1214
+ window.wsConnection = new WebSocket(wsUrl);
1215
+
1216
+ window.wsConnection.onopen = () => {
1217
+ console.log('[Visual Editor] WebSocket connected');
1218
+ // Notify parent that editor is ready
1219
+ window.parent.postMessage({ type: 'editor-ready' }, '*');
1220
+ };
1221
+
1222
+ window.wsConnection.onmessage = (e) => {
1223
+ try {
1224
+ const msg = JSON.parse(e.data);
1225
+
1226
+ // Forward commands to app iframe
1227
+ if (msg.type === 'highlight' || msg.type === 'scroll-to' || msg.type === 'reload') {
1228
+ iframe.contentWindow.postMessage(msg, '*');
1229
+ }
1230
+ } catch (err) {
1231
+ console.error('[Visual Editor] Invalid message:', err);
1232
+ }
1233
+ };
1234
+
1235
+ window.wsConnection.onerror = (e) => {
1236
+ console.error('[Visual Editor] WebSocket error');
1237
+ window.parent.postMessage({
1238
+ type: 'editor-error',
1239
+ data: { message: 'WebSocket connection error' }
1240
+ }, '*');
1241
+ };
1242
+
1243
+ window.wsConnection.onclose = () => {
1244
+ console.log('[Visual Editor] WebSocket closed, reconnecting in 3s...');
1245
+ setTimeout(connectWebSocket, 3000);
1246
+ };
1247
+ }
1248
+
1249
+ connectWebSocket();
1250
+
1251
+ // Handle iframe load timeout (35s to allow proxy's 30s timeout to report actual error)
1252
+ setTimeout(() => {
1253
+ if (!loadingOverlay.classList.contains('hidden')) {
1254
+ loadingOverlay.classList.add('hidden');
1255
+ errorText.textContent = 'Preview is taking too long to load. Check if ' + appUrl + ' is accessible and responding.';
1256
+ errorMessage.style.display = 'block';
1257
+ }
1258
+ }, 35000);
1259
+ </script>
1260
+ </body>
1261
+ </html>`;
1262
+ }
1263
+
1264
+ /**
1265
+ * Get overlay script for element selection
1266
+ * @private
1267
+ */
1268
+ _getOverlayScript() {
1269
+ return `/**
1270
+ * Loxia Visual Editor Overlay Script
1271
+ * Injected into user's app for element selection
1272
+ */
1273
+ (function() {
1274
+ 'use strict';
1275
+
1276
+ // Prevent double injection
1277
+ if (window.__LOXIA_VISUAL_EDITOR_LOADED__) return;
1278
+ window.__LOXIA_VISUAL_EDITOR_LOADED__ = true;
1279
+
1280
+ let isEnabled = true;
1281
+ let hoveredElement = null;
1282
+ let selectedElement = null;
1283
+ let highlightOverlay = null;
1284
+ let selectionOverlay = null;
1285
+ let tooltip = null;
1286
+
1287
+ // Apply cursor style based on mode
1288
+ function updateCursorStyle(enabled) {
1289
+ document.body.style.cursor = enabled ? 'crosshair' : '';
1290
+ }
1291
+ updateCursorStyle(true);
1292
+
1293
+ // Create highlight overlay element (hover)
1294
+ function createHighlightOverlay() {
1295
+ const overlay = document.createElement('div');
1296
+ overlay.id = 'loxia-highlight-overlay';
1297
+ overlay.style.cssText = \`
1298
+ position: fixed;
1299
+ pointer-events: none;
1300
+ background: rgba(59, 130, 246, 0.1);
1301
+ border: 2px solid #3b82f6;
1302
+ border-radius: 4px;
1303
+ z-index: 999998;
1304
+ transition: all 0.1s ease;
1305
+ display: none;
1306
+ \`;
1307
+ document.body.appendChild(overlay);
1308
+ return overlay;
1309
+ }
1310
+
1311
+ // Create selection overlay element (click)
1312
+ function createSelectionOverlay() {
1313
+ const overlay = document.createElement('div');
1314
+ overlay.id = 'loxia-selection-overlay';
1315
+ overlay.style.cssText = \`
1316
+ position: fixed;
1317
+ pointer-events: none;
1318
+ background: rgba(34, 197, 94, 0.15);
1319
+ border: 2px solid #22c55e;
1320
+ border-radius: 4px;
1321
+ z-index: 999999;
1322
+ display: none;
1323
+ \`;
1324
+ document.body.appendChild(overlay);
1325
+ return overlay;
1326
+ }
1327
+
1328
+ // Create tooltip for element info
1329
+ function createTooltip() {
1330
+ const tip = document.createElement('div');
1331
+ tip.id = 'loxia-tooltip';
1332
+ tip.style.cssText = \`
1333
+ position: fixed;
1334
+ background: #1f2937;
1335
+ color: white;
1336
+ padding: 4px 8px;
1337
+ border-radius: 4px;
1338
+ font-size: 12px;
1339
+ font-family: ui-monospace, monospace;
1340
+ z-index: 1000000;
1341
+ pointer-events: none;
1342
+ display: none;
1343
+ max-width: 300px;
1344
+ overflow: hidden;
1345
+ text-overflow: ellipsis;
1346
+ white-space: nowrap;
1347
+ \`;
1348
+ document.body.appendChild(tip);
1349
+ return tip;
1350
+ }
1351
+
1352
+ // Generate CSS selector for element
1353
+ function getSelector(el) {
1354
+ if (!el || el === document.body || el === document.documentElement) {
1355
+ return el ? el.tagName.toLowerCase() : '';
1356
+ }
1357
+
1358
+ // Try ID first
1359
+ if (el.id && /^[a-zA-Z][\\w-]*$/.test(el.id)) {
1360
+ return '#' + el.id;
1361
+ }
1362
+
1363
+ let path = [];
1364
+ let current = el;
1365
+
1366
+ while (current && current !== document.body && path.length < 5) {
1367
+ let selector = current.tagName.toLowerCase();
1368
+
1369
+ // Add id if available
1370
+ if (current.id && /^[a-zA-Z][\\w-]*$/.test(current.id)) {
1371
+ path.unshift('#' + current.id);
1372
+ break;
1373
+ }
1374
+
1375
+ // Add meaningful classes (skip utility classes)
1376
+ if (current.className && typeof current.className === 'string') {
1377
+ const classes = current.className
1378
+ .split(/\\s+/)
1379
+ .filter(c => c && c.length > 2 && !c.match(/^(w-|h-|p-|m-|text-|bg-|flex|grid|block|inline)/))
1380
+ .slice(0, 2);
1381
+ if (classes.length) {
1382
+ selector += '.' + classes.join('.');
1383
+ }
1384
+ }
1385
+
1386
+ // Add nth-child if needed for uniqueness
1387
+ const siblings = current.parentElement ?
1388
+ Array.from(current.parentElement.children).filter(s => s.tagName === current.tagName) : [];
1389
+ if (siblings.length > 1) {
1390
+ const index = siblings.indexOf(current) + 1;
1391
+ selector += ':nth-child(' + index + ')';
1392
+ }
1393
+
1394
+ path.unshift(selector);
1395
+ current = current.parentElement;
1396
+ }
1397
+
1398
+ return path.join(' > ');
1399
+ }
1400
+
1401
+ // Try to get React component info
1402
+ function getReactInfo(el) {
1403
+ // Look for React fiber
1404
+ const fiberKey = Object.keys(el).find(k =>
1405
+ k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance')
1406
+ );
1407
+
1408
+ if (!fiberKey) return null;
1409
+
1410
+ let fiber = el[fiberKey];
1411
+ let depth = 0;
1412
+ const maxDepth = 20;
1413
+
1414
+ while (fiber && depth < maxDepth) {
1415
+ if (fiber.type && typeof fiber.type === 'function') {
1416
+ const name = fiber.type.displayName || fiber.type.name;
1417
+ if (name && name !== 'Anonymous' && !name.startsWith('_')) {
1418
+ return {
1419
+ component: name,
1420
+ source: fiber._debugSource || null
1421
+ };
1422
+ }
1423
+ }
1424
+ fiber = fiber.return;
1425
+ depth++;
1426
+ }
1427
+
1428
+ return null;
1429
+ }
1430
+
1431
+ // Extract a useful relative path from absolute file path
1432
+ function getRelativePath(fullPath) {
1433
+ if (!fullPath) return null;
1434
+
1435
+ // Common source folder markers (prioritized)
1436
+ const sourceMarkers = ['/src/', '/app/', '/pages/', '/components/', '/lib/', '/utils/'];
1437
+
1438
+ for (const marker of sourceMarkers) {
1439
+ const index = fullPath.indexOf(marker);
1440
+ if (index !== -1) {
1441
+ // Return path starting from the marker (e.g., 'src/components/Button.tsx')
1442
+ return fullPath.substring(index + 1);
1443
+ }
1444
+ }
1445
+
1446
+ // Fallback: return last 3 path segments
1447
+ const parts = fullPath.split('/').filter(Boolean);
1448
+ if (parts.length <= 3) {
1449
+ return parts.join('/');
1450
+ }
1451
+ return parts.slice(-3).join('/');
1452
+ }
1453
+
1454
+ // Get element info for selection
1455
+ function getElementInfo(el) {
1456
+ const rect = el.getBoundingClientRect();
1457
+ const reactInfo = getReactInfo(el);
1458
+ const computedStyle = window.getComputedStyle(el);
1459
+
1460
+ return {
1461
+ selector: getSelector(el),
1462
+ tagName: el.tagName.toLowerCase(),
1463
+ text: (el.textContent || '').trim().slice(0, 100),
1464
+ attributes: {
1465
+ id: el.id || null,
1466
+ class: el.className || null,
1467
+ href: el.href || null,
1468
+ src: el.src || null,
1469
+ type: el.type || null,
1470
+ name: el.name || null
1471
+ },
1472
+ boundingRect: {
1473
+ top: Math.round(rect.top),
1474
+ left: Math.round(rect.left),
1475
+ width: Math.round(rect.width),
1476
+ height: Math.round(rect.height)
1477
+ },
1478
+ computedStyle: {
1479
+ display: computedStyle.display,
1480
+ position: computedStyle.position,
1481
+ color: computedStyle.color,
1482
+ backgroundColor: computedStyle.backgroundColor,
1483
+ fontSize: computedStyle.fontSize
1484
+ },
1485
+ sourceHint: reactInfo ? {
1486
+ component: reactInfo.component,
1487
+ file: getRelativePath(reactInfo.source?.fileName),
1488
+ fullPath: reactInfo.source?.fileName,
1489
+ line: reactInfo.source?.lineNumber,
1490
+ confidence: reactInfo.source ? 'high' : 'low'
1491
+ } : null
1492
+ };
1493
+ }
1494
+
1495
+ // Position overlay on element
1496
+ function positionOverlay(overlay, el) {
1497
+ const rect = el.getBoundingClientRect();
1498
+ overlay.style.top = rect.top + 'px';
1499
+ overlay.style.left = rect.left + 'px';
1500
+ overlay.style.width = rect.width + 'px';
1501
+ overlay.style.height = rect.height + 'px';
1502
+ overlay.style.display = 'block';
1503
+ }
1504
+
1505
+ // Handle element selection
1506
+ function selectElement(el, event) {
1507
+ event.preventDefault();
1508
+ event.stopPropagation();
1509
+
1510
+ selectedElement = el;
1511
+ const info = getElementInfo(el);
1512
+
1513
+ // Show selection overlay
1514
+ if (!selectionOverlay) selectionOverlay = createSelectionOverlay();
1515
+ positionOverlay(selectionOverlay, el);
1516
+
1517
+ // Send selection to parent
1518
+ const message = {
1519
+ type: 'element-selected',
1520
+ data: info
1521
+ };
1522
+
1523
+ window.parent.postMessage(message, '*');
1524
+
1525
+ console.log('[Loxia] Element selected:', info.selector);
1526
+ }
1527
+
1528
+ // Mouse move - highlight hovered element
1529
+ document.addEventListener('mousemove', (e) => {
1530
+ if (!isEnabled) return;
1531
+
1532
+ const el = e.target;
1533
+ if (el === hoveredElement) return;
1534
+ if (el.id?.startsWith('loxia-')) return;
1535
+
1536
+ hoveredElement = el;
1537
+
1538
+ if (!highlightOverlay) highlightOverlay = createHighlightOverlay();
1539
+ if (!tooltip) tooltip = createTooltip();
1540
+
1541
+ positionOverlay(highlightOverlay, el);
1542
+
1543
+ // Update tooltip
1544
+ const tagName = el.tagName.toLowerCase();
1545
+ const id = el.id ? '#' + el.id : '';
1546
+ const classes = el.className && typeof el.className === 'string' ?
1547
+ '.' + el.className.split(' ').slice(0, 2).join('.') : '';
1548
+
1549
+ tooltip.textContent = tagName + id + classes;
1550
+ tooltip.style.left = (e.clientX + 10) + 'px';
1551
+ tooltip.style.top = (e.clientY + 10) + 'px';
1552
+ tooltip.style.display = 'block';
1553
+ }, true);
1554
+
1555
+ // Mouse leave - hide highlight
1556
+ document.addEventListener('mouseleave', () => {
1557
+ if (highlightOverlay) highlightOverlay.style.display = 'none';
1558
+ if (tooltip) tooltip.style.display = 'none';
1559
+ }, true);
1560
+
1561
+ // Click - select element
1562
+ document.addEventListener('click', (e) => {
1563
+ if (!isEnabled) return;
1564
+ if (e.target.id?.startsWith('loxia-')) return;
1565
+
1566
+ selectElement(e.target, e);
1567
+ }, true);
1568
+
1569
+ // Keyboard shortcuts
1570
+ document.addEventListener('keydown', (e) => {
1571
+ // Escape - toggle overlay
1572
+ if (e.key === 'Escape') {
1573
+ isEnabled = !isEnabled;
1574
+ updateCursorStyle(isEnabled);
1575
+ if (!isEnabled) {
1576
+ if (highlightOverlay) highlightOverlay.style.display = 'none';
1577
+ if (selectionOverlay) selectionOverlay.style.display = 'none';
1578
+ if (tooltip) tooltip.style.display = 'none';
1579
+ hoveredElement = null;
1580
+ }
1581
+ // Notify parent of mode change
1582
+ window.parent.postMessage({ type: 'mode-toggled', enabled: isEnabled }, '*');
1583
+ console.log('[Loxia] Visual editor ' + (isEnabled ? 'enabled' : 'disabled'));
1584
+ }
1585
+ });
1586
+
1587
+ // Listen for commands from editor
1588
+ window.addEventListener('message', (e) => {
1589
+ if (!e.data || !e.data.type) return;
1590
+
1591
+ switch (e.data.type) {
1592
+ case 'highlight':
1593
+ const highlightEl = document.querySelector(e.data.selector);
1594
+ if (highlightEl) {
1595
+ if (!selectionOverlay) selectionOverlay = createSelectionOverlay();
1596
+ positionOverlay(selectionOverlay, highlightEl);
1597
+
1598
+ // Auto-hide after duration
1599
+ setTimeout(() => {
1600
+ if (selectionOverlay) selectionOverlay.style.display = 'none';
1601
+ }, e.data.duration || 2000);
1602
+ }
1603
+ break;
1604
+
1605
+ case 'scroll-to':
1606
+ const scrollEl = document.querySelector(e.data.selector);
1607
+ if (scrollEl) {
1608
+ scrollEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
1609
+ }
1610
+ break;
1611
+
1612
+ case 'reload':
1613
+ location.reload();
1614
+ break;
1615
+
1616
+ case 'toggle':
1617
+ isEnabled = e.data.enabled !== undefined ? e.data.enabled : !isEnabled;
1618
+ updateCursorStyle(isEnabled);
1619
+ // Hide/show overlays based on new state
1620
+ if (!isEnabled) {
1621
+ if (highlightOverlay) highlightOverlay.style.display = 'none';
1622
+ if (selectionOverlay) selectionOverlay.style.display = 'none';
1623
+ if (tooltip) tooltip.style.display = 'none';
1624
+ hoveredElement = null;
1625
+ }
1626
+ console.log('[Loxia] Visual editor ' + (isEnabled ? 'enabled (Select mode)' : 'disabled (Preview mode)'));
1627
+ break;
1628
+
1629
+ case 'set-error-reporting':
1630
+ errorReportingEnabled = !!e.data.enabled;
1631
+ console.log('[Loxia] Error reporting ' + (errorReportingEnabled ? 'enabled' : 'disabled'));
1632
+ break;
1633
+ }
1634
+ });
1635
+
1636
+ // === Console Error Capture ===
1637
+ let errorReportingEnabled = true;
1638
+ const capturedErrors = [];
1639
+ const MAX_CAPTURED = 20;
1640
+
1641
+ function reportError(error) {
1642
+ if (!errorReportingEnabled) return;
1643
+ if (capturedErrors.length >= MAX_CAPTURED) return;
1644
+ // Skip Loxia's own logs
1645
+ if (typeof error.message === 'string' && error.message.startsWith('[Loxia]')) return;
1646
+
1647
+ const entry = {
1648
+ type: error.type || 'error',
1649
+ message: String(error.message || error).slice(0, 500),
1650
+ source: error.source || null,
1651
+ line: error.line || null,
1652
+ col: error.col || null,
1653
+ timestamp: Date.now()
1654
+ };
1655
+ capturedErrors.push(entry);
1656
+
1657
+ window.parent.postMessage({
1658
+ type: 'console-error',
1659
+ data: entry
1660
+ }, '*');
1661
+ }
1662
+
1663
+ // Capture unhandled errors
1664
+ window.addEventListener('error', (e) => {
1665
+ reportError({
1666
+ type: 'runtime-error',
1667
+ message: e.message,
1668
+ source: e.filename,
1669
+ line: e.lineno,
1670
+ col: e.colno
1671
+ });
1672
+ });
1673
+
1674
+ // Capture unhandled promise rejections
1675
+ window.addEventListener('unhandledrejection', (e) => {
1676
+ reportError({
1677
+ type: 'unhandled-rejection',
1678
+ message: e.reason ? (e.reason.message || String(e.reason)) : 'Unknown rejection'
1679
+ });
1680
+ });
1681
+
1682
+ // Intercept console.error
1683
+ const originalConsoleError = console.error;
1684
+ console.error = function() {
1685
+ const msg = Array.from(arguments).map(a => {
1686
+ if (a instanceof Error) return a.message + (a.stack ? '\\n' + a.stack.split('\\n').slice(0, 3).join('\\n') : '');
1687
+ if (typeof a === 'object') try { return JSON.stringify(a).slice(0, 300); } catch { return String(a); }
1688
+ return String(a);
1689
+ }).join(' ');
1690
+
1691
+ reportError({ type: 'console-error', message: msg });
1692
+ originalConsoleError.apply(console, arguments);
1693
+ };
1694
+
1695
+ // Intercept console.warn for build warnings
1696
+ const originalConsoleWarn = console.warn;
1697
+ console.warn = function() {
1698
+ const msg = Array.from(arguments).map(a => typeof a === 'string' ? a : String(a)).join(' ');
1699
+ // Only capture warnings that look like build/framework issues
1700
+ if (/deprecat|warning|failed|error|cannot|invalid/i.test(msg)) {
1701
+ reportError({ type: 'console-warning', message: msg });
1702
+ }
1703
+ originalConsoleWarn.apply(console, arguments);
1704
+ };
1705
+
1706
+ console.log('[Loxia] Visual Editor overlay loaded - Click elements to select, ESC to toggle');
1707
+ })();`;
1708
+ }
1709
+ }
1710
+
1711
+ // Singleton instance
1712
+ let serverInstance = null;
1713
+
1714
+ /**
1715
+ * Get or create the Visual Editor Server singleton
1716
+ * @param {Object} config - Configuration (only used on first call)
1717
+ * @returns {VisualEditorServer}
1718
+ */
1719
+ export function getVisualEditorServer(config = {}) {
1720
+ if (!serverInstance) {
1721
+ serverInstance = new VisualEditorServer(config);
1722
+ }
1723
+ return serverInstance;
1724
+ }
1725
+
1726
+ /**
1727
+ * Reset the singleton (for testing)
1728
+ */
1729
+ export async function resetVisualEditorServer() {
1730
+ if (serverInstance) {
1731
+ await serverInstance.stop();
1732
+ serverInstance = null;
1733
+ }
1734
+ }
1735
+
1736
+ /**
1737
+ * Set the bridge getter function to enable element selection forwarding
1738
+ * This avoids circular dependencies between visualEditorServer and visualEditorBridge
1739
+ * @param {Function} getter - Function that returns the visualEditorBridge instance
1740
+ */
1741
+ export { setBridgeGetter };
1742
+
1743
+ /**
1744
+ * Get the Visual Editor port from service registry (source of truth)
1745
+ * Falls back to server instance port or default if not registered
1746
+ * @returns {number} The port number
1747
+ */
1748
+ export function getVisualEditorPort() {
1749
+ // Check service registry first (source of truth)
1750
+ const service = registry.get(SERVICE_NAME);
1751
+ if (service) {
1752
+ return service.port;
1753
+ }
1754
+
1755
+ // Fall back to server instance or default
1756
+ return serverInstance?.port || getDefaultPort();
1757
+ }
1758
+
1759
+ /**
1760
+ * Get the Visual Editor base URL from service registry
1761
+ * @returns {string} The base URL (e.g., http://localhost:4000)
1762
+ */
1763
+ export function getVisualEditorBaseUrl() {
1764
+ const port = getVisualEditorPort();
1765
+ return `http://localhost:${port}`;
1766
+ }
1767
+
1768
+ export { FALLBACK_PORT as VISUAL_EDITOR_DEFAULT_PORT };
1769
+ export default VisualEditorServer;