crewswarm 0.8.1-beta

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 (362) hide show
  1. package/.env.example +155 -0
  2. package/LICENSE +21 -0
  3. package/README.md +316 -0
  4. package/apps/dashboard/dist/assets/chat-core-BwSoInmZ.js +1 -0
  5. package/apps/dashboard/dist/assets/chat-core-BwSoInmZ.js.br +0 -0
  6. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js +1 -0
  7. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
  8. package/apps/dashboard/dist/assets/components-CSUb80ze.js +1 -0
  9. package/apps/dashboard/dist/assets/components-CSUb80ze.js.br +0 -0
  10. package/apps/dashboard/dist/assets/core-utils-CAVnDoe1.js +1 -0
  11. package/apps/dashboard/dist/assets/core-utils-CAVnDoe1.js.br +0 -0
  12. package/apps/dashboard/dist/assets/index-CF0aJRtC.css +1 -0
  13. package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
  14. package/apps/dashboard/dist/assets/index-Px49zu76.js +2 -0
  15. package/apps/dashboard/dist/assets/index-Px49zu76.js.br +0 -0
  16. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js +1 -0
  17. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  18. package/apps/dashboard/dist/assets/setup-wizard-i3eEixlo.js +1 -0
  19. package/apps/dashboard/dist/assets/setup-wizard-i3eEixlo.js.br +0 -0
  20. package/apps/dashboard/dist/assets/tab-agents-tab-BThdsdJY.js +1 -0
  21. package/apps/dashboard/dist/assets/tab-agents-tab-BThdsdJY.js.br +0 -0
  22. package/apps/dashboard/dist/assets/tab-benchmarks-tab-DfCuAClu.js +1 -0
  23. package/apps/dashboard/dist/assets/tab-comms-tab-eHpOSBhG.js +1 -0
  24. package/apps/dashboard/dist/assets/tab-comms-tab-eHpOSBhG.js.br +0 -0
  25. package/apps/dashboard/dist/assets/tab-contacts-tab-yEegNyO4.js +1 -0
  26. package/apps/dashboard/dist/assets/tab-contacts-tab-yEegNyO4.js.br +0 -0
  27. package/apps/dashboard/dist/assets/tab-engines-tab-C3DYxTwy.js +1 -0
  28. package/apps/dashboard/dist/assets/tab-engines-tab-C3DYxTwy.js.br +0 -0
  29. package/apps/dashboard/dist/assets/tab-memory-tab-C59BYFQD.js +1 -0
  30. package/apps/dashboard/dist/assets/tab-memory-tab-C59BYFQD.js.br +0 -0
  31. package/apps/dashboard/dist/assets/tab-models-tab-9Ur7pXWA.js +1 -0
  32. package/apps/dashboard/dist/assets/tab-models-tab-9Ur7pXWA.js.br +0 -0
  33. package/apps/dashboard/dist/assets/tab-pm-loop-tab-D7mnDelU.js +1 -0
  34. package/apps/dashboard/dist/assets/tab-pm-loop-tab-D7mnDelU.js.br +0 -0
  35. package/apps/dashboard/dist/assets/tab-projects-tab-C6h2Mv1K.js +1 -0
  36. package/apps/dashboard/dist/assets/tab-projects-tab-C6h2Mv1K.js.br +0 -0
  37. package/apps/dashboard/dist/assets/tab-prompts-tab-C0wZvWK3.js +1 -0
  38. package/apps/dashboard/dist/assets/tab-prompts-tab-C0wZvWK3.js.br +0 -0
  39. package/apps/dashboard/dist/assets/tab-services-tab-DBj_w3bc.js +1 -0
  40. package/apps/dashboard/dist/assets/tab-services-tab-DBj_w3bc.js.br +0 -0
  41. package/apps/dashboard/dist/assets/tab-settings-tab-ezeqAjZk.js +1 -0
  42. package/apps/dashboard/dist/assets/tab-settings-tab-ezeqAjZk.js.br +0 -0
  43. package/apps/dashboard/dist/assets/tab-skills-tab-BYdU2whk.js +1 -0
  44. package/apps/dashboard/dist/assets/tab-skills-tab-BYdU2whk.js.br +0 -0
  45. package/apps/dashboard/dist/assets/tab-spending-tab-Bg6w9t_p.js +1 -0
  46. package/apps/dashboard/dist/assets/tab-spending-tab-Bg6w9t_p.js.br +0 -0
  47. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BBV9HB2X.js +1 -0
  48. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BBV9HB2X.js.br +0 -0
  49. package/apps/dashboard/dist/assets/tab-swarm-tab-ChqLlEVs.js +1 -0
  50. package/apps/dashboard/dist/assets/tab-swarm-tab-ChqLlEVs.js.br +0 -0
  51. package/apps/dashboard/dist/assets/tab-usage-tab-B2UWXenJ.js +1 -0
  52. package/apps/dashboard/dist/assets/tab-usage-tab-B2UWXenJ.js.br +0 -0
  53. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js +1 -0
  54. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  55. package/apps/dashboard/dist/assets/tab-workflows-tab-6QSXLJ0i.js +1 -0
  56. package/apps/dashboard/dist/assets/tab-workflows-tab-6QSXLJ0i.js.br +0 -0
  57. package/apps/dashboard/dist/favicon.png +0 -0
  58. package/apps/dashboard/dist/index.html +6466 -0
  59. package/apps/dashboard/dist/index.html.br +0 -0
  60. package/apps/dashboard/dist/index.html.gz +0 -0
  61. package/apps/dashboard/dist/signup.html +446 -0
  62. package/apps/dashboard/index.html +6442 -0
  63. package/apps/dashboard/package.json +15 -0
  64. package/apps/dashboard/src/app.js +2823 -0
  65. package/apps/dashboard/src/app.js.br +0 -0
  66. package/apps/dashboard/src/app.js.gz +0 -0
  67. package/apps/dashboard/src/chat/chat-actions.js +1847 -0
  68. package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
  69. package/apps/dashboard/src/chat/unified-messages.js +327 -0
  70. package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
  71. package/apps/dashboard/src/cli-process.js +208 -0
  72. package/apps/dashboard/src/cli-process.js.br +0 -0
  73. package/apps/dashboard/src/cli-process.js.gz +0 -0
  74. package/apps/dashboard/src/components/active-tasks-panel.js +175 -0
  75. package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
  76. package/apps/dashboard/src/core/api.js +18 -0
  77. package/apps/dashboard/src/core/api.js.br +0 -0
  78. package/apps/dashboard/src/core/dom.js +220 -0
  79. package/apps/dashboard/src/core/dom.js.br +0 -0
  80. package/apps/dashboard/src/core/state.js +91 -0
  81. package/apps/dashboard/src/core/state.js.br +0 -0
  82. package/apps/dashboard/src/core/task-manager.js +134 -0
  83. package/apps/dashboard/src/core/task-manager.js.br +0 -0
  84. package/apps/dashboard/src/orchestration-status.js +127 -0
  85. package/apps/dashboard/src/orchestration-status.js.br +0 -0
  86. package/apps/dashboard/src/setup-wizard.js +555 -0
  87. package/apps/dashboard/src/setup-wizard.js.br +0 -0
  88. package/apps/dashboard/src/styles.css +2085 -0
  89. package/apps/dashboard/src/styles.css.br +0 -0
  90. package/apps/dashboard/src/styles.css.gz +0 -0
  91. package/apps/dashboard/src/tabs/agents-tab.js +2237 -0
  92. package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
  93. package/apps/dashboard/src/tabs/benchmarks-tab.js +229 -0
  94. package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
  95. package/apps/dashboard/src/tabs/comms-tab.js +955 -0
  96. package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
  97. package/apps/dashboard/src/tabs/contacts-tab.js +654 -0
  98. package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
  99. package/apps/dashboard/src/tabs/engines-tab.js +175 -0
  100. package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
  101. package/apps/dashboard/src/tabs/memory-tab.js +182 -0
  102. package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
  103. package/apps/dashboard/src/tabs/models-tab.js +441 -0
  104. package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
  105. package/apps/dashboard/src/tabs/pm-loop-tab.js +185 -0
  106. package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
  107. package/apps/dashboard/src/tabs/projects-tab.js +663 -0
  108. package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
  109. package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
  110. package/apps/dashboard/src/tabs/prompts-tab.js +160 -0
  111. package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
  112. package/apps/dashboard/src/tabs/services-tab.js +202 -0
  113. package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
  114. package/apps/dashboard/src/tabs/settings-tab.js +803 -0
  115. package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
  116. package/apps/dashboard/src/tabs/skills-tab.js +284 -0
  117. package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
  118. package/apps/dashboard/src/tabs/spending-tab.js +173 -0
  119. package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
  120. package/apps/dashboard/src/tabs/swarm-chat-tab.js +660 -0
  121. package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
  122. package/apps/dashboard/src/tabs/swarm-tab.js +538 -0
  123. package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
  124. package/apps/dashboard/src/tabs/usage-tab.js +390 -0
  125. package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
  126. package/apps/dashboard/src/tabs/waves-tab.js +238 -0
  127. package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
  128. package/apps/dashboard/src/tabs/workflows-tab.js +747 -0
  129. package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
  130. package/apps/vibe/.crew/agent-memory/pipeline.json +249 -0
  131. package/apps/vibe/.crew/cost.json +17 -0
  132. package/apps/vibe/.crew/json-parse-metrics.jsonl +22 -0
  133. package/apps/vibe/.crew/pipeline-metrics.jsonl +22 -0
  134. package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +5 -0
  135. package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +5 -0
  136. package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +1 -0
  137. package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +5 -0
  138. package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +5 -0
  139. package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +5 -0
  140. package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +5 -0
  141. package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +5 -0
  142. package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +5 -0
  143. package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +5 -0
  144. package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +5 -0
  145. package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +5 -0
  146. package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +5 -0
  147. package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +5 -0
  148. package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +5 -0
  149. package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +5 -0
  150. package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +5 -0
  151. package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +5 -0
  152. package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +2 -0
  153. package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +5 -0
  154. package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +5 -0
  155. package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +5 -0
  156. package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +5 -0
  157. package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +5 -0
  158. package/apps/vibe/.crew/sandbox.json +7 -0
  159. package/apps/vibe/.crew/session.json +285 -0
  160. package/apps/vibe/.crew/training-data.jsonl +0 -0
  161. package/apps/vibe/.github/workflows/studio-quality.yml +37 -0
  162. package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +12 -0
  163. package/apps/vibe/.studio-data/project-messages/general.jsonl +54 -0
  164. package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +10 -0
  165. package/apps/vibe/ARCHITECTURE.md +3393 -0
  166. package/apps/vibe/QUICK-REFERENCE.md +211 -0
  167. package/apps/vibe/README.md +76 -0
  168. package/apps/vibe/ROADMAP.md +41 -0
  169. package/apps/vibe/STUDIO-SETUP-COMPLETE.md +35 -0
  170. package/apps/vibe/VISUAL-GUIDE.md +378 -0
  171. package/apps/vibe/capture-demo.mjs +160 -0
  172. package/apps/vibe/capture-vibe-assets.mjs +71 -0
  173. package/apps/vibe/capture-vibe-video.mjs +260 -0
  174. package/apps/vibe/check-buttons.js +41 -0
  175. package/apps/vibe/diagnose.html +106 -0
  176. package/apps/vibe/fix-buttons.js +103 -0
  177. package/apps/vibe/index.html +3401 -0
  178. package/apps/vibe/package-lock.json +920 -0
  179. package/apps/vibe/package.json +31 -0
  180. package/apps/vibe/public/favicon.png +0 -0
  181. package/apps/vibe/scripts/studio-pty-host.py +117 -0
  182. package/apps/vibe/server.mjs +1835 -0
  183. package/apps/vibe/src/main.js +2846 -0
  184. package/apps/vibe/src/register-all-languages.js +98 -0
  185. package/apps/vibe/start-studio.sh +11 -0
  186. package/apps/vibe/test/accessibility-tests.js +77 -0
  187. package/apps/vibe/test/browser-performance-audit.mjs +205 -0
  188. package/apps/vibe/test/performance-tests.js +120 -0
  189. package/apps/vibe/test/security-tests.js +213 -0
  190. package/apps/vibe/tests/e2e.local.mjs +54 -0
  191. package/apps/vibe/tests/server.smoke.mjs +106 -0
  192. package/apps/vibe/update_website.mjs +74 -0
  193. package/apps/vibe/vite.config.js +19 -0
  194. package/apps/vibe/watch-server.mjs +108 -0
  195. package/contrib/openclaw-plugin/README.md +199 -0
  196. package/contrib/openclaw-plugin/index.ts +306 -0
  197. package/contrib/openclaw-plugin/openclaw.plugin.json +41 -0
  198. package/contrib/openclaw-plugin/package.json +27 -0
  199. package/contrib/openclaw-plugin/skills/crewswarm/SKILL.md +88 -0
  200. package/crew-lead.mjs +649 -0
  201. package/engines/claude-code.json +36 -0
  202. package/engines/codex.json +37 -0
  203. package/engines/crew-cli.json +42 -0
  204. package/engines/cursor.json +40 -0
  205. package/engines/docker-sandbox.json +38 -0
  206. package/engines/gemini-cli.json +75 -0
  207. package/engines/opencode.json +31 -0
  208. package/gateway-bridge.mjs +1575 -0
  209. package/install.sh +738 -0
  210. package/lib/agent-registry.mjs +232 -0
  211. package/lib/agents/daemon.mjs +121 -0
  212. package/lib/agents/dispatch.mjs +225 -0
  213. package/lib/agents/permissions.mjs +90 -0
  214. package/lib/agents/platform-formatting.mjs +102 -0
  215. package/lib/agents/registry.mjs +81 -0
  216. package/lib/agents/tool-instructions.mjs +257 -0
  217. package/lib/agents/validation.mjs +75 -0
  218. package/lib/approval/policy-manager.mjs +221 -0
  219. package/lib/autoharness/index.mjs +391 -0
  220. package/lib/bridges/cli-executor.mjs +332 -0
  221. package/lib/bridges/gateway-ws.mjs +345 -0
  222. package/lib/bridges/integration.mjs +229 -0
  223. package/lib/bridges/rag-helper.mjs +90 -0
  224. package/lib/browser/opencode-passthrough-filter.js +44 -0
  225. package/lib/browser/passthrough-stderr.js +109 -0
  226. package/lib/chat/autonomous-mentions.mjs +373 -0
  227. package/lib/chat/history.mjs +82 -0
  228. package/lib/chat/mention-routing-intent.mjs +136 -0
  229. package/lib/chat/participants.mjs +95 -0
  230. package/lib/chat/project-messages-rag.mjs +265 -0
  231. package/lib/chat/project-messages.mjs +479 -0
  232. package/lib/chat/shared-chat-prompt-overlay.mjs +52 -0
  233. package/lib/chat/thread-binding.mjs +34 -0
  234. package/lib/chat/unified-history.mjs +223 -0
  235. package/lib/chat/unified-wrapper.mjs +41 -0
  236. package/lib/cli-process-tracker.mjs +228 -0
  237. package/lib/collections/index.mjs +433 -0
  238. package/lib/contacts/identity-linker.mjs +248 -0
  239. package/lib/contacts/index.mjs +341 -0
  240. package/lib/crew-judge/PROMPT.md +93 -0
  241. package/lib/crew-judge/judge.mjs +260 -0
  242. package/lib/crew-lead/agent-manager.mjs +125 -0
  243. package/lib/crew-lead/background.mjs +270 -0
  244. package/lib/crew-lead/brain.mjs +110 -0
  245. package/lib/crew-lead/chat-handler.mjs +2603 -0
  246. package/lib/crew-lead/chat-handler.mjs.bak +1274 -0
  247. package/lib/crew-lead/classifier.mjs +83 -0
  248. package/lib/crew-lead/http-server.mjs +4824 -0
  249. package/lib/crew-lead/intent.mjs +102 -0
  250. package/lib/crew-lead/interval-manager.mjs +41 -0
  251. package/lib/crew-lead/llm-caller.mjs +544 -0
  252. package/lib/crew-lead/prompts.mjs +392 -0
  253. package/lib/crew-lead/retry-manager.mjs +118 -0
  254. package/lib/crew-lead/tools.mjs +318 -0
  255. package/lib/crew-lead/wave-dispatcher.mjs +798 -0
  256. package/lib/crew-lead/waves-config.json +73 -0
  257. package/lib/crew-lead/waves-loader.mjs +110 -0
  258. package/lib/crew-lead/ws-router.mjs +428 -0
  259. package/lib/dispatch/parsers.mjs +299 -0
  260. package/lib/domain-planning/detector.mjs +196 -0
  261. package/lib/domain-planning/prompts/crew-pm-cli.md +96 -0
  262. package/lib/domain-planning/prompts/crew-pm-core.md +122 -0
  263. package/lib/domain-planning/prompts/crew-pm-frontend.md +111 -0
  264. package/lib/engines/crew-cli-sandbox.mjs +422 -0
  265. package/lib/engines/crew-cli.mjs +155 -0
  266. package/lib/engines/cursor-launcher.mjs +110 -0
  267. package/lib/engines/engine-registry.mjs +253 -0
  268. package/lib/engines/llm-direct.mjs +184 -0
  269. package/lib/engines/opencode.mjs +256 -0
  270. package/lib/engines/ouroboros.mjs +114 -0
  271. package/lib/engines/rt-envelope.mjs +1643 -0
  272. package/lib/engines/rt-envelope.mjs.backup-current +870 -0
  273. package/lib/engines/runners.mjs +1367 -0
  274. package/lib/gemini-cli-passthrough-noise.mjs +37 -0
  275. package/lib/integrations/code-search.mjs +259 -0
  276. package/lib/integrations/greptile.mjs +148 -0
  277. package/lib/integrations/multimodal.mjs +313 -0
  278. package/lib/integrations/telegram-streaming.mjs +153 -0
  279. package/lib/integrations/tts.mjs +312 -0
  280. package/lib/integrations/twitter-links.mjs +294 -0
  281. package/lib/memory/shared-adapter.mjs +296 -0
  282. package/lib/pipeline/manager.mjs +539 -0
  283. package/lib/preferences/extractor.mjs +347 -0
  284. package/lib/project-dir.mjs +20 -0
  285. package/lib/runtime/config.mjs +388 -0
  286. package/lib/runtime/dlq.mjs +170 -0
  287. package/lib/runtime/log-rotation.mjs +82 -0
  288. package/lib/runtime/logger.mjs +58 -0
  289. package/lib/runtime/memory.mjs +421 -0
  290. package/lib/runtime/paths.mjs +76 -0
  291. package/lib/runtime/project-dir.mjs +127 -0
  292. package/lib/runtime/spending.mjs +204 -0
  293. package/lib/runtime/startup-guard.mjs +291 -0
  294. package/lib/runtime/task-lease.mjs +234 -0
  295. package/lib/runtime/telemetry-schema.mjs +208 -0
  296. package/lib/runtime/telemetry.mjs +101 -0
  297. package/lib/runtime/utils.mjs +64 -0
  298. package/lib/skills/index.mjs +265 -0
  299. package/lib/tools/browser.mjs +135 -0
  300. package/lib/tools/executor.mjs +913 -0
  301. package/lib/types.d.ts +57 -0
  302. package/package.json +106 -0
  303. package/pm-loop.mjs +1626 -0
  304. package/prompts/coder-back.md +27 -0
  305. package/prompts/coder-front.md +27 -0
  306. package/prompts/coder.md +28 -0
  307. package/prompts/copywriter.md +17 -0
  308. package/prompts/fixer.md +39 -0
  309. package/prompts/frontend.md +23 -0
  310. package/prompts/github.md +24 -0
  311. package/prompts/main.md +39 -0
  312. package/prompts/pm-cli.md +95 -0
  313. package/prompts/pm-core.md +121 -0
  314. package/prompts/pm-frontend.md +110 -0
  315. package/prompts/pm.md +234 -0
  316. package/prompts/qa.md +44 -0
  317. package/prompts/security.md +19 -0
  318. package/scripts/build-crew-chat.sh +28 -0
  319. package/scripts/build-llms-full.mjs +52 -0
  320. package/scripts/chatmock-login.sh +16 -0
  321. package/scripts/chatmock-serve.sh +16 -0
  322. package/scripts/check-dashboard.mjs +88 -0
  323. package/scripts/crew-scribe.mjs +326 -0
  324. package/scripts/dashboard-helpers.mjs +391 -0
  325. package/scripts/dashboard-validation.mjs +198 -0
  326. package/scripts/dashboard.mjs +9717 -0
  327. package/scripts/dlq-replay.mjs +61 -0
  328. package/scripts/doctor.mjs +196 -0
  329. package/scripts/file-lock.mjs +186 -0
  330. package/scripts/fresh-machine-smoke.sh +323 -0
  331. package/scripts/generate-changelog.mjs +227 -0
  332. package/scripts/generate-openapi.mjs +334 -0
  333. package/scripts/health-check.mjs +229 -0
  334. package/scripts/install-docker.sh +213 -0
  335. package/scripts/mcp-server.mjs +1625 -0
  336. package/scripts/opencrew-rt-daemon.mjs +568 -0
  337. package/scripts/openswitchctl +646 -0
  338. package/scripts/refactor-configs.mjs +39 -0
  339. package/scripts/release-check.sh +46 -0
  340. package/scripts/resolve-node-bin.sh +25 -0
  341. package/scripts/restart-all-from-repo.sh +329 -0
  342. package/scripts/restart-crew-lead.sh +98 -0
  343. package/scripts/restart-dashboard.sh +104 -0
  344. package/scripts/restart-service.sh +274 -0
  345. package/scripts/run-accessibility-audit.mjs +356 -0
  346. package/scripts/run-integration-bounded.mjs +188 -0
  347. package/scripts/run-scheduled-pipeline.mjs +230 -0
  348. package/scripts/run.mjs +41 -0
  349. package/scripts/scan-skills.mjs +79 -0
  350. package/scripts/setup-firewall.sh +128 -0
  351. package/scripts/smoke-dispatch.mjs +149 -0
  352. package/scripts/smoke.sh +163 -0
  353. package/scripts/start-crew.mjs +328 -0
  354. package/scripts/start.mjs +146 -0
  355. package/scripts/swiftbar-restart-service.sh +19 -0
  356. package/scripts/sync-agents.mjs +152 -0
  357. package/scripts/sync-prompts.mjs +79 -0
  358. package/scripts/validate-config.mjs +337 -0
  359. package/scripts/wow.mjs +89 -0
  360. package/telegram-bridge.mjs +2421 -0
  361. package/unified-orchestrator.mjs +519 -0
  362. package/whatsapp-bridge.mjs +1481 -0
