crewswarm 0.9.2 → 0.9.4

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 (228) hide show
  1. package/README.md +22 -9
  2. package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js +1 -0
  3. package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -0
  4. package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js +1 -0
  5. package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js.br +0 -0
  6. package/apps/dashboard/dist/assets/index-BeVllEj_.js +2 -0
  7. package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
  8. package/apps/dashboard/dist/assets/{index-CF0aJRtC.css → index-D-sRshvg.css} +1 -1
  9. package/apps/dashboard/dist/assets/index-D-sRshvg.css.br +0 -0
  10. package/apps/dashboard/dist/assets/tab-benchmarks-tab-BHjKCPm3.js.br +0 -0
  11. package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js +1 -0
  12. package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js.br +0 -0
  13. package/apps/dashboard/dist/assets/{tab-pm-loop-tab-Bfd449B4.js → tab-pm-loop-tab-DiAPTJXu.js} +1 -1
  14. package/apps/dashboard/dist/assets/tab-pm-loop-tab-DiAPTJXu.js.br +0 -0
  15. package/apps/dashboard/dist/assets/{tab-projects-tab-DhNWnlzt.js → tab-projects-tab-SFH4E--a.js} +1 -1
  16. package/apps/dashboard/dist/assets/tab-projects-tab-SFH4E--a.js.br +0 -0
  17. package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js +1 -0
  18. package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js.br +0 -0
  19. package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js +1 -0
  20. package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
  21. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js +1 -0
  22. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js.br +0 -0
  23. package/apps/dashboard/dist/index.html +135 -15
  24. package/apps/dashboard/dist/index.html.br +0 -0
  25. package/apps/dashboard/dist/index.html.gz +0 -0
  26. package/apps/vibe/README.md +2 -2
  27. package/apps/vibe/package.json +1 -1
  28. package/apps/vibe/server.mjs +101 -56
  29. package/crew-lead.mjs +34 -4
  30. package/lib/bridges/cli-executor.mjs +1 -1
  31. package/lib/bridges/gateway-ws.mjs +4 -0
  32. package/lib/browser/passthrough-stderr.js +1 -0
  33. package/lib/chat/project-messages.mjs +3 -5
  34. package/lib/cli-process-tracker.mjs +3 -2
  35. package/lib/contacts/identity-linker.mjs +1 -0
  36. package/lib/crew-judge/judge.mjs +19 -18
  37. package/lib/crew-lead/agent-manager.mjs +1 -1
  38. package/lib/crew-lead/background.mjs +14 -1
  39. package/lib/crew-lead/chat-handler.mjs +38 -1
  40. package/lib/crew-lead/http-server.mjs +106 -57
  41. package/lib/crew-lead/llm-caller.mjs +24 -8
  42. package/lib/crew-lead/prompts.mjs +14 -1
  43. package/lib/crew-lead/tools.mjs +3 -2
  44. package/lib/crew-lead/wave-dispatcher.mjs +19 -5
  45. package/lib/crew-lead/ws-router.mjs +219 -27
  46. package/lib/engines/crew-cli.mjs +1 -1
  47. package/lib/engines/engine-registry.mjs +14 -3
  48. package/lib/engines/rt-envelope.mjs +1 -0
  49. package/lib/engines/runners.mjs +28 -4
  50. package/lib/gemini-cli-passthrough-noise.mjs +1 -1
  51. package/lib/integrations/code-search.mjs +4 -3
  52. package/lib/memory/shared-adapter.mjs +23 -10
  53. package/lib/pipeline/manager.mjs +2 -1
  54. package/lib/runtime/config.mjs +1 -1
  55. package/lib/runtime/paths.mjs +12 -8
  56. package/lib/runtime/spending.mjs +2 -1
  57. package/package.json +42 -14
  58. package/scripts/capture-build-flow.mjs +118 -0
  59. package/scripts/coverage-report.mjs +209 -0
  60. package/scripts/coverage-summary.mjs +47 -0
  61. package/scripts/dashboard-validation.mjs +76 -0
  62. package/scripts/dashboard.mjs +1667 -551
  63. package/scripts/generate-openapi.mjs +683 -277
  64. package/scripts/live-bridge-matrix.mjs +79 -0
  65. package/scripts/live-cli-matrix.mjs +166 -0
  66. package/scripts/live-crewchat-check.mjs +42 -0
  67. package/scripts/live-engine-matrix.mjs +50 -0
  68. package/scripts/live-provider-failover-matrix.mjs +107 -0
  69. package/scripts/live-provider-matrix.mjs +228 -0
  70. package/scripts/restart-all-from-repo.sh +4 -4
  71. package/scripts/restart-service.sh +12 -9
  72. package/scripts/smoke-dispatch.mjs +4 -1
  73. package/scripts/test-blast-radius.mjs +204 -0
  74. package/scripts/test-report-summary.mjs +88 -0
  75. package/scripts/test-reporter.mjs +651 -0
  76. package/scripts/test-rerun.mjs +136 -0
  77. package/scripts/tmux-bridge +130 -0
  78. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js +0 -1
  79. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
  80. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js +0 -1
  81. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
  82. package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
  83. package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
  84. package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
  85. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js +0 -1
  86. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
  87. package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
  88. package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
  89. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
  90. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
  91. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js +0 -1
  92. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
  93. package/apps/dashboard/index.html +0 -6529
  94. package/apps/dashboard/package.json +0 -15
  95. package/apps/dashboard/src/app.js +0 -2828
  96. package/apps/dashboard/src/app.js.br +0 -0
  97. package/apps/dashboard/src/app.js.gz +0 -0
  98. package/apps/dashboard/src/chat/chat-actions.js +0 -1847
  99. package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
  100. package/apps/dashboard/src/chat/unified-messages.js +0 -327
  101. package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
  102. package/apps/dashboard/src/cli-process.js +0 -208
  103. package/apps/dashboard/src/cli-process.js.br +0 -0
  104. package/apps/dashboard/src/cli-process.js.gz +0 -0
  105. package/apps/dashboard/src/components/active-tasks-panel.js +0 -175
  106. package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
  107. package/apps/dashboard/src/core/api.js +0 -18
  108. package/apps/dashboard/src/core/api.js.br +0 -0
  109. package/apps/dashboard/src/core/dom.js +0 -228
  110. package/apps/dashboard/src/core/dom.js.br +0 -0
  111. package/apps/dashboard/src/core/state.js +0 -91
  112. package/apps/dashboard/src/core/state.js.br +0 -0
  113. package/apps/dashboard/src/core/task-manager.js +0 -134
  114. package/apps/dashboard/src/core/task-manager.js.br +0 -0
  115. package/apps/dashboard/src/orchestration-status.js +0 -127
  116. package/apps/dashboard/src/orchestration-status.js.br +0 -0
  117. package/apps/dashboard/src/setup-wizard.js +0 -562
  118. package/apps/dashboard/src/setup-wizard.js.br +0 -0
  119. package/apps/dashboard/src/styles.css +0 -2085
  120. package/apps/dashboard/src/styles.css.br +0 -0
  121. package/apps/dashboard/src/styles.css.gz +0 -0
  122. package/apps/dashboard/src/tabs/agents-tab.js +0 -2237
  123. package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
  124. package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
  125. package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
  126. package/apps/dashboard/src/tabs/comms-tab.js +0 -955
  127. package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
  128. package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
  129. package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
  130. package/apps/dashboard/src/tabs/engines-tab.js +0 -175
  131. package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
  132. package/apps/dashboard/src/tabs/memory-tab.js +0 -182
  133. package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
  134. package/apps/dashboard/src/tabs/models-tab.js +0 -450
  135. package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
  136. package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
  137. package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
  138. package/apps/dashboard/src/tabs/projects-tab.js +0 -663
  139. package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
  140. package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
  141. package/apps/dashboard/src/tabs/prompts-tab.js +0 -160
  142. package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
  143. package/apps/dashboard/src/tabs/services-tab.js +0 -202
  144. package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
  145. package/apps/dashboard/src/tabs/settings-tab.js +0 -861
  146. package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
  147. package/apps/dashboard/src/tabs/skills-tab.js +0 -284
  148. package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
  149. package/apps/dashboard/src/tabs/spending-tab.js +0 -173
  150. package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
  151. package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
  152. package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
  153. package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
  154. package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
  155. package/apps/dashboard/src/tabs/usage-tab.js +0 -390
  156. package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
  157. package/apps/dashboard/src/tabs/waves-tab.js +0 -238
  158. package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
  159. package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
  160. package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
  161. package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
  162. package/apps/vibe/.crew/cost.json +0 -17
  163. package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
  164. package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
  165. package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
  166. package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
  167. package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
  168. package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
  169. package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
  170. package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
  171. package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
  172. package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
  173. package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
  174. package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
  175. package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
  176. package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
  177. package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
  178. package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
  179. package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
  180. package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
  181. package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
  182. package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
  183. package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
  184. package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
  185. package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
  186. package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
  187. package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
  188. package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
  189. package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
  190. package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
  191. package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
  192. package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
  193. package/apps/vibe/.crew/sandbox.json +0 -7
  194. package/apps/vibe/.crew/session.json +0 -330
  195. package/apps/vibe/.crew/training-data.jsonl +0 -0
  196. package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
  197. package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
  198. package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
  199. package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
  200. package/apps/vibe/ARCHITECTURE.md +0 -3393
  201. package/apps/vibe/QUICK-REFERENCE.md +0 -211
  202. package/apps/vibe/ROADMAP.md +0 -41
  203. package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
  204. package/apps/vibe/VISUAL-GUIDE.md +0 -378
  205. package/apps/vibe/capture-demo.mjs +0 -160
  206. package/apps/vibe/capture-full-demo.mjs +0 -255
  207. package/apps/vibe/capture-quickstart.mjs +0 -256
  208. package/apps/vibe/capture-vibe-assets.mjs +0 -71
  209. package/apps/vibe/capture-vibe-video.mjs +0 -260
  210. package/apps/vibe/check-buttons.js +0 -41
  211. package/apps/vibe/diagnose.html +0 -106
  212. package/apps/vibe/fix-buttons.js +0 -103
  213. package/apps/vibe/index.html +0 -3404
  214. package/apps/vibe/package-lock.json +0 -920
  215. package/apps/vibe/scripts/studio-pty-host.py +0 -117
  216. package/apps/vibe/src/main.js +0 -2940
  217. package/apps/vibe/src/register-all-languages.js +0 -98
  218. package/apps/vibe/start-studio.sh +0 -11
  219. package/apps/vibe/test/accessibility-tests.js +0 -77
  220. package/apps/vibe/test/browser-performance-audit.mjs +0 -205
  221. package/apps/vibe/test/performance-tests.js +0 -120
  222. package/apps/vibe/test/security-tests.js +0 -213
  223. package/apps/vibe/tests/e2e.local.mjs +0 -54
  224. package/apps/vibe/tests/server.smoke.mjs +0 -106
  225. package/apps/vibe/update_website.mjs +0 -74
  226. package/apps/vibe/vite.config.js +0 -19
  227. package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
  228. package/lib/engines/rt-envelope.mjs.backup-current +0 -870
@@ -1,2828 +0,0 @@
1
- import { getJSON, postJSON } from "./core/api.js";
2
- import { checkFirstRun } from "./setup-wizard.js";
3
- import {
4
- escHtml,
5
- showNotification,
6
- fmt,
7
- createdAt,
8
- appendChatBubble,
9
- showLoading,
10
- showEmpty,
11
- showError,
12
- renderStatusBadge,
13
- } from "./core/dom.js";
14
- import {
15
- state,
16
- persistState,
17
- saveScrollPosition,
18
- restoreScrollPosition,
19
- } from "./core/state.js";
20
- import { initActiveTasksPanel } from "./components/active-tasks-panel.js";
21
- import { startOrchestrationStatusUpdates } from "./orchestration-status.js";
22
- import "./cli-process.js";
23
- import "./chat/unified-messages.js";
24
- import {
25
- initSwarmChatTab,
26
- showSwarmChat,
27
- handleSwarmSSEEvent,
28
- } from "./tabs/swarm-chat-tab.js";
29
- // Lazy-loaded benchmarks tab for code splitting
30
- let benchmarksTabModule = null;
31
- async function loadBenchmarksTabModule() {
32
- if (!benchmarksTabModule) {
33
- benchmarksTabModule = await import("./tabs/benchmarks-tab.js");
34
- }
35
- return benchmarksTabModule;
36
- }
37
- import { initWavesTab } from "./tabs/waves-tab.js";
38
- import { initWorkflowsTab, showWorkflows } from "./tabs/workflows-tab.js";
39
- import {
40
- initMemoryTab,
41
- showMemory,
42
- loadMemoryStats,
43
- searchMemory,
44
- migrateMemory,
45
- compactMemory,
46
- } from "./tabs/memory-tab.js";
47
- import {
48
- initServicesTab,
49
- showServices,
50
- loadServices,
51
- restartService,
52
- stopService,
53
- } from "./tabs/services-tab.js";
54
- import {
55
- initAgentsTab,
56
- showAgents,
57
- loadAgents_cfg,
58
- applyToolPreset,
59
- toggleAgentBody,
60
- deleteAgent,
61
- saveAgentModel,
62
- saveAgentFallback,
63
- saveAgentVoice,
64
- toggleEmojiPicker,
65
- saveAgentIdentity,
66
- saveAgentPrompt,
67
- resetAgentSession,
68
- saveAgentTools,
69
- setRoute,
70
- saveOpenCodeConfig,
71
- saveOpenCodeFallback,
72
- saveCursorCliConfig,
73
- saveClaudeCodeConfig,
74
- saveCodexConfig,
75
- saveGeminiCliConfig,
76
- saveCrewCLIConfig,
77
- bulkSetRoute,
78
- startCrew,
79
- populateModelDropdown,
80
- applyNewAgentToolPreset,
81
- applyPromptPreset,
82
- } from "./tabs/agents-tab.js";
83
- import { initPromptsTab, initPromptsTabDeps } from "./tabs/prompts-tab.js";
84
- import {
85
- showSkills,
86
- showRunSkills,
87
- loadRunSkills,
88
- runSkillFromUI,
89
- loadSkills,
90
- renderSkillsList,
91
- filterSkills,
92
- editSkill,
93
- toggleAddSkill,
94
- toggleImportSkill,
95
- importSkillFromUrl,
96
- cancelSkillForm,
97
- updateSkillAuthFields,
98
- saveSkill,
99
- deleteSkill,
100
- } from "./tabs/skills-tab.js";
101
- import {
102
- showContacts,
103
- loadContacts,
104
- applyContactFilters,
105
- initContactsList,
106
- } from "./tabs/contacts-tab.js";
107
- import {
108
- loadEngines,
109
- deleteEngine,
110
- toggleImportEngine,
111
- importEngineFromUrl,
112
- } from "./tabs/engines-tab.js";
113
- import { initChatActions } from "./chat/chat-actions.js";
114
- import {
115
- initSwarmTab,
116
- showSwarm,
117
- showRT,
118
- showDLQ,
119
- loadSessions,
120
- loadMessages,
121
- loadRTMessages,
122
- toggleRTPause,
123
- clearRTMessages,
124
- loadDLQ,
125
- replayDLQ,
126
- deleteDLQ,
127
- } from "./tabs/swarm-tab.js";
128
- import {
129
- initModelsTab,
130
- initAddProviderForm,
131
- showModels,
132
- showProviders,
133
- loadSearchTools,
134
- saveSearchTool,
135
- testSearchTool,
136
- loadBuiltinProviders,
137
- saveBuiltinKey,
138
- testBuiltinProvider,
139
- fetchBuiltinModels,
140
- loadProviders,
141
- toggleKeyVis,
142
- saveKey,
143
- testKey,
144
- fetchModels,
145
- } from "./tabs/models-tab.js";
146
- import {
147
- initSettingsTab,
148
- loadOpenClawStatus,
149
- loadRTToken,
150
- saveRTToken,
151
- loadConfigLockStatus,
152
- lockConfig,
153
- unlockConfig,
154
- loadOpencodeProject,
155
- saveOpencodeSettings,
156
- saveOpencodeModel,
157
- loadBgConsciousness,
158
- toggleBgConsciousness,
159
- saveBgConsciousnessModel,
160
- loadCursorWaves,
161
- toggleCursorWaves,
162
- loadAutonomousMentions,
163
- toggleAutonomousMentions,
164
- loadClaudeCode,
165
- toggleClaudeCode,
166
- loadCodexExecutor,
167
- toggleCodexExecutor,
168
- loadGeminiCliExecutor,
169
- toggleGeminiCliExecutor,
170
- loadCrewCliExecutor,
171
- toggleCrewCliExecutor,
172
- loadOpencodeExecutor,
173
- toggleOpencodeExecutor,
174
- loadGlobalFallback,
175
- saveGlobalFallback,
176
- loadGlobalOcLoop,
177
- saveGlobalOcLoop,
178
- saveGlobalOcLoopRounds,
179
- loadPassthroughNotify,
180
- savePassthroughNotify,
181
- loadLoopBrain,
182
- saveLoopBrain,
183
- loadEnvAdvanced,
184
- loadTmuxBridge,
185
- toggleTmuxBridge,
186
- } from "./tabs/settings-tab.js";
187
- import {
188
- initCommsTab,
189
- showMessaging,
190
- loadCommsTabData,
191
- loadTgStatus,
192
- loadTgConfig,
193
- saveTgConfig,
194
- startTgBridge,
195
- stopTgBridge,
196
- loadWaStatus,
197
- renderWaContactRows,
198
- loadWaConfig,
199
- saveWaConfig,
200
- startWaBridge,
201
- stopWaBridge,
202
- loadWaMessages,
203
- loadTgMessages,
204
- loadTelegramSessions,
205
- } from "./tabs/comms-tab.js";
206
- import {
207
- showBuild as _showBuild,
208
- showProjects as _showProjects,
209
- loadProjects,
210
- toggleProjectEdit,
211
- saveProjectEdit,
212
- initProjectsList,
213
- populateChatProjectDropdown,
214
- onChatProjectChange,
215
- updateChatProjectHint,
216
- autoSelectChatProject,
217
- resumeProject,
218
- stopProjectPMLoop,
219
- startProjectPMLoop,
220
- deleteProject,
221
- openProjectInBuild as _openProjectInBuild,
222
- loadBuildProjectPicker,
223
- onBuildProjectChange,
224
- stopBuild,
225
- stopContinuousBuild,
226
- retryFailed,
227
- openRoadmapEditor,
228
- closeRoadmapEditor,
229
- saveRoadmap,
230
- addRoadmapItem,
231
- skipNextItem,
232
- resetAllFailed,
233
- loadPhasedProgress,
234
- runBuild,
235
- enhancePrompt,
236
- continuousBuildRun,
237
- } from "./tabs/projects-tab.js";
238
- import {
239
- loadTokenUsage,
240
- loadOcStats as loadOcStatsFromUsage,
241
- loadToolMatrix,
242
- restartAgentFromUI,
243
- checkCrewLeadStatus,
244
- renderTaskLifecycle,
245
- } from "./tabs/usage-tab.js";
246
- import {
247
- loadAllUsage,
248
- loadSpending,
249
- resetSpending,
250
- saveGlobalCaps,
251
- reportOcCost,
252
- } from "./tabs/spending-tab.js";
253
- import {
254
- checkPmStatus,
255
- startPmLoop,
256
- stopPmLoop,
257
- toggleRoadmap,
258
- initPmLoopTab,
259
- } from "./tabs/pm-loop-tab.js";
260
-
261
- let selected = null;
262
- // Lightweight status updater (just status dot + DLQ badge, no content loading)
263
- async function updateStatusBadges() {
264
- try {
265
- const dot = document.getElementById("statusDot");
266
- document.getElementById("status").textContent = "online";
267
- dot.className = "status-dot online";
268
- await checkCrewLeadStatus();
269
- const dlqData = await getJSON("/api/dlq");
270
- const badge = document.getElementById("dlqBadge");
271
- if (dlqData.length) {
272
- badge.textContent = dlqData.length;
273
- badge.classList.remove("hidden");
274
- } else {
275
- badge.classList.add("hidden");
276
- }
277
- } catch (e) {
278
- document.getElementById("status").textContent = "error";
279
- document.getElementById("statusDot").className = "status-dot error";
280
- }
281
- }
282
-
283
- // Legacy alias for backwards compatibility
284
- async function refreshAll() {
285
- await updateStatusBadges();
286
- }
287
- function setNavActive(navId) {
288
- document
289
- .querySelectorAll(".nav-item")
290
- .forEach((b) => b.classList.remove("active"));
291
- const el = document.getElementById(navId);
292
- if (el) el.classList.add("active");
293
- }
294
- function hideAllViews() {
295
- // Save scroll position of the current active view before hiding
296
- saveScrollPosition(state.activeTab);
297
- document.querySelectorAll(".view, .view-sessions").forEach((el) => {
298
- el.classList.remove("active");
299
- // Also clear any inline display styles that might interfere
300
- if (el.style.display) el.style.display = "";
301
- });
302
- const mb = document.querySelector(".msg-bar");
303
- if (mb) mb.style.display = "";
304
- }
305
-
306
- initServicesTab({ hideAllViews, setNavActive });
307
- initAgentsTab({ hideAllViews, setNavActive, refreshAgents: loadAgents_cfg });
308
- initPromptsTabDeps({ hideAllViews, setNavActive });
309
- initSwarmTab({ hideAllViews, setNavActive });
310
- initMemoryTab(state);
311
- initWavesTab();
312
- initWorkflowsTab({ hideAllViews, setNavActive });
313
-
314
- async function pickFolder(inputId) {
315
- const input = document.getElementById(inputId);
316
- const def = encodeURIComponent(input?.value || window._crewHome || "");
317
- const d = await getJSON("/api/pick-folder?default=" + def).catch(() => null);
318
- if (d?.path) {
319
- if (input) input.value = d.path;
320
- }
321
- }
322
- async function loadCrewLeadInfo() {
323
- try {
324
- const d = await getJSON("/api/agents-config");
325
- const cl = (d.agents || []).find((a) => a.id === "crew-lead");
326
- if (!cl) return;
327
- window._crewLeadInfo = {
328
- emoji: cl.emoji || "🧠",
329
- name: cl.name || "crew-lead",
330
- theme: cl.theme || "",
331
- };
332
- const titleEl = document.getElementById("chatAgentTitle");
333
- const subEl = document.getElementById("chatAgentSub");
334
- if (titleEl)
335
- titleEl.textContent = (cl.emoji || "🧠") + " " + (cl.name || "Crew Lead");
336
- if (subEl && cl.theme)
337
- subEl.textContent =
338
- cl.theme + " — chat naturally, dispatch tasks to the crew";
339
- } catch (e) {
340
- /* keep defaults */
341
- }
342
- }
343
-
344
- /**
345
- * Hash routing: view id is only the segment before ? and before /.
346
- * e.g. #chat?project=general → view "chat" (NOT "chat?project=general").
347
- */
348
- function parseRouteFromHash() {
349
- const raw = (location.hash || "#chat").slice(1);
350
- const noQuery = raw.split("?")[0] || "chat";
351
- const parts = noQuery.split("/");
352
- const view = parts[0] || "chat";
353
- const subtab = parts[1];
354
- return { view, subtab, raw };
355
- }
356
-
357
- function currentHashBaseView() {
358
- return parseRouteFromHash().view;
359
- }
360
-
361
- /**
362
- * Set chat project in the URL without firing hashchange — prevents a second
363
- * showChat() + loadChatHistory() that clears the thread while a reply streams.
364
- */
365
- function setChatHashProject(projectId) {
366
- const id =
367
- projectId && String(projectId).trim() && projectId !== "undefined"
368
- ? projectId
369
- : "general";
370
- const next = `#chat?project=${encodeURIComponent(id)}`;
371
- if (location.hash !== next) {
372
- history.replaceState(null, "", next);
373
- }
374
- }
375
-
376
- async function loadSharedActiveProjectId() {
377
- try {
378
- const data = await getJSON("/api/ui/active-project");
379
- const projectId = String(data?.projectId || "").trim();
380
- return projectId || "general";
381
- } catch {
382
- return "general";
383
- }
384
- }
385
-
386
- async function persistSharedActiveProjectId(projectId) {
387
- const normalizedId =
388
- projectId && String(projectId).trim() && projectId !== "undefined"
389
- ? String(projectId).trim()
390
- : "general";
391
- try {
392
- await postJSON("/api/ui/active-project", { projectId: normalizedId });
393
- } catch {
394
- /* best effort */
395
- }
396
- }
397
-
398
- async function syncChatProjectFromSharedState() {
399
- if (parseRouteFromHash().view !== "chat") return;
400
- const sharedProjectId = await loadSharedActiveProjectId();
401
- const normalizedSharedId =
402
- sharedProjectId && sharedProjectId !== "undefined"
403
- ? sharedProjectId
404
- : "general";
405
- const currentProjectId =
406
- state.chatActiveProjectId && state.chatActiveProjectId !== "undefined"
407
- ? state.chatActiveProjectId
408
- : "general";
409
- if (normalizedSharedId !== currentProjectId && window.selectProjectTab) {
410
- window.selectProjectTab(normalizedSharedId);
411
- }
412
- }
413
-
414
- async function showChat() {
415
- hideAllViews();
416
- document.getElementById("chatView").classList.add("active");
417
- setNavActive("navChat");
418
- state.activeTab = "chat";
419
- persistState();
420
- const mb = document.querySelector(".msg-bar");
421
- if (mb) mb.style.display = "none";
422
-
423
- // Start orchestration status updates
424
- startOrchestrationStatusUpdates();
425
-
426
- // Check if chat content is already loaded (DOM preserved from last visit)
427
- const chatBox = document.getElementById("chatMessages");
428
- if (chatBox?.dataset.historyLoading === "true") {
429
- await waitForChatHistoryIdle();
430
- }
431
- const alreadyLoaded =
432
- chatBox &&
433
- chatBox.dataset.historyLoaded === "true" &&
434
- chatBox.children.length > 0;
435
-
436
- // Refresh project dropdown (uses TTL cache so fast on re-visits)
437
- try {
438
- const data = await getJSON("/api/projects");
439
- const projects = data.projects || [];
440
- state.projectsData = {};
441
- projects.forEach((p) => {
442
- state.projectsData[p.id] = p;
443
- });
444
- persistState();
445
- populateChatProjectDropdown(projects);
446
- } catch (e) {
447
- console.warn("Failed to refresh projects dropdown:", e);
448
- }
449
-
450
- // Read project from URL first (most explicit), then shared UI state, then localStorage.
451
- const urlParams = new URLSearchParams(
452
- window.location.hash.replace(/^#chat\?/, ""),
453
- );
454
- const urlProject = urlParams.get("project");
455
- if (urlProject) {
456
- state.chatActiveProjectId = urlProject;
457
- } else {
458
- const sharedProjectId = await loadSharedActiveProjectId();
459
- try {
460
- state.chatActiveProjectId =
461
- sharedProjectId ||
462
- localStorage.getItem("crewswarm_chat_active_project_id") ||
463
- "general";
464
- } catch {
465
- state.chatActiveProjectId = sharedProjectId || "general";
466
- }
467
- }
468
-
469
- // Set initial URL if not set (replaceState — no hashchange storm)
470
- if (!window.location.hash.includes("?project=")) {
471
- setChatHashProject(state.chatActiveProjectId);
472
- }
473
-
474
- console.log("🔵 [INIT] Active project from URL:", state.chatActiveProjectId);
475
-
476
- // Highlight the correct tab based on URL
477
- const tabsContainer = document.getElementById("chatProjectTabs");
478
- if (tabsContainer) {
479
- Array.from(tabsContainer.children).forEach((tab) => {
480
- if (tab.dataset.projectId === state.chatActiveProjectId) {
481
- tab.classList.add("active");
482
- } else {
483
- tab.classList.remove("active");
484
- }
485
- });
486
- }
487
-
488
- const sel = document.getElementById("chatProjectSelect");
489
- if (
490
- sel &&
491
- state.chatActiveProjectId &&
492
- sel.querySelector('option[value="' + state.chatActiveProjectId + '"]')
493
- )
494
- sel.value = state.chatActiveProjectId;
495
- persistSharedActiveProjectId(state.chatActiveProjectId);
496
- checkCrewLeadStatus();
497
- startAgentReplyListener();
498
- loadCrewLeadInfo();
499
-
500
- // Load agents into chat agent selector
501
- if (window.loadChatAgentSelector) {
502
- window.loadChatAgentSelector();
503
- }
504
-
505
- // Only reload history if the chat box is empty (first visit or after clear)
506
- if (!alreadyLoaded) {
507
- await loadChatHistory();
508
- } else {
509
- // Restore scroll position from previous visit
510
- restoreScrollPosition("chat");
511
- }
512
- }
513
- function showFiles() {
514
- hideAllViews();
515
- document.getElementById("filesView").classList.add("active");
516
- setNavActive("navFiles");
517
- state.activeTab = "files";
518
- persistState();
519
- loadFiles();
520
- }
521
-
522
- // ── Chat / crew-lead ──────────────────────────────────────────────────────────
523
- // Session ID: Always "owner" for dashboard, projectId handles isolation
524
- function getChatSessionId() {
525
- return "owner";
526
- }
527
-
528
- let chatPollInterval = null;
529
- let agentReplySSE = null;
530
-
531
- /** Assistant rows from appendChatBubble use align-items:flex-start on the wrapper. */
532
- function chatThreadHasAssistantText(box, text) {
533
- if (!box || text == null) return false;
534
- const want = String(text).trim();
535
- if (!want) return false;
536
- for (let i = box.children.length - 1; i >= 0; i--) {
537
- const row = box.children[i];
538
- if (row.id === "streaming-wrapper") continue;
539
- if (row.children.length < 2) continue;
540
- if (!String(row.style.alignItems || "").includes("flex-start")) continue;
541
- const bubble = row.children[1];
542
- if ((bubble.textContent || "").trim() === want) return true;
543
- }
544
- return false;
545
- }
546
-
547
- /** Most recent assistant bubble text (for consecutive duplicate SSE guard). */
548
- function lastAssistantBubbleText(box) {
549
- if (!box) return "";
550
- for (let i = box.children.length - 1; i >= 0; i--) {
551
- const row = box.children[i];
552
- if (row.id === "streaming-wrapper") continue;
553
- if (row.children.length < 2) continue;
554
- if (!String(row.style.alignItems || "").includes("flex-start")) continue;
555
- return (row.children[1].textContent || "").trim();
556
- }
557
- return "";
558
- }
559
-
560
- function startAgentReplyListener() {
561
- if (agentReplySSE) return; // already listening
562
-
563
- // Connect DIRECTLY to crew-lead's SSE endpoint (not via dashboard proxy)
564
- // This prevents SSE breakage when dashboard restarts
565
- const crewLeadPort = 5010;
566
- // Use same hostname as dashboard to avoid CORS issues (localhost vs 127.0.0.1)
567
- const hostname = window.location.hostname || '127.0.0.1';
568
- const sseUrl = `http://${hostname}:${crewLeadPort}/events`;
569
-
570
- console.log("[crewswarm] Starting EventSource listener for", sseUrl);
571
- agentReplySSE = new EventSource(sseUrl);
572
- const sseLog =
573
- typeof localStorage !== "undefined" &&
574
- localStorage.getItem("crewswarm_debug_sse") === "1";
575
- agentReplySSE.onmessage = (e) => {
576
- if (!e.data) {
577
- console.warn("[crewswarm] SSE message with null/empty data");
578
- return;
579
- }
580
- try {
581
- const d = JSON.parse(e.data);
582
- const normalizeProjectId = (value) =>
583
- !value || value === "general" ? "general" : value;
584
- const currentSessionId = getChatSessionId();
585
-
586
- if (sseLog) {
587
- console.log("[crewswarm] SSE:", d.type, e.data.slice(0, 120));
588
- }
589
-
590
- const box = document.getElementById("chatMessages");
591
-
592
- if (handleSwarmSSEEvent(d)) {
593
- return;
594
- }
595
-
596
- // Handle streaming tokens
597
- if (d.type === "chat_stream" && d.sessionId === currentSessionId) {
598
- const messageProjectId = normalizeProjectId(d.projectId);
599
- const currentProjId = normalizeProjectId(state.chatActiveProjectId);
600
-
601
- if (currentProjId !== messageProjectId) {
602
- return; // Wrong project, skip
603
- }
604
-
605
- let streamBubble = document.getElementById("streaming-bubble");
606
- if (!streamBubble) {
607
- // Create wrapper + label + bubble (matching appendChatBubble structure)
608
- const wrapper = document.createElement("div");
609
- wrapper.id = "streaming-wrapper";
610
- wrapper.style.cssText =
611
- "display:flex;flex-direction:column;align-items:flex-start;gap:4px;";
612
-
613
- const label = document.createElement("div");
614
- label.style.cssText =
615
- "font-size:11px;color:var(--text-3);padding:0 6px;";
616
- const cl = window._crewLeadInfo || { emoji: "🧠", name: "crew-lead" };
617
- label.textContent = cl.emoji + " " + cl.name + " (streaming...)";
618
-
619
- streamBubble = document.createElement("div");
620
- streamBubble.id = "streaming-bubble";
621
- streamBubble.className = "chat-bubble assistant";
622
- streamBubble.style.cssText =
623
- "max-width:80%;padding:10px 14px;border-radius:14px 14px 14px 4px;background:var(--surface-2);color:var(--text-2);font-size:14px;line-height:1.5;white-space:pre-wrap;word-break:break-word;border:1px solid var(--border);";
624
- // Keep a persistent text node so we can append chunks incrementally
625
- // instead of rewriting the whole bubble content on every frame.
626
- streamBubble._textNode = document.createTextNode("");
627
- streamBubble.appendChild(streamBubble._textNode);
628
-
629
- wrapper.appendChild(label);
630
- wrapper.appendChild(streamBubble);
631
- if (box) box.appendChild(wrapper);
632
- }
633
-
634
- // Batch token paint to animation frames for smoother streaming.
635
- const nextChunk = (streamBubble.dataset.streamChunk || "") + d.token;
636
- streamBubble.dataset.streamChunk = nextChunk;
637
- if (!streamBubble._rafId) {
638
- streamBubble._rafId = requestAnimationFrame(() => {
639
- const chunk = streamBubble.dataset.streamChunk || "";
640
- if (chunk) {
641
- if (!streamBubble._textNode) {
642
- streamBubble._textNode = document.createTextNode("");
643
- streamBubble.appendChild(streamBubble._textNode);
644
- }
645
- streamBubble._textNode.textContent += chunk;
646
- streamBubble.dataset.streamChunk = "";
647
- }
648
- if (box) box.scrollTop = box.scrollHeight;
649
- streamBubble._rafId = null;
650
- });
651
- }
652
- return;
653
- }
654
-
655
- if (d.type === "draft_discarded" && d.draftId) {
656
- const el = document.querySelector(
657
- '[data-draft-id="' + d.draftId + '"]',
658
- );
659
- if (el) el.remove();
660
- return;
661
- }
662
- if (d.type === "context_warning" && d.sessionId === getChatSessionId()) {
663
- const existing = document.getElementById("contextWarningBanner");
664
- if (existing) existing.remove();
665
- const banner = document.createElement("div");
666
- banner.id = "contextWarningBanner";
667
- const isCritical = d.level === "critical";
668
- banner.style.cssText = `display:flex;align-items:center;gap:10px;padding:8px 14px;border-radius:8px;margin:6px 0;font-size:12px;background:${isCritical ? "rgba(239,68,68,0.1)" : "rgba(245,158,11,0.1)"};border:1px solid ${isCritical ? "rgba(239,68,68,0.3)" : "rgba(245,158,11,0.3)"};color:${isCritical ? "#f87171" : "#f59e0b"};`;
669
- banner.innerHTML = `<span style="flex:1;">${d.message}</span><button onclick="clearChatHistory()" style="padding:2px 8px;font-size:11px;border-radius:4px;border:1px solid currentColor;background:transparent;color:inherit;cursor:pointer;">Clear now</button><button onclick="this.parentElement.remove()" style="background:none;border:none;cursor:pointer;color:inherit;font-size:14px;padding:0 2px;">✕</button>`;
670
- const box = document.getElementById("chatMessages");
671
- if (box) {
672
- box.appendChild(banner);
673
- box.scrollTop = box.scrollHeight;
674
- }
675
- return;
676
- }
677
- if (d.type === "chat_message" && d.sessionId === getChatSessionId()) {
678
- // Additional check: if message has projectId, only show if it matches current project
679
- const currentProjectId = normalizeProjectId(state.chatActiveProjectId);
680
- const messageProjectId = normalizeProjectId(d.projectId);
681
-
682
- if (currentProjectId !== messageProjectId) {
683
- console.log("[crewswarm] ❌ SKIP - projectId mismatch:", {
684
- current: currentProjectId || "(General)",
685
- message: messageProjectId || "(General)",
686
- });
687
- return;
688
- }
689
-
690
- console.log("[crewswarm] ✅ Displaying message for current session");
691
-
692
- if (d.role === "user") {
693
- // Skip SSE echo of messages we already appended locally via sendChat()
694
- if (d.content === lastSentContent) {
695
- console.log("[crewswarm] Skipping SSE echo of locally-sent message");
696
- lastSentContent = null;
697
- return;
698
- }
699
- if (d.content !== lastAppendedUserContent) {
700
- console.log(
701
- "[crewswarm] Appending user bubble:",
702
- d.content.slice(0, 50),
703
- );
704
- appendChatBubble("user", d.content);
705
- lastAppendedUserContent = d.content;
706
- } else {
707
- console.log("[crewswarm] Skipping duplicate user message");
708
- }
709
- } else if (d.role === "assistant") {
710
- document
711
- .querySelectorAll('[id^="typing-"]')
712
- .forEach((el) => el.remove());
713
-
714
- const assistantTrimmed = String(d.content || "").trim();
715
- if (
716
- assistantTrimmed &&
717
- lastAssistantBubbleText(box) === assistantTrimmed
718
- ) {
719
- lastAppendedAssistantContent = d.content;
720
- if (box) box.scrollTop = box.scrollHeight;
721
- return;
722
- }
723
-
724
- const streamWrapper = document.getElementById("streaming-wrapper");
725
- const streamBubble = document.getElementById("streaming-bubble");
726
-
727
- if (streamBubble) {
728
- if (streamBubble._rafId) cancelAnimationFrame(streamBubble._rafId);
729
- streamBubble._rafId = null;
730
- const pending = streamBubble.dataset.streamChunk || "";
731
- if (pending) {
732
- if (!streamBubble._textNode) {
733
- streamBubble._textNode = document.createTextNode("");
734
- streamBubble.appendChild(streamBubble._textNode);
735
- }
736
- streamBubble._textNode.textContent += pending;
737
- streamBubble.dataset.streamChunk = "";
738
- }
739
- }
740
-
741
- // Promote streaming row → final bubble in place (do NOT remove then re-append).
742
- // Removing the stream loses the only visible copy if append/skip heuristics race.
743
- if (streamWrapper && streamBubble) {
744
- const cl = window._crewLeadInfo || { emoji: "🧠", name: "crew-lead" };
745
- const labelEl = streamWrapper.firstElementChild;
746
- if (labelEl && labelEl !== streamBubble) {
747
- labelEl.textContent = cl.emoji + " " + cl.name;
748
- }
749
- const finalText = d.content ?? "";
750
- if (streamBubble._textNode) {
751
- streamBubble._textNode.textContent = finalText;
752
- } else {
753
- streamBubble.textContent = finalText;
754
- }
755
- streamWrapper.removeAttribute("id");
756
- streamBubble.removeAttribute("id");
757
- delete streamBubble.dataset.streamChunk;
758
- lastAppendedAssistantContent = d.content;
759
- } else {
760
- if (streamWrapper) streamWrapper.remove();
761
- const skipDuplicate =
762
- d.content === lastAppendedAssistantContent &&
763
- chatThreadHasAssistantText(box, d.content);
764
- if (!skipDuplicate) {
765
- console.log("[crewswarm] Appending assistant bubble (final)");
766
- appendChatBubble(
767
- "assistant",
768
- d.content,
769
- d.fallbackModel,
770
- d.fallbackReason,
771
- d.model,
772
- d.engineUsed,
773
- );
774
- lastAppendedAssistantContent = d.content;
775
- } else {
776
- console.log("[crewswarm] Skipping duplicate assistant message");
777
- }
778
- }
779
- }
780
- if (box) box.scrollTop = box.scrollHeight;
781
- return;
782
- }
783
- if (
784
- d.type === "pending_project" &&
785
- d.sessionId === getChatSessionId() &&
786
- d.pendingProject &&
787
- box
788
- ) {
789
- appendRoadmapCard(box, d.pendingProject);
790
- box.scrollTop = box.scrollHeight;
791
- return;
792
- }
793
- // agent_working from OpenCode bridge — show pulsing coding dot on agent card
794
- if (d.type === "agent_working" && d.agent) {
795
- const dot = document.getElementById("coding-dot-" + d.agent);
796
- if (dot) dot.style.display = "inline-flex";
797
- }
798
- // agent_idle from OpenCode bridge — hide coding dot
799
- if (d.type === "agent_idle" && d.agent) {
800
- const dot = document.getElementById("coding-dot-" + d.agent);
801
- if (dot) dot.style.display = "none";
802
- }
803
- // OpenCode serve live events — tool calls, file edits, session boundaries
804
- if (d.type === "opencode_event") {
805
- const feed = document.getElementById("ocFeed");
806
- const liveDot = document.getElementById("ocFeedDot");
807
- if (!feed) return;
808
- if (liveDot) liveDot.style.display = "inline-block";
809
- const row = document.createElement("div");
810
- row.style.cssText =
811
- "display:flex;align-items:center;gap:8px;padding:5px 10px;border-radius:8px;background:var(--bg-2);font-size:12px;font-family:var(--font-mono,monospace);animation:fadeIn .25s ease;";
812
- const time = new Date(d.ts || Date.now()).toLocaleTimeString([], {
813
- hour: "2-digit",
814
- minute: "2-digit",
815
- second: "2-digit",
816
- });
817
- let icon = "⚙️",
818
- label = "";
819
- if (d.kind === "session_start") {
820
- icon = "▶";
821
- row.style.borderLeft = "3px solid var(--green-hi)";
822
- var _sd = d.dir || "";
823
- label = "session started" + (_sd ? " — " + _sd.split("/").pop() : "");
824
- } else if (d.kind === "session_end") {
825
- icon = "■";
826
- row.style.borderLeft = "3px solid var(--text-3)";
827
- label = "session ended";
828
- if (liveDot) liveDot.style.display = "none";
829
- } else if (d.kind === "file_edit") {
830
- icon = "✏️";
831
- row.style.borderLeft = "3px solid var(--amber)";
832
- label =
833
- (d.file || d.path || "") +
834
- (d.extra
835
- ? ' <span style="opacity:.5;">' + d.extra + "</span>"
836
- : "");
837
- } else if (d.kind === "error") {
838
- icon = "✗";
839
- row.style.borderLeft = "3px solid var(--red-hi)";
840
- row.style.color = "var(--red-hi)";
841
- label = d.message || "error";
842
- } else if (d.kind === "tool") {
843
- const toolColors = {
844
- read_file: "var(--accent)",
845
- write_file: "var(--amber)",
846
- bash: "var(--purple)",
847
- list_directory: "var(--green)",
848
- grep: "var(--green)",
849
- };
850
- const tc = toolColors[d.tool] || "var(--text-2)";
851
- icon = d.phase === "done" ? "✓" : "→";
852
- row.style.borderLeft = "3px solid " + tc;
853
- row.style.color =
854
- d.phase === "done" ? "var(--text-2)" : "var(--text-1)";
855
- label =
856
- '<span style="color:' +
857
- tc +
858
- ';font-weight:600;">' +
859
- (d.tool || "") +
860
- "</span>" +
861
- (d.label
862
- ? ' <span style="opacity:.6;">' + d.label + "</span>"
863
- : "");
864
- }
865
- row.innerHTML =
866
- '<span style="opacity:.4;flex-shrink:0;">' +
867
- time +
868
- "</span>" +
869
- '<span style="flex-shrink:0;">' +
870
- icon +
871
- "</span>" +
872
- '<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' +
873
- label +
874
- "</span>";
875
- feed.appendChild(row);
876
- // Cap at 80 rows
877
- while (feed.children.length > 80) feed.removeChild(feed.firstChild);
878
- feed.scrollTop = feed.scrollHeight;
879
- return;
880
- }
881
- // agent_working: crew-lead dispatched a task — show a "waiting" indicator
882
- if (d.type === "agent_working" && d.agent) {
883
- const spinnerId = "agent-spinner-" + (d.taskId || d.agent);
884
- if (box && !document.getElementById(spinnerId)) {
885
- const el = document.createElement("div");
886
- el.id = spinnerId;
887
- el.className = "msg a";
888
- el.style.cssText = "opacity:.7; font-style:italic;";
889
- el.innerHTML =
890
- '<div class="meta"><strong>' +
891
- d.agent +
892
- "</strong> · working…</div>" +
893
- '<div class="t" style="display:flex;align-items:center;gap:8px;">' +
894
- '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--accent);animation:pulse 1s ease-in-out infinite;"></span>' +
895
- "Processing task…</div>";
896
- box.appendChild(el);
897
- box.scrollTop = box.scrollHeight;
898
- }
899
- return;
900
- }
901
- // agent_reply: task completion from any crew member — replace spinner, show reply, notify
902
- if (d.type === "agent_reply" || (d.from && d.content)) {
903
- if (!d.from || !d.content) return;
904
- // Skip passthrough summaries — the dashboard already rendered the live stream
905
- if (d._passthroughSummary) return;
906
- const spinnerId = "agent-spinner-" + (d.taskId || d.from);
907
- const spinnerEl = document.getElementById(spinnerId);
908
- if (spinnerEl) spinnerEl.remove();
909
- const agentSpinner = document.getElementById("agent-spinner-" + d.from);
910
- if (agentSpinner) agentSpinner.remove();
911
- appendChatBubble(
912
- "🤖 " + d.from,
913
- d.content,
914
- false,
915
- null,
916
- null,
917
- d.engineUsed,
918
- );
919
- if (box) box.scrollTop = box.scrollHeight;
920
- showNotification(d.from + " finished a task");
921
- return;
922
- }
923
- // task.timeout: dispatch never claimed or timed out — replace spinner with "No reply" message
924
- if (d.type === "task.timeout" && d.agent) {
925
- const spinnerId = "agent-spinner-" + (d.taskId || d.agent);
926
- const spinnerEl = document.getElementById(spinnerId);
927
- if (spinnerEl) spinnerEl.remove();
928
- const agentSpinner = document.getElementById(
929
- "agent-spinner-" + d.agent,
930
- );
931
- if (agentSpinner) agentSpinner.remove();
932
- const msg =
933
- "[crew-lead] Task to " +
934
- d.agent +
935
- " timed out (no reply in 90s). Consider @@SERVICE restart " +
936
- d.agent +
937
- " or re-dispatch to another agent.";
938
- if (box) {
939
- const el = document.createElement("div");
940
- el.className = "msg a";
941
- el.style.cssText =
942
- "opacity:.85; font-style:italic; color:var(--text-3);";
943
- el.innerHTML =
944
- '<div class="meta"><strong>' +
945
- d.agent +
946
- '</strong> · no reply</div><div class="t">' +
947
- escHtml(msg) +
948
- "</div>";
949
- box.appendChild(el);
950
- box.scrollTop = box.scrollHeight;
951
- }
952
- showNotification("Task to " + d.agent + " timed out");
953
- return;
954
- }
955
- // pipeline_progress: a wave or step dispatched
956
- if (d.type === "pipeline_progress") {
957
- let label;
958
- if (d.agents) {
959
- label =
960
- "Wave " +
961
- (d.waveIndex + 1) +
962
- "/" +
963
- d.totalWaves +
964
- " → " +
965
- d.agents.join(" + ");
966
- } else {
967
- label = "Step " + (d.stepIndex + 1) + "/" + d.total + " → " + d.agent;
968
- }
969
- const el = document.createElement("div");
970
- el.style.cssText =
971
- "font-size:11px;color:var(--text-3);padding:2px 8px;margin:2px 0;";
972
- el.textContent = "↳ " + label;
973
- if (box) {
974
- box.appendChild(el);
975
- box.scrollTop = box.scrollHeight;
976
- }
977
- return;
978
- }
979
- // pipeline_quality_gate: wave had issues
980
- if (d.type === "pipeline_quality_gate") {
981
- const el = document.createElement("div");
982
- const retryNote = d.willRetry
983
- ? " — retrying wave"
984
- : " — advancing anyway";
985
- el.style.cssText =
986
- "font-size:11px;color:var(--warning, #e8a030);padding:2px 8px;margin:2px 0;";
987
- el.textContent =
988
- "⚠️ Wave " +
989
- (d.waveIndex + 1) +
990
- " quality gate: " +
991
- (d.issues || []).join("; ") +
992
- retryNote;
993
- if (box) {
994
- box.appendChild(el);
995
- box.scrollTop = box.scrollHeight;
996
- }
997
- return;
998
- }
999
- // project_launched: new project registered — reload dropdown and auto-select
1000
- if (d.type === "project_launched" && d.project) {
1001
- const newId = d.project.projectId || d.project.id;
1002
- setTimeout(async () => {
1003
- await loadProjects();
1004
- if (newId) autoSelectChatProject(newId);
1005
- const box = document.getElementById("chatMessages");
1006
- if (box) {
1007
- const el = document.createElement("div");
1008
- el.style.cssText =
1009
- "font-size:11px;color:var(--green);padding:2px 8px;margin:2px 0;";
1010
- el.textContent =
1011
- '📁 Project "' +
1012
- (d.project.name || newId) +
1013
- '" registered — selected in chat';
1014
- box.appendChild(el);
1015
- box.scrollTop = box.scrollHeight;
1016
- }
1017
- }, 800);
1018
- return;
1019
- }
1020
- // pipeline_done: all steps complete
1021
- if (d.type === "pipeline_done") {
1022
- const el = document.createElement("div");
1023
- el.style.cssText =
1024
- "font-size:11px;color:var(--green);padding:2px 8px;margin:2px 0;";
1025
- el.textContent = "✅ Pipeline complete";
1026
- if (box) {
1027
- box.appendChild(el);
1028
- box.scrollTop = box.scrollHeight;
1029
- }
1030
- return;
1031
- }
1032
- // confirm_run_cmd: an agent wants to run a shell command — show approval toast
1033
- if (d.type === "confirm_run_cmd" && d.approvalId) {
1034
- showCmdApprovalToast(d.approvalId, d.agent, d.cmd);
1035
- return;
1036
- }
1037
- // telemetry: task.lifecycle (schema 1.1) — keep list and refresh Task lifecycle panel if visible
1038
- if (d.type === "telemetry" && d.payload) {
1039
- window._telemetryEvents = window._telemetryEvents || [];
1040
- window._telemetryEvents.push(d.payload);
1041
- if (window._telemetryEvents.length > 100)
1042
- window._telemetryEvents.shift();
1043
- const tlView = document.getElementById("toolMatrixView");
1044
- if (tlView && tlView.classList.contains("active"))
1045
- renderTaskLifecycle(window._telemetryEvents);
1046
- }
1047
- } catch {}
1048
- };
1049
- agentReplySSE.onopen = () => {
1050
- console.log("[crewswarm] SSE connection opened");
1051
- window._sseReconnectDelay = 2000;
1052
- };
1053
- agentReplySSE.onerror = (err) => {
1054
- console.error("[crewswarm] SSE error:", err);
1055
- agentReplySSE.close();
1056
- agentReplySSE = null;
1057
- // Reconnect with exponential backoff (2s → 4s → 8s → 30s max)
1058
- if (window._sseReconnectTimer) clearTimeout(window._sseReconnectTimer);
1059
- window._sseReconnectTimer = setTimeout(() => {
1060
- window._sseReconnectTimer = null;
1061
- window._sseReconnectDelay = Math.min(
1062
- (window._sseReconnectDelay || 2000) * 2,
1063
- 30000,
1064
- );
1065
- startAgentReplyListener();
1066
- }, window._sseReconnectDelay || 2000);
1067
- };
1068
- }
1069
-
1070
- // ── Command approval toast ────────────────────────────────────────────────────
1071
-
1072
- function showCmdApprovalToast(approvalId, agent, cmd) {
1073
- const existing = document.getElementById("cmd-approval-" + approvalId);
1074
- if (existing) return;
1075
-
1076
- const toast = document.createElement("div");
1077
- toast.id = "cmd-approval-" + approvalId;
1078
- toast.style.cssText = [
1079
- "position:fixed;bottom:80px;right:24px;z-index:9999;",
1080
- "background:var(--bg-card);border:1px solid var(--border);border-radius:12px;",
1081
- "padding:16px 20px;max-width:440px;box-shadow:0 8px 32px rgba(0,0,0,.4);",
1082
- "display:flex;flex-direction:column;gap:10px;",
1083
- ].join("");
1084
-
1085
- const header = document.createElement("div");
1086
- header.style.cssText = "font-size:13px;font-weight:600;color:var(--text-1);";
1087
- header.textContent = "🔐 " + agent + " wants to run a command";
1088
-
1089
- const cmdEl = document.createElement("code");
1090
- cmdEl.style.cssText =
1091
- "display:block;font-size:12px;color:var(--accent);background:var(--bg-1);padding:6px 10px;border-radius:6px;word-break:break-all;";
1092
- cmdEl.textContent = cmd;
1093
-
1094
- // "Always allow" toggle — infers pattern from first word of command
1095
- const alwaysRow = document.createElement("label");
1096
- alwaysRow.style.cssText =
1097
- "display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text-2);cursor:pointer;";
1098
- const alwaysChk = document.createElement("input");
1099
- alwaysChk.type = "checkbox";
1100
- alwaysChk.style.cssText =
1101
- "width:14px;height:14px;cursor:pointer;accent-color:var(--green);";
1102
- const cmdBase = cmd.trim().split(/\s+/)[0];
1103
- const suggestedPattern = cmdBase + " *";
1104
- alwaysRow.appendChild(alwaysChk);
1105
- alwaysRow.appendChild(document.createTextNode("Always allow "));
1106
- const patternSpan = document.createElement("code");
1107
- patternSpan.style.cssText =
1108
- "font-size:11px;background:var(--bg-1);padding:2px 6px;border-radius:4px;color:var(--accent);";
1109
- patternSpan.textContent = suggestedPattern;
1110
- alwaysRow.appendChild(patternSpan);
1111
-
1112
- const timer = document.createElement("div");
1113
- timer.style.cssText = "font-size:11px;color:var(--text-3);";
1114
- let secs = 60;
1115
- timer.textContent = "Auto-reject in " + secs + "s";
1116
- const countdown = setInterval(() => {
1117
- secs--;
1118
- timer.textContent = "Auto-reject in " + secs + "s";
1119
- if (secs <= 0) {
1120
- clearInterval(countdown);
1121
- toast.remove();
1122
- }
1123
- }, 1000);
1124
-
1125
- const btns = document.createElement("div");
1126
- btns.style.cssText = "display:flex;gap:8px;";
1127
-
1128
- const approve = document.createElement("button");
1129
- approve.textContent = "✅ Allow";
1130
- approve.style.cssText =
1131
- "flex:1;padding:8px;border-radius:8px;border:none;background:var(--green);color:#fff;cursor:pointer;font-weight:600;font-size:13px;";
1132
- approve.onclick = async () => {
1133
- clearInterval(countdown);
1134
- toast.remove();
1135
- if (alwaysChk.checked) {
1136
- await fetch("/api/cmd-allowlist", {
1137
- method: "POST",
1138
- headers: { "content-type": "application/json" },
1139
- body: JSON.stringify({ pattern: suggestedPattern }),
1140
- });
1141
- showNotification("Allowlisted: " + suggestedPattern);
1142
- }
1143
- await fetch("/api/cmd-approve", {
1144
- method: "POST",
1145
- headers: { "content-type": "application/json" },
1146
- body: JSON.stringify({ approvalId }),
1147
- }).catch((e) => showNotification("Approve failed: " + e.message, true));
1148
- if (!alwaysChk.checked) showNotification(agent + ": command approved");
1149
- };
1150
-
1151
- const reject = document.createElement("button");
1152
- reject.textContent = "⛔ Deny";
1153
- reject.style.cssText =
1154
- "flex:1;padding:8px;border-radius:8px;border:none;background:var(--red-hi);color:#fff;cursor:pointer;font-weight:600;font-size:13px;";
1155
- reject.onclick = async () => {
1156
- clearInterval(countdown);
1157
- toast.remove();
1158
- await fetch("/api/cmd-reject", {
1159
- method: "POST",
1160
- headers: { "content-type": "application/json" },
1161
- body: JSON.stringify({ approvalId }),
1162
- }).catch((e) => showNotification("Reject failed: " + e.message, true));
1163
- showNotification(agent + ": command denied");
1164
- };
1165
-
1166
- btns.appendChild(approve);
1167
- btns.appendChild(reject);
1168
- toast.appendChild(header);
1169
- toast.appendChild(cmdEl);
1170
- toast.appendChild(alwaysRow);
1171
- toast.appendChild(timer);
1172
- toast.appendChild(btns);
1173
- document.body.appendChild(toast);
1174
- }
1175
-
1176
- // ── Cmd allowlist manager ──────────────────────────────────────────────────────
1177
-
1178
- const CMD_PRESETS = [
1179
- { label: "npm", pattern: "npm *", desc: "install, run, build, test…" },
1180
- { label: "node", pattern: "node *", desc: "run any node script" },
1181
- { label: "python", pattern: "python *", desc: "python / python3 scripts" },
1182
- { label: "pip", pattern: "pip *", desc: "pip install packages" },
1183
- { label: "git", pattern: "git *", desc: "all git operations" },
1184
- { label: "cursor", pattern: "cursor *", desc: "open files in Cursor" },
1185
- { label: "make", pattern: "make *", desc: "Makefile targets" },
1186
- { label: "yarn", pattern: "yarn *", desc: "yarn install / build / run" },
1187
- { label: "pnpm", pattern: "pnpm *", desc: "pnpm package manager" },
1188
- {
1189
- label: "ls / cat / echo",
1190
- pattern: "ls *",
1191
- desc: "read-only shell utilities",
1192
- },
1193
- ];
1194
-
1195
- async function loadCmdAllowlist() {
1196
- const box = document.getElementById("cmdAllowlistItems");
1197
- const presetsBox = document.getElementById("cmdPresets");
1198
- if (!box) return;
1199
-
1200
- const d = await getJSON("/api/cmd-allowlist").catch(() => ({ list: [] }));
1201
- const list = d.list || [];
1202
-
1203
- // Render presets checklist (only when the presets container exists — Settings view)
1204
- if (presetsBox) {
1205
- presetsBox.innerHTML = "";
1206
- CMD_PRESETS.forEach(function (preset) {
1207
- const checked = list.includes(preset.pattern);
1208
- const row = document.createElement("label");
1209
- row.style.cssText =
1210
- "display:flex;align-items:center;gap:8px;cursor:pointer;padding:4px 6px;border-radius:6px;transition:background 0.1s;";
1211
- row.onmouseover = function () {
1212
- row.style.background = "var(--bg-hover)";
1213
- };
1214
- row.onmouseout = function () {
1215
- row.style.background = "";
1216
- };
1217
-
1218
- const chk = document.createElement("input");
1219
- chk.type = "checkbox";
1220
- chk.checked = checked;
1221
- chk.style.cssText =
1222
- "width:14px;height:14px;cursor:pointer;accent-color:var(--green);flex-shrink:0;";
1223
- chk.onchange = async function () {
1224
- if (chk.checked) {
1225
- await fetch("/api/cmd-allowlist", {
1226
- method: "POST",
1227
- headers: { "content-type": "application/json" },
1228
- body: JSON.stringify({ pattern: preset.pattern }),
1229
- }).catch((e) =>
1230
- showNotification("Failed to add pattern: " + e.message, true),
1231
- );
1232
- } else {
1233
- await fetch("/api/cmd-allowlist", {
1234
- method: "DELETE",
1235
- headers: { "content-type": "application/json" },
1236
- body: JSON.stringify({ pattern: preset.pattern }),
1237
- }).catch((e) =>
1238
- showNotification("Failed to remove pattern: " + e.message, true),
1239
- );
1240
- }
1241
- loadCmdAllowlist();
1242
- };
1243
-
1244
- const nameEl = document.createElement("code");
1245
- nameEl.style.cssText =
1246
- "font-size:12px;color:var(--accent);min-width:90px;";
1247
- nameEl.textContent = preset.pattern;
1248
-
1249
- const descEl = document.createElement("span");
1250
- descEl.style.cssText = "font-size:11px;color:var(--text-3);";
1251
- descEl.textContent = preset.desc;
1252
-
1253
- row.appendChild(chk);
1254
- row.appendChild(nameEl);
1255
- row.appendChild(descEl);
1256
- presetsBox.appendChild(row);
1257
- });
1258
- }
1259
-
1260
- // Render active list (non-preset patterns only, or all if no presets box)
1261
- const presetPatterns = new Set(
1262
- CMD_PRESETS.map(function (p) {
1263
- return p.pattern;
1264
- }),
1265
- );
1266
- const customPatterns = presetsBox
1267
- ? list.filter(function (p) {
1268
- return !presetPatterns.has(p);
1269
- })
1270
- : list;
1271
-
1272
- box.innerHTML = "";
1273
- if (!customPatterns.length) {
1274
- box.innerHTML =
1275
- '<div style="color:var(--text-3);font-size:12px;padding:4px 0;">' +
1276
- (presetsBox ? "No custom patterns yet." : "No patterns yet.") +
1277
- "</div>";
1278
- return;
1279
- }
1280
- for (const pattern of customPatterns) {
1281
- const row = document.createElement("div");
1282
- row.style.cssText =
1283
- "display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid var(--border);";
1284
- const code = document.createElement("code");
1285
- code.style.cssText = "flex:1;font-size:12px;color:var(--accent);";
1286
- code.textContent = pattern;
1287
- const del = document.createElement("button");
1288
- del.textContent = "✕";
1289
- del.style.cssText =
1290
- "border:none;background:transparent;color:var(--text-3);cursor:pointer;font-size:14px;padding:0 4px;";
1291
- del.title = "Remove";
1292
- del.onclick = async function () {
1293
- await fetch("/api/cmd-allowlist", {
1294
- method: "DELETE",
1295
- headers: { "content-type": "application/json" },
1296
- body: JSON.stringify({ pattern }),
1297
- }).catch((e) =>
1298
- showNotification("Failed to delete pattern: " + e.message, true),
1299
- );
1300
- loadCmdAllowlist();
1301
- };
1302
- row.appendChild(code);
1303
- row.appendChild(del);
1304
- box.appendChild(row);
1305
- }
1306
- }
1307
-
1308
- async function addAllowlistPattern() {
1309
- const inp = document.getElementById("cmdAllowlistInput");
1310
- const pattern = inp ? inp.value.trim() : "";
1311
- if (!pattern) return;
1312
- await fetch("/api/cmd-allowlist", {
1313
- method: "POST",
1314
- headers: { "content-type": "application/json" },
1315
- body: JSON.stringify({ pattern }),
1316
- }).catch((e) =>
1317
- showNotification("Failed to add pattern: " + e.message, true),
1318
- );
1319
- inp.value = "";
1320
- loadCmdAllowlist();
1321
- }
1322
-
1323
- // Token usage + Tool Matrix → tabs/usage-tab.js
1324
- window._telemetryEvents = window._telemetryEvents || [];
1325
-
1326
- const loadOcStats = () => loadOcStatsFromUsage(reportOcCost);
1327
-
1328
- function appendRoadmapCard(box, { draftId, name, outputDir, roadmapMd }) {
1329
- function countTasks(md) {
1330
- return (md.match(/^- \[ \]/gm) || []).length;
1331
- }
1332
-
1333
- const wrap = document.createElement("div");
1334
- wrap.setAttribute("data-draft-id", draftId);
1335
- wrap.style.cssText = "width:100%;display:flex;flex-direction:column;gap:4px;";
1336
-
1337
- const lbl = document.createElement("div");
1338
- lbl.style.cssText = "font-size:11px;color:var(--text-3);padding:0 6px;";
1339
- lbl.textContent = "🗺️ Roadmap draft — review before building";
1340
-
1341
- const card = document.createElement("div");
1342
- card.style.cssText =
1343
- "width:100%;border:1px solid var(--border);border-radius:12px;overflow:hidden;background:var(--bg-card);";
1344
-
1345
- const header = document.createElement("div");
1346
- header.style.cssText =
1347
- "background:var(--bg-card2);padding:10px 14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);";
1348
- header.innerHTML =
1349
- '<div><div style="font-size:13px;font-weight:600;color:var(--accent);">🚀 ' +
1350
- name +
1351
- '</div><div style="font-size:11px;color:var(--blue);margin-top:2px;">' +
1352
- outputDir +
1353
- "</div></div>" +
1354
- '<span style="font-size:10px;color:var(--text-3);padding:2px 7px;background:var(--bg-card2);border-radius:10px;" class="task-count">' +
1355
- countTasks(roadmapMd) +
1356
- " tasks</span>";
1357
-
1358
- const ta = document.createElement("textarea");
1359
- ta.value = roadmapMd;
1360
- ta.spellcheck = false;
1361
- ta.style.cssText =
1362
- "width:100%;background:var(--bg-card);border:none;outline:none;color:var(--text-1);font-size:11.5px;font-family:SF Mono,Monaco,Menlo,monospace;line-height:1.6;padding:12px 14px;resize:none;min-height:160px;max-height:320px;display:block;";
1363
- setTimeout(() => {
1364
- ta.style.height = "";
1365
- ta.style.height = Math.min(ta.scrollHeight, 320) + "px";
1366
- }, 50);
1367
- ta.addEventListener("input", () => {
1368
- ta.style.height = "";
1369
- ta.style.height = Math.min(ta.scrollHeight, 320) + "px";
1370
- header.querySelector(".task-count").textContent =
1371
- countTasks(ta.value) + " tasks";
1372
- });
1373
-
1374
- const actions = document.createElement("div");
1375
- actions.style.cssText =
1376
- "display:flex;gap:8px;align-items:center;padding:10px 14px 12px;border-top:1px solid var(--border);background:var(--bg-card2);";
1377
-
1378
- const startBtn = document.createElement("button");
1379
- startBtn.textContent = "▶ Start Building";
1380
- startBtn.style.cssText =
1381
- "background:var(--green-hi);color:#000;border:none;border-radius:8px;padding:8px 16px;font-size:12px;font-weight:700;cursor:pointer;";
1382
- startBtn.onclick = async () => {
1383
- startBtn.disabled = true;
1384
- startBtn.textContent = "⏳ Launching…";
1385
- try {
1386
- const r = await postJSON("/api/crew-lead/confirm-project", {
1387
- draftId,
1388
- roadmapMd: ta.value,
1389
- });
1390
- if (r.ok) {
1391
- card.innerHTML =
1392
- '<div style="padding:14px;color:var(--green-hi);font-size:13px;font-weight:600;">✅ ' +
1393
- name +
1394
- ' — project created, PM loop running!<br><span style="color:var(--blue);font-size:11px;font-weight:400">' +
1395
- (r.outputDir || outputDir) +
1396
- "</span></div>";
1397
- appendChatBubble(
1398
- "assistant",
1399
- "🚀 " +
1400
- name +
1401
- " is building. Check the Projects tab to watch progress.",
1402
- );
1403
- } else {
1404
- startBtn.disabled = false;
1405
- startBtn.textContent = "▶ Start Building";
1406
- status.textContent = "⚠️ " + (r.error || "Launch failed");
1407
- }
1408
- } catch (e) {
1409
- startBtn.disabled = false;
1410
- startBtn.textContent = "▶ Start Building";
1411
- status.textContent = "⚠️ " + e.message;
1412
- }
1413
- };
1414
-
1415
- const discardBtn = document.createElement("button");
1416
- discardBtn.textContent = "Discard";
1417
- discardBtn.style.cssText =
1418
- "background:none;border:1px solid var(--border);color:var(--text-3);border-radius:8px;padding:8px 14px;font-size:12px;cursor:pointer;";
1419
- discardBtn.onclick = async () => {
1420
- await postJSON("/api/crew-lead/discard-project", { draftId }).catch(
1421
- () => {},
1422
- );
1423
- wrap.remove();
1424
- };
1425
-
1426
- const status = document.createElement("span");
1427
- status.style.cssText = "font-size:11px;color:var(--blue);margin-left:auto;";
1428
- status.textContent = "Edit above, then confirm";
1429
-
1430
- actions.appendChild(startBtn);
1431
- actions.appendChild(discardBtn);
1432
- actions.appendChild(status);
1433
- card.appendChild(header);
1434
- card.appendChild(ta);
1435
- card.appendChild(actions);
1436
- wrap.appendChild(lbl);
1437
- wrap.appendChild(card);
1438
- box.appendChild(wrap);
1439
- box.scrollTop = box.scrollHeight;
1440
- }
1441
-
1442
- let lastAppendedAssistantContent = "";
1443
- let lastAppendedUserContent = "";
1444
- let lastSentContent = null;
1445
- const {
1446
- loadChatHistory,
1447
- waitForChatHistoryIdle,
1448
- chatAtAtInput,
1449
- chatKeydown,
1450
- sendChat,
1451
- clearChatHistory,
1452
- restorePassthroughLog,
1453
- sendPassthrough,
1454
- stopAll,
1455
- killAll,
1456
- killPassthrough,
1457
- refreshSessionIndicator,
1458
- clearPassthroughSession,
1459
- resetSendButton,
1460
- handleImageUpload,
1461
- toggleVoiceRecording,
1462
- } = initChatActions({
1463
- postJSON,
1464
- getJSON,
1465
- appendChatBubble,
1466
- showNotification,
1467
- state,
1468
- getChatSessionId: () => getChatSessionId(),
1469
- getChatActiveProjectId: () => state.chatActiveProjectId,
1470
- getCrewLeadInfo: () => window._crewLeadInfo,
1471
- appendRoadmapCard,
1472
- getLastAppendedAssistantContent: () => lastAppendedAssistantContent,
1473
- setLastAppendedAssistantContent: (value) => {
1474
- lastAppendedAssistantContent = value;
1475
- },
1476
- setLastAppendedUserContent: (value) => {
1477
- lastAppendedUserContent = value;
1478
- },
1479
- setLastSentContent: (value) => {
1480
- lastSentContent = value;
1481
- },
1482
- });
1483
-
1484
- // Wire up multimodal buttons
1485
- document.getElementById("attachImageBtn")?.addEventListener("click", () => {
1486
- document.getElementById("imageUpload").click();
1487
- });
1488
- document
1489
- .getElementById("imageUpload")
1490
- ?.addEventListener("change", handleImageUpload);
1491
- document
1492
- .getElementById("recordVoiceBtn")
1493
- ?.addEventListener("click", toggleVoiceRecording);
1494
-
1495
- // Expose loadChatHistory globally for project tab switching
1496
- window.loadChatHistory = loadChatHistory;
1497
-
1498
- // Expose getChatSessionId for testing and debugging
1499
- window.getChatSessionId = getChatSessionId;
1500
-
1501
- // Expose selectProjectTab for General tab onclick in HTML
1502
- window.selectProjectTab = (projectId) => {
1503
- // Normalize: missing or invalid id => general (avoids #chat?project=undefined)
1504
- const normalizedId =
1505
- projectId && String(projectId).trim() && projectId !== "undefined"
1506
- ? projectId
1507
- : "general";
1508
- const currentProjectId = state.chatActiveProjectId;
1509
- console.log(
1510
- "🔵 [TAB CLICK] START",
1511
- normalizedId,
1512
- "- from:",
1513
- currentProjectId,
1514
- );
1515
-
1516
- const tabsContainer = document.getElementById("chatProjectTabs");
1517
- if (!tabsContainer) {
1518
- console.error("🔵 [TAB CLICK] ERROR: chatProjectTabs container not found!");
1519
- return;
1520
- }
1521
-
1522
- if (currentProjectId === normalizedId) {
1523
- console.log("🔵 [TAB CLICK] Already on this tab, skipping reload");
1524
- return;
1525
- }
1526
-
1527
- setChatHashProject(normalizedId);
1528
-
1529
- // Update UI: deactivate all, activate selected
1530
- Array.from(tabsContainer.children).forEach((tab) => {
1531
- tab.classList.remove("active");
1532
- });
1533
-
1534
- const selectedTab = Array.from(tabsContainer.children).find(
1535
- (tab) => tab.dataset.projectId === normalizedId,
1536
- );
1537
- if (selectedTab) {
1538
- selectedTab.classList.add("active");
1539
- }
1540
-
1541
- state.chatActiveProjectId = normalizedId;
1542
- try {
1543
- localStorage.setItem("crewswarm_chat_active_project_id", normalizedId);
1544
- } catch {}
1545
- persistSharedActiveProjectId(normalizedId);
1546
-
1547
- console.log("🔵 [TAB CLICK] Updated state:", {
1548
- projectId: state.chatActiveProjectId,
1549
- sessionId: getChatSessionId(),
1550
- url: window.location.hash,
1551
- });
1552
-
1553
- console.log("🔵 [TAB CLICK] Calling loadChatHistory()...");
1554
-
1555
- // Reload history
1556
- loadChatHistory()
1557
- .then(() => {
1558
- console.log("🔵 [TAB CLICK] loadChatHistory() completed");
1559
- const box = document.getElementById("chatMessages");
1560
- console.log("🔵 [TAB CLICK] Messages in DOM:", box?.children.length || 0);
1561
- })
1562
- .catch((err) => {
1563
- console.error("🔵 [TAB CLICK] loadChatHistory() ERROR:", err);
1564
- });
1565
- };
1566
-
1567
- window.addEventListener("focus", () => {
1568
- syncChatProjectFromSharedState().catch(() => {});
1569
- });
1570
-
1571
- /* services tab extracted to tabs/services-tab.js */
1572
- async function loadFiles(forceRefresh) {
1573
- const el = document.getElementById("filesContent");
1574
- const dir =
1575
- document.getElementById("filesDir").value.trim() ||
1576
- window._crewCwd ||
1577
- (window._crewHome ? window._crewHome + "/CrewSwarm" : "");
1578
- showLoading(el, "Scanning " + dir + "...");
1579
- try {
1580
- const data = await getJSON("/api/files?dir=" + encodeURIComponent(dir));
1581
- if (!data.files || !data.files.length) {
1582
- showEmpty(el, "No files found in " + dir);
1583
- return;
1584
- }
1585
- const grouped = {};
1586
- data.files.forEach((f) => {
1587
- const ext = f.path.split(".").pop().toLowerCase() || "other";
1588
- if (!grouped[ext]) grouped[ext] = [];
1589
- grouped[ext].push(f);
1590
- });
1591
- const extOrder = [
1592
- "html",
1593
- "css",
1594
- "js",
1595
- "mjs",
1596
- "ts",
1597
- "json",
1598
- "md",
1599
- "sh",
1600
- "txt",
1601
- "other",
1602
- ];
1603
- const extEmoji = {
1604
- html: "🌐",
1605
- css: "🎨",
1606
- js: "⚡",
1607
- mjs: "⚡",
1608
- ts: "🔷",
1609
- json: "📋",
1610
- md: "📝",
1611
- sh: "🖥️",
1612
- txt: "📄",
1613
- other: "📁",
1614
- };
1615
- let html = '<div style="display:grid;gap:1rem;padding:4px 0;">';
1616
- for (const ext of extOrder) {
1617
- if (!grouped[ext]) continue;
1618
- html += "<div>";
1619
- html +=
1620
- '<div style="font-size:11px;font-weight:600;color:var(--text-2);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:8px;padding-left:2px;">' +
1621
- (extEmoji[ext] || "📁") +
1622
- " ." +
1623
- ext +
1624
- " — " +
1625
- grouped[ext].length +
1626
- " file" +
1627
- (grouped[ext].length > 1 ? "s" : "") +
1628
- "</div>";
1629
- html += '<div style="display:grid;gap:6px;">';
1630
- grouped[ext]
1631
- .sort((a, b) => b.mtime - a.mtime)
1632
- .forEach((f) => {
1633
- const rel = f.path.replace(dir + "/", "");
1634
- const age = formatAge(f.mtime);
1635
- const sz = formatSize(f.size);
1636
- html += '<div class="file-row">';
1637
- html +=
1638
- '<div class="file-info"><span class="file-name">' +
1639
- rel +
1640
- '</span><span class="file-meta">' +
1641
- sz +
1642
- " · " +
1643
- age +
1644
- "</span></div>";
1645
- html += '<div class="file-actions">';
1646
- html +=
1647
- '<a href="cursor://file/' +
1648
- f.path +
1649
- '" class="file-btn file-btn-cursor" title="Open in Cursor">Cursor</a>';
1650
- html +=
1651
- '<a href="opencode://open?path=' +
1652
- encodeURIComponent(f.path) +
1653
- '" class="file-btn file-btn-opencode" title="Open in OpenCode">OpenCode</a>';
1654
- html +=
1655
- '<button data-action="previewFile" data-arg=\'' +
1656
- f.path.replace(/'/g, "&#39;") +
1657
- '\' data-self="1" class="file-btn" title="Preview">👁</button>';
1658
- html += "</div></div>";
1659
- });
1660
- html += "</div></div>";
1661
- }
1662
- html += "</div>";
1663
- html +=
1664
- '<div id="file-preview-pane" style="display:none;margin-top:1rem;background:#0d1117;border:1px solid var(--border);border-radius:8px;overflow:hidden;"><div id="file-preview-bar" style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:#0d1420;border-bottom:1px solid var(--border);font-size:12px;color:var(--text-2);"><span id="file-preview-name"></span><button data-action="closePreviewPane" style="margin-left:auto;background:none;border:none;color:var(--text-2);cursor:pointer;">✕</button></div><pre id="file-preview-content" style="margin:0;padding:1rem;font-size:0.75rem;overflow:auto;max-height:400px;"></pre></div>';
1665
- el.innerHTML = html;
1666
- } catch (e) {
1667
- showError(el, "Error: " + e.message);
1668
- }
1669
- }
1670
- async function previewFile(filePath, btn) {
1671
- const pane = document.getElementById("file-preview-pane");
1672
- const content = document.getElementById("file-preview-content");
1673
- const name = document.getElementById("file-preview-name");
1674
- if (!pane) return;
1675
- name.textContent = filePath.split("/").pop();
1676
- content.textContent = "Loading...";
1677
- pane.style.display = "block";
1678
- pane.scrollIntoView({ behavior: "smooth", block: "nearest" });
1679
- try {
1680
- const data = await getJSON(
1681
- "/api/file-content?path=" + encodeURIComponent(filePath),
1682
- );
1683
- content.textContent = data.content || "(empty)";
1684
- } catch (e) {
1685
- content.textContent = "Error: " + e.message;
1686
- }
1687
- }
1688
- function closePreviewPane() {
1689
- const pane = document.getElementById("file-preview-pane");
1690
- if (pane) pane.style.display = "none";
1691
- }
1692
- function formatAge(mtime) {
1693
- const diff = Date.now() - mtime;
1694
- const m = Math.floor(diff / 60000);
1695
- if (m < 1) return "just now";
1696
- if (m < 60) return m + "m ago";
1697
- const h = Math.floor(m / 60);
1698
- if (h < 24) return h + "h ago";
1699
- return Math.floor(h / 24) + "d ago";
1700
- }
1701
- function formatSize(bytes) {
1702
- if (bytes < 1024) return bytes + "B";
1703
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + "KB";
1704
- return (bytes / 1024 / 1024).toFixed(1) + "MB";
1705
- }
1706
- function showSettings() {
1707
- hideAllViews();
1708
- document.getElementById("settingsView").classList.add("active");
1709
- setNavActive("navSettings");
1710
- state.activeTab = "settings";
1711
- persistState();
1712
- // Restore last active sub-tab from hash (e.g. #settings/telegram → telegram)
1713
- const hashSubtab = (location.hash || "").replace("#settings/", "");
1714
- // Support legacy deep-link aliases
1715
- const TAB_ALIASES = {
1716
- system: "engines",
1717
- telegram: "comms",
1718
- whatsapp: "comms",
1719
- };
1720
- const knownTabs = ["usage", "engines", "comms", "security", "webhooks"];
1721
- const resolved = TAB_ALIASES[hashSubtab] || hashSubtab;
1722
- showSettingsTab(knownTabs.includes(resolved) ? resolved : "usage");
1723
- }
1724
- function showSettingsTab(tab) {
1725
- const knownTabs = ["usage", "engines", "comms", "security", "webhooks"];
1726
- knownTabs.forEach((t) => {
1727
- const panel = document.getElementById("stab-panel-" + t);
1728
- const btn = document.getElementById("stab-" + t);
1729
- if (!panel || !btn) return;
1730
- panel.style.display =
1731
- t === tab ? (t === "usage" ? "grid" : "block") : "none";
1732
- btn.classList.toggle("active", t === tab);
1733
- });
1734
- if (tab === "usage") {
1735
- loadTokenUsage();
1736
- loadAllUsage();
1737
- }
1738
- if (tab === "engines") {
1739
- loadOpencodeProject();
1740
- loadBgConsciousness();
1741
- loadGlobalFallback();
1742
- loadConfigLockStatus();
1743
- loadCursorWaves();
1744
- loadTmuxBridge();
1745
- loadAutonomousMentions();
1746
- loadClaudeCode();
1747
- loadCodexExecutor();
1748
- loadGeminiCliExecutor();
1749
- loadCrewCliExecutor();
1750
- loadOpencodeExecutor();
1751
- loadGlobalOcLoop();
1752
- loadLoopBrain();
1753
- loadPassthroughNotify();
1754
- }
1755
- if (tab === "comms") {
1756
- loadCommsTabData();
1757
- }
1758
- if (tab === "security") {
1759
- loadCmdAllowlist();
1760
- loadEnvAdvanced();
1761
- }
1762
- if (tab === "webhooks") {
1763
- /* static */
1764
- }
1765
- // Update URL hash for deep linking — e.g. #settings/telegram
1766
- if (document.getElementById("settingsView")?.classList.contains("active")) {
1767
- history.replaceState(null, "", "#settings/" + tab);
1768
- }
1769
- }
1770
-
1771
- initCommsTab({ showSettings, showSettingsTab });
1772
- initSettingsTab({ getModels: loadAgents_cfg, populateModelDropdown });
1773
- initModelsTab({ hideAllViews, setNavActive, loadAgents: loadAgents_cfg });
1774
- initAddProviderForm();
1775
- initSwarmChatTab({ hideAllViews, setNavActive });
1776
-
1777
- // ── Engines → engines-tab.js ─────────────────────────────────────────────────
1778
- function showEngines() {
1779
- hideAllViews();
1780
- document.getElementById("enginesView").classList.add("active");
1781
- setNavActive("navEngines");
1782
- loadEngines();
1783
- }
1784
-
1785
- // showSkills / showRunSkills → skills-tab.js
1786
-
1787
- const showBenchmarks = async () => {
1788
- const { showBenchmarks: showBenchmarksTab } = await loadBenchmarksTabModule();
1789
- showBenchmarksTab({ hideAllViews, setNavActive });
1790
- };
1791
-
1792
- function showMemoryView() {
1793
- hideAllViews();
1794
- document.getElementById("memoryView").classList.add("active");
1795
- setNavActive("navMemory");
1796
- showMemory();
1797
- }
1798
-
1799
- function showCLIProcess() {
1800
- hideAllViews();
1801
- document.getElementById("cliProcessView").classList.add("active");
1802
- setNavActive("navCLI");
1803
- // Initialize CLI process view
1804
- if (window.initCLIProcess) window.initCLIProcess();
1805
- }
1806
-
1807
- function showToolMatrix() {
1808
- hideAllViews();
1809
- document.getElementById("toolMatrixView").classList.add("active");
1810
- setNavActive("navToolMatrix");
1811
- loadToolMatrix();
1812
- }
1813
-
1814
- // keep old name working for any legacy calls
1815
- function showIntegrations() {
1816
- showSkills();
1817
- }
1818
-
1819
- // loadRunSkills / runSkillFromUI → skills-tab.js
1820
-
1821
- // Tool Matrix → tabs/usage-tab.js
1822
-
1823
- // Spending → tabs/spending-tab.js
1824
-
1825
- // ── Webhooks ──────────────────────────────────────────────────────────────────
1826
- async function sendTestWebhook() {
1827
- const channel =
1828
- document.getElementById("webhookChannel").value.trim() || "test";
1829
- let payload = {};
1830
- try {
1831
- const v = document.getElementById("webhookPayload").value.trim();
1832
- if (v) payload = JSON.parse(v);
1833
- } catch {
1834
- payload = { raw: document.getElementById("webhookPayload").value };
1835
- }
1836
- const el = document.getElementById("webhookTestResult");
1837
- try {
1838
- const res = await fetch("/proxy-webhook/" + channel, {
1839
- method: "POST",
1840
- headers: { "Content-Type": "application/json" },
1841
- body: JSON.stringify(payload),
1842
- });
1843
- const d = await res.json();
1844
- el.textContent = d.ok ? "✅ Sent to RT bus" : "❌ " + (d.error || "failed");
1845
- el.style.color = d.ok ? "var(--green)" : "var(--red)";
1846
- } catch (e) {
1847
- el.textContent = "❌ " + e.message;
1848
- el.style.color = "var(--red)";
1849
- }
1850
- }
1851
-
1852
- // ── Pending Approvals ─────────────────────────────────────────────────────────
1853
- async function loadPendingApprovals() {
1854
- const el = document.getElementById("pendingApprovals");
1855
- // pending-skills.json is at ~/.crewswarm/pending-skills.json — no direct API yet;
1856
- // crew-lead should expose this but for now show instructions.
1857
- el.innerHTML =
1858
- '<div style="color:var(--text-3);font-size:12px;">Pending skill approvals appear here when an agent triggers a skill marked requiresApproval. You will also receive a Telegram notification with inline Approve/Reject buttons if Telegram is configured.</div>';
1859
- }
1860
- async function approveSkill(approvalId) {
1861
- try {
1862
- await fetch("/api/skills/approve", {
1863
- method: "POST",
1864
- headers: { "content-type": "application/json" },
1865
- body: JSON.stringify({ approvalId }),
1866
- });
1867
- showNotification("Approved");
1868
- loadPendingApprovals();
1869
- } catch (e) {
1870
- showNotification("Failed: " + e.message, "error");
1871
- }
1872
- }
1873
- async function rejectSkill(approvalId) {
1874
- try {
1875
- await fetch("/api/skills/reject", {
1876
- method: "POST",
1877
- headers: { "content-type": "application/json" },
1878
- body: JSON.stringify({ approvalId }),
1879
- });
1880
- showNotification("Rejected");
1881
- loadPendingApprovals();
1882
- } catch (e) {
1883
- showNotification("Failed: " + e.message, "error");
1884
- }
1885
- }
1886
-
1887
- /* agents tab extracted to tabs/agents-tab.js */
1888
- function showBuild() {
1889
- _showBuild({ hideAllViews, setNavActive });
1890
- }
1891
- function showProjects() {
1892
- _showProjects({ hideAllViews, setNavActive });
1893
- }
1894
-
1895
- // ── Projects / Build → projects-tab.js ───────────────────────────────────────
1896
- // Wire project list delegated click handler
1897
- initProjectsList({ showChat, showBuild });
1898
-
1899
- // Initial status check
1900
- updateStatusBadges();
1901
-
1902
- // Lightweight status badge updates only (DLQ count + status dot)
1903
- // Content loads on-demand when switching tabs (event-driven, not polling)
1904
- setInterval(updateStatusBadges, 30000); // Every 30s for badge/status only
1905
- // Populate chat project dropdown on load; respect #projects deep link (e.g. from native app)
1906
- (async () => {
1907
- try {
1908
- const data = await getJSON("/api/projects");
1909
- const projects = data.projects || [];
1910
- state.projectsData = {};
1911
- projects.forEach((p) => {
1912
- state.projectsData[p.id] = p;
1913
- });
1914
- populateChatProjectDropdown(projects);
1915
- persistState();
1916
- if (location.hash === "#projects") showProjects();
1917
- } catch {}
1918
- })();
1919
- document.getElementById("refreshBtn").onclick = refreshAll;
1920
- document.getElementById("runBuildBtn").onclick = runBuild;
1921
- document.getElementById("continuousBuildBtn").onclick = continuousBuildRun;
1922
- document.getElementById("stopBuildBtn").onclick = stopBuild;
1923
- document.getElementById("stopContinuousBtn").onclick = stopContinuousBuild;
1924
- document.getElementById("enhancePromptBtn").onclick = enhancePrompt;
1925
- initPmLoopTab();
1926
- document.getElementById("newProjectBtn").onclick = () => {
1927
- const form = document.getElementById("newProjectForm");
1928
- form.style.display = form.style.display === "none" ? "block" : "none";
1929
- };
1930
- document.getElementById("npCancelBtn").onclick = () => {
1931
- document.getElementById("newProjectForm").style.display = "none";
1932
- };
1933
- document.getElementById("npCreateBtn").onclick = async () => {
1934
- const name = document.getElementById("npName").value.trim();
1935
- const desc = document.getElementById("npDesc").value.trim();
1936
- const outputDir = document.getElementById("npOutputDir").value.trim();
1937
- const featuresDoc = document.getElementById("npFeaturesDoc").value.trim();
1938
- if (!name || !outputDir) {
1939
- showNotification("Name and output directory required", true);
1940
- return;
1941
- }
1942
- try {
1943
- const r = await postJSON("/api/projects", {
1944
- name,
1945
- description: desc,
1946
- outputDir,
1947
- featuresDoc,
1948
- });
1949
- showNotification(`Project "${r.project.name}" created!`);
1950
- document.getElementById("newProjectForm").style.display = "none";
1951
- document.getElementById("npName").value = "";
1952
- document.getElementById("npDesc").value = "";
1953
- document.getElementById("npOutputDir").value = "";
1954
- document.getElementById("npFeaturesDoc").value = "";
1955
- loadProjects();
1956
- } catch (e) {
1957
- showNotification("Failed: " + e.message, true);
1958
- }
1959
- };
1960
- // sendBtn / messageInput removed (replaced by crew-lead chat)
1961
-
1962
- // PM Loop controls → tabs/pm-loop-tab.js
1963
- // ── Hash routing — persist active view across refresh ────────────────────────
1964
- // ── Hash routing ─────────────────────────────────────────────────────────────
1965
- // Patch each top-level show* function so calling it (via onclick or code)
1966
- // automatically updates location.hash. Refresh → restores the same tab.
1967
- const VIEW_MAP = {
1968
- chat: showChat,
1969
- "swarm-chat": showSwarmChat,
1970
- swarm: showSwarm,
1971
- rt: showRT,
1972
- dlq: showDLQ,
1973
- files: showFiles,
1974
- services: showServices,
1975
- agents: showAgents,
1976
- models: showModels,
1977
- settings: showSettings,
1978
- engines: showEngines,
1979
- skills: showSkills,
1980
- "run-skills": showRunSkills,
1981
- benchmarks: showBenchmarks,
1982
- "tool-matrix": showToolMatrix,
1983
- build: showBuild,
1984
- messaging: showMessaging,
1985
- projects: showProjects,
1986
- contacts: showContacts,
1987
- memory: showMemoryView,
1988
- workflows: showWorkflows,
1989
- "cli-process": showCLIProcess,
1990
- prompts: initPromptsTab,
1991
- };
1992
-
1993
- // Wrap each show* so it updates the hash when called from anywhere
1994
- for (const [hash, fn] of Object.entries(VIEW_MAP)) {
1995
- const original = fn;
1996
- const wrapped = function (...args) {
1997
- const cur = location.hash || "";
1998
- if (hash === "chat") {
1999
- // Never replaceState to bare #chat when already on #chat?project=… — that
2000
- // fires hashchange in some browsers and doubles showChat + 500-msg reloads.
2001
- if (!cur.startsWith("#chat")) {
2002
- history.replaceState(null, "", "#chat");
2003
- }
2004
- } else {
2005
- history.replaceState(null, "", "#" + hash);
2006
- }
2007
- return original(...args);
2008
- };
2009
- // Update the reference in the map and on window (for onclick= handlers)
2010
- VIEW_MAP[hash] = wrapped;
2011
- window[original.name] = wrapped;
2012
- }
2013
-
2014
- function navigateTo(view) {
2015
- const base = String(view || "chat").split("?")[0].split("/")[0];
2016
- const fn = VIEW_MAP[base] || VIEW_MAP["chat"];
2017
- fn();
2018
- }
2019
-
2020
- // On load: check first-run status, then restore from hash or default to chat
2021
- // Supports top-level (#chat, #services) and sub-tab deep links (#settings/telegram)
2022
- (async () => {
2023
- const wizardShown = await checkFirstRun();
2024
- if (wizardShown) return; // wizard handles its own flow; reload on completion
2025
-
2026
- const { view: startView, subtab: startSubtab } = parseRouteFromHash();
2027
- const params = new URLSearchParams(window.location.search);
2028
- if (params.get("focus") === "1") {
2029
- setTimeout(() => {
2030
- const ci = document.getElementById("chatInput");
2031
- if (ci) {
2032
- navigateTo("chat");
2033
- ci.focus();
2034
- }
2035
- }, 500);
2036
- } else {
2037
- navigateTo(startView || "chat");
2038
- if (startView === "settings" && startSubtab) {
2039
- showSettingsTab(startSubtab);
2040
- }
2041
- }
2042
- })();
2043
-
2044
- // Handle browser back/forward buttons
2045
- window.addEventListener("hashchange", () => {
2046
- const { view, subtab } = parseRouteFromHash();
2047
-
2048
- // Navigate to the view from hash
2049
- const viewFn = NAV_VIEW_MAP[view];
2050
- if (viewFn) {
2051
- viewFn();
2052
- // If it's a settings subtab, show that too
2053
- if (view === "settings" && subtab) {
2054
- showSettingsTab(subtab);
2055
- }
2056
- } else {
2057
- // Fallback to chat if invalid hash
2058
- showChat();
2059
- }
2060
- });
2061
- // Resolve server-side env vars (HOME, cwd) once on boot
2062
- fetch("/api/env")
2063
- .then((r) => r.json())
2064
- .then((env) => {
2065
- window._crewHome = env.HOME || "";
2066
- window._crewCwd = env.cwd || "";
2067
- const filesDir = document.getElementById("filesDir");
2068
- if (filesDir && !filesDir.value) filesDir.value = env.cwd || "";
2069
- })
2070
- .catch(() => {});
2071
-
2072
- loadAgents_cfg().catch((e) => console.error("Initial agents-config load failed:", e));
2073
- refreshAll();
2074
-
2075
- // Wrap every type="password" input in a <form display:contents> so Chrome
2076
- // stops emitting "Password field is not contained in a form" warnings.
2077
- // Works for both static inputs and dynamically rendered provider key fields.
2078
- (function () {
2079
- function wrapOrphanPwd(inp) {
2080
- if (inp.closest("form")) return;
2081
- const form = document.createElement("form");
2082
- form.autocomplete = "off";
2083
- form.onsubmit = () => false;
2084
- form.style.cssText = "margin:0;padding:0;display:contents;";
2085
- // Hidden username field — satisfies Chrome's "password forms need a username" check
2086
- const u = document.createElement("input");
2087
- u.type = "text";
2088
- u.autocomplete = "username";
2089
- u.setAttribute("aria-hidden", "true");
2090
- u.style.cssText =
2091
- "display:none;position:absolute;width:0;height:0;opacity:0;";
2092
- form.appendChild(u);
2093
- inp.parentNode.insertBefore(form, inp);
2094
- form.appendChild(inp);
2095
- }
2096
- function scanAndWrap(root) {
2097
- (root || document)
2098
- .querySelectorAll('input[type="password"]')
2099
- .forEach(wrapOrphanPwd);
2100
- }
2101
- scanAndWrap();
2102
- const obs = new MutationObserver((mutations) => {
2103
- for (const m of mutations) {
2104
- for (const node of m.addedNodes) {
2105
- if (node.nodeType !== 1) continue;
2106
- if (node.matches && node.matches('input[type="password"]'))
2107
- wrapOrphanPwd(node);
2108
- else scanAndWrap(node);
2109
- }
2110
- }
2111
- });
2112
- obs.observe(document.body, { childList: true, subtree: true });
2113
- })();
2114
-
2115
- // ── Expose functions to global scope for inline HTML event handlers ───────────
2116
- // ── Global delegated click dispatcher ──────────────────────────────────────────
2117
- // MetaMask's SES lockdown runs onclick handlers in an isolated Compartment where
2118
- // neither globalThis.fn nor window.fn resolves. Using data-action + addEventListener
2119
- // bypasses the Compartment entirely — the listener closure has full module scope.
2120
- const ACTION_REGISTRY = {
2121
- // Nav views
2122
- showChat,
2123
- showSwarm,
2124
- showRT,
2125
- showBuild,
2126
- showFiles,
2127
- showDLQ,
2128
- showProjects,
2129
- showAgents,
2130
- showModels,
2131
- showEngines,
2132
- showSkills,
2133
- showRunSkills,
2134
- showBenchmarks,
2135
- showToolMatrix,
2136
- showServices,
2137
- showSettings,
2138
- // Static HTML actions (previously onclick="window.fn()")
2139
- pickFolder: (id) => pickFolder(id),
2140
- loadFiles: (force) => loadFiles(force === "true" || force === true),
2141
- clearChatHistory,
2142
- clearAgentChat: () => {
2143
- const agentSelect = document.getElementById("agentChatSelector");
2144
- const messages = document.getElementById("agentChatMessages");
2145
- const input = document.getElementById("agentChatInput");
2146
- if (messages)
2147
- messages.innerHTML =
2148
- '<div class="empty-state">No messages yet. Start chatting!</div>';
2149
- if (input) input.value = "";
2150
- if (agentSelect?.value) {
2151
- // Clears visible chat; backend history retained in session files
2152
- showNotification("Chat history cleared", "success");
2153
- }
2154
- },
2155
- sendChat,
2156
- stopAll,
2157
- killAll,
2158
- stopPassthrough: killPassthrough,
2159
- clearPassthroughSession,
2160
- loadServices,
2161
- saveRTToken,
2162
- lockConfig,
2163
- unlockConfig,
2164
- startCrew,
2165
- toggleEmojiPicker: (id) => toggleEmojiPicker(id),
2166
- bulkSetRoute: (route, model) => bulkSetRoute(route, model),
2167
- loadSpending,
2168
- resetSpending,
2169
- saveGlobalCaps,
2170
- loadOcStats,
2171
- addAllowlistPattern,
2172
- sendTestWebhook,
2173
- startTgBridge,
2174
- stopTgBridge,
2175
- saveTgConfig,
2176
- loadTelegramSessions,
2177
- loadTgMessages,
2178
- startWaBridge,
2179
- stopWaBridge,
2180
- saveWaConfig,
2181
- loadWaMessages,
2182
- saveOpencodeSettings,
2183
- saveOpencodeModel,
2184
- saveGlobalFallback,
2185
- toggleBgConsciousness,
2186
- toggleCursorWaves,
2187
- toggleTmuxBridge,
2188
- toggleAutonomousMentions,
2189
- toggleClaudeCode,
2190
- toggleCodexExecutor,
2191
- toggleGeminiCliExecutor,
2192
- toggleCrewCliExecutor,
2193
- toggleOpencodeExecutor,
2194
- saveGlobalOcLoop,
2195
- saveGlobalOcLoopRounds,
2196
- savePassthroughNotify,
2197
- toggleAddSkill,
2198
- toggleImportSkill,
2199
- importSkillFromUrl,
2200
- showSkills,
2201
- saveSkill,
2202
- cancelSkillForm,
2203
- loadRunSkills,
2204
- loadBenchmarks: async () => {
2205
- const mod = await loadBenchmarksTabModule();
2206
- return mod.loadBenchmarks();
2207
- },
2208
- loadBenchmarkLeaderboard: async () => {
2209
- const mod = await loadBenchmarksTabModule();
2210
- return mod.loadBenchmarkLeaderboard();
2211
- },
2212
- loadBenchmarkTasks: async () => {
2213
- const mod = await loadBenchmarksTabModule();
2214
- return mod.loadBenchmarkTasks();
2215
- },
2216
- onBenchmarkTaskSelect: async (taskId) => {
2217
- const mod = await loadBenchmarksTabModule();
2218
- return mod.onBenchmarkTaskSelect(taskId);
2219
- },
2220
- runBenchmarkTask: async () => {
2221
- const mod = await loadBenchmarksTabModule();
2222
- return mod.runBenchmarkTask();
2223
- },
2224
- stopBenchmarkRun: async () => {
2225
- const mod = await loadBenchmarksTabModule();
2226
- return mod.stopBenchmarkRun();
2227
- },
2228
- // Memory
2229
- loadMemoryStats,
2230
- searchMemory,
2231
- migrateMemory,
2232
- compactMemory,
2233
- loadEngines,
2234
- toggleImportEngine,
2235
- importEngineFromUrl,
2236
- deleteEngine: (id) => deleteEngine(id),
2237
- loadToolMatrix,
2238
- loadBuildProjectPicker,
2239
- // RT scroll button
2240
- scrollRTToBottom: () => {
2241
- const v = document.getElementById("rtView");
2242
- if (v) v.scrollTop = v.scrollHeight;
2243
- },
2244
- toggleRTPause,
2245
- clearRTMessages,
2246
- togglePmAdvanced: () => {
2247
- const el = document.getElementById("pmAdvanced");
2248
- if (el) el.style.display = el.style.display === "none" ? "block" : "none";
2249
- },
2250
- // RT token visibility toggle
2251
- toggleRTTokenVis: () => {
2252
- const i = document.getElementById("rtTokenInput");
2253
- if (i) i.type = i.type === "password" ? "text" : "password";
2254
- },
2255
- // Services
2256
- restartService: (id) => restartService(id),
2257
- stopService: (id) => stopService(id),
2258
- // Files
2259
- closePreviewPane,
2260
- previewFile: (path, el) => previewFile(path, el),
2261
- // DLQ
2262
- replayDLQ: (key) => replayDLQ(key),
2263
- deleteDLQ: (key) => deleteDLQ(key),
2264
- // Skills
2265
- runSkillFromUI: (name) => runSkillFromUI(name),
2266
- editSkill: (name) => editSkill(name),
2267
- deleteSkill: (name) => deleteSkill(name),
2268
- // Tool matrix
2269
- restartAgentFromUI: (id) => restartAgentFromUI(id),
2270
- // Models / providers
2271
- saveSearchTool: (id) => saveSearchTool(id),
2272
- testSearchTool: (id) => testSearchTool(id),
2273
- saveBuiltinKey: (id) => saveBuiltinKey(id),
2274
- testBuiltinProvider: (id) => testBuiltinProvider(id),
2275
- fetchBuiltinModels: (id, el) => fetchBuiltinModels(id, el),
2276
- saveKey: (id) => saveKey(id),
2277
- testKey: (id) => testKey(id),
2278
- fetchModels: (id, el) => fetchModels(id, el),
2279
- toggleKeyVis: (inputId, el) => toggleKeyVis(inputId, el),
2280
- // Agents
2281
- toggleAgentBody: (id) => toggleAgentBody(id),
2282
- deleteAgent: (id) => deleteAgent(id),
2283
- saveAgentModel: (id) => saveAgentModel(id),
2284
- saveAgentFallback: (id) => saveAgentFallback(id),
2285
- saveAgentVoice: (id) => saveAgentVoice(id),
2286
- toggleEmojiPicker: (id) => toggleEmojiPicker(id),
2287
- saveAgentIdentity: (id) => saveAgentIdentity(id),
2288
- saveAgentPrompt: (id) => saveAgentPrompt(id),
2289
- resetAgentSession: (id) => resetAgentSession(id),
2290
- saveAgentTools: (id) => saveAgentTools(id),
2291
- applyToolPreset: (id) => applyToolPreset(id),
2292
- setRoute: (id, route) => setRoute(id, route),
2293
- saveOpenCodeConfig: (id) => saveOpenCodeConfig(id),
2294
- saveOpenCodeFallback: (id) => saveOpenCodeFallback(id),
2295
- saveCursorCliConfig: (id) => saveCursorCliConfig(id),
2296
- saveClaudeCodeConfig: (id) => saveClaudeCodeConfig(id),
2297
- saveCodexConfig: (id) => saveCodexConfig(id),
2298
- saveGeminiCliConfig: (id) => saveGeminiCliConfig(id),
2299
- saveCrewCLIConfig: (id) => saveCrewCLIConfig(id),
2300
- // PM Loop / Projects
2301
- "pm-toggle": (id) => {
2302
- const proj = state.projects?.find((p) => p.id === id);
2303
- proj && proj.running ? stopProjectPMLoop(id) : startProjectPMLoop(id);
2304
- },
2305
- "edit-roadmap": (id) => {
2306
- const proj = state.projects?.find((p) => p.id === id);
2307
- if (proj) openRoadmapEditor(id, proj.roadmapFile);
2308
- },
2309
- "retry-failed": (id) => {
2310
- const proj = state.projects?.find((p) => p.id === id);
2311
- if (proj) retryFailed(proj.roadmapFile);
2312
- },
2313
- "save-roadmap": (id) => saveRoadmap(id),
2314
- "reset-failed": (id) => resetAllFailed(id),
2315
- // Settings tabs
2316
- showSettingsTab: (tab) => showSettingsTab(tab),
2317
- };
2318
-
2319
- // ── Touch and click event handling for mobile responsiveness ───────────────
2320
- let touchHandled = false;
2321
-
2322
- document.addEventListener("touchstart", (e) => {
2323
- if (!(e.target instanceof Element)) return;
2324
- const el = e.target.closest("[data-action]");
2325
- if (!el) return;
2326
-
2327
- // Mark that touch was handled to prevent duplicate click event
2328
- touchHandled = true;
2329
- setTimeout(() => { touchHandled = false; }, 500);
2330
- }, { passive: true });
2331
-
2332
- document.addEventListener("click", (e) => {
2333
- if (!(e.target instanceof Element)) return;
2334
- const el = e.target.closest("[data-action]");
2335
- if (!el) return;
2336
-
2337
- // Skip if already handled by touch
2338
- if (touchHandled) {
2339
- e.preventDefault();
2340
- return;
2341
- }
2342
-
2343
- e.stopPropagation();
2344
- const action = el.dataset.action;
2345
- const fn = ACTION_REGISTRY[action];
2346
- if (!fn) {
2347
- console.warn("[crewswarm] unknown data-action:", action);
2348
- return;
2349
- }
2350
- const arg = el.dataset.arg ?? null;
2351
- const arg2 = el.dataset.arg2 ?? null;
2352
- const needsEl = el.dataset.self === "1";
2353
- if (arg !== null && arg2 !== null) fn(arg, arg2);
2354
- else if (arg !== null && needsEl) fn(arg, el);
2355
- else if (arg !== null) fn(arg);
2356
- else if (needsEl) fn(el);
2357
- else fn();
2358
- });
2359
-
2360
- // ── Delegated change listener (data-onchange) ────────────────────────────────
2361
- document.addEventListener("change", (e) => {
2362
- const el = e.target.closest("[data-onchange]");
2363
- if (!el) return;
2364
- const fn = ACTION_REGISTRY[el.dataset.onchange];
2365
- if (!fn) return;
2366
- // Pass element value if data-onchange-arg="this.value", otherwise no arg
2367
- const arg = el.dataset.onchangeArg === "this.value" ? el.value : null;
2368
- arg !== null ? fn(arg) : fn();
2369
- });
2370
-
2371
- // Wire chatInput keydown + oninput via addEventListener (SES-safe)
2372
- document.addEventListener(
2373
- "DOMContentLoaded",
2374
- () => {
2375
- // Initialize active tasks panel
2376
- initActiveTasksPanel("activeTasksPanel");
2377
-
2378
- // Initialize contacts tab event listeners
2379
- initContactsList();
2380
-
2381
- // Set sidebar self-link to actual origin (avoids hardcoded localhost:4319)
2382
- const dashLink = document.getElementById("dashSelfLink");
2383
- if (dashLink) {
2384
- dashLink.href = window.location.origin;
2385
- dashLink.textContent = window.location.host;
2386
- }
2387
-
2388
- // Wire nav buttons for all tabs
2389
- document.querySelectorAll(".nav-item").forEach((btn) => {
2390
- btn.addEventListener("click", (event) => {
2391
- event.preventDefault();
2392
- event.stopPropagation();
2393
- const view = btn.dataset.view;
2394
- if (!view) return;
2395
- if (currentHashBaseView() !== view) {
2396
- window.location.hash = view;
2397
- return;
2398
- }
2399
- const fn = NAV_VIEW_MAP[view];
2400
- if (fn) fn();
2401
- });
2402
- });
2403
-
2404
- const chatInput = document.getElementById("chatInput");
2405
- if (chatInput && !chatInput.dataset.boundChatComposer) {
2406
- chatInput.dataset.boundChatComposer = "1";
2407
- chatInput.addEventListener("keydown", chatKeydown);
2408
- chatInput.addEventListener("input", chatAtAtInput);
2409
- }
2410
- const chatSendBtn =
2411
- document.getElementById("chatSendBtn") ||
2412
- document.querySelector('[data-action="sendChat"]');
2413
- if (chatSendBtn && !chatSendBtn.dataset.boundChatComposer) {
2414
- chatSendBtn.dataset.boundChatComposer = "1";
2415
- chatSendBtn.addEventListener("click", (event) => {
2416
- event.preventDefault();
2417
- event.stopPropagation();
2418
- sendChat();
2419
- });
2420
- }
2421
- const cmdInput = document.getElementById("cmdAllowlistInput");
2422
- if (cmdInput) {
2423
- cmdInput.addEventListener("keydown", (e) => {
2424
- if (e.key === "Enter") addAllowlistPattern();
2425
- });
2426
- }
2427
- const waNumbers = document.getElementById("waAllowedNumbers");
2428
- if (waNumbers) waNumbers.addEventListener("input", renderWaContactRows);
2429
- const skillSearchInput = document.getElementById("skillSearch");
2430
- if (skillSearchInput)
2431
- skillSearchInput.addEventListener("input", (e) =>
2432
- filterSkills(e.target.value),
2433
- );
2434
-
2435
- // Refresh session indicator when engine or project changes
2436
- const engineSel = document.getElementById("passthroughEngine");
2437
- if (engineSel) {
2438
- engineSel.addEventListener("change", () => {
2439
- refreshSessionIndicator();
2440
- updatePassthroughModelDropdown();
2441
- // Reset send button state when switching engines
2442
- resetSendButton();
2443
- });
2444
- }
2445
- const projSel = document.getElementById("chatProjectSelect");
2446
- if (projSel) projSel.addEventListener("change", refreshSessionIndicator);
2447
-
2448
- // Reset send button when model changes
2449
- const modelSel = document.getElementById("passthroughModel");
2450
- if (modelSel) {
2451
- modelSel.addEventListener("change", () => {
2452
- resetSendButton();
2453
- });
2454
- }
2455
- },
2456
- { once: true },
2457
- );
2458
-
2459
- // Update passthrough model dropdown based on selected engine
2460
- function updatePassthroughModelDropdown() {
2461
- const engineSel = document.getElementById("passthroughEngine");
2462
- const modelSel = document.getElementById("passthroughModel");
2463
- if (!engineSel || !modelSel) return;
2464
-
2465
- const engine = engineSel.value;
2466
- const modelsByEngine = {
2467
- cursor: [
2468
- { value: "", label: "— default (opus-4.6-thinking) —" },
2469
- { optgroup: "Recommended (No Rate Limits)" },
2470
- { value: "gemini-3-flash", label: "🟢 Gemini 3 Flash (fastest)" },
2471
- { value: "gemini-3-pro", label: "🟢 Gemini 3 Pro" },
2472
- { value: "gemini-3.1-pro", label: "🟢 Gemini 3.1 Pro" },
2473
- { value: "gpt-5.2-codex", label: "🟢 GPT-5.2 Codex" },
2474
- { value: "gpt-5.3-codex", label: "🟢 GPT-5.3 Codex" },
2475
- { optgroup: "Claude Models (May Hit Rate Limits)" },
2476
- { value: "sonnet-4.5", label: "🟡 Claude 4.5 Sonnet" },
2477
- { value: "sonnet-4.6", label: "🟡 Claude 4.6 Sonnet (current)" },
2478
- { value: "opus-4.5", label: "🟡 Claude 4.5 Opus" },
2479
- { value: "opus-4.6", label: "🟡 Claude 4.6 Opus" },
2480
- { optgroup: "Thinking Models (Slower)" },
2481
- { value: "sonnet-4.5-thinking", label: "Claude 4.5 Sonnet Thinking" },
2482
- { value: "opus-4.6-thinking", label: "Claude 4.6 Opus Thinking" },
2483
- { optgroup: "Other" },
2484
- { value: "grok", label: "xAI Grok" },
2485
- { value: "kimi-k2.5", label: "Moonshot Kimi K2.5" },
2486
- ],
2487
- claude: [
2488
- { value: "", label: "— default (Sonnet 4.6) —" },
2489
- { optgroup: "Recommended" },
2490
- { value: "sonnet", label: "🟢 Sonnet (alias for latest)" },
2491
- { value: "Default", label: "🟢 Default (Sonnet 4.6)" },
2492
- { optgroup: "Specific Versions" },
2493
- {
2494
- value: "claude-sonnet-4-6",
2495
- label: "Sonnet 4.6 · Best for everyday tasks",
2496
- },
2497
- {
2498
- value: "Opus",
2499
- label: "Opus (Opus 4.6) · Most capable for complex work",
2500
- },
2501
- { value: "claude-opus-4-6", label: "Opus 4.6 · Most capable" },
2502
- {
2503
- value: "Haiku",
2504
- label: "Haiku (Haiku 4.5) · Fastest for quick answers",
2505
- },
2506
- { value: "claude-haiku-4-5", label: "Haiku 4.5 · Fastest" },
2507
- { optgroup: "Legacy" },
2508
- { value: "claude-sonnet-4-5", label: "Sonnet 4.5 (legacy)" },
2509
- ],
2510
- codex: [
2511
- { value: "", label: "— default (gpt-5.3-codex) —" },
2512
- { optgroup: "Recommended" },
2513
- { value: "gpt-5.3-codex", label: "🟢 GPT-5.3 Codex (current)" },
2514
- { value: "gpt-5.2-codex", label: "🟢 GPT-5.2 Codex" },
2515
- { optgroup: "Specialized" },
2516
- {
2517
- value: "gpt-5.1-codex-max",
2518
- label: "GPT-5.1 Codex Max (deep reasoning)",
2519
- },
2520
- { value: "gpt-5.2", label: "GPT-5.2 (general purpose)" },
2521
- {
2522
- value: "gpt-5.1-codex-mini",
2523
- label: "GPT-5.1 Codex Mini (fast & cheap)",
2524
- },
2525
- ],
2526
- opencode: [
2527
- { value: "", label: "— default —" },
2528
- { optgroup: "Free Models 🎁" },
2529
- { value: "opencode/big-pickle", label: "🆓 Big Pickle (Free)" },
2530
- { value: "opencode/minimax-m2.5-free", label: "🆓 MiniMax M2.5 Free" },
2531
- { value: "openai/gpt-5-nano", label: "🆓 GPT 5 Nano (Free)" },
2532
- { optgroup: "Budget Models 💰" },
2533
- {
2534
- value: "openai/gpt-5.1-codex-mini",
2535
- label: "💰 GPT 5.1 Codex Mini ($0.25/$2)",
2536
- },
2537
- { value: "google/gemini-3-flash", label: "💰 Gemini 3 Flash ($0.50/$3)" },
2538
- {
2539
- value: "anthropic/claude-haiku-4-5",
2540
- label: "💰 Claude Haiku 4.5 ($1/$5)",
2541
- },
2542
- { optgroup: "Interesting Models 🎯" },
2543
- { value: "moonshot/kimi-k2.5", label: "Kimi K2.5 ($0.60/$3)" },
2544
- {
2545
- value: "moonshot/kimi-k2-thinking",
2546
- label: "Kimi K2 Thinking ($0.40/$2.50)",
2547
- },
2548
- {
2549
- value: "alibaba/qwen3-coder-480b",
2550
- label: "Qwen3 Coder 480B ($0.45/$1.50)",
2551
- },
2552
- { value: "zhipu/glm-5", label: "GLM 5 ($1/$3.20)" },
2553
- { optgroup: "Premium Claude" },
2554
- {
2555
- value: "anthropic/claude-sonnet-4-6",
2556
- label: "Claude Sonnet 4.6 ($3/$15)",
2557
- },
2558
- { value: "anthropic/claude-opus-4-6", label: "Claude Opus 4.6 ($5/$25)" },
2559
- { optgroup: "Premium OpenAI" },
2560
- { value: "openai/gpt-5.3-codex", label: "GPT 5.3 Codex ($1.75/$14)" },
2561
- { value: "openai/gpt-5.2-codex", label: "GPT 5.2 Codex ($1.75/$14)" },
2562
- {
2563
- value: "openai/gpt-5.1-codex-max",
2564
- label: "GPT 5.1 Codex Max ($1.25/$10)",
2565
- },
2566
- { optgroup: "Premium Google" },
2567
- { value: "google/gemini-3.1-pro", label: "Gemini 3.1 Pro ($2/$12)" },
2568
- { value: "google/gemini-3-pro", label: "Gemini 3 Pro ($2/$12)" },
2569
- ],
2570
- gemini: [
2571
- { value: "", label: "— default (gemini-3-flash-preview) —" },
2572
- { optgroup: "Recommended (Latest)" },
2573
- {
2574
- value: "gemini-3-flash-preview",
2575
- label: "🟢 Gemini 3 Flash Preview (current)",
2576
- },
2577
- { value: "gemini-3.1-pro-preview", label: "🟢 Gemini 3.1 Pro Preview" },
2578
- { optgroup: "Gemini 2.5 Series" },
2579
- { value: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
2580
- { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
2581
- {
2582
- value: "gemini-2.5-flash-lite",
2583
- label: "Gemini 2.5 Flash Lite (fastest)",
2584
- },
2585
- ],
2586
- };
2587
-
2588
- if (!engine || !modelsByEngine[engine]) {
2589
- modelSel.style.display = "none";
2590
- return;
2591
- }
2592
-
2593
- modelSel.style.display = "inline-block";
2594
- modelSel.innerHTML = "";
2595
-
2596
- let currentOptgroup = null;
2597
- for (const item of modelsByEngine[engine]) {
2598
- if (item.optgroup) {
2599
- // Create optgroup
2600
- currentOptgroup = document.createElement("optgroup");
2601
- currentOptgroup.label = item.optgroup;
2602
- modelSel.appendChild(currentOptgroup);
2603
- } else {
2604
- // Create option
2605
- const opt = document.createElement("option");
2606
- opt.value = item.value;
2607
- opt.textContent = item.label;
2608
- if (currentOptgroup) {
2609
- currentOptgroup.appendChild(opt);
2610
- } else {
2611
- modelSel.appendChild(opt);
2612
- }
2613
- }
2614
- }
2615
- }
2616
-
2617
- // Nav view delegation (data-view buttons in sidebar)
2618
- const NAV_VIEW_MAP = {
2619
- chat: showChat,
2620
- "swarm-chat": showSwarmChat,
2621
- swarm: showSwarm,
2622
- rt: showRT,
2623
- build: showBuild,
2624
- files: showFiles,
2625
- dlq: showDLQ,
2626
- projects: showProjects,
2627
- contacts: showContacts,
2628
- agents: showAgents,
2629
- models: showModels,
2630
- engines: showEngines,
2631
- skills: showSkills,
2632
- "run-skills": showRunSkills,
2633
- waves: () => {
2634
- hideAllViews();
2635
- document.getElementById("wavesView").style.display = "block";
2636
- setNavActive("navWaves");
2637
- },
2638
- workflows: showWorkflows,
2639
- benchmarks: showBenchmarks,
2640
- "tool-matrix": showToolMatrix,
2641
- memory: showMemoryView,
2642
- "cli-process": showCLIProcess,
2643
- services: showServices,
2644
- prompts: initPromptsTab,
2645
- settings: showSettings,
2646
- };
2647
- document.addEventListener("click", (e) => {
2648
- const btn = e.target.closest("[data-view]");
2649
- if (btn) {
2650
- const viewName = btn.dataset.view;
2651
- const fn = NAV_VIEW_MAP[viewName];
2652
- if (fn) {
2653
- // Let hashchange drive navigation to avoid double-render/jitter.
2654
- if (currentHashBaseView() !== viewName) {
2655
- window.location.hash = viewName;
2656
- } else {
2657
- fn();
2658
- }
2659
- }
2660
- return;
2661
- }
2662
- const stab = e.target.closest("[data-stab]");
2663
- if (stab) {
2664
- const subtab = stab.dataset.stab;
2665
- // Update URL hash for settings sub-tabs
2666
- window.location.hash = `settings/${subtab}`;
2667
- showSettingsTab(subtab);
2668
- }
2669
- // Collapse/expand panels with data-toggle-child
2670
- const tog = e.target.closest("[data-toggle-child]");
2671
- if (tog) {
2672
- const sel = tog.dataset.toggleChild;
2673
- const body = tog.parentElement && tog.parentElement.querySelector(sel);
2674
- if (body)
2675
- body.style.display = body.style.display === "none" ? "block" : "none";
2676
- }
2677
- // Collapse/expand next sibling with data-toggle-sibling (e.g. provider-header → provider-body)
2678
- const togSib = e.target.closest("[data-toggle-sibling]");
2679
- if (togSib && togSib.nextElementSibling) {
2680
- togSib.nextElementSibling.classList.toggle(togSib.dataset.toggleSibling);
2681
- }
2682
- });
2683
-
2684
- // Vite wraps modules in a closure; onclick="window.fn()" attrs in static + dynamic HTML need window.fn.
2685
- Object.assign(window, {
2686
- // ── Static HTML handlers ──
2687
- addAllowlistPattern,
2688
- applyNewAgentToolPreset,
2689
- applyPromptPreset,
2690
- bulkSetRoute,
2691
- cancelSkillForm,
2692
- chatAtAtInput,
2693
- chatKeydown,
2694
- clearChatHistory,
2695
- filterSkills,
2696
- loadAllUsage,
2697
- loadBenchmarkLeaderboard: async () => {
2698
- const mod = await loadBenchmarksTabModule();
2699
- return mod.loadBenchmarkLeaderboard();
2700
- },
2701
- loadBenchmarks: async () => {
2702
- const mod = await loadBenchmarksTabModule();
2703
- return mod.loadBenchmarks();
2704
- },
2705
- loadBenchmarkTasks: async () => {
2706
- const mod = await loadBenchmarksTabModule();
2707
- return mod.loadBenchmarkTasks();
2708
- },
2709
- onBenchmarkTaskSelect: async (taskId) => {
2710
- const mod = await loadBenchmarksTabModule();
2711
- return mod.onBenchmarkTaskSelect(taskId);
2712
- },
2713
- runBenchmarkTask: async () => {
2714
- const mod = await loadBenchmarksTabModule();
2715
- return mod.runBenchmarkTask();
2716
- },
2717
- stopBenchmarkRun: async () => {
2718
- const mod = await loadBenchmarksTabModule();
2719
- return mod.stopBenchmarkRun();
2720
- },
2721
- loadMemoryStats,
2722
- searchMemory,
2723
- migrateMemory,
2724
- compactMemory,
2725
- loadBuildProjectPicker,
2726
- loadFiles,
2727
- loadOcStats,
2728
- loadRunSkills,
2729
- loadServices,
2730
- loadSpending,
2731
- loadTelegramSessions,
2732
- loadTgMessages,
2733
- loadToolMatrix,
2734
- loadWaMessages,
2735
- onBuildProjectChange,
2736
- onChatProjectChange,
2737
- pickFolder,
2738
- renderWaContactRows,
2739
- resetSpending,
2740
- approveSkill,
2741
- loadPendingApprovals,
2742
- rejectSkill,
2743
- saveGlobalCaps,
2744
- saveGlobalFallback,
2745
- saveBgConsciousnessModel,
2746
- saveOpencodeSettings,
2747
- saveRTToken,
2748
- saveSkill,
2749
- saveTgConfig,
2750
- saveWaConfig,
2751
- sendChat,
2752
- sendTestWebhook,
2753
- showAgents,
2754
- showBenchmarks,
2755
- showBuild,
2756
- showChat,
2757
- showContacts,
2758
- showDLQ,
2759
- showFiles,
2760
- showModels,
2761
- showProjects,
2762
- showRT,
2763
- showRunSkills,
2764
- showServices,
2765
- showSettings,
2766
- showSettingsTab,
2767
- showSkills,
2768
- showSwarm,
2769
- showToolMatrix,
2770
- showMemoryView,
2771
- startCrew,
2772
- startTgBridge,
2773
- startWaBridge,
2774
- stopTgBridge,
2775
- stopWaBridge,
2776
- toggleAddSkill,
2777
- toggleBgConsciousness,
2778
- toggleCursorWaves,
2779
- toggleTmuxBridge,
2780
- toggleClaudeCode,
2781
- toggleEmojiPicker,
2782
- updateSkillAuthFields,
2783
- navigateTo,
2784
- renderStatusBadge,
2785
- showLoading,
2786
- showEmpty,
2787
- showError,
2788
- loadContacts,
2789
- applyContactFilters,
2790
- // ── Dynamic HTML handlers (innerHTML-rendered) ──
2791
- applyToolPreset,
2792
- closePreviewPane,
2793
- deleteAgent,
2794
- deleteSkill,
2795
- editSkill,
2796
- fetchBuiltinModels,
2797
- fetchModels,
2798
- previewFile,
2799
- resetAgentSession,
2800
- restartAgentFromUI,
2801
- restartService,
2802
- runSkillFromUI,
2803
- saveAgentFallback,
2804
- saveAgentVoice,
2805
- saveAgentIdentity,
2806
- saveAgentModel,
2807
- saveAgentPrompt,
2808
- saveAgentTools,
2809
- saveBuiltinKey,
2810
- saveCursorCliConfig,
2811
- saveClaudeCodeConfig,
2812
- saveGeminiCliConfig,
2813
- saveKey,
2814
- saveOpenCodeConfig,
2815
- saveOpenCodeFallback,
2816
- saveSearchTool,
2817
- saveCodexConfig,
2818
- setRoute,
2819
- stopService,
2820
- testBuiltinProvider,
2821
- testKey,
2822
- testSearchTool,
2823
- toggleAgentBody,
2824
- toggleKeyVis,
2825
- });
2826
-
2827
- // Project tabs: startup IIFE (near initProjectsList) already fetches /api/projects
2828
- // and populates the dropdown — avoid a duplicate fetch here.