@@ -0,0 +1,2846 @@
1
+ import { Terminal } from "@xterm/xterm";
2
+ import "@xterm/xterm/css/xterm.css";
3
+ import { filterOpenCodePassthroughTextChunk } from "../../../lib/browser/opencode-passthrough-filter.js";
4
+ import { filterGeminiPassthroughTextChunk } from "../../../lib/gemini-cli-passthrough-noise.mjs";
5
+ import {
6
+ createPassthroughStderrLineFilter,
7
+ summarizePassthroughTopErrorLine,
8
+ } from "../../../lib/browser/passthrough-stderr.js";
9
+
10
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
11
+ // CONFIG
12
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13
+
14
+ const RT_WS = "ws://127.0.0.1:18889"; // RT message bus
15
+ const STUDIO_WATCH_WS = "ws://127.0.0.1:3334/ws"; // Studio watch server (CLI file changes)
16
+ const STUDIO_API = window.location.origin;
17
+ /** Same origin — Vibe server proxies to dashboard :4319 (avoids CORS: localhost:3333 vs 127.0.0.1:4319). */
18
+ const DASHBOARD_API = STUDIO_API;
19
+ const CHAT_MODE_STORAGE_KEY = "vibe-chat-mode";
20
+
21
+ window.Terminal = Terminal;
22
+
23
+ let AUTH_TOKEN = ""; // Loaded from dashboard
24
+ const SESSION_ID = "studio-" + Date.now(); // Unique session per Studio instance
25
+
26
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
27
+ // STATE
28
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
29
+
30
+ let editor = null;
31
+ let openTabs = [];
32
+ let activeTab = null;
33
+ let currentProject = null;
34
+ let allProjects = [];
35
+ let allAgents = [];
36
+ let chatMode = "crew-lead"; // 'crew-lead', 'direct', or 'cli'
37
+ let selectedAgent = null;
38
+ let ws = null;
39
+ let watchWs = null; // WebSocket for CLI file changes
40
+ let crewLeadEvents = null;
41
+ let crewLeadEventsReconnectTimer = null;
42
+ let lastAppendedAssistantContent = "";
43
+ let lastAppendedUserContent = "";
44
+ let inlineChatAnchor = null;
45
+ let languageBootstrapFailed = false;
46
+ let hasProjectContextLoaded = false;
47
+ let watchReconnectTimer = null;
48
+ let watchReconnectEnabled = true;
49
+ let monaco = null;
50
+ let monacoLoadPromise = null;
51
+ let fileTreeLoadToken = 0;
52
+ let fileTreeRefreshTimer = null;
53
+ let projectReplyPollTimer = null;
54
+
55
+ const MAX_TERMINAL_ENTRIES = 250;
56
+ const FILE_TREE_REFRESH_DEBOUNCE_MS = 150;
57
+ const DEFAULT_LOCAL_WORKSPACE_DIR = "apps/vibe";
58
+ const CHAT_STREAM_TIMEOUT_MS = 600000;
59
+
60
+ function getPreferredMonacoTheme() {
61
+ return document.documentElement.classList.contains("dark") ? "vs-dark" : "vs";
62
+ }
63
+
64
+ function normalizeProjectsPayload(data) {
65
+ if (Array.isArray(data)) return data;
66
+ return Array.isArray(data?.projects) ? data.projects : [];
67
+ }
68
+
69
+ function normalizeProjectId(value) {
70
+ return !value || value === "general" ? "general" : String(value);
71
+ }
72
+
73
+ async function loadSharedActiveProjectId() {
74
+ try {
75
+ const data = await fetchJSON(`${STUDIO_API}/api/studio/active-project`);
76
+ const projectId = String(data?.projectId || "").trim();
77
+ return projectId || "general";
78
+ } catch {
79
+ return "general";
80
+ }
81
+ }
82
+
83
+ async function persistSharedActiveProjectId(projectId) {
84
+ const normalizedProjectId =
85
+ projectId && String(projectId).trim() && projectId !== "undefined"
86
+ ? String(projectId).trim()
87
+ : "general";
88
+ try {
89
+ await fetchJSON(`${STUDIO_API}/api/studio/active-project`, {
90
+ method: "POST",
91
+ headers: { "content-type": "application/json" },
92
+ body: JSON.stringify({ projectId: normalizedProjectId }),
93
+ });
94
+ } catch {
95
+ // Best effort only.
96
+ }
97
+ }
98
+
99
+ async function syncProjectFromSharedState() {
100
+ const sharedProjectId = await loadSharedActiveProjectId();
101
+ const normalizedSharedId =
102
+ sharedProjectId && sharedProjectId !== "undefined"
103
+ ? sharedProjectId
104
+ : "general";
105
+ const currentProjectId =
106
+ currentProject?.id && currentProject.id !== "undefined"
107
+ ? currentProject.id
108
+ : "general";
109
+ if (normalizedSharedId === currentProjectId) return;
110
+ const selector = document.getElementById("projectSelector");
111
+ if (
112
+ selector &&
113
+ Array.from(selector.options).some((option) => option.value === normalizedSharedId)
114
+ ) {
115
+ selector.value = normalizedSharedId;
116
+ await switchProject(normalizedSharedId);
117
+ }
118
+ }
119
+
120
+ async function fetchJSON(url, options = {}) {
121
+ const response = await fetch(url, options);
122
+ if (!response.ok) {
123
+ throw new Error(`HTTP ${response.status}`);
124
+ }
125
+ return response.json();
126
+ }
127
+
128
+ function getStudioWorkspaceProject() {
129
+ return allProjects.find((project) => project.id === "studio-local") || null;
130
+ }
131
+
132
+ function getBrowseDirectory() {
133
+ if (currentProject?.outputDir) {
134
+ return currentProject.outputDir;
135
+ }
136
+
137
+ return getStudioWorkspaceProject()?.outputDir || DEFAULT_LOCAL_WORKSPACE_DIR;
138
+ }
139
+
140
+ window.__studioGetCurrentProjectDir = function () {
141
+ return currentProject?.outputDir || getBrowseDirectory();
142
+ };
143
+
144
+ function getRelativeWorkspacePath(filePath, rootDir) {
145
+ return filePath.replace(rootDir, "").replace(/^\//, "");
146
+ }
147
+
148
+ function shouldHideFromExplorer(relativePath) {
149
+ if (!relativePath) return true;
150
+
151
+ return [
152
+ relativePath.startsWith("."),
153
+ relativePath.startsWith("dist/"),
154
+ relativePath.startsWith("node_modules/"),
155
+ relativePath.startsWith("output/"),
156
+ relativePath.startsWith(".crew/"),
157
+ relativePath.startsWith(".crewswarm/"),
158
+ relativePath.includes("/dist/"),
159
+ ].some(Boolean);
160
+ }
161
+
162
+ function scoreExplorerPath(relativePath) {
163
+ if (
164
+ relativePath.startsWith("src/") ||
165
+ relativePath.startsWith("tests/") ||
166
+ relativePath.startsWith("public/")
167
+ ) {
168
+ return 0;
169
+ }
170
+
171
+ if (
172
+ relativePath === "index.html" ||
173
+ relativePath === "package.json" ||
174
+ relativePath === "README.md" ||
175
+ relativePath === "server.mjs" ||
176
+ relativePath === "vite.config.js"
177
+ ) {
178
+ return 1;
179
+ }
180
+
181
+ return 2;
182
+ }
183
+
184
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
185
+ // MONACO EDITOR
186
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
187
+
188
+ function renderEditorPlaceholder() {
189
+ const container = document.getElementById("editor-container");
190
+ if (!container || editor) return;
191
+
192
+ container.innerHTML = `
193
+ <div style="height:100%;display:flex;align-items:center;justify-content:center;padding:24px;text-align:center;color:var(--text-2);">
194
+ <div>
195
+ <div style="font-size:14px;font-weight:600;color:var(--text-1);margin-bottom:8px;">Editor loads on demand</div>
196
+ <div style="font-size:12px;line-height:1.6;max-width:320px;">Open a file to load Monaco only when you need the full editor.</div>
197
+ </div>
198
+ </div>
199
+ `;
200
+ }
201
+
202
+ let editorStatusTimer = null;
203
+
204
+ function showEditorStatus(message, tone = "info", sticky = false) {
205
+ const statusEl = document.getElementById("editor-status");
206
+ if (!statusEl) return;
207
+
208
+ statusEl.classList.add("visible");
209
+ statusEl.dataset.tone = tone;
210
+ statusEl.innerHTML = `<strong>${tone}</strong><span>${message}</span>`;
211
+
212
+ if (editorStatusTimer) {
213
+ clearTimeout(editorStatusTimer);
214
+ editorStatusTimer = null;
215
+ }
216
+
217
+ if (!sticky && tone !== "error") {
218
+ editorStatusTimer = window.setTimeout(() => {
219
+ hideEditorStatus();
220
+ }, 2500);
221
+ }
222
+ }
223
+
224
+ function hideEditorStatus() {
225
+ const statusEl = document.getElementById("editor-status");
226
+ if (!statusEl) return;
227
+
228
+ statusEl.classList.remove("visible");
229
+ statusEl.dataset.tone = "info";
230
+ statusEl.textContent = "";
231
+
232
+ if (editorStatusTimer) {
233
+ clearTimeout(editorStatusTimer);
234
+ editorStatusTimer = null;
235
+ }
236
+ }
237
+
238
+ function updateEditorToolbarState() {
239
+ const buttons = document.querySelectorAll(".editor-toolbar-btn");
240
+ const hasEditor = Boolean(editor);
241
+ const hasActiveTab = Boolean(activeTab);
242
+ const activeLanguage = activeTab?.language || editor?.getModel()?.getLanguageId() || "plaintext";
243
+ const canComment = isCommentActionAvailable(activeLanguage);
244
+ const canFormat = isFormatActionAvailable(activeLanguage);
245
+ const canFind = hasEditor;
246
+ const canReplace = hasEditor;
247
+
248
+ buttons.forEach((button) => {
249
+ const action = button.id.replace("editor-", "");
250
+ const requiresTab = action === "save";
251
+ const missingEditor = requiresTab ? !hasEditor || !hasActiveTab : !hasEditor;
252
+
253
+ if (missingEditor) {
254
+ button.disabled = true;
255
+ button.title = "Open a file to use this action.";
256
+ return;
257
+ }
258
+
259
+ if (action === "comment" && !canComment) {
260
+ button.disabled = true;
261
+ button.title = `Comment toggle is not available for ${activeLanguage}.`;
262
+ return;
263
+ }
264
+
265
+ if (action === "find" && !canFind) {
266
+ button.disabled = true;
267
+ button.title = "Find is not available until Monaco finishes loading.";
268
+ return;
269
+ }
270
+
271
+ if (action === "replace" && !canReplace) {
272
+ button.disabled = true;
273
+ button.title = "Replace is not available until Monaco finishes loading.";
274
+ return;
275
+ }
276
+
277
+ if (action === "format" && !canFormat) {
278
+ button.disabled = true;
279
+ button.title = `Formatting is not available for ${activeLanguage}.`;
280
+ return;
281
+ }
282
+
283
+ button.disabled = false;
284
+ button.title = "";
285
+ });
286
+ }
287
+
288
+ function getCommentToken(languageId) {
289
+ const commentTokens = {
290
+ javascript: "//",
291
+ typescript: "//",
292
+ json: null,
293
+ markdown: null,
294
+ html: "<!--",
295
+ css: "/*",
296
+ python: "#",
297
+ shell: "#",
298
+ sh: "#",
299
+ yaml: "#",
300
+ yml: "#",
301
+ };
302
+
303
+ return commentTokens[languageId] ?? "//";
304
+ }
305
+
306
+ function isCommentActionAvailable(languageId) {
307
+ return getCommentToken(languageId) !== null;
308
+ }
309
+
310
+ function isFormatActionAvailable(languageId) {
311
+ const formattableLanguages = new Set([
312
+ "javascript",
313
+ "typescript",
314
+ "html",
315
+ "css",
316
+ "json",
317
+ "markdown",
318
+ ]);
319
+
320
+ return formattableLanguages.has(languageId);
321
+ }
322
+
323
+ function isEditorActionSupported(actionId) {
324
+ if (!editor) return false;
325
+
326
+ const action = editor.getAction(actionId);
327
+ if (!action) {
328
+ return false;
329
+ }
330
+
331
+ if (typeof action.isSupported === "function") {
332
+ try {
333
+ return Boolean(action.isSupported());
334
+ } catch {
335
+ return false;
336
+ }
337
+ }
338
+
339
+ return true;
340
+ }
341
+
342
+ function fallbackToggleComment(languageId) {
343
+ if (!editor) return false;
344
+
345
+ const token = getCommentToken(languageId);
346
+ if (!token) {
347
+ return false;
348
+ }
349
+
350
+ const model = editor.getModel();
351
+ const selection = editor.getSelection();
352
+ if (!model || !selection) {
353
+ return false;
354
+ }
355
+
356
+ const startLine = selection.startLineNumber;
357
+ const endLine = selection.endLineNumber;
358
+ const lines = [];
359
+
360
+ for (let lineNumber = startLine; lineNumber <= endLine; lineNumber += 1) {
361
+ lines.push(model.getLineContent(lineNumber));
362
+ }
363
+
364
+ const shouldUncomment = lines.every((line) => {
365
+ const trimmed = line.trimStart();
366
+ if (!trimmed) return true;
367
+ if (token === "<!--") return trimmed.startsWith("<!--") && trimmed.endsWith("-->");
368
+ if (token === "/*") return trimmed.startsWith("/*") && trimmed.endsWith("*/");
369
+ return trimmed.startsWith(token);
370
+ });
371
+
372
+ editor.executeEdits(
373
+ "toolbar-comment-fallback",
374
+ lines.map((line, index) => {
375
+ const lineNumber = startLine + index;
376
+ const lineLength = model.getLineLength(lineNumber);
377
+ const trimmed = line.trimStart();
378
+ const indentLength = line.length - trimmed.length;
379
+
380
+ let nextLine = line;
381
+ if (shouldUncomment) {
382
+ if (token === "<!--" && trimmed.startsWith("<!--") && trimmed.endsWith("-->")) {
383
+ nextLine = `${line.slice(0, indentLength)}${trimmed.slice(4, -3).trim()}`;
384
+ } else if (token === "/*" && trimmed.startsWith("/*") && trimmed.endsWith("*/")) {
385
+ nextLine = `${line.slice(0, indentLength)}${trimmed.slice(2, -2).trim()}`;
386
+ } else if (trimmed.startsWith(token)) {
387
+ nextLine = `${line.slice(0, indentLength)}${trimmed.slice(token.length).replace(/^ /, "")}`;
388
+ }
389
+ } else if (trimmed) {
390
+ if (token === "<!--") {
391
+ nextLine = `${line.slice(0, indentLength)}<!-- ${trimmed} -->`;
392
+ } else if (token === "/*") {
393
+ nextLine = `${line.slice(0, indentLength)}/* ${trimmed} */`;
394
+ } else {
395
+ nextLine = `${line.slice(0, indentLength)}${token} ${trimmed}`;
396
+ }
397
+ }
398
+
399
+ return {
400
+ range: new monaco.Range(lineNumber, 1, lineNumber, lineLength + 1),
401
+ text: nextLine,
402
+ };
403
+ }),
404
+ );
405
+
406
+ editor.focus();
407
+ return true;
408
+ }
409
+
410
+ async function runEditorAction(action) {
411
+ if (!editor) {
412
+ showEditorStatus("Open a file to use editor actions.", "warning");
413
+ return;
414
+ }
415
+
416
+ if (action === "save") {
417
+ if (!activeTab) {
418
+ showEditorStatus("Open a file before saving.", "warning");
419
+ return;
420
+ }
421
+ await saveFile(activeTab);
422
+ showEditorStatus(`Saved ${activeTab.name}`, "success");
423
+ return;
424
+ }
425
+
426
+ if (action === "undo" || action === "redo") {
427
+ editor.trigger("toolbar", action, null);
428
+ editor.focus();
429
+ return;
430
+ }
431
+
432
+ if (action === "find" || action === "replace") {
433
+ editor.focus();
434
+ const commandId =
435
+ action === "find" ? "actions.find" : "editor.action.startFindReplaceAction";
436
+ try {
437
+ const editorAction = editor.getAction(commandId);
438
+ if (editorAction && typeof editorAction.run === "function") {
439
+ await editorAction.run();
440
+ } else {
441
+ editor.trigger("toolbar", commandId, null);
442
+ }
443
+ return;
444
+ } catch (error) {
445
+ showEditorStatus(`Editor action failed: ${error.message}`, "error", true);
446
+ return;
447
+ }
448
+ }
449
+
450
+ const actionMap = {
451
+ comment: "editor.action.commentLine",
452
+ format: "editor.action.formatDocument",
453
+ };
454
+
455
+ const actionId = actionMap[action];
456
+ if (!actionId) return;
457
+ const editorAction = editor.getAction(actionId);
458
+ const activeLanguage = activeTab?.language || editor.getModel()?.getLanguageId() || "plaintext";
459
+
460
+ if (action === "comment" && !isCommentActionAvailable(activeLanguage)) {
461
+ showEditorStatus(`Comment toggle is not available for ${activeLanguage}.`, "warning");
462
+ return;
463
+ }
464
+
465
+ if (action === "format" && !isFormatActionAvailable(activeLanguage)) {
466
+ showEditorStatus(`Formatting is not available for ${activeLanguage}.`, "warning");
467
+ return;
468
+ }
469
+
470
+ try {
471
+ if (isEditorActionSupported(actionId)) {
472
+ await editorAction.run();
473
+ editor.focus();
474
+ return;
475
+ }
476
+
477
+ if (action === "comment" && fallbackToggleComment(activeLanguage)) {
478
+ showEditorStatus(`Toggled comments in ${activeTab?.name || "current file"}`, "success");
479
+ return;
480
+ }
481
+
482
+ showEditorStatus(`${action} is not available for ${activeLanguage}.`, "warning");
483
+ editor.focus();
484
+ } catch (error) {
485
+ showEditorStatus(`Editor action failed: ${error.message}`, "error", true);
486
+ }
487
+ }
488
+
489
+ function bindEditorToolbar() {
490
+ const actionIds = ["undo", "redo", "save", "find", "replace", "comment", "format"];
491
+
492
+ actionIds.forEach((action) => {
493
+ const button = document.getElementById(`editor-${action}`);
494
+ button?.addEventListener("click", () => {
495
+ runEditorAction(action);
496
+ });
497
+ });
498
+
499
+ updateEditorToolbarState();
500
+ }
501
+
502
+ async function loadMonaco() {
503
+ if (monaco) {
504
+ return monaco;
505
+ }
506
+
507
+ if (!monacoLoadPromise) {
508
+ monacoLoadPromise = (async () => {
509
+ const [
510
+ monacoModule,
511
+ { default: editorWorker },
512
+ { default: jsonWorker },
513
+ { default: cssWorker },
514
+ { default: htmlWorker },
515
+ { default: tsWorker },
516
+ _findController,
517
+ ] = await Promise.all([
518
+ import("monaco-editor/esm/vs/editor/editor.api"),
519
+ import("monaco-editor/esm/vs/editor/editor.worker?worker"),
520
+ import("monaco-editor/esm/vs/language/json/json.worker?worker"),
521
+ import("monaco-editor/esm/vs/language/css/css.worker?worker"),
522
+ import("monaco-editor/esm/vs/language/html/html.worker?worker"),
523
+ import("monaco-editor/esm/vs/language/typescript/ts.worker?worker"),
524
+ import("monaco-editor/esm/vs/editor/contrib/find/browser/findController.js"),
525
+ ]);
526
+
527
+ self.MonacoEnvironment = {
528
+ getWorker(_, label) {
529
+ if (label === "json") {
530
+ return new jsonWorker();
531
+ }
532
+ if (label === "css" || label === "scss" || label === "less") {
533
+ return new cssWorker();
534
+ }
535
+ if (label === "html" || label === "handlebars" || label === "razor") {
536
+ return new htmlWorker();
537
+ }
538
+ if (label === "typescript" || label === "javascript") {
539
+ return new tsWorker();
540
+ }
541
+ return new editorWorker();
542
+ },
543
+ };
544
+
545
+ monaco = monacoModule;
546
+ window.monaco = monacoModule;
547
+ await import("./register-all-languages.js");
548
+ if (window.__studioLanguageRegistrationReady) {
549
+ window.__studioLanguageRegistrationReady.catch((err) => {
550
+ languageBootstrapFailed = true;
551
+ console.warn("Studio language bootstrap degraded:", err);
552
+ addTerminalLine(
553
+ "⚠️ Monaco language extras failed to load; continuing with fallback editor mode",
554
+ "warning",
555
+ );
556
+ });
557
+ }
558
+ return monacoModule;
559
+ })().catch((error) => {
560
+ monacoLoadPromise = null;
561
+ throw error;
562
+ });
563
+ }
564
+
565
+ return monacoLoadPromise;
566
+ }
567
+
568
+ async function ensureEditorReady() {
569
+ if (editor) {
570
+ return editor;
571
+ }
572
+
573
+ await loadMonaco();
574
+ initEditor();
575
+ updateEditorToolbarState();
576
+ return editor;
577
+ }
578
+
579
+ function initEditor() {
580
+ const container = document.getElementById("editor-container");
581
+ if (!container) {
582
+ throw new Error("Editor container not found");
583
+ }
584
+
585
+ // Remove the lazy-load placeholder before Monaco mounts into the container.
586
+ container.innerHTML = "";
587
+
588
+ editor = monaco.editor.create(container, {
589
+ value:
590
+ "// crewswarm Vibe is ready.\n// Open a project, edit a file, or run cli:codex from chat.\n",
591
+ language: "plaintext",
592
+ theme: getPreferredMonacoTheme(),
593
+ fontSize: 13,
594
+ minimap: { enabled: true },
595
+ automaticLayout: true,
596
+ scrollBeyondLastLine: false,
597
+ lineNumbers: "on",
598
+ renderWhitespace: "selection",
599
+ tabSize: 2,
600
+ });
601
+
602
+ // Auto-save on change (debounced)
603
+ let saveTimeout;
604
+ editor.onDidChangeModelContent(() => {
605
+ if (!activeTab) return;
606
+ clearTimeout(saveTimeout);
607
+ saveTimeout = setTimeout(() => saveFile(activeTab), 1000);
608
+ });
609
+
610
+ // Cmd+K for inline chat
611
+ editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, () => {
612
+ showInlineChat();
613
+ });
614
+
615
+ editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
616
+ runEditorAction("save");
617
+ });
618
+
619
+ editor.onDidChangeCursorPosition(() => {
620
+ const overlay = document.getElementById("inline-chat-overlay");
621
+ if (overlay?.classList.contains("visible")) {
622
+ positionInlineChat();
623
+ }
624
+ });
625
+
626
+ editor.onDidScrollChange(() => {
627
+ const overlay = document.getElementById("inline-chat-overlay");
628
+ if (overlay?.classList.contains("visible")) {
629
+ positionInlineChat();
630
+ }
631
+ });
632
+
633
+ updateEditorToolbarState();
634
+ }
635
+
636
+ async function ensureEditorLanguage(languageId) {
637
+ await loadMonaco();
638
+
639
+ if (!languageId || typeof window.__studioEnsureLanguageRegistered !== "function") {
640
+ return;
641
+ }
642
+
643
+ try {
644
+ await window.__studioEnsureLanguageRegistered(languageId);
645
+ } catch (error) {
646
+ console.warn(`Failed to load Monaco language contribution for ${languageId}:`, error);
647
+ }
648
+ }
649
+
650
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
651
+ // PROJECT MANAGEMENT
652
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
653
+
654
+ async function loadProjects() {
655
+ try {
656
+ const data = await fetchJSON(`${STUDIO_API}/api/studio/projects`);
657
+ allProjects = normalizeProjectsPayload(data);
658
+
659
+ console.log(
660
+ "[loadProjects] Loaded projects:",
661
+ allProjects.map((p) => ({ id: p.id, name: p.name })),
662
+ );
663
+
664
+ const selector = document.getElementById("projectSelector");
665
+ if (!selector) {
666
+ console.warn("Project selector element not found");
667
+ return;
668
+ }
669
+ selector.innerHTML = '<option value="general">General Chat</option>';
670
+
671
+ allProjects.forEach((proj) => {
672
+ console.log("[loadProjects] Adding option:", proj.id, proj.name);
673
+ const option = document.createElement("option");
674
+ option.value = proj.id;
675
+ option.textContent = proj.name;
676
+ selector.appendChild(option);
677
+ });
678
+
679
+ // Check URL hash for project (restore from URL like dashboard)
680
+ const hash = window.location.hash;
681
+ const match = hash.match(/project=([^&]+)/);
682
+ const urlProjectId = match ? decodeURIComponent(match[1]) : null;
683
+ const sharedProjectId = await loadSharedActiveProjectId();
684
+
685
+ if (
686
+ urlProjectId &&
687
+ (urlProjectId === "general" ||
688
+ allProjects.find((p) => p.id === urlProjectId))
689
+ ) {
690
+ // Restore project from URL
691
+ selector.value = urlProjectId;
692
+ await switchProject(urlProjectId);
693
+ } else if (
694
+ sharedProjectId &&
695
+ (sharedProjectId === "general" ||
696
+ allProjects.find((p) => p.id === sharedProjectId))
697
+ ) {
698
+ selector.value = sharedProjectId;
699
+ await switchProject(sharedProjectId);
700
+ } else {
701
+ const defaultProjectId =
702
+ allProjects.find((project) => project.id === DEFAULT_PROJECT_ID)?.id ||
703
+ allProjects[0]?.id ||
704
+ "general";
705
+ selector.value = defaultProjectId;
706
+ await switchProject(defaultProjectId);
707
+ }
708
+
709
+ addTerminalLine(`✅ Loaded ${allProjects.length} project(s)`, "info");
710
+ } catch (err) {
711
+ const selector = document.getElementById("projectSelector");
712
+ if (selector) {
713
+ selector.innerHTML = '<option value="studio-local">Studio Workspace</option>';
714
+ selector.value = "studio-local";
715
+ }
716
+ allProjects = [
717
+ {
718
+ id: "studio-local",
719
+ name: "Studio Workspace",
720
+ outputDir: DEFAULT_LOCAL_WORKSPACE_DIR,
721
+ },
722
+ ];
723
+ await switchProject("studio-local");
724
+ addTerminalLine(
725
+ `⚠️ Studio project store unavailable - using local workspace fallback`,
726
+ "warning",
727
+ );
728
+ }
729
+ }
730
+
731
+ async function loadAgents() {
732
+ try {
733
+ // Same-origin /api/agents is proxied to dashboard :4319. After restart-all, dashboard (launchd)
734
+ // can lag Studio; retry 502/503 so the agent list isn't empty on first paint.
735
+ const maxAttempts = 12;
736
+ const delayMs = 800;
737
+ let response;
738
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
739
+ try {
740
+ response = await fetch(`${DASHBOARD_API}/api/agents`);
741
+ } catch (netErr) {
742
+ if (attempt < maxAttempts) {
743
+ console.warn(
744
+ `[loadAgents] fetch failed (${netErr?.message || netErr}), retry ${attempt}/${maxAttempts}`,
745
+ );
746
+ await new Promise((r) => setTimeout(r, delayMs));
747
+ continue;
748
+ }
749
+ throw netErr;
750
+ }
751
+ if (
752
+ (response.status === 502 || response.status === 503) &&
753
+ attempt < maxAttempts
754
+ ) {
755
+ console.warn(
756
+ `[loadAgents] dashboard proxy not ready (${response.status}), retry ${attempt}/${maxAttempts}`,
757
+ );
758
+ await new Promise((r) => setTimeout(r, delayMs));
759
+ continue;
760
+ }
761
+ break;
762
+ }
763
+ if (!response.ok) {
764
+ throw new Error(`Dashboard API error: ${response.status}`);
765
+ }
766
+ const data = await response.json();
767
+
768
+ // Dashboard returns plain array
769
+ allAgents = Array.isArray(data) ? data : [];
770
+
771
+ // Populate agents optgroup
772
+ const optgroup = document.getElementById("agentsOptgroup");
773
+ const selector = document.getElementById("chat-mode-selector");
774
+ if (!optgroup) {
775
+ console.warn("Agents optgroup element not found");
776
+ return;
777
+ }
778
+ const preferredMode =
779
+ localStorage.getItem(CHAT_MODE_STORAGE_KEY) || selector?.value || chatMode;
780
+
781
+ optgroup.innerHTML = "";
782
+ allAgents.forEach((agent) => {
783
+ const option = document.createElement("option");
784
+ option.value = agent.id;
785
+ option.textContent = `${agent.id}`;
786
+ optgroup.appendChild(option);
787
+ });
788
+
789
+ if (selector) {
790
+ const hasPreferredMode = Array.from(selector.options).some(
791
+ (option) => option.value === preferredMode,
792
+ );
793
+ selector.value = hasPreferredMode ? preferredMode : "crew-lead";
794
+ chatMode = selector.value;
795
+ }
796
+
797
+ addTerminalLine(`🤖 Loaded ${allAgents.length} agents`, "success");
798
+ } catch (err) {
799
+ addTerminalLine(
800
+ `⚠️ Dashboard not responding - agents unavailable`,
801
+ "warning",
802
+ );
803
+ console.error("loadAgents error:", err);
804
+ }
805
+ }
806
+
807
+ window.switchChatMode = function () {
808
+ const mode = document.getElementById("chat-mode-selector").value;
809
+ chatMode = mode;
810
+ localStorage.setItem(CHAT_MODE_STORAGE_KEY, mode);
811
+
812
+ const chatInput = document.getElementById("chat-input");
813
+
814
+ if (mode.startsWith("cli:")) {
815
+ const cliName = mode.replace("cli:", "");
816
+ chatInput.placeholder = `Direct ${cliName.toUpperCase()} passthrough (Enter to send)`;
817
+ addTerminalLine(
818
+ `⚡ Mode: ${cliName} CLI passthrough - NO LLM, direct execution`,
819
+ "info",
820
+ );
821
+ } else if (mode !== "crew-lead") {
822
+ // Direct agent mode
823
+ selectedAgent = mode;
824
+ chatInput.placeholder = `Talk directly to ${mode} (Enter to send)`;
825
+ addTerminalLine(`💬 Mode: Direct chat with ${mode}`, "info");
826
+ } else {
827
+ selectedAgent = null;
828
+ chatInput.placeholder =
829
+ "Ask the crew anything... (Enter to send, Shift+Enter for new line)";
830
+ addTerminalLine(`🧠 Mode: crew-lead (smart routing)`, "info");
831
+ }
832
+ };
833
+
834
+ document.getElementById("agent-selector")?.addEventListener("change", (e) => {
835
+ selectedAgent = e.target.value;
836
+ if (selectedAgent) {
837
+ addTerminalLine(`🎯 Selected agent: ${selectedAgent}`, "info");
838
+ }
839
+ });
840
+
841
+ async function switchProject(projectId) {
842
+ console.log(
843
+ "[switchProject] Called with projectId:",
844
+ projectId,
845
+ "type:",
846
+ typeof projectId,
847
+ );
848
+
849
+ // Track current state
850
+ const currentId = currentProject?.id || "general";
851
+
852
+ // Don't reload if already on this project
853
+ if (hasProjectContextLoaded && currentId === projectId) {
854
+ console.log("Already on project:", projectId);
855
+ return;
856
+ }
857
+
858
+ // Validate projectId
859
+ if (!projectId || projectId === "undefined") {
860
+ console.error("[switchProject] Invalid projectId:", projectId);
861
+ projectId = "general";
862
+ }
863
+
864
+ // Update URL hash BEFORE setting currentProject (use projectId param, not currentProject)
865
+ window.location.hash = `studio?project=${encodeURIComponent(projectId)}`;
866
+ await persistSharedActiveProjectId(projectId);
867
+
868
+ // Set currentProject based on projectId
869
+ if (projectId === "general") {
870
+ currentProject = null;
871
+ } else {
872
+ currentProject = allProjects.find((p) => p.id === projectId);
873
+ if (!currentProject) {
874
+ console.warn(
875
+ "[switchProject] Project not found, falling back to general:",
876
+ projectId,
877
+ );
878
+ projectId = "general";
879
+ currentProject = null;
880
+ }
881
+ }
882
+
883
+ // Update project context hint in chat header
884
+ const hint = document.getElementById("project-context-hint");
885
+ const nameEl = document.getElementById("project-context-name");
886
+ const pathEl = document.getElementById("project-context-path");
887
+
888
+ if (currentProject && hint && nameEl && pathEl) {
889
+ hint.style.display = "block";
890
+ nameEl.textContent = currentProject.name;
891
+ pathEl.textContent = currentProject.outputDir || "";
892
+ addTerminalLine(`📁 Switched to project: ${currentProject.name}`, "info");
893
+ addTerminalLine(`📂 Directory: ${currentProject.outputDir}`, "info");
894
+ } else if (projectId === "general" && hint && nameEl && pathEl) {
895
+ hint.style.display = "block";
896
+ nameEl.textContent = "General Chat";
897
+ pathEl.textContent = "No specific project";
898
+ addTerminalLine(`🌐 Switched to general chat (no project context)`, "info");
899
+ } else if (hint) {
900
+ hint.style.display = "none";
901
+ }
902
+
903
+ window.dispatchEvent(
904
+ new CustomEvent("studio-projectchange", {
905
+ detail: {
906
+ projectId: currentProject?.id || "general",
907
+ projectName: currentProject?.name || "General Chat",
908
+ projectDir: currentProject?.outputDir || getBrowseDirectory(),
909
+ },
910
+ }),
911
+ );
912
+
913
+ // Clear chat history when switching projects (project-scoped sessions)
914
+ const chatMessages = document.getElementById("chat-messages");
915
+ if (chatMessages) {
916
+ chatMessages.innerHTML = "";
917
+ }
918
+ lastAppendedAssistantContent = "";
919
+ lastAppendedUserContent = "";
920
+
921
+ // Load chat history and file tree
922
+ await loadChatHistory();
923
+ await loadFileTree();
924
+ hasProjectContextLoaded = true;
925
+ }
926
+
927
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
928
+ // CHAT HISTORY (Project-Scoped)
929
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
930
+
931
+ async function loadChatHistory() {
932
+ const projectId = currentProject?.id || "general";
933
+
934
+ try {
935
+ const data = await fetchJSON(
936
+ `${STUDIO_API}/api/studio/project-messages?projectId=${encodeURIComponent(projectId)}&limit=50`,
937
+ );
938
+
939
+ const chatMessages = document.getElementById("chat-messages");
940
+ if (!chatMessages) {
941
+ console.warn("Chat messages container not found");
942
+ return;
943
+ }
944
+ chatMessages.innerHTML = "";
945
+
946
+ if (Array.isArray(data.messages) && data.messages.length > 0) {
947
+ data.messages.forEach((msg) => {
948
+ if (!msg || typeof msg !== "object") {
949
+ return;
950
+ }
951
+ const sourceEmoji = {
952
+ dashboard: "💻",
953
+ cli: "⚡",
954
+ "studio-cli": "🟣",
955
+ "sub-agent": "👷",
956
+ agent: "🤖",
957
+ };
958
+ const agentId = msg.agent || msg.metadata?.agentId || null;
959
+ const agentInfo = agentId
960
+ ? allAgents.find((a) => a.id === agentId)
961
+ : null;
962
+ const emoji =
963
+ msg.metadata?.agentEmoji ||
964
+ agentInfo?.emoji ||
965
+ sourceEmoji[msg.source] ||
966
+ "📝";
967
+ const agentName =
968
+ msg.metadata?.agentName || agentInfo?.name || agentId || null;
969
+ const timestamp = new Date(msg.ts).toLocaleTimeString();
970
+
971
+ appendChatBubble(
972
+ msg.role === "user" ? "user" : "assistant",
973
+ msg.content,
974
+ {
975
+ emoji,
976
+ source: msg.source,
977
+ agent: agentName,
978
+ agentName,
979
+ agentId,
980
+ targetAgent:
981
+ msg.metadata?.targetAgent || msg.metadata?.agentId || null,
982
+ engine: msg.metadata?.engine || null,
983
+ timestamp,
984
+ },
985
+ );
986
+ });
987
+ chatMessages.scrollTop = chatMessages.scrollHeight;
988
+ }
989
+ } catch (err) {
990
+ console.warn("Failed to load chat history:", err);
991
+ }
992
+ }
993
+
994
+ function appendChatSystemNote(text) {
995
+ if (!chatMessages) return;
996
+ const note = document.createElement("div");
997
+ note.className = "message assistant";
998
+ note.innerHTML = `
999
+ <div class="message-header">⚡ crew-lead</div>
1000
+ <div class="message-content">${escapeHtml(text)}</div>
1001
+ `;
1002
+ chatMessages.appendChild(note);
1003
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1004
+ }
1005
+
1006
+ function scheduleProjectReplyRefresh(durationMs = 30000, intervalMs = 3000) {
1007
+ if (projectReplyPollTimer) {
1008
+ clearInterval(projectReplyPollTimer);
1009
+ projectReplyPollTimer = null;
1010
+ }
1011
+ const startedAt = Date.now();
1012
+ projectReplyPollTimer = setInterval(async () => {
1013
+ if (Date.now() - startedAt > durationMs) {
1014
+ clearInterval(projectReplyPollTimer);
1015
+ projectReplyPollTimer = null;
1016
+ return;
1017
+ }
1018
+ try {
1019
+ await loadChatHistory();
1020
+ } catch {
1021
+ // best effort
1022
+ }
1023
+ }, intervalMs);
1024
+ }
1025
+
1026
+ function connectCrewLeadEvents() {
1027
+ if (crewLeadEvents) return;
1028
+ const eventsUrl = `${STUDIO_API}/api/crew-lead/events`;
1029
+ crewLeadEvents = new EventSource(eventsUrl);
1030
+
1031
+ crewLeadEvents.onmessage = async (event) => {
1032
+ if (!event.data) return;
1033
+ let payload = null;
1034
+ try {
1035
+ payload = JSON.parse(event.data);
1036
+ } catch {
1037
+ return;
1038
+ }
1039
+ const currentProjectId = normalizeProjectId(currentProject?.id || "general");
1040
+ const eventProjectId = normalizeProjectId(payload.projectId);
1041
+ if (currentProjectId !== eventProjectId) return;
1042
+
1043
+ if (payload.type === "agent_working" && payload.agent) {
1044
+ appendChatSystemNote(`${payload.agent} is working...`);
1045
+ return;
1046
+ }
1047
+
1048
+ if (payload.type === "agent_reply" || (payload.from && payload.content)) {
1049
+ addTerminalLine(`🤖 ${payload.from || "agent"} replied`, "info");
1050
+ await loadChatHistory();
1051
+ }
1052
+ };
1053
+
1054
+ crewLeadEvents.onerror = () => {
1055
+ try {
1056
+ crewLeadEvents?.close();
1057
+ } catch {}
1058
+ crewLeadEvents = null;
1059
+ if (crewLeadEventsReconnectTimer) return;
1060
+ crewLeadEventsReconnectTimer = window.setTimeout(() => {
1061
+ crewLeadEventsReconnectTimer = null;
1062
+ connectCrewLeadEvents();
1063
+ }, 2000);
1064
+ };
1065
+ }
1066
+
1067
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1068
+ // FILE TREE
1069
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1070
+
1071
+ async function loadFileTree() {
1072
+ const loadToken = ++fileTreeLoadToken;
1073
+ const container = document.getElementById("file-tree");
1074
+ if (!container) {
1075
+ console.warn("File tree container not found");
1076
+ return;
1077
+ }
1078
+ container.innerHTML = '<li class="loading">Loading files...</li>';
1079
+
1080
+ // Determine directory to load
1081
+ const outputDir = getBrowseDirectory();
1082
+
1083
+ try {
1084
+ const data = await fetchJSON(
1085
+ `${STUDIO_API}/api/studio/files?dir=${encodeURIComponent(outputDir)}`,
1086
+ );
1087
+ if (loadToken !== fileTreeLoadToken) {
1088
+ return;
1089
+ }
1090
+ const files = (data.files || []).filter((file) => {
1091
+ const relativePath = getRelativeWorkspacePath(file.path, outputDir);
1092
+ return !shouldHideFromExplorer(relativePath);
1093
+ });
1094
+
1095
+ if (files.length === 0) {
1096
+ container.innerHTML =
1097
+ '<li style="padding: 16px; color: var(--text-3); font-size: 12px;">No files found in project directory</li>';
1098
+ return;
1099
+ }
1100
+
1101
+ // Group by directory and display as tree
1102
+ const tree = {};
1103
+ files.forEach((f) => {
1104
+ const relativePath = getRelativeWorkspacePath(f.path, outputDir);
1105
+ tree[relativePath] = f;
1106
+ });
1107
+
1108
+ // Sort source files and primary project files ahead of generated or peripheral files.
1109
+ const sorted = Object.keys(tree).sort((a, b) => {
1110
+ const scoreDelta = scoreExplorerPath(a) - scoreExplorerPath(b);
1111
+ if (scoreDelta !== 0) return scoreDelta;
1112
+ return a.localeCompare(b);
1113
+ });
1114
+
1115
+ container.innerHTML = "";
1116
+ sorted.slice(0, 100).forEach((relPath) => {
1117
+ const ext = relPath.split(".").pop();
1118
+ const icon =
1119
+ ext === "md"
1120
+ ? "📝"
1121
+ : ext === "json"
1122
+ ? "📦"
1123
+ : ext === "js" || ext === "mjs"
1124
+ ? "📄"
1125
+ : "📄";
1126
+ const item = document.createElement("li");
1127
+ item.dataset.path = tree[relPath].path;
1128
+ item.innerHTML = `
1129
+ <span class="icon">${icon}</span>
1130
+ <span title="${relPath}">${relPath.length > 40 ? "..." + relPath.slice(-37) : relPath}</span>
1131
+ `;
1132
+ item.addEventListener("click", () => {
1133
+ openFile(tree[relPath].path);
1134
+ });
1135
+ container.appendChild(item);
1136
+ });
1137
+
1138
+ if (sorted.length > 100) {
1139
+ const overflow = document.createElement("li");
1140
+ overflow.style.cssText =
1141
+ "padding: 8px; color: var(--text-3); font-size: 11px;";
1142
+ overflow.textContent = `... and ${sorted.length - 100} more files`;
1143
+ container.appendChild(overflow);
1144
+ }
1145
+ } catch (err) {
1146
+ container.innerHTML = `<li style="padding: 16px; color: var(--red);">Failed to load files: ${err.message}</li>`;
1147
+ addTerminalLine(`⚠️ Failed to load file tree: ${err.message}`, "error");
1148
+ }
1149
+ }
1150
+
1151
+ function scheduleFileTreeRefresh() {
1152
+ if (fileTreeRefreshTimer) {
1153
+ clearTimeout(fileTreeRefreshTimer);
1154
+ }
1155
+
1156
+ fileTreeRefreshTimer = window.setTimeout(() => {
1157
+ fileTreeRefreshTimer = null;
1158
+ loadFileTree();
1159
+ }, FILE_TREE_REFRESH_DEBOUNCE_MS);
1160
+ }
1161
+
1162
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1163
+ // FILE OPERATIONS
1164
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1165
+
1166
+ async function openFile(filePath) {
1167
+ const browseDirectory = getBrowseDirectory();
1168
+ const relativePath = getRelativeWorkspacePath(filePath, browseDirectory);
1169
+ if (shouldHideFromExplorer(relativePath)) {
1170
+ addTerminalLine(`Skipping generated file: ${relativePath}`, "warning");
1171
+ showEditorStatus(`Skipping generated file: ${relativePath}`, "warning");
1172
+ return;
1173
+ }
1174
+
1175
+ const existingTab = openTabs.find((t) => t.path === filePath);
1176
+ if (existingTab) {
1177
+ showEditorStatus(`Switched to ${existingTab.name}`, "success");
1178
+ await switchToTab(existingTab);
1179
+ return;
1180
+ }
1181
+
1182
+ try {
1183
+ showEditorStatus(`Opening ${relativePath}...`, "info", true);
1184
+ const { content, error } = await readFile(filePath);
1185
+
1186
+ const tab = {
1187
+ path: filePath,
1188
+ name: filePath.split("/").pop(),
1189
+ content,
1190
+ language: detectLanguage(filePath),
1191
+ };
1192
+
1193
+ openTabs.push(tab);
1194
+ await ensureEditorReady();
1195
+ await switchToTab(tab);
1196
+ renderTabs();
1197
+
1198
+ if (error) {
1199
+ showEditorStatus(`Failed to load ${relativePath}: ${error}`, "error", true);
1200
+ } else {
1201
+ showEditorStatus(`Loaded ${relativePath}`, "success");
1202
+ }
1203
+
1204
+ // Notify user that this file is now in chat context
1205
+ addTerminalLine(
1206
+ `📎 ${tab.name} is now in chat context (agents can see this file)`,
1207
+ "info",
1208
+ );
1209
+ } catch (err) {
1210
+ addTerminalLine(`Failed to open ${filePath}: ${err.message}`, "error");
1211
+ }
1212
+ }
1213
+
1214
+ async function readFile(filePath) {
1215
+ try {
1216
+ const payload = await fetchJSON(
1217
+ `${STUDIO_API}/api/studio/file-content?path=${encodeURIComponent(filePath)}`,
1218
+ );
1219
+ if (payload.error) {
1220
+ throw new Error(payload.error);
1221
+ }
1222
+ return {
1223
+ content: payload.content || "",
1224
+ error: null,
1225
+ };
1226
+ } catch (err) {
1227
+ addTerminalLine(`⚠️ Failed to read ${filePath}: ${err.message}`, "error");
1228
+ return {
1229
+ content: `// Error loading file: ${err.message}\n`,
1230
+ error: err.message,
1231
+ };
1232
+ }
1233
+ }
1234
+
1235
+ async function saveFile(tab) {
1236
+ if (!editor) return;
1237
+ const content = editor.getValue();
1238
+ try {
1239
+ await fetchJSON(`${STUDIO_API}/api/studio/file-content`, {
1240
+ method: "POST",
1241
+ headers: {
1242
+ "Content-Type": "application/json",
1243
+ },
1244
+ body: JSON.stringify({
1245
+ path: tab.path,
1246
+ content,
1247
+ }),
1248
+ });
1249
+ tab.content = content;
1250
+ addTerminalLine(`💾 Saved ${tab.path}`, "success");
1251
+ updateEditorToolbarState();
1252
+ } catch (err) {
1253
+ addTerminalLine(`❌ Failed to save ${tab.path}: ${err.message}`, "error");
1254
+ }
1255
+ }
1256
+
1257
+ function detectLanguage(filePath) {
1258
+ const ext = filePath.split(".").pop();
1259
+ const languageMap = {
1260
+ js: "javascript",
1261
+ ts: "typescript",
1262
+ jsx: "javascript",
1263
+ tsx: "typescript",
1264
+ json: "json",
1265
+ md: "markdown",
1266
+ html: "html",
1267
+ css: "css",
1268
+ py: "python",
1269
+ };
1270
+ return languageMap[ext] || "plaintext";
1271
+ }
1272
+
1273
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1274
+ // TABS
1275
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1276
+
1277
+ function renderTabs() {
1278
+ const container = document.getElementById("editor-tabs");
1279
+ container.innerHTML = "";
1280
+
1281
+ openTabs.forEach((tab) => {
1282
+ const button = document.createElement("button");
1283
+ button.className = `editor-tab ${tab === activeTab ? "active" : ""}`;
1284
+ button.type = "button";
1285
+ button.append(document.createTextNode(tab.name));
1286
+ button.addEventListener("click", () => {
1287
+ switchToTab(tab);
1288
+ });
1289
+
1290
+ const close = document.createElement("span");
1291
+ close.className = "close";
1292
+ close.textContent = "×";
1293
+ close.addEventListener("click", (event) => {
1294
+ closeTab(tab.path, event);
1295
+ });
1296
+ button.appendChild(close);
1297
+ container.appendChild(button);
1298
+ });
1299
+
1300
+ updateEditorToolbarState();
1301
+ }
1302
+
1303
+ window.switchToTab = async function (tab) {
1304
+ await ensureEditorReady();
1305
+ const previousTab = activeTab;
1306
+ activeTab = tab;
1307
+ await ensureEditorLanguage(tab.language);
1308
+ monaco.editor.setModelLanguage(editor.getModel(), tab.language);
1309
+ editor.setValue(tab.content);
1310
+ renderTabs();
1311
+ updateEditorToolbarState();
1312
+ hideEditorStatus();
1313
+
1314
+ document.querySelectorAll(".file-tree li").forEach((el) => {
1315
+ el.classList.toggle("active", el.dataset.path === tab.path);
1316
+ });
1317
+
1318
+ // Notify context change if switching from another file
1319
+ if (previousTab && previousTab.path !== tab.path) {
1320
+ addTerminalLine(`📎 Chat context: ${tab.name}`, "info");
1321
+ }
1322
+ };
1323
+
1324
+ window.closeTab = function (filePath, event) {
1325
+ event?.stopPropagation();
1326
+ openTabs = openTabs.filter((t) => t.path !== filePath);
1327
+
1328
+ if (activeTab?.path === filePath) {
1329
+ activeTab = openTabs[0] || null;
1330
+ if (activeTab) {
1331
+ switchToTab(activeTab);
1332
+ } else {
1333
+ editor?.setValue("// No files open");
1334
+ showEditorStatus("No file is open. Select one from the Explorer.", "info");
1335
+ updateEditorToolbarState();
1336
+ }
1337
+ }
1338
+
1339
+ renderTabs();
1340
+ };
1341
+
1342
+ window.openFile = openFile;
1343
+
1344
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1345
+ // CHAT (Uses EXACT Same API as Dashboard)
1346
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1347
+
1348
+ const chatInput = document.getElementById("chat-input");
1349
+ const chatMessages = document.getElementById("chat-messages");
1350
+
1351
+ // ── Image attachments (drag/drop, paste, picker) ──────────────────────────────
1352
+ let pendingChatImages = []; // Array of { dataUri, name, size }
1353
+ const chatPanel = document.getElementById("chat-panel");
1354
+ const chatImageBtn = document.getElementById("chat-image-btn");
1355
+ const chatImageFile = document.getElementById("chat-image-file");
1356
+ const chatImagePreview = document.getElementById("chat-image-preview");
1357
+ const chatDragOverlay = document.getElementById("chat-drag-overlay");
1358
+
1359
+ function addPendingImage(file) {
1360
+ if (!file || !file.type.startsWith("image/")) return;
1361
+ if (pendingChatImages.length >= 3) return; // max 3 images
1362
+ const reader = new FileReader();
1363
+ reader.onload = () => {
1364
+ pendingChatImages.push({ dataUri: reader.result, name: file.name, size: file.size });
1365
+ renderImagePreview();
1366
+ };
1367
+ reader.readAsDataURL(file);
1368
+ }
1369
+
1370
+ function renderImagePreview() {
1371
+ if (!pendingChatImages.length) {
1372
+ chatImagePreview.style.display = "none";
1373
+ chatImagePreview.innerHTML = "";
1374
+ return;
1375
+ }
1376
+ chatImagePreview.style.display = "flex";
1377
+ chatImagePreview.innerHTML = pendingChatImages.map((img, i) => `
1378
+ <div style="display:inline-flex;align-items:center;gap:6px;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:4px 8px;margin-right:6px;">
1379
+ <img src="${img.dataUri}" style="height:36px;width:36px;object-fit:cover;border-radius:4px;" />
1380
+ <span style="font-size:11px;color:var(--text-2);max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${img.name}</span>
1381
+ <button onclick="removePendingImage(${i})" style="background:none;border:none;color:var(--text-3);cursor:pointer;font-size:14px;padding:0 2px;" title="Remove">&times;</button>
1382
+ </div>
1383
+ `).join("");
1384
+ }
1385
+ window.removePendingImage = function(i) {
1386
+ pendingChatImages.splice(i, 1);
1387
+ renderImagePreview();
1388
+ };
1389
+
1390
+ // Image button click
1391
+ if (chatImageBtn) {
1392
+ chatImageBtn.addEventListener("click", () => chatImageFile?.click());
1393
+ }
1394
+ if (chatImageFile) {
1395
+ chatImageFile.addEventListener("change", (e) => {
1396
+ if (e.target.files?.[0]) addPendingImage(e.target.files[0]);
1397
+ e.target.value = "";
1398
+ });
1399
+ }
1400
+
1401
+ // Drag & drop on chat panel
1402
+ if (chatPanel) {
1403
+ let dragCounter = 0;
1404
+ chatPanel.addEventListener("dragenter", (e) => {
1405
+ e.preventDefault();
1406
+ dragCounter++;
1407
+ if (chatDragOverlay) chatDragOverlay.style.display = "flex";
1408
+ });
1409
+ chatPanel.addEventListener("dragleave", (e) => {
1410
+ e.preventDefault();
1411
+ dragCounter--;
1412
+ if (dragCounter <= 0) { dragCounter = 0; if (chatDragOverlay) chatDragOverlay.style.display = "none"; }
1413
+ });
1414
+ chatPanel.addEventListener("dragover", (e) => e.preventDefault());
1415
+ chatPanel.addEventListener("drop", (e) => {
1416
+ e.preventDefault();
1417
+ dragCounter = 0;
1418
+ if (chatDragOverlay) chatDragOverlay.style.display = "none";
1419
+ for (const file of e.dataTransfer?.files || []) {
1420
+ if (file.type.startsWith("image/")) addPendingImage(file);
1421
+ }
1422
+ });
1423
+ }
1424
+
1425
+ // Ctrl/Cmd+V paste image
1426
+ chatInput?.addEventListener("paste", (e) => {
1427
+ const items = e.clipboardData?.items || [];
1428
+ for (const item of items) {
1429
+ if (item.type.startsWith("image/")) {
1430
+ e.preventDefault();
1431
+ addPendingImage(item.getAsFile());
1432
+ return;
1433
+ }
1434
+ }
1435
+ });
1436
+
1437
+ chatInput.addEventListener("keydown", (e) => {
1438
+ // Cmd+Enter or just Enter to send
1439
+ if (e.key === "Enter" && !e.shiftKey) {
1440
+ e.preventDefault();
1441
+ sendChatMessage();
1442
+ }
1443
+ });
1444
+
1445
+ function getChatModeLabel() {
1446
+ if (chatMode.startsWith("cli:")) {
1447
+ return chatMode.replace("cli:", "");
1448
+ }
1449
+ if (chatMode !== "crew-lead") {
1450
+ return chatMode;
1451
+ }
1452
+ return "crew-lead";
1453
+ }
1454
+
1455
+ /** Agent id / label for error bubbles (cli:* must not show as crew-lead). */
1456
+ function getErrorBubbleAgentId() {
1457
+ if (chatMode.startsWith("cli:")) return chatMode.replace("cli:", "");
1458
+ if (chatMode !== "crew-lead") return chatMode;
1459
+ return "crew-lead";
1460
+ }
1461
+
1462
+ async function sendChatMessage() {
1463
+ const message = chatInput.value.trim();
1464
+ if (!message && !pendingChatImages.length) return;
1465
+
1466
+ // Show user message (with image indicators if any)
1467
+ const imageLabel = pendingChatImages.length ? `\n📷 ${pendingChatImages.map(i => i.name).join(", ")}` : "";
1468
+ appendChatBubble("user", (message || "(image)") + imageLabel);
1469
+ chatInput.value = "";
1470
+ const sentImages = [...pendingChatImages];
1471
+ pendingChatImages = [];
1472
+ renderImagePreview();
1473
+ lastAppendedUserContent = message;
1474
+
1475
+ // Typing indicator
1476
+ const typingDiv = document.createElement("div");
1477
+ typingDiv.id = "typing-indicator";
1478
+ typingDiv.className = "message agent";
1479
+ const thinkingAgent = getChatModeLabel();
1480
+ typingDiv.innerHTML = `<div class="message-content" style="color: var(--text-3);">${thinkingAgent} is thinking...</div>`;
1481
+ chatMessages.appendChild(typingDiv);
1482
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1483
+
1484
+ // Capture file context
1485
+ const fileContext = {};
1486
+ if (activeTab) {
1487
+ fileContext.activeFile = activeTab.path;
1488
+ fileContext.activeFileName = activeTab.name;
1489
+
1490
+ // If there's a selection, include it
1491
+ if (editor) {
1492
+ const selection = editor.getSelection();
1493
+ const selectedText = editor.getModel().getValueInRange(selection);
1494
+ if (selectedText && selectedText.trim()) {
1495
+ fileContext.selectedText = selectedText;
1496
+ fileContext.selectionStart = selection.startLineNumber;
1497
+ fileContext.selectionEnd = selection.endLineNumber;
1498
+ }
1499
+ }
1500
+ }
1501
+
1502
+ try {
1503
+ // Route based on selected mode
1504
+ let apiUrl;
1505
+ let body;
1506
+ let isSSE = true; // All modes now stream via SSE
1507
+
1508
+ if (chatMode.startsWith("cli:")) {
1509
+ // CLI Passthrough mode (cli:crew-cli, cli:cursor, etc.) — SSE STREAM
1510
+ const cliName = chatMode.replace("cli:", "");
1511
+ // All CLI engines run locally via Studio server (uses OAuth from each CLI)
1512
+ apiUrl = `${STUDIO_API}/api/studio/chat/unified`;
1513
+ body = {
1514
+ mode: "cli",
1515
+ engine: cliName,
1516
+ message,
1517
+ sessionId: SESSION_ID,
1518
+ projectDir: currentProject?.outputDir || "",
1519
+ projectId: currentProject?.id || "general", // ✅ Added for unified history
1520
+ ...fileContext, // ✅ Include active file context
1521
+ };
1522
+ } else if (chatMode !== "crew-lead") {
1523
+ // Direct agent mode — SSE STREAM via dashboard unified endpoint
1524
+ apiUrl = `${DASHBOARD_API}/api/chat/unified`;
1525
+ body = {
1526
+ mode: "agent",
1527
+ agentId: chatMode,
1528
+ message,
1529
+ sessionId: `studio-${chatMode}-${SESSION_ID}`,
1530
+ projectId: currentProject?.id || "general",
1531
+ ...fileContext,
1532
+ };
1533
+ } else {
1534
+ // crew-lead mode (default) — SSE STREAM via dashboard unified endpoint
1535
+ apiUrl = `${DASHBOARD_API}/api/chat/unified`;
1536
+ body = {
1537
+ mode: "crew-lead",
1538
+ message,
1539
+ sessionId: SESSION_ID,
1540
+ projectId: currentProject?.id || "general",
1541
+ ...(currentProject?.outputDir ? { projectDir: currentProject.outputDir } : {}),
1542
+ };
1543
+ }
1544
+
1545
+ // Attach images if any
1546
+ if (sentImages.length) {
1547
+ body.images = sentImages.map(img => img.dataUri);
1548
+ }
1549
+
1550
+ const dashboardUnified =
1551
+ apiUrl.includes("/api/chat/unified") && !apiUrl.includes("/api/studio/");
1552
+ const response = await fetch(apiUrl, {
1553
+ method: "POST",
1554
+ headers: {
1555
+ "Content-Type": "application/json",
1556
+ ...(dashboardUnified ? { Accept: "text/event-stream" } : {}),
1557
+ ...(AUTH_TOKEN ? { Authorization: `Bearer ${AUTH_TOKEN}` } : {}),
1558
+ },
1559
+ body: JSON.stringify(body),
1560
+ signal: AbortSignal.timeout(CHAT_STREAM_TIMEOUT_MS),
1561
+ });
1562
+
1563
+ document.getElementById("typing-indicator")?.remove();
1564
+
1565
+ if (!response.ok) {
1566
+ throw new Error(`HTTP ${response.status}`);
1567
+ }
1568
+
1569
+ // Handle SSE streaming for CLI passthrough
1570
+ if (isSSE) {
1571
+ const chatLabel = getChatModeLabel();
1572
+ const bubble = createStreamingChatBubble(chatLabel);
1573
+ const activityTrace = createActivityTrace(chatLabel);
1574
+ const passthroughEngine = chatMode.startsWith("cli:")
1575
+ ? chatMode.slice(4)
1576
+ : "";
1577
+ const stderrFilter = createPassthroughStderrLineFilter(passthroughEngine);
1578
+ let stderrFilteredAccum = "";
1579
+ let sawAssistantChunk = false;
1580
+ const reader = response.body.getReader();
1581
+ const decoder = new TextDecoder();
1582
+ let buffer = "";
1583
+ let rawTranscript = "";
1584
+ let traceTranscript = "";
1585
+ let exitCode = 0;
1586
+
1587
+ updateStreamingChatBubble(bubble, rawTranscript, { pending: true });
1588
+
1589
+ try {
1590
+ while (true) {
1591
+ const { done, value } = await reader.read();
1592
+ if (done) break;
1593
+
1594
+ buffer += decoder.decode(value, { stream: true });
1595
+ const lines = buffer.split("\n");
1596
+ buffer = lines.pop() || "";
1597
+
1598
+ for (const line of lines) {
1599
+ if (!line.startsWith("data: ")) continue;
1600
+ try {
1601
+ const event = JSON.parse(line.slice(6));
1602
+ if (event.type === "chunk" && event.text) {
1603
+ let piece = filterOpenCodePassthroughTextChunk(
1604
+ passthroughEngine,
1605
+ event.text,
1606
+ );
1607
+ piece = filterGeminiPassthroughTextChunk(passthroughEngine, piece);
1608
+ if (piece) {
1609
+ sawAssistantChunk = true;
1610
+ rawTranscript += piece;
1611
+ updateStreamingChatBubble(bubble, rawTranscript, { pending: true });
1612
+ }
1613
+ } else if (event.type === "trace" && event.text) {
1614
+ traceTranscript += event.text;
1615
+ activityTrace?.append(event.text);
1616
+ } else if (event.type === "stderr" && event.text) {
1617
+ const cleaned = stderrFilter.push(event.text);
1618
+ if (cleaned) {
1619
+ stderrFilteredAccum += cleaned;
1620
+ // OpenCode Ink status (e.g. "> build · model/id") often arrives on stderr — same as dashboard chat-actions.
1621
+ let stderrPiece = filterOpenCodePassthroughTextChunk(
1622
+ passthroughEngine,
1623
+ cleaned,
1624
+ );
1625
+ stderrPiece = filterGeminiPassthroughTextChunk(
1626
+ passthroughEngine,
1627
+ stderrPiece,
1628
+ );
1629
+ if (stderrPiece) {
1630
+ traceTranscript += stderrPiece;
1631
+ activityTrace?.append(stderrPiece);
1632
+ }
1633
+ // Promote stderr into the main bubble until we see real assistant chunks
1634
+ // (Cursor prints fatal errors on stderr only — avoids empty "No response returned.")
1635
+ if (stderrPiece && !sawAssistantChunk) {
1636
+ rawTranscript += stderrPiece;
1637
+ updateStreamingChatBubble(bubble, rawTranscript, { pending: true });
1638
+ }
1639
+ }
1640
+ } else if (event.type === "done") {
1641
+ exitCode = event.exitCode ?? 0;
1642
+ const stderrTail = stderrFilter.flush();
1643
+ if (stderrTail) {
1644
+ stderrFilteredAccum += stderrTail;
1645
+ let tailPiece = filterOpenCodePassthroughTextChunk(
1646
+ passthroughEngine,
1647
+ stderrTail,
1648
+ );
1649
+ tailPiece = filterGeminiPassthroughTextChunk(
1650
+ passthroughEngine,
1651
+ tailPiece,
1652
+ );
1653
+ if (tailPiece) {
1654
+ traceTranscript += tailPiece;
1655
+ activityTrace?.append(tailPiece);
1656
+ }
1657
+ if (tailPiece && !sawAssistantChunk) {
1658
+ rawTranscript += tailPiece;
1659
+ updateStreamingChatBubble(bubble, rawTranscript, { pending: true });
1660
+ }
1661
+ }
1662
+ if (!rawTranscript.trim() && event.transcript) {
1663
+ let t = filterOpenCodePassthroughTextChunk(
1664
+ passthroughEngine,
1665
+ event.transcript,
1666
+ );
1667
+ t = filterGeminiPassthroughTextChunk(passthroughEngine, t);
1668
+ rawTranscript = t;
1669
+ }
1670
+ if (!traceTranscript.trim() && event.transcript) {
1671
+ activityTrace?.append(event.transcript);
1672
+ }
1673
+ const topErr = summarizePassthroughTopErrorLine(
1674
+ stderrFilteredAccum,
1675
+ passthroughEngine,
1676
+ );
1677
+ if (exitCode !== 0 && topErr) {
1678
+ if (!rawTranscript.trim()) {
1679
+ rawTranscript = `↳ ${topErr}`;
1680
+ } else if (!rawTranscript.includes(topErr)) {
1681
+ rawTranscript += `\n\n↳ ${topErr}`;
1682
+ }
1683
+ }
1684
+ if (!sawAssistantChunk && stderrFilteredAccum.trim()) {
1685
+ let acc = filterOpenCodePassthroughTextChunk(
1686
+ passthroughEngine,
1687
+ stderrFilteredAccum,
1688
+ );
1689
+ acc = filterGeminiPassthroughTextChunk(passthroughEngine, acc);
1690
+ rawTranscript = acc.trim();
1691
+ }
1692
+ }
1693
+ } catch (e) {
1694
+ console.warn("Failed to parse SSE event:", line, e);
1695
+ }
1696
+ }
1697
+ }
1698
+ const strayStderr = stderrFilter.flush();
1699
+ if (strayStderr) {
1700
+ stderrFilteredAccum += strayStderr;
1701
+ let stray = filterOpenCodePassthroughTextChunk(
1702
+ passthroughEngine,
1703
+ strayStderr,
1704
+ );
1705
+ stray = filterGeminiPassthroughTextChunk(passthroughEngine, stray);
1706
+ if (stray) {
1707
+ traceTranscript += stray;
1708
+ activityTrace?.append(stray);
1709
+ }
1710
+ if (stray && !sawAssistantChunk) {
1711
+ rawTranscript += stray;
1712
+ }
1713
+ }
1714
+ if (!sawAssistantChunk && stderrFilteredAccum.trim()) {
1715
+ let acc = filterOpenCodePassthroughTextChunk(
1716
+ passthroughEngine,
1717
+ stderrFilteredAccum,
1718
+ );
1719
+ acc = filterGeminiPassthroughTextChunk(passthroughEngine, acc);
1720
+ rawTranscript = acc.trim();
1721
+ }
1722
+ } catch (streamErr) {
1723
+ const streamMessage = [
1724
+ streamErr?.name,
1725
+ streamErr?.message,
1726
+ streamErr?.cause?.message,
1727
+ String(streamErr),
1728
+ ]
1729
+ .filter(Boolean)
1730
+ .join(" ");
1731
+ // Chrome: "BodyStreamBuffer was aborted" — disconnect, SSE close, or upstream abort
1732
+ const abortedStream =
1733
+ streamErr?.name === "AbortError" ||
1734
+ /BodyStreamBuffer|stream.*abort|The operation was aborted|user aborted|Loading is aborted/i.test(
1735
+ streamMessage,
1736
+ );
1737
+ const hasVisibleOutput =
1738
+ Boolean(rawTranscript.trim()) || Boolean(traceTranscript.trim());
1739
+
1740
+ if (abortedStream && hasVisibleOutput) {
1741
+ console.warn("Chat stream interrupted after partial output:", streamErr);
1742
+ updateStreamingChatBubble(bubble, rawTranscript, {
1743
+ pending: false,
1744
+ exitCode,
1745
+ });
1746
+ activityTrace?.finish(exitCode);
1747
+ return;
1748
+ }
1749
+
1750
+ throw streamErr;
1751
+ }
1752
+
1753
+ updateStreamingChatBubble(bubble, rawTranscript, {
1754
+ pending: false,
1755
+ exitCode,
1756
+ });
1757
+ activityTrace?.finish(exitCode);
1758
+ if (
1759
+ chatMode === "crew-lead" &&
1760
+ /dispatch(?:ed)?\s+to\b|reply will show here|working/i.test(rawTranscript)
1761
+ ) {
1762
+ scheduleProjectReplyRefresh();
1763
+ }
1764
+
1765
+ return;
1766
+ }
1767
+
1768
+ // Fallback: if server returned JSON instead of SSE (e.g. error before stream started)
1769
+ const contentType = response.headers.get("content-type") || "";
1770
+ if (contentType.includes("application/json")) {
1771
+ const data = await response.json();
1772
+
1773
+ const respondingAgent = getErrorBubbleAgentId();
1774
+ const agentInfo =
1775
+ allAgents.find(
1776
+ (a) => a.id === respondingAgent || a.id === `crew-${respondingAgent}`,
1777
+ ) || { emoji: "⚡", agent: respondingAgent };
1778
+ const sourceInfo = {
1779
+ emoji: agentInfo.emoji || "🤖",
1780
+ agent: respondingAgent,
1781
+ };
1782
+
1783
+ if (data.error) {
1784
+ appendChatBubble("assistant", `⚠️ ${data.error}`, sourceInfo);
1785
+ } else if (data.reply) {
1786
+ if (data.reply !== lastAppendedAssistantContent) {
1787
+ appendChatBubble("assistant", data.reply, sourceInfo);
1788
+ lastAppendedAssistantContent = data.reply;
1789
+ }
1790
+ }
1791
+
1792
+ if (data.dispatched) {
1793
+ const note = document.createElement("div");
1794
+ note.style.cssText =
1795
+ "font-size:11px;color:var(--text-3);text-align:center;padding:4px;";
1796
+ note.textContent = `⚡ Dispatched to ${data.dispatched.agent}`;
1797
+ chatMessages.appendChild(note);
1798
+ scheduleProjectReplyRefresh();
1799
+ }
1800
+ }
1801
+
1802
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1803
+ } catch (err) {
1804
+ document.getElementById("typing-indicator")?.remove();
1805
+ // sourceInfo is now accessible here since it's declared outside the try block
1806
+ const respondingAgent = getErrorBubbleAgentId();
1807
+ const agentInfo =
1808
+ allAgents.find(
1809
+ (a) => a.id === respondingAgent || a.id === `crew-${respondingAgent}`,
1810
+ ) || { emoji: "⚡", agent: respondingAgent };
1811
+ const errorSourceInfo = {
1812
+ emoji: agentInfo.emoji || "🤖",
1813
+ agent: respondingAgent,
1814
+ };
1815
+ const msg = err?.message || String(err || "");
1816
+ const benignDisconnect =
1817
+ /BodyStreamBuffer|stream.*abort|AbortError|The operation was aborted/i.test(
1818
+ [err?.name, msg, err?.cause?.message].filter(Boolean).join(" "),
1819
+ );
1820
+ const note = benignDisconnect
1821
+ ? `${msg}\n\n(Stream ended early: timeout, tab refresh, or disconnect. Partial reply above may still be valid.)`
1822
+ : msg;
1823
+ appendChatBubble("assistant", `⚠️ Error: ${note}`, errorSourceInfo);
1824
+ }
1825
+ }
1826
+
1827
+ function appendChatBubble(role, content, sourceInfo = null) {
1828
+ if (!chatMessages) {
1829
+ console.warn("Chat messages container not found");
1830
+ return;
1831
+ }
1832
+
1833
+ const msgDiv = document.createElement("div");
1834
+ msgDiv.className = `message ${role}`;
1835
+
1836
+ let header = role === "user" ? "You" : "crew-lead";
1837
+ if (sourceInfo) {
1838
+ let label = role === "user" ? "You" : null;
1839
+ if (!label) {
1840
+ if (sourceInfo.agent) label = sourceInfo.agent;
1841
+ else if (sourceInfo.source === "cli") label = sourceInfo.engine || "cli";
1842
+ else if (sourceInfo.source === "sub-agent") label = "sub-agent";
1843
+ else if (sourceInfo.source === "agent")
1844
+ label = sourceInfo.targetAgent || "agent";
1845
+ else label = "crew-lead";
1846
+ }
1847
+ header = `${sourceInfo.emoji} ${label}`;
1848
+ }
1849
+
1850
+ const rawContent = String(content || "");
1851
+ const displayContent =
1852
+ role === "assistant"
1853
+ ? deriveCleanAssistantAnswer(rawContent) || rawContent
1854
+ : rawContent;
1855
+
1856
+ msgDiv.innerHTML = `
1857
+ <div class="message-header">${escapeHtml(header)}</div>
1858
+ <div class="message-content">${escapeHtml(displayContent)}</div>
1859
+ ${role === "assistant" ? createTranscriptDetails(rawContent) : ""}
1860
+ `;
1861
+ chatMessages.appendChild(msgDiv);
1862
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1863
+ }
1864
+
1865
+ function escapeHtml(text) {
1866
+ const div = document.createElement("div");
1867
+ div.textContent = text;
1868
+ return div.innerHTML;
1869
+ }
1870
+
1871
+ function createTranscriptDetails(rawTranscript) {
1872
+ const normalizedRaw = String(rawTranscript || "").trim();
1873
+ const display = deriveCleanAssistantAnswer(normalizedRaw);
1874
+ const normalizedDisplay = display.trim();
1875
+
1876
+ if (!normalizedRaw || normalizedRaw === normalizedDisplay) {
1877
+ return "";
1878
+ }
1879
+
1880
+ return `
1881
+ <details class="message-transcript">
1882
+ <summary>Show transcript</summary>
1883
+ <pre>${escapeHtml(normalizedRaw)}</pre>
1884
+ </details>
1885
+ `;
1886
+ }
1887
+
1888
+ function getInlineChatElements() {
1889
+ return {
1890
+ response: document.getElementById("inline-chat-response"),
1891
+ answer: document.getElementById("inline-chat-answer"),
1892
+ transcript: document.getElementById("inline-chat-transcript"),
1893
+ transcriptBody: document.getElementById("inline-chat-transcript-body"),
1894
+ };
1895
+ }
1896
+
1897
+ function isTranscriptTraceBlock(block, { hasTooling = false } = {}) {
1898
+ if (!block) return false;
1899
+
1900
+ if (
1901
+ /(^|\n)(exec|read_mcp_resource|apply_patch|write_stdin|list_mcp_resources|list_mcp_resource_templates)\s*$/m.test(
1902
+ block,
1903
+ )
1904
+ ) {
1905
+ return true;
1906
+ }
1907
+
1908
+ if (
1909
+ /\/bin\/(?:zsh|bash|sh)\s+-lc\b|succeeded in \d+ms:|failed in \d+ms:|Process exited with code|Chunk ID:|Wall time:|Original token count:/i.test(
1910
+ block,
1911
+ )
1912
+ ) {
1913
+ return true;
1914
+ }
1915
+
1916
+ if (
1917
+ hasTooling &&
1918
+ /^(I(?:'m| am)\b|Checking\b|Reading\b|Tracing\b|Looking\b|Inspecting\b|Searching\b|Reviewing\b)/i.test(
1919
+ block,
1920
+ )
1921
+ ) {
1922
+ return true;
1923
+ }
1924
+
1925
+ return false;
1926
+ }
1927
+
1928
+ function deriveCleanAssistantAnswer(rawTranscript) {
1929
+ const normalized = String(rawTranscript || "").replace(/\r\n/g, "\n").trim();
1930
+ if (!normalized) return "";
1931
+
1932
+ const blocks = normalized
1933
+ .split(/\n{2,}/)
1934
+ .map((block) => block.trim())
1935
+ .filter(Boolean);
1936
+ if (!blocks.length) return "";
1937
+
1938
+ const hasTooling = blocks.some((block) => isTranscriptTraceBlock(block));
1939
+ if (!hasTooling) {
1940
+ return normalized;
1941
+ }
1942
+
1943
+ let lastTraceIndex = -1;
1944
+ blocks.forEach((block, index) => {
1945
+ if (isTranscriptTraceBlock(block, { hasTooling: true })) {
1946
+ lastTraceIndex = index;
1947
+ }
1948
+ });
1949
+
1950
+ const answerBlocks = blocks.filter(
1951
+ (block, index) =>
1952
+ index > lastTraceIndex && !isTranscriptTraceBlock(block, { hasTooling: true }),
1953
+ );
1954
+ if (answerBlocks.length) {
1955
+ return answerBlocks.join("\n\n").trim();
1956
+ }
1957
+
1958
+ const cleanedLines = normalized
1959
+ .split("\n")
1960
+ .filter((line) => {
1961
+ const trimmed = line.trim();
1962
+ if (!trimmed) return true;
1963
+ if (
1964
+ /^(exec|read_mcp_resource|apply_patch|write_stdin|list_mcp_resources|list_mcp_resource_templates)$/i.test(
1965
+ trimmed,
1966
+ )
1967
+ ) {
1968
+ return false;
1969
+ }
1970
+ if (
1971
+ /\/bin\/(?:zsh|bash|sh)\s+-lc\b|succeeded in \d+ms:|failed in \d+ms:|Process exited with code|Chunk ID:|Wall time:|Original token count:/i.test(
1972
+ trimmed,
1973
+ )
1974
+ ) {
1975
+ return false;
1976
+ }
1977
+ return true;
1978
+ })
1979
+ .join("\n")
1980
+ .trim();
1981
+
1982
+ return cleanedLines || normalized;
1983
+ }
1984
+
1985
+ function renderInlineChatResponse(rawTranscript, options = {}) {
1986
+ const { response, answer, transcript, transcriptBody } = getInlineChatElements();
1987
+ const raw = String(rawTranscript || "");
1988
+ const display = deriveCleanAssistantAnswer(raw);
1989
+ const visibleText = display || (options.pending ? "Thinking..." : "No response yet.");
1990
+
1991
+ answer.textContent = visibleText;
1992
+ answer.dataset.empty = display ? "false" : "true";
1993
+
1994
+ const normalizedRaw = raw.trim();
1995
+ const normalizedDisplay = display.trim();
1996
+ const shouldShowTranscript =
1997
+ normalizedRaw &&
1998
+ normalizedRaw !== normalizedDisplay &&
1999
+ !options.hideTranscript;
2000
+
2001
+ transcript.hidden = !shouldShowTranscript;
2002
+ transcriptBody.textContent = shouldShowTranscript ? normalizedRaw : "";
2003
+
2004
+ if (!shouldShowTranscript) {
2005
+ transcript.open = false;
2006
+ }
2007
+
2008
+ response.classList.toggle(
2009
+ "visible",
2010
+ Boolean(raw || options.pending || options.forceVisible),
2011
+ );
2012
+ }
2013
+
2014
+ function createStreamingChatBubble(label) {
2015
+ const msgDiv = document.createElement("div");
2016
+ msgDiv.className = "message assistant";
2017
+ msgDiv.innerHTML = `
2018
+ <div class="message-header"></div>
2019
+ <div class="message-content"></div>
2020
+ <details class="message-transcript" hidden>
2021
+ <summary>Show transcript</summary>
2022
+ <pre></pre>
2023
+ </details>
2024
+ `;
2025
+
2026
+ chatMessages.appendChild(msgDiv);
2027
+ chatMessages.scrollTop = chatMessages.scrollHeight;
2028
+
2029
+ return {
2030
+ label,
2031
+ root: msgDiv,
2032
+ header: msgDiv.querySelector(".message-header"),
2033
+ content: msgDiv.querySelector(".message-content"),
2034
+ transcript: msgDiv.querySelector(".message-transcript"),
2035
+ transcriptBody: msgDiv.querySelector("pre"),
2036
+ };
2037
+ }
2038
+
2039
+ function updateStreamingChatBubble(view, rawTranscript, options = {}) {
2040
+ const normalizedRaw = String(rawTranscript || "");
2041
+ const cleanAnswer = deriveCleanAssistantAnswer(normalizedRaw);
2042
+ const exitCode = options.exitCode ?? 0;
2043
+ const visibleText =
2044
+ cleanAnswer ||
2045
+ (options.pending
2046
+ ? `${view.label} is working...`
2047
+ : exitCode !== 0
2048
+ ? "No assistant output — see stderr in the trace or fix Cursor CLI (e.g. agent login / model id)."
2049
+ : "No response returned.");
2050
+
2051
+ view.header.textContent = options.pending
2052
+ ? `${view.label} · working`
2053
+ : `${view.label} · exit ${options.exitCode ?? 0}`;
2054
+ view.content.textContent = visibleText;
2055
+
2056
+ const normalizedClean = cleanAnswer.trim();
2057
+ const trimmedRaw = normalizedRaw.trim();
2058
+ const shouldShowTranscript = trimmedRaw && trimmedRaw !== normalizedClean;
2059
+
2060
+ view.transcript.hidden = !shouldShowTranscript;
2061
+ view.transcriptBody.textContent = shouldShowTranscript ? trimmedRaw : "";
2062
+
2063
+ if (!shouldShowTranscript) {
2064
+ view.transcript.open = false;
2065
+ }
2066
+
2067
+ chatMessages.scrollTop = chatMessages.scrollHeight;
2068
+ }
2069
+
2070
+ function createActivityTrace(label) {
2071
+ const container = document.getElementById("terminal-content");
2072
+ if (!container) {
2073
+ return null;
2074
+ }
2075
+
2076
+ const trace = document.createElement("details");
2077
+ trace.className = "terminal-trace";
2078
+ trace.open = true;
2079
+
2080
+ const summary = document.createElement("summary");
2081
+ summary.textContent = `${label} · live trace`;
2082
+
2083
+ const body = document.createElement("pre");
2084
+ body.className = "terminal-trace-body";
2085
+
2086
+ trace.append(summary, body);
2087
+ container.appendChild(trace);
2088
+ while (container.children.length > MAX_TERMINAL_ENTRIES) {
2089
+ container.removeChild(container.firstElementChild);
2090
+ }
2091
+ container.scrollTop = container.scrollHeight;
2092
+
2093
+ return {
2094
+ append(text) {
2095
+ if (!text) return;
2096
+ body.textContent += text;
2097
+ container.scrollTop = container.scrollHeight;
2098
+ },
2099
+ finish(exitCode) {
2100
+ summary.textContent = `${label} · exit ${exitCode ?? 0}`;
2101
+ container.scrollTop = container.scrollHeight;
2102
+ },
2103
+ fail(message) {
2104
+ if (message) {
2105
+ if (body.textContent && !body.textContent.endsWith("\n")) {
2106
+ body.textContent += "\n";
2107
+ }
2108
+ body.textContent += message;
2109
+ }
2110
+ summary.textContent = `${label} · failed`;
2111
+ container.scrollTop = container.scrollHeight;
2112
+ },
2113
+ };
2114
+ }
2115
+
2116
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2117
+ // INLINE CHAT (Cmd+K)
2118
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2119
+
2120
+ async function showInlineChat() {
2121
+ const overlay = document.getElementById("inline-chat-overlay");
2122
+ const box = document.getElementById("inline-chat-box");
2123
+ const input = document.getElementById("inline-chat-input");
2124
+ const meta = document.getElementById("inline-chat-meta");
2125
+ const context = document.getElementById("inline-chat-context");
2126
+
2127
+ await ensureEditorReady();
2128
+
2129
+ const selection = editor.getSelection();
2130
+ const selectedText = editor.getModel().getValueInRange(selection);
2131
+ const position = editor.getPosition();
2132
+ const fileName = activeTab?.name || "this file";
2133
+
2134
+ if (selectedText) {
2135
+ input.placeholder = `Ask about the selected code in ${fileName}...`;
2136
+ } else {
2137
+ input.placeholder = `What do you want to do at ${fileName}:${position?.lineNumber || 1}?`;
2138
+ }
2139
+
2140
+ context.textContent = position
2141
+ ? `${fileName} · line ${position.lineNumber}, column ${position.column}`
2142
+ : `${fileName} · current cursor`;
2143
+ meta.textContent = "No response yet";
2144
+ renderInlineChatResponse("", { hideTranscript: true });
2145
+ overlay.classList.add("visible");
2146
+ box.setAttribute("data-open", "true");
2147
+ requestAnimationFrame(() => {
2148
+ positionInlineChat();
2149
+ input.focus();
2150
+ input.setSelectionRange(input.value.length, input.value.length);
2151
+ });
2152
+ }
2153
+
2154
+ window.hideInlineChat = function () {
2155
+ const overlay = document.getElementById("inline-chat-overlay");
2156
+ const box = document.getElementById("inline-chat-box");
2157
+ overlay.classList.remove("visible");
2158
+ box.removeAttribute("style");
2159
+ box.removeAttribute("data-open");
2160
+ document.getElementById("inline-chat-input").value = "";
2161
+ renderInlineChatResponse("", { hideTranscript: true });
2162
+ inlineChatAnchor = null;
2163
+ editor?.focus();
2164
+ };
2165
+
2166
+ window.sendInlineChat = async function () {
2167
+ const input = document.getElementById("inline-chat-input");
2168
+ const model = document.getElementById("inline-chat-model");
2169
+ const meta = document.getElementById("inline-chat-meta");
2170
+ const message = input.value.trim();
2171
+ if (!message) return;
2172
+
2173
+ if (model.value !== "codex") {
2174
+ renderInlineChatResponse("Inline local mode currently supports Codex only.", {
2175
+ hideTranscript: true,
2176
+ forceVisible: true,
2177
+ });
2178
+ meta.textContent = `Switch model to Codex to run inline requests`;
2179
+ return;
2180
+ }
2181
+
2182
+ const selection = editor.getSelection();
2183
+ const selectedText = editor.getModel().getValueInRange(selection);
2184
+ const position = editor.getPosition();
2185
+ const targetFile = activeTab?.path || activeTab?.name || "untitled";
2186
+ const prompt = [
2187
+ `Inline request for ${targetFile}${position ? `:${position.lineNumber}:${position.column}` : ""}.`,
2188
+ selectedText
2189
+ ? `Selected code:\n${selectedText}`
2190
+ : "No code is selected.",
2191
+ `User request: ${message}`,
2192
+ ].join("\n\n");
2193
+
2194
+ let rawTranscript = "";
2195
+ renderInlineChatResponse("", { pending: true, hideTranscript: true });
2196
+ meta.textContent = "Running Codex...";
2197
+
2198
+ try {
2199
+ const result = await fetch(`${STUDIO_API}/api/studio/chat/unified`, {
2200
+ method: "POST",
2201
+ headers: {
2202
+ "Content-Type": "application/json",
2203
+ },
2204
+ body: JSON.stringify({
2205
+ mode: "cli",
2206
+ engine: "codex",
2207
+ message: prompt,
2208
+ projectId: currentProject?.id || "general",
2209
+ projectDir: currentProject?.outputDir || getBrowseDirectory(),
2210
+ activeFile: targetFile,
2211
+ }),
2212
+ });
2213
+
2214
+ if (!result.ok) {
2215
+ throw new Error(`HTTP ${result.status}`);
2216
+ }
2217
+
2218
+ const reader = result.body.getReader();
2219
+ const decoder = new TextDecoder();
2220
+ let buffer = "";
2221
+
2222
+ while (true) {
2223
+ const { done, value } = await reader.read();
2224
+ if (done) break;
2225
+
2226
+ buffer += decoder.decode(value, { stream: true });
2227
+ const lines = buffer.split("\n");
2228
+ buffer = lines.pop() || "";
2229
+
2230
+ for (const line of lines) {
2231
+ if (!line.startsWith("data: ")) continue;
2232
+ const event = JSON.parse(line.slice(6));
2233
+ if (event.type === "chunk" && event.text) {
2234
+ rawTranscript += event.text;
2235
+ renderInlineChatResponse(rawTranscript, { pending: true });
2236
+ }
2237
+ if (event.type === "done") {
2238
+ if (!rawTranscript.trim() && event.transcript) {
2239
+ rawTranscript = event.transcript;
2240
+ }
2241
+ meta.textContent = `Codex finished with exit ${event.exitCode ?? 0}`;
2242
+ renderInlineChatResponse(rawTranscript);
2243
+ }
2244
+ }
2245
+ }
2246
+ } catch (error) {
2247
+ renderInlineChatResponse(`Inline Codex failed: ${error.message}`, {
2248
+ hideTranscript: true,
2249
+ forceVisible: true,
2250
+ });
2251
+ meta.textContent = "Inline Codex request failed";
2252
+ }
2253
+
2254
+ input.value = "";
2255
+ };
2256
+
2257
+ function positionInlineChat() {
2258
+ const overlay = document.getElementById("inline-chat-overlay");
2259
+ const box = document.getElementById("inline-chat-box");
2260
+ if (!editor || !overlay || !box) return;
2261
+
2262
+ const position = editor.getPosition();
2263
+ const domNode = editor.getDomNode();
2264
+ if (!position || !domNode) return;
2265
+
2266
+ const cursorCoords = editor.getScrolledVisiblePosition(position);
2267
+ const editorRect = domNode.getBoundingClientRect();
2268
+ const fallbackTop = editorRect.top + 48;
2269
+ const fallbackLeft = editorRect.left + 48;
2270
+
2271
+ const anchorTop = cursorCoords
2272
+ ? editorRect.top + cursorCoords.top + cursorCoords.height + 14
2273
+ : fallbackTop;
2274
+ const anchorLeft = cursorCoords
2275
+ ? editorRect.left + cursorCoords.left + 8
2276
+ : fallbackLeft;
2277
+
2278
+ inlineChatAnchor = { top: anchorTop, left: anchorLeft };
2279
+
2280
+ box.style.top = "0px";
2281
+ box.style.left = "0px";
2282
+ box.style.maxWidth = `min(420px, calc(100vw - 24px))`;
2283
+
2284
+ const boxRect = box.getBoundingClientRect();
2285
+ const viewportWidth = window.innerWidth;
2286
+ const viewportHeight = window.innerHeight;
2287
+ const clampedLeft = Math.min(
2288
+ Math.max(12, anchorLeft),
2289
+ Math.max(12, viewportWidth - boxRect.width - 12),
2290
+ );
2291
+ const clampedTop = Math.min(
2292
+ Math.max(12, anchorTop),
2293
+ Math.max(12, viewportHeight - boxRect.height - 12),
2294
+ );
2295
+
2296
+ box.style.left = `${clampedLeft}px`;
2297
+ box.style.top = `${clampedTop}px`;
2298
+ }
2299
+
2300
+ document.getElementById("inline-chat-overlay").addEventListener("mousedown", (e) => {
2301
+ if (e.target.id === "inline-chat-overlay") {
2302
+ hideInlineChat();
2303
+ }
2304
+ });
2305
+
2306
+ document.getElementById("inline-chat-input").addEventListener("keydown", (e) => {
2307
+ if (e.key === "Escape") {
2308
+ e.preventDefault();
2309
+ hideInlineChat();
2310
+ } else if (e.key === "Enter" && !e.shiftKey) {
2311
+ e.preventDefault();
2312
+ sendInlineChat();
2313
+ } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
2314
+ e.preventDefault();
2315
+ hideInlineChat();
2316
+ }
2317
+ });
2318
+
2319
+ window.addEventListener("keydown", (e) => {
2320
+ const isShortcut = (e.metaKey || e.ctrlKey) && e.code === "KeyK";
2321
+ if (!isShortcut) return;
2322
+
2323
+ const overlay = document.getElementById("inline-chat-overlay");
2324
+ if (!overlay.classList.contains("visible")) return;
2325
+
2326
+ e.preventDefault();
2327
+ hideInlineChat();
2328
+ });
2329
+
2330
+ window.addEventListener("keydown", (e) => {
2331
+ if (e.key === "Escape") {
2332
+ const overlay = document.getElementById("inline-chat-overlay");
2333
+ if (overlay.classList.contains("visible")) {
2334
+ e.preventDefault();
2335
+ hideInlineChat();
2336
+ }
2337
+ }
2338
+ });
2339
+
2340
+ window.addEventListener("resize", () => {
2341
+ const overlay = document.getElementById("inline-chat-overlay");
2342
+ if (overlay.classList.contains("visible")) {
2343
+ positionInlineChat();
2344
+ }
2345
+ });
2346
+
2347
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2348
+ // DIFF PREVIEW
2349
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2350
+
2351
+ let diffEditor = null;
2352
+ let pendingChange = null;
2353
+
2354
+ function parseFileChanges(agentReply) {
2355
+ // Parse agent response for @@WRITE_FILE markers
2356
+ const fileRegex =
2357
+ /@@WRITE_FILE\s+(.+?)\n([\s\S]+?)(?=@@END_FILE|@@WRITE_FILE|$)/g;
2358
+ const changes = [];
2359
+
2360
+ let match;
2361
+ while ((match = fileRegex.exec(agentReply)) !== null) {
2362
+ changes.push({
2363
+ path: match[1].trim(),
2364
+ newContent: match[2].trim(),
2365
+ });
2366
+ }
2367
+
2368
+ return changes;
2369
+ }
2370
+
2371
+ async function showDiffPreview(change) {
2372
+ pendingChange = change;
2373
+ await ensureEditorReady();
2374
+ await ensureEditorLanguage(detectLanguage(change.path));
2375
+
2376
+ const overlay = document.getElementById("diff-preview-overlay");
2377
+ const container = document.getElementById("diff-editor");
2378
+ const filePathEl = document.getElementById("diff-file-path");
2379
+
2380
+ // Get current file content
2381
+ const oldContent =
2382
+ activeTab?.path === change.path
2383
+ ? activeTab.content
2384
+ : await readFile(change.path);
2385
+
2386
+ // Create diff editor
2387
+ if (diffEditor) {
2388
+ diffEditor.dispose();
2389
+ }
2390
+
2391
+ diffEditor = monaco.editor.createDiffEditor(container, {
2392
+ theme: getPreferredMonacoTheme(),
2393
+ readOnly: false,
2394
+ fontSize: 13,
2395
+ renderSideBySide: true,
2396
+ automaticLayout: true,
2397
+ });
2398
+
2399
+ diffEditor.setModel({
2400
+ original: monaco.editor.createModel(
2401
+ oldContent,
2402
+ detectLanguage(change.path),
2403
+ ),
2404
+ modified: monaco.editor.createModel(
2405
+ change.newContent,
2406
+ detectLanguage(change.path),
2407
+ ),
2408
+ });
2409
+
2410
+ filePathEl.textContent = change.path;
2411
+ overlay.classList.add("visible");
2412
+ }
2413
+
2414
+ window.acceptDiff = async function () {
2415
+ if (!pendingChange) return;
2416
+
2417
+ try {
2418
+ await fetchJSON(`${STUDIO_API}/api/studio/file-content`, {
2419
+ method: "POST",
2420
+ headers: {
2421
+ "Content-Type": "application/json",
2422
+ },
2423
+ body: JSON.stringify({
2424
+ path: pendingChange.path,
2425
+ content: pendingChange.newContent,
2426
+ }),
2427
+ });
2428
+
2429
+ const openTab = openTabs.find((tab) => tab.path === pendingChange.path);
2430
+ if (openTab) {
2431
+ openTab.content = pendingChange.newContent;
2432
+ if (activeTab?.path === pendingChange.path) {
2433
+ editor.setValue(pendingChange.newContent);
2434
+ }
2435
+ }
2436
+
2437
+ await loadFileTree();
2438
+ addTerminalLine(`✅ Applied proposed changes to ${pendingChange.path}`, "success");
2439
+ closeDiffPreview();
2440
+ } catch (err) {
2441
+ addTerminalLine(`❌ Failed to apply diff: ${err.message}`, "error");
2442
+ }
2443
+ };
2444
+
2445
+ window.rejectDiff = function () {
2446
+ addTerminalLine(
2447
+ `🗑️ Dismissed proposed changes for ${pendingChange?.path}`,
2448
+ "warning",
2449
+ );
2450
+ closeDiffPreview();
2451
+ };
2452
+
2453
+ window.addEventListener("studio-themechange", () => {
2454
+ if (monaco) {
2455
+ monaco.editor.setTheme(getPreferredMonacoTheme());
2456
+ }
2457
+ });
2458
+
2459
+ function closeDiffPreview() {
2460
+ const overlay = document.getElementById("diff-preview-overlay");
2461
+ overlay.classList.remove("visible");
2462
+ if (diffEditor) {
2463
+ diffEditor.dispose();
2464
+ diffEditor = null;
2465
+ }
2466
+ pendingChange = null;
2467
+ }
2468
+
2469
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2470
+ // TERMINAL
2471
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2472
+
2473
+ function addTerminalLine(text, type = "info") {
2474
+ const container = document.getElementById("terminal-content");
2475
+ if (!container) return;
2476
+ const line = document.createElement("div");
2477
+ line.className = `terminal-line ${type}`;
2478
+ line.textContent = `[${new Date().toLocaleTimeString()}] ${text}`;
2479
+ container.appendChild(line);
2480
+ while (container.children.length > MAX_TERMINAL_ENTRIES) {
2481
+ container.removeChild(container.firstElementChild);
2482
+ }
2483
+ container.scrollTop = container.scrollHeight;
2484
+
2485
+ // Update status bar
2486
+ const statusText = document.getElementById("status-text");
2487
+ if (statusText) {
2488
+ statusText.textContent =
2489
+ text.slice(0, 60) + (text.length > 60 ? "..." : "");
2490
+ }
2491
+ }
2492
+
2493
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2494
+ // RT MESSAGE BUS (WebSocket)
2495
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2496
+
2497
+ function connectRTBus() {
2498
+ ws = new WebSocket(RT_WS);
2499
+
2500
+ ws.onopen = () => {
2501
+ addTerminalLine("🔗 Connected to RT message bus", "success");
2502
+ document.getElementById("statusDot").style.background = "var(--green)";
2503
+ document.getElementById("statusText").textContent = "Connected";
2504
+ };
2505
+
2506
+ ws.onmessage = (event) => {
2507
+ try {
2508
+ const msg = JSON.parse(event.data);
2509
+
2510
+ if (msg.type === "task_claimed") {
2511
+ addTerminalLine(`⚡ ${msg.agent} started working on task`, "info");
2512
+ } else if (msg.type === "task_completed") {
2513
+ addTerminalLine(`✅ ${msg.agent} completed task`, "success");
2514
+ } else if (msg.type === "tool_call") {
2515
+ addTerminalLine(`🔧 ${msg.agent} → ${msg.tool}`, "info");
2516
+ } else if (msg.type === "error") {
2517
+ addTerminalLine(`❌ ${msg.agent}: ${msg.error}`, "error");
2518
+ }
2519
+ } catch (err) {
2520
+ // Ignore parse errors
2521
+ }
2522
+ };
2523
+
2524
+ ws.onerror = (err) => {
2525
+ addTerminalLine("❌ RT bus connection error", "error");
2526
+ document.getElementById("statusDot").style.background = "var(--red)";
2527
+ document.getElementById("statusText").textContent = "Disconnected";
2528
+ };
2529
+
2530
+ ws.onclose = () => {
2531
+ addTerminalLine("🔌 RT bus disconnected", "warning");
2532
+ document.getElementById("statusDot").style.background = "var(--yellow)";
2533
+ document.getElementById("statusText").textContent = "Reconnecting...";
2534
+ setTimeout(connectRTBus, 3000);
2535
+ };
2536
+ }
2537
+
2538
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2539
+ // STUDIO WATCH (CLI FILE CHANGES)
2540
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2541
+
2542
+ function connectStudioWatch() {
2543
+ if (!watchReconnectEnabled) {
2544
+ updateWatchStatus("disabled");
2545
+ return;
2546
+ }
2547
+
2548
+ if (watchWs && watchWs.readyState === WebSocket.OPEN) {
2549
+ return; // Already connected
2550
+ }
2551
+
2552
+ if (watchReconnectTimer) {
2553
+ clearTimeout(watchReconnectTimer);
2554
+ watchReconnectTimer = null;
2555
+ }
2556
+
2557
+ updateWatchStatus("connecting");
2558
+
2559
+ watchWs = new WebSocket(STUDIO_WATCH_WS);
2560
+
2561
+ watchWs.onopen = () => {
2562
+ addTerminalLine(
2563
+ "🔗 Connected to CLI watch server (live reload enabled)",
2564
+ "success",
2565
+ );
2566
+ updateWatchStatus("connected");
2567
+ };
2568
+
2569
+ watchWs.onmessage = async (event) => {
2570
+ try {
2571
+ const msg = JSON.parse(event.data);
2572
+
2573
+ if (msg.type === "file-changed") {
2574
+ // File changed by CLI
2575
+ addTerminalLine(`🔄 ${msg.path} updated by CLI`, "info");
2576
+
2577
+ // If file is currently open in editor, reload it
2578
+ if (activeTab && activeTab.path === msg.path && msg.content) {
2579
+ activeTab.content = msg.content;
2580
+ editor?.setValue(msg.content);
2581
+ addTerminalLine(` ↳ Reloaded in editor`, "success");
2582
+ }
2583
+
2584
+ // Refresh file tree to show changes
2585
+ scheduleFileTreeRefresh();
2586
+ } else if (msg.type === "file-created") {
2587
+ addTerminalLine(`✨ ${msg.path} created by CLI`, "success");
2588
+ scheduleFileTreeRefresh();
2589
+ } else if (msg.type === "file-deleted") {
2590
+ addTerminalLine(`🗑️ ${msg.path} deleted by CLI`, "warning");
2591
+
2592
+ // Close tab if deleted file is open
2593
+ if (activeTab && activeTab.path === msg.path) {
2594
+ closeTab(activeTab.path);
2595
+ }
2596
+
2597
+ scheduleFileTreeRefresh();
2598
+ } else if (msg.type === "connected") {
2599
+ addTerminalLine(`💬 ${msg.message}`, "info");
2600
+ }
2601
+ } catch (err) {
2602
+ // Ignore parse errors
2603
+ }
2604
+ };
2605
+
2606
+ watchWs.onerror = () => {
2607
+ // Watch server not running - that's OK, just won't get live updates
2608
+ updateWatchStatus("error");
2609
+ };
2610
+
2611
+ watchWs.onclose = () => {
2612
+ watchWs = null;
2613
+ if (!watchReconnectEnabled) {
2614
+ updateWatchStatus("disabled");
2615
+ return;
2616
+ }
2617
+
2618
+ updateWatchStatus("disconnected");
2619
+ watchReconnectTimer = setTimeout(() => {
2620
+ watchReconnectTimer = null;
2621
+ connectStudioWatch();
2622
+ }, 5000);
2623
+ };
2624
+ }
2625
+
2626
+ function updateWatchStatus(status) {
2627
+ const dot = document.getElementById("watchStatusDot");
2628
+ const text = document.getElementById("watchStatusText");
2629
+
2630
+ if (!dot || !text) return;
2631
+
2632
+ if (status === "connected") {
2633
+ dot.style.background = "var(--green)";
2634
+ text.textContent = "Watch Server";
2635
+ } else if (status === "disconnected") {
2636
+ dot.style.background = "var(--yellow)";
2637
+ text.textContent = "Watch Server (reconnecting...)";
2638
+ } else if (status === "error") {
2639
+ dot.style.background = "var(--red)";
2640
+ text.textContent = "Watch Server (offline)";
2641
+ } else if (status === "connecting") {
2642
+ dot.style.background = "var(--yellow)";
2643
+ text.textContent = "Watch Server (connecting...)";
2644
+ } else if (status === "disabled") {
2645
+ dot.style.background = "var(--text-3)";
2646
+ text.textContent = "Watch Server (disabled)";
2647
+ }
2648
+ }
2649
+
2650
+ window.toggleWatchConnection = function () {
2651
+ if (!watchReconnectEnabled) {
2652
+ watchReconnectEnabled = true;
2653
+ addTerminalLine("🔄 Reconnecting to watch server...", "info");
2654
+ connectStudioWatch();
2655
+ } else {
2656
+ watchReconnectEnabled = false;
2657
+ if (watchReconnectTimer) {
2658
+ clearTimeout(watchReconnectTimer);
2659
+ watchReconnectTimer = null;
2660
+ }
2661
+ addTerminalLine("⏸️ Disconnecting from watch server...", "warning");
2662
+ watchWs?.close();
2663
+ updateWatchStatus("disabled");
2664
+ }
2665
+ };
2666
+
2667
+ function bindWatchToggleButton() {
2668
+ const watchToggle = document.getElementById("watchToggle");
2669
+ if (!watchToggle) return;
2670
+ watchToggle.addEventListener("click", () => {
2671
+ window.toggleWatchConnection();
2672
+ });
2673
+ }
2674
+
2675
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2676
+ // AUTH
2677
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2678
+
2679
+ async function loadAuthToken() {
2680
+ try {
2681
+ const response = await fetch(`${DASHBOARD_API}/api/auth/token`);
2682
+ if (response.ok) {
2683
+ const data = await response.json();
2684
+ AUTH_TOKEN = data.token || "";
2685
+ if (AUTH_TOKEN) {
2686
+ addTerminalLine(`✅ Loaded auth token`, "success");
2687
+ } else {
2688
+ addTerminalLine(
2689
+ `⚠️ No auth token configured (running in open mode)`,
2690
+ "warning",
2691
+ );
2692
+ }
2693
+ } else {
2694
+ addTerminalLine(
2695
+ `⚠️ Dashboard not reachable - running without auth`,
2696
+ "warning",
2697
+ );
2698
+ }
2699
+ } catch (err) {
2700
+ addTerminalLine(`⚠️ Could not load auth token: ${err.message}`, "warning");
2701
+ }
2702
+ }
2703
+
2704
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2705
+ // PROJECT SELECTOR EVENT
2706
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2707
+
2708
+ document.getElementById("projectSelector")?.addEventListener("change", (e) => {
2709
+ const projectId = e.target.value;
2710
+ console.log(
2711
+ "[projectSelector] Change event - value:",
2712
+ projectId,
2713
+ "options:",
2714
+ Array.from(e.target.options).map((o) => ({
2715
+ value: o.value,
2716
+ text: o.textContent,
2717
+ })),
2718
+ );
2719
+ switchProject(projectId);
2720
+ });
2721
+
2722
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2723
+ // INIT
2724
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2725
+
2726
+ // Listen to hash changes for project routing (like dashboard)
2727
+ window.addEventListener("hashchange", () => {
2728
+ const hash = window.location.hash;
2729
+ const match = hash.match(/project=([^&]+)/);
2730
+ if (match) {
2731
+ const projectId = decodeURIComponent(match[1]);
2732
+ const selector = document.getElementById("projectSelector");
2733
+ if (selector && selector.value !== projectId) {
2734
+ selector.value = projectId;
2735
+ switchProject(projectId);
2736
+ }
2737
+ }
2738
+ });
2739
+
2740
+ window.addEventListener("focus", () => {
2741
+ syncProjectFromSharedState().catch(() => {});
2742
+ });
2743
+
2744
+ async function init() {
2745
+ try {
2746
+ addTerminalLine("🐝 crewswarm Vibe starting...", "info");
2747
+
2748
+ renderEditorPlaceholder();
2749
+ bindEditorToolbar();
2750
+ bindWatchToggleButton();
2751
+ await loadAuthToken();
2752
+ await loadProjects();
2753
+ await loadAgents();
2754
+ window.switchChatMode();
2755
+ connectCrewLeadEvents();
2756
+ connectRTBus();
2757
+ connectStudioWatch(); // Connect to CLI watch server for live reload
2758
+
2759
+ addTerminalLine("✅ Vibe ready", "success");
2760
+ if (languageBootstrapFailed) {
2761
+ addTerminalLine(
2762
+ "ℹ️ Syntax highlighting may be limited until the language loader issue is fixed",
2763
+ "info",
2764
+ );
2765
+ }
2766
+ addTerminalLine(
2767
+ "💡 Tip: Press Cmd+K in the editor to chat about your code",
2768
+ "info",
2769
+ );
2770
+ } catch (err) {
2771
+ console.error("Studio init failed:", err);
2772
+ addTerminalLine(`❌ Studio failed to initialize: ${err.message}`, "error");
2773
+ }
2774
+ }
2775
+
2776
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2777
+ // NEW PROJECT MODAL
2778
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2779
+
2780
+ window.showNewProjectModal = function () {
2781
+ document.getElementById("new-project-overlay").style.display = "flex";
2782
+ document.getElementById("new-project-name").focus();
2783
+ };
2784
+
2785
+ window.hideNewProjectModal = function () {
2786
+ document.getElementById("new-project-overlay").style.display = "none";
2787
+ document.getElementById("new-project-name").value = "";
2788
+ document.getElementById("new-project-desc").value = "";
2789
+ document.getElementById("new-project-dir").value = "";
2790
+ };
2791
+
2792
+ window.createNewProject = async function () {
2793
+ const name = document.getElementById("new-project-name").value.trim();
2794
+ const description = document.getElementById("new-project-desc").value.trim();
2795
+ const outputDir = document.getElementById("new-project-dir").value.trim();
2796
+
2797
+ if (!name) {
2798
+ alert("Project name is required");
2799
+ return;
2800
+ }
2801
+
2802
+ if (!outputDir) {
2803
+ alert("Output directory is required");
2804
+ return;
2805
+ }
2806
+
2807
+ try {
2808
+ const response = await fetch(`${STUDIO_API}/api/studio/projects`, {
2809
+ method: "POST",
2810
+ headers: {
2811
+ "Content-Type": "application/json",
2812
+ },
2813
+ body: JSON.stringify({
2814
+ name,
2815
+ description,
2816
+ outputDir,
2817
+ }),
2818
+ });
2819
+
2820
+ if (!response.ok) {
2821
+ throw new Error(`HTTP ${response.status}`);
2822
+ }
2823
+
2824
+ const data = await response.json();
2825
+
2826
+ if (data.ok && data.project) {
2827
+ addTerminalLine(`✅ Created project: ${name}`, "success");
2828
+ hideNewProjectModal();
2829
+ await loadProjects();
2830
+
2831
+ // Auto-select the new project
2832
+ const selector = document.getElementById("projectSelector");
2833
+ selector.value = data.project.id;
2834
+ await switchProject(data.project.id);
2835
+ } else {
2836
+ addTerminalLine(
2837
+ `❌ Failed to create project: ${data.error || "Unknown error"}`,
2838
+ "error",
2839
+ );
2840
+ }
2841
+ } catch (err) {
2842
+ addTerminalLine(`❌ Failed to create project: ${err.message}`, "error");
2843
+ }
2844
+ };
2845
+
2846
+ init();