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,1847 +0,0 @@
1
- import { taskManager } from "../core/task-manager.js";
2
- import { filterOpenCodePassthroughTextChunk } from "../../../../lib/browser/opencode-passthrough-filter.js";
3
- import { filterGeminiPassthroughTextChunk } from "../../../../lib/gemini-cli-passthrough-noise.mjs";
4
- import {
5
- createPassthroughStderrLineFilter,
6
- shouldDropPassthroughStderrLine,
7
- summarizePassthroughTopErrorLine,
8
- } from "../../../../lib/browser/passthrough-stderr.js";
9
-
10
- export function initChatActions(deps) {
11
- const {
12
- postJSON,
13
- getJSON,
14
- appendChatBubble,
15
- showNotification,
16
- state,
17
- getChatSessionId,
18
- getChatActiveProjectId,
19
- getCrewLeadInfo,
20
- appendRoadmapCard,
21
- getLastAppendedAssistantContent,
22
- setLastAppendedAssistantContent,
23
- setLastAppendedUserContent,
24
- setLastSentContent,
25
- } = deps;
26
-
27
- const PASSTHROUGH_LOG_KEY = "crewswarm_passthrough_log";
28
- const PASSTHROUGH_LOG_MAX = 200;
29
-
30
- function resolveVisibleChatProjectId() {
31
- const selector = document.getElementById("chatProjectSelect");
32
- const selectedValue = String(selector?.value || "").trim();
33
- const activeTab = document.querySelector(
34
- '#chatProjectTabs [data-project-id].active',
35
- );
36
- const tabValue = String(activeTab?.dataset?.projectId || "").trim();
37
- const resolved =
38
- selectedValue && selectedValue !== "undefined"
39
- ? selectedValue
40
- : tabValue && tabValue !== "undefined"
41
- ? tabValue
42
- : getChatActiveProjectId() || state.chatActiveProjectId || "general";
43
- state.chatActiveProjectId = resolved;
44
- try {
45
- localStorage.setItem("crewswarm_chat_active_project_id", resolved);
46
- } catch { }
47
- return resolved;
48
- }
49
- const ATAT_COMMANDS = [
50
- {
51
- id: "RESET",
52
- label: "Clear session history and start fresh",
53
- template: "",
54
- },
55
- {
56
- id: "STOP",
57
- label: "Cancel all running pipelines (agents keep running)",
58
- template: "",
59
- },
60
- {
61
- id: "KILL",
62
- label: "Kill all pipelines + terminate all agent bridges",
63
- template: "",
64
- },
65
- {
66
- id: "SEARCH_HISTORY",
67
- label: "Search long-term chat history by keyword",
68
- template: "your search terms",
69
- },
70
- {
71
- id: "DISPATCH",
72
- label: "Dispatch task to an agent",
73
- template: '{"agent":"crew-coder","task":"Your task here"}',
74
- },
75
- {
76
- id: "PIPELINE",
77
- label: "Multi-step pipeline (waves of agents)",
78
- template:
79
- '[{"wave":1,"agent":"crew-coder","task":"..."},{"wave":2,"agent":"crew-qa","task":"..."}]',
80
- },
81
- {
82
- id: "PROMPT",
83
- label: "Append or set agent system prompt",
84
- template: '{"agent":"crew-lead","append":"Your new rule here"}',
85
- },
86
- {
87
- id: "SKILL",
88
- label: "Run a skill by name",
89
- template: 'skillName {"param":"value"}',
90
- },
91
- {
92
- id: "SERVICE",
93
- label: "Restart/stop a service or agent",
94
- template: "restart crew-coder",
95
- },
96
- {
97
- id: "READ_FILE",
98
- label: "Read a file and get its contents",
99
- template: "/path/to/file",
100
- },
101
- {
102
- id: "RUN_CMD",
103
- label: "Run a shell command",
104
- template: "ls -la /home/user/CrewSwarm",
105
- },
106
- {
107
- id: "WEB_SEARCH",
108
- label: "Search the web (Perplexity)",
109
- template: "your search query",
110
- },
111
- {
112
- id: "WEB_FETCH",
113
- label: "Fetch a webpage or URL",
114
- template: "https://example.com",
115
- },
116
- {
117
- id: "PROJECT",
118
- label: "Draft a new project roadmap",
119
- template:
120
- '{"name":"MyApp","description":"...","outputDir":"/path/to/dir"}',
121
- },
122
- {
123
- id: "BRAIN",
124
- label: "Append a fact to brain.md",
125
- template: "crew-lead: fact to remember",
126
- },
127
- {
128
- id: "TOOLS",
129
- label: "Grant/revoke tools for an agent",
130
- template: '{"agent":"crew-qa","allow":["read_file","write_file"]}',
131
- },
132
- {
133
- id: "CREATE_AGENT",
134
- label: "Create a dynamic agent",
135
- template: '{"id":"crew-ml","role":"coder","description":"ML specialist"}',
136
- },
137
- {
138
- id: "REMOVE_AGENT",
139
- label: "Remove a dynamic agent",
140
- template: "crew-ml",
141
- },
142
- {
143
- id: "DEFINE_SKILL",
144
- label: "Define a new skill (then @@END_SKILL)",
145
- template: 'skillName\\n{"description":"...","url":"..."}',
146
- },
147
- {
148
- id: "DEFINE_WORKFLOW",
149
- label: "Save a workflow for cron",
150
- template: 'name\\n[{"agent":"crew-copywriter","task":"..."}]',
151
- },
152
- ];
153
-
154
- let latestHistoryLoadId = 0;
155
- /** Resolvers notified when loadChatHistory finishes (success/cancel/error). */
156
- const _historyIdleWaiters = [];
157
-
158
- function waitForChatHistoryIdle() {
159
- const box = document.getElementById("chatMessages");
160
- if (!box || box.dataset.historyLoading !== "true") {
161
- return Promise.resolve();
162
- }
163
- return new Promise((resolve) => {
164
- _historyIdleWaiters.push(resolve);
165
- });
166
- }
167
-
168
- function flushHistoryIdleWaiters() {
169
- const pending = _historyIdleWaiters.splice(0);
170
- pending.forEach((r) => {
171
- try {
172
- r();
173
- } catch {
174
- /* ignore */
175
- }
176
- });
177
- }
178
- let mentionAgents = [];
179
- let lastMentionAgentLoadAt = 0;
180
-
181
- async function loadMentionAgents(force = false) {
182
- const now = Date.now();
183
- if (!force && mentionAgents.length && now - lastMentionAgentLoadAt < 30000) {
184
- return mentionAgents;
185
- }
186
- const data = await getJSON("/api/agents-config");
187
- mentionAgents = (data.agents || [])
188
- .filter((agent) => agent.id && agent.id !== "crew-lead")
189
- .sort((a, b) => a.id.localeCompare(b.id));
190
- lastMentionAgentLoadAt = now;
191
- return mentionAgents;
192
- }
193
-
194
- async function resolveLeadingMentionAgent(text) {
195
- const match = String(text || "").match(/^\s*@([a-zA-Z0-9_-]+)\b([\s\S]*)$/);
196
- if (!match) return null;
197
-
198
- const agentId = match[1];
199
- if (!agentId || agentId === "crew-lead") return null;
200
-
201
- const agents = await loadMentionAgents();
202
- const exists = agents.some((agent) => agent.id === agentId);
203
- if (!exists) return null;
204
-
205
- return {
206
- agentId,
207
- message: match[2].trim() || text.trim(),
208
- };
209
- }
210
-
211
- async function loadChatHistory() {
212
- const loadId = ++latestHistoryLoadId;
213
- const isStale = () => loadId !== latestHistoryLoadId;
214
- const chatBoxEl = document.getElementById("chatMessages");
215
- if (chatBoxEl) chatBoxEl.dataset.historyLoading = "true";
216
-
217
- try {
218
- const projectId = getChatActiveProjectId();
219
- const normalizedProjectId =
220
- projectId && projectId !== "undefined" ? projectId : "general";
221
- console.log("📚 [LOAD HISTORY] ==================");
222
- console.log("📚 [LOAD HISTORY] START - projectId:", projectId);
223
- console.log(
224
- "📚 [LOAD HISTORY] state.chatActiveProjectId:",
225
- state.chatActiveProjectId,
226
- );
227
- console.log("📚 [LOAD HISTORY] URL hash:", window.location.hash);
228
-
229
- // UNIFIED VIEW: Always load from project-messages (all sources), including "general"
230
- if (normalizedProjectId) {
231
- console.log(
232
- "📚 [LOAD HISTORY] Loading unified project messages (all sources)",
233
- );
234
- console.log("📚 [LOAD HISTORY] ProjectId:", projectId);
235
-
236
- try {
237
- // Cap payload; chunk-render below so the main thread stays responsive.
238
- const url = `/api/crew-lead/project-messages?projectId=${encodeURIComponent(normalizedProjectId)}&limit=250`;
239
- console.log("📚 [LOAD HISTORY] Fetching:", url);
240
-
241
- const d = await getJSON(url);
242
- if (isStale()) return;
243
- console.log("📚 [LOAD HISTORY] Unified response:", {
244
- ok: d.ok,
245
- messagesCount: d.messages?.length || 0,
246
- sources: d.messages
247
- ? [...new Set(d.messages.map((m) => m.source))]
248
- : [],
249
- });
250
-
251
- const box = document.getElementById("chatMessages");
252
- if (!box) {
253
- console.error(
254
- "📚 [LOAD HISTORY] ERROR: chatMessages element not found!",
255
- );
256
- return;
257
- }
258
- if (isStale()) return;
259
-
260
- // Clear existing messages
261
- box.innerHTML = "";
262
- box.dataset.historyLoaded = "false";
263
- setLastAppendedAssistantContent("");
264
- setLastAppendedUserContent("");
265
-
266
- // Display messages with source indicators
267
- if (d.messages && d.messages.length > 0) {
268
- const sourceEmoji = {
269
- dashboard: "💻",
270
- cli: "⚡",
271
- agent: "🤖", // Direct agent chat (crew-main, crew-security)
272
- "sub-agent": "👷", // Dispatched task completions (crew-coder, crew-qa)
273
- };
274
- let agentsById = new Map();
275
- try {
276
- const agentsData = await getJSON("/api/agents-config");
277
- agentsById = new Map(
278
- (agentsData?.agents || []).map((a) => [a.id, a]),
279
- );
280
- } catch {}
281
- if (isStale()) return;
282
-
283
- const messages = d.messages;
284
- console.log(
285
- "📚 [LOAD HISTORY] Appending",
286
- messages.length,
287
- "unified messages (chunked rAF)...",
288
- );
289
-
290
- const BATCH = 32;
291
- await new Promise((resolve) => {
292
- let idx = 0;
293
- const pump = () => {
294
- if (isStale()) {
295
- resolve();
296
- return;
297
- }
298
- const end = Math.min(idx + BATCH, messages.length);
299
- for (; idx < end; idx++) {
300
- if (isStale()) {
301
- resolve();
302
- return;
303
- }
304
- const msg = messages[idx];
305
- const agentId = msg.agent || msg.metadata?.agentId || null;
306
- const catalogAgent = agentId ? agentsById.get(agentId) : null;
307
- const emoji =
308
- msg.metadata?.agentEmoji ||
309
- catalogAgent?.emoji ||
310
- sourceEmoji[msg.source] ||
311
- "📝";
312
- const agentName =
313
- msg.metadata?.agentName ||
314
- catalogAgent?.name ||
315
- agentId ||
316
- null;
317
- const timestamp = new Date(msg.ts).toLocaleTimeString();
318
-
319
- const sourceInfo = {
320
- emoji,
321
- source: msg.source,
322
- agent: agentName,
323
- agentName,
324
- agentId,
325
- targetAgent:
326
- msg.metadata?.targetAgent || msg.metadata?.agentId || null,
327
- engine:
328
- msg.metadata?.engine ||
329
- msg.metadata?.runtime ||
330
- msg.metadata?.model ||
331
- null,
332
- timestamp,
333
- };
334
-
335
- appendChatBubble(
336
- msg.role === "user" ? "user" : "assistant",
337
- msg.content,
338
- null,
339
- null,
340
- msg.metadata?.model,
341
- msg.metadata?.engine,
342
- sourceInfo,
343
- );
344
- if (msg.role === "assistant")
345
- setLastAppendedAssistantContent(msg.content);
346
- if (msg.role === "user")
347
- setLastAppendedUserContent(msg.content);
348
- }
349
- if (idx < messages.length) {
350
- requestAnimationFrame(pump);
351
- } else {
352
- resolve();
353
- }
354
- };
355
- requestAnimationFrame(pump);
356
- });
357
-
358
- if (isStale()) return;
359
- console.log(
360
- "📚 [LOAD HISTORY] ✅ Loaded unified view with all sources",
361
- );
362
- box.dataset.historyLoaded = "true";
363
- box.scrollTop = box.scrollHeight;
364
- return;
365
- } else {
366
- console.log(
367
- "📚 [LOAD HISTORY] No messages in unified response (might be empty project)",
368
- );
369
- box.dataset.historyLoaded = "true";
370
- // Don't fall through - empty is valid for new projects
371
- return;
372
- }
373
- } catch (e) {
374
- console.error("📚 [LOAD HISTORY] ⚠️ Unified view failed:", e);
375
- console.error("📚 [LOAD HISTORY] Error details:", {
376
- message: e.message,
377
- stack: e.stack,
378
- });
379
-
380
- // Show error message to user
381
- const box = document.getElementById("chatMessages");
382
- if (box) {
383
- const errorDiv = document.createElement("div");
384
- errorDiv.style.cssText =
385
- "padding:12px;margin:8px;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);border-radius:8px;color:#ef4444;font-size:13px;";
386
- errorDiv.innerHTML = `⚠️ <strong>crew-lead unavailable</strong> — Cannot load project message history.<br><small>Check that crew-lead is running: <code>node crew-lead.mjs</code></small>`;
387
- box.appendChild(errorDiv);
388
- }
389
-
390
- // Fall through to load standard crew-lead history (if crew-lead comes back up)
391
- console.log(
392
- "📚 [LOAD HISTORY] Falling back to crew-lead-only history...",
393
- );
394
- }
395
- }
396
-
397
- // STANDARD VIEW: Load crew-lead history only (fallback or general chat)
398
- let url = "/api/crew-lead/history?sessionId=owner";
399
- if (normalizedProjectId && normalizedProjectId !== "general") {
400
- url += "&projectId=" + encodeURIComponent(normalizedProjectId);
401
- }
402
- console.log("📚 [LOAD HISTORY] Fetching crew-lead history:", url);
403
-
404
- const d = await getJSON(url);
405
- if (isStale()) return;
406
- console.log("📚 [LOAD HISTORY] Response:", {
407
- historyCount: d.history?.length || 0,
408
- });
409
- console.log("📚 [LOAD HISTORY] Response projectId:", d.projectId);
410
-
411
- // Log first and last message for debugging
412
- if (d.history && d.history.length > 0) {
413
- const userMsgs = d.history.filter((m) => m.role === "user");
414
- if (userMsgs.length > 0) {
415
- console.log(
416
- "📚 [LOAD HISTORY] First user msg:",
417
- userMsgs[0].content.slice(0, 50),
418
- );
419
- console.log(
420
- "📚 [LOAD HISTORY] Last user msg:",
421
- userMsgs[userMsgs.length - 1].content.slice(0, 50),
422
- );
423
- }
424
- }
425
-
426
- const box = document.getElementById("chatMessages");
427
- if (!box) {
428
- console.error(
429
- "📚 [LOAD HISTORY] ERROR: chatMessages element not found!",
430
- );
431
- return;
432
- }
433
- if (isStale()) return;
434
-
435
- // ALWAYS clear on load - fixes hard refresh showing old messages
436
- console.log("📚 [LOAD HISTORY] Clearing chatMessages...");
437
- box.innerHTML = "";
438
- box.dataset.historyLoaded = "false";
439
- setLastAppendedAssistantContent("");
440
- setLastAppendedUserContent("");
441
-
442
- // Load crew-lead history if available
443
- if (d.history && d.history.length) {
444
- // Only show recent messages to avoid overwhelming UI (last 50)
445
- const recentHistory = d.history.slice(-50);
446
- console.log(
447
- "📚 [LOAD HISTORY] Appending",
448
- recentHistory.length,
449
- "messages...",
450
- );
451
- recentHistory.forEach((h) => {
452
- if (isStale()) return;
453
- appendChatBubble(h.role === "user" ? "user" : "assistant", h.content);
454
- if (h.role === "assistant")
455
- setLastAppendedAssistantContent(h.content);
456
- if (h.role === "user") setLastAppendedUserContent(h.content);
457
- });
458
- console.log(
459
- "📚 [LOAD HISTORY] Appended",
460
- recentHistory.length,
461
- "messages",
462
- );
463
- } else {
464
- console.log("📚 [LOAD HISTORY] No history found");
465
- }
466
-
467
- // Load passthrough logs (CLI interactions) ONLY if no crew-lead history exists
468
- // This prevents mixing old CLI logs with current crew-lead conversations
469
- if (!d.history || d.history.length === 0) {
470
- const passthroughLog = JSON.parse(
471
- localStorage.getItem(PASSTHROUGH_LOG_KEY) || "[]",
472
- );
473
-
474
- // Strict timestamp validation: only last 6 hours + valid timestamp
475
- const sixHoursAgo = Date.now() - 6 * 60 * 60 * 1000;
476
- const recentLog = passthroughLog.filter((entry) => {
477
- // Must have timestamp AND be within last 6 hours AND have valid content
478
- return (
479
- entry.timestamp &&
480
- typeof entry.timestamp === "number" &&
481
- entry.timestamp > sixHoursAgo &&
482
- entry.text &&
483
- entry.text.trim().length > 0
484
- );
485
- });
486
-
487
- if (recentLog.length > 0) {
488
- appendPassthroughLogsToChat(recentLog);
489
- }
490
-
491
- // Clean up localStorage - remove old entries
492
- if (recentLog.length !== passthroughLog.length) {
493
- localStorage.setItem(PASSTHROUGH_LOG_KEY, JSON.stringify(recentLog));
494
- }
495
- }
496
-
497
- box.scrollTop = box.scrollHeight;
498
- box.dataset.historyLoaded = "true";
499
- } catch (err) {
500
- if (isStale()) return;
501
- console.warn("Failed to load chat history:", err);
502
- // On error, still mark as loaded to prevent infinite retry
503
- const box = document.getElementById("chatMessages");
504
- if (box) box.dataset.historyLoaded = "true";
505
- } finally {
506
- if (chatBoxEl) chatBoxEl.dataset.historyLoading = "false";
507
- flushHistoryIdleWaiters();
508
- }
509
- }
510
-
511
- function appendPassthroughLogsToChat(log) {
512
- const box = document.getElementById("chatMessages");
513
- if (!box || !log.length) return;
514
- const engineLabels = {
515
- claude: "Claude Code",
516
- cursor: "Cursor CLI",
517
- opencode: "OpenCode",
518
- codex: "Codex CLI",
519
- gemini: "Gemini CLI",
520
- "gemini-cli": "Gemini CLI",
521
- "docker-sandbox": "Docker Sandbox",
522
- "crew-cli": "Crew CLI",
523
- };
524
- for (const entry of log) {
525
- if (entry.role === "user") {
526
- appendChatBubble("user", entry.text);
527
- } else {
528
- let cleanedText = String(entry.text || "")
529
- .split("\n")
530
- .filter((line) => !shouldDropPassthroughStderrLine(entry.engine, line))
531
- .join("\n")
532
- .trim();
533
- cleanedText = filterOpenCodePassthroughTextChunk(entry.engine, cleanedText);
534
- cleanedText = filterGeminiPassthroughTextChunk(entry.engine, cleanedText);
535
- const bubble = document.createElement("div");
536
- bubble.className = "chat-bubble assistant";
537
- bubble.style.cssText =
538
- "background:var(--surface-2);border-radius:10px;padding:12px 14px;font-size:14px;line-height:1.6;white-space:pre-wrap;word-break:break-word;font-family:monospace;font-size:12px;color:var(--text-2);";
539
- const lbl = document.createElement("div");
540
- lbl.style.cssText =
541
- "font-size:11px;font-weight:700;color:var(--text-3);margin-bottom:6px;";
542
- const ex = entry.exitCode ?? 0;
543
- lbl.textContent =
544
- (engineLabels[entry.engine] || entry.engine) +
545
- " · direct passthrough " +
546
- (ex === 0 ? "✓" : "⚠") +
547
- " (exit " +
548
- ex +
549
- ")";
550
- const cnt = document.createElement("div");
551
- cnt.textContent = cleanedText || entry.text;
552
- bubble.appendChild(lbl);
553
- bubble.appendChild(cnt);
554
- box.appendChild(bubble);
555
- }
556
- }
557
- }
558
-
559
- function chatAtAtInput() {
560
- const ta = document.getElementById("chatInput");
561
- const menu = document.getElementById("chatAtAtMenu");
562
- const hint = document.getElementById("chatAtAtTemplate");
563
- if (!ta || !menu || !hint) return;
564
- try {
565
- const val = ta.value;
566
- const caret = ta.selectionStart;
567
- const before = val.slice(0, caret);
568
- const mentionMatch = before.match(/(^|\s)@([a-zA-Z0-9_-]*)$/);
569
- if (mentionMatch && before.lastIndexOf("@@") !== before.length - mentionMatch[0].length) {
570
- loadMentionAgents()
571
- .then((agents) => {
572
- const prefix = (mentionMatch[2] || "").toLowerCase();
573
- const filtered = agents
574
- .filter((agent) => agent.id.toLowerCase().includes(prefix))
575
- .slice(0, 8);
576
- if (!filtered.length) {
577
- menu.style.display = "none";
578
- hint.style.display = "none";
579
- return;
580
- }
581
- menu.style.display = "block";
582
- menu.dataset.mode = "mention";
583
- menu.innerHTML = "";
584
- filtered.forEach((agent) => {
585
- const row = document.createElement("div");
586
- row.style.cssText =
587
- "padding:8px 12px;cursor:pointer;font-size:13px;border-bottom:1px solid var(--border);";
588
- row.onmouseenter = function onmouseenter() {
589
- row.style.background = "var(--bg-hover)";
590
- };
591
- row.onmouseleave = function onmouseleave() {
592
- row.style.background = "";
593
- };
594
- row.innerHTML =
595
- `<span style="color:var(--accent);font-weight:600;">@${agent.id}</span> <span style="color:var(--text-3);">${agent.name || agent.role || "agent"}</span>`;
596
- row.onclick = function onclick() {
597
- const tokenStart =
598
- caret - mentionMatch[0].length + mentionMatch[1].length;
599
- const insert = `@${agent.id} `;
600
- ta.value = val.slice(0, tokenStart) + insert + val.slice(caret);
601
- ta.selectionStart = ta.selectionEnd = tokenStart + insert.length;
602
- ta.focus();
603
- menu.style.display = "none";
604
- hint.style.display = "block";
605
- hint.textContent = `Mention target: @${agent.id}`;
606
- };
607
- menu.appendChild(row);
608
- });
609
- hint.style.display = "block";
610
- hint.textContent = prefix
611
- ? `Matching agents for @${prefix}`
612
- : "Type an agent name, e.g. @crew-coder";
613
- })
614
- .catch(() => {
615
- menu.style.display = "none";
616
- hint.style.display = "none";
617
- });
618
- return;
619
- }
620
- const lastAt = before.lastIndexOf("@@");
621
- if (lastAt === -1) {
622
- menu.style.display = "none";
623
- hint.style.display = "none";
624
- return;
625
- }
626
- const afterAt = before.slice(lastAt + 2);
627
- if (/\s/.test(afterAt)) {
628
- menu.style.display = "none";
629
- hint.style.display = "none";
630
- return;
631
- }
632
- const prefix = afterAt.toUpperCase();
633
- const filtered = ATAT_COMMANDS.filter((c) => c.id.indexOf(prefix) === 0);
634
- if (filtered.length === 0) {
635
- menu.style.display = "none";
636
- hint.style.display = "none";
637
- return;
638
- }
639
- menu.style.display = "block";
640
- menu.style.visibility = "visible";
641
- menu.dataset.mode = "atat";
642
- menu.innerHTML = "";
643
- filtered.forEach((c) => {
644
- const row = document.createElement("div");
645
- row.style.cssText =
646
- "padding:8px 12px;cursor:pointer;font-size:13px;border-bottom:1px solid var(--border);";
647
- row.onmouseenter = function onmouseenter() {
648
- row.style.background = "var(--bg-hover)";
649
- };
650
- row.onmouseleave = function onmouseleave() {
651
- row.style.background = "";
652
- };
653
- row.innerHTML =
654
- '<span style="color:var(--accent);font-weight:600;">@@' +
655
- c.id +
656
- '</span> <span style="color:var(--text-3);">' +
657
- c.label +
658
- "</span>";
659
- row.onclick = function onclick() {
660
- const insert = "@@" + c.id + (c.template ? " " + c.template : "");
661
- ta.value = val.slice(0, lastAt) + insert + val.slice(caret);
662
- ta.selectionStart = ta.selectionEnd = lastAt + insert.length;
663
- ta.focus();
664
- menu.style.display = "none";
665
- hint.style.display = "block";
666
- hint.textContent =
667
- (c.id === "PROMPT"
668
- ? "Full line to send: @@PROMPT "
669
- : "Template: ") + (c.template ? c.template : "");
670
- };
671
- menu.appendChild(row);
672
- });
673
- const exact = filtered.find((c) => c.id === prefix);
674
- if (exact) {
675
- hint.style.display = "block";
676
- hint.textContent =
677
- (exact.id === "PROMPT" ? "Full line: @@PROMPT " : "Template: ") +
678
- (exact.template || "");
679
- } else {
680
- hint.style.display = "none";
681
- }
682
- } catch (err) {
683
- if (typeof console !== "undefined") console.warn("chatAtAtInput", err);
684
- }
685
- }
686
-
687
- function chatKeydown(e) {
688
- const menu = document.getElementById("chatAtAtMenu");
689
- if (
690
- menu &&
691
- menu.style.display === "block" &&
692
- (e.key === "Enter" || e.key === "Tab")
693
- ) {
694
- const first = menu.firstElementChild;
695
- if (first) {
696
- e.preventDefault();
697
- first.click();
698
- return;
699
- }
700
- }
701
- if (e.key === "Enter" && !e.shiftKey) {
702
- e.preventDefault();
703
- sendChat();
704
- }
705
- if (
706
- menu &&
707
- menu.style.display === "block" &&
708
- (e.key === "Escape" || e.key === "Tab")
709
- )
710
- menu.style.display = "none";
711
- }
712
-
713
- // Track active chat abort controller so we can cancel regular (non-passthrough) messages
714
- // DEPRECATED: Now using TaskManager for individual task control
715
- let _chatAbort = null;
716
-
717
- async function sendChat() {
718
- const input = document.getElementById("chatInput");
719
- const sendBtn = document.querySelector('[data-action="sendChat"]');
720
- const text = input.value.trim();
721
- if (!text) return;
722
-
723
- // If already sending, abort it (legacy single-task mode)
724
- if (_chatAbort) {
725
- _chatAbort.abort();
726
- _chatAbort = null;
727
- input.disabled = false;
728
- if (sendBtn) {
729
- sendBtn.disabled = false;
730
- sendBtn.textContent = "Send";
731
- sendBtn.className = "btn-green";
732
- }
733
- input.focus();
734
- return;
735
- }
736
-
737
- const engine = document.getElementById("passthroughEngine")?.value || "";
738
- const selectedAgent =
739
- document.getElementById("chatAgentSelector")?.value || "";
740
-
741
- // NEW: Check unified mode selector
742
- const modeSelector = document.getElementById("chatModeSelector");
743
- const selectedMode = modeSelector?.value || "crew-lead";
744
-
745
- if (selectedMode.startsWith("cli:")) {
746
- // Direct CLI mode (cli:opencode, cli:cursor, etc.)
747
- const cliName = selectedMode.replace("cli:", "");
748
- await sendPassthrough(text, cliName);
749
- return;
750
- } else if (selectedMode !== "crew-lead") {
751
- // Direct agent mode (crew-coder, crew-qa, etc.)
752
- await sendDirectAgent(text, selectedMode);
753
- return;
754
- }
755
-
756
- // Legacy fallback: Priority: passthroughEngine > chatAgentSelector > crew-lead
757
- if (engine) {
758
- await sendPassthrough(text, engine);
759
- return;
760
- }
761
- if (selectedAgent) {
762
- await sendDirectAgent(text, selectedAgent);
763
- return;
764
- }
765
-
766
- const directMention = await resolveLeadingMentionAgent(text);
767
- if (directMention) {
768
- await sendDirectAgent(directMention.message, directMention.agentId);
769
- return;
770
- }
771
-
772
- input.value = "";
773
- // DON'T disable input - allow concurrent messages
774
- // input.disabled = true;
775
- if (sendBtn) {
776
- sendBtn.disabled = false;
777
- sendBtn.textContent = "Send";
778
- sendBtn.className = "btn-green";
779
- }
780
- appendChatBubble("user", text);
781
- setLastAppendedUserContent(text);
782
- setLastSentContent(text);
783
- setLastAppendedAssistantContent(""); // Reset so HTTP fallback can display if SSE is silent
784
-
785
- const typingId = "typing-" + Date.now();
786
- const typingDiv = document.createElement("div");
787
- typingDiv.id = typingId;
788
- typingDiv.style.cssText =
789
- "font-size:12px;color:var(--text-3);padding:4px 6px;";
790
- const cl = getCrewLeadInfo() || { emoji: "🧠", name: "crew-lead" };
791
- typingDiv.textContent = cl.emoji + " " + cl.name + " is thinking...";
792
- const box = document.getElementById("chatMessages");
793
- box.appendChild(typingDiv);
794
- box.scrollTop = box.scrollHeight;
795
-
796
- const controller = new AbortController();
797
- const taskId = "chat-" + Date.now();
798
-
799
- // DON'T register chat messages as tasks - they're just conversations
800
- // Only agent dispatches should show in tasks panel
801
- // taskManager.registerTask(taskId, {
802
- // agent: 'crew-lead',
803
- // type: 'chat',
804
- // description: text.slice(0, 60) + (text.length > 60 ? '...' : ''),
805
- // controller,
806
- // });
807
-
808
- try {
809
- const activeProject = resolveVisibleChatProjectId();
810
- const activeProj = activeProject && state.projectsData[activeProject];
811
- const d = await postJSON(
812
- "/api/chat/unified",
813
- {
814
- mode: "crew-lead",
815
- message: text,
816
- sessionId: getChatSessionId(),
817
- projectId: activeProject || "general",
818
- ...(activeProj?.outputDir ? { projectDir: activeProj.outputDir } : {}),
819
- },
820
- controller.signal,
821
- );
822
- document.querySelectorAll('[id^="typing-"]').forEach((el) => el.remove());
823
- if (d.ok === false && d.error) {
824
- appendChatBubble("assistant", "⚠️ " + d.error);
825
- setLastAppendedAssistantContent("");
826
- // Don't fail task since we didn't register it
827
- // taskManager.failTask(taskId, d.error);
828
- } else if (d.reply) {
829
- // SSE chat_message is the canonical display path — it removes
830
- // the streaming bubble and creates the final one. Only use
831
- // the HTTP reply when SSE was completely silent (connection drop).
832
- if (!getLastAppendedAssistantContent()) {
833
- appendChatBubble("assistant", d.reply);
834
- setLastAppendedAssistantContent(d.reply);
835
- if (box) box.scrollTop = box.scrollHeight;
836
- }
837
- // Don't complete task since we didn't register it
838
- // taskManager.completeTask(taskId);
839
- }
840
- if (d.dispatched) {
841
- const dispatchedTargets = Array.isArray(d.dispatched)
842
- ? d.dispatched
843
- .map((item) => item?.agent || item?.id)
844
- .filter(Boolean)
845
- : [d.dispatched.agent].filter(Boolean);
846
- const note = document.createElement("div");
847
- note.style.cssText =
848
- "font-size:11px;color:var(--text-3);text-align:center;padding:4px;";
849
- if (dispatchedTargets.length) {
850
- note.textContent = "⚡ Dispatched to " + dispatchedTargets.join(", ");
851
- box.appendChild(note);
852
- }
853
- }
854
- if (d.pendingProject) appendRoadmapCard(box, d.pendingProject);
855
- box.scrollTop = box.scrollHeight;
856
- } catch (e) {
857
- document.querySelectorAll('[id^="typing-"]').forEach((el) => el.remove());
858
- if (e.name === "AbortError") {
859
- appendChatBubble("assistant", "⚠️ Message cancelled");
860
- setLastAppendedAssistantContent("");
861
- // Don't stop task since we didn't register it
862
- // taskManager.stopTask(taskId);
863
- } else {
864
- let errMsg = e.message || String(e);
865
- try {
866
- const parsed = JSON.parse(errMsg);
867
- if (parsed && typeof parsed.error === "string") errMsg = parsed.error;
868
- } catch {}
869
- appendChatBubble("assistant", "⚠️ Error: " + errMsg);
870
- setLastAppendedAssistantContent("");
871
- // Don't fail task since we didn't register it
872
- // taskManager.failTask(taskId, errMsg);
873
- }
874
- box.scrollTop = box.scrollHeight;
875
- } finally {
876
- _chatAbort = null;
877
- // input.disabled = false; // Already enabled for concurrent mode
878
- if (sendBtn) {
879
- sendBtn.disabled = false;
880
- sendBtn.textContent = "Send";
881
- sendBtn.className = "btn-green";
882
- }
883
- input.focus();
884
- }
885
- }
886
-
887
- async function clearChatHistory() {
888
- if (!confirm("Clear chat history for this session?")) return;
889
- const box = document.getElementById("chatMessages");
890
- box.innerHTML = "";
891
- box.dataset.historyLoaded = "false"; // Reset the flag so history reloads
892
- localStorage.removeItem(PASSTHROUGH_LOG_KEY);
893
- await postJSON("/api/crew-lead/clear", {
894
- sessionId: getChatSessionId(),
895
- }).catch(() => {});
896
- // Reload fresh history after clearing
897
- await loadChatHistory();
898
- }
899
-
900
- function savePassthroughMsg(role, engine, text, exitCode) {
901
- try {
902
- const log = JSON.parse(localStorage.getItem(PASSTHROUGH_LOG_KEY) || "[]");
903
- log.push({ role, engine, text, exitCode, timestamp: Date.now() }); // Changed ts → timestamp
904
- if (log.length > PASSTHROUGH_LOG_MAX)
905
- log.splice(0, log.length - PASSTHROUGH_LOG_MAX);
906
- localStorage.setItem(PASSTHROUGH_LOG_KEY, JSON.stringify(log));
907
- } catch {}
908
- }
909
-
910
- function restorePassthroughLog() {
911
- try {
912
- const log = JSON.parse(localStorage.getItem(PASSTHROUGH_LOG_KEY) || "[]");
913
- if (!log.length) return;
914
-
915
- // Check if loadChatHistory is still pending - if so, don't append yet
916
- // (loadChatHistory will call appendPassthroughLogsToChat after it finishes)
917
- const box = document.getElementById("chatMessages");
918
- if (!box) return;
919
-
920
- // Only restore if box is empty or if we're in passthrough mode
921
- const engine = document.getElementById("passthroughEngine")?.value;
922
- if (engine && box.children.length === 0) {
923
- appendPassthroughLogsToChat(log);
924
- box.scrollTop = box.scrollHeight;
925
- }
926
- } catch {}
927
- }
928
-
929
- // Track active passthrough abort controller so the kill button can cancel it
930
- // DEPRECATED: Now using TaskManager for individual task control
931
- let _passthroughAbort = null;
932
-
933
- // Update the session indicator badge — shows green dot when a session exists for current engine+project
934
- // Backend keys: engine:projectDir:sessionScope (e.g. gemini:/path/to/crew-cli:owner)
935
- async function refreshSessionIndicator() {
936
- const indicator = document.getElementById("passthroughSessionIndicator");
937
- if (!indicator) return;
938
- const engine = document.getElementById("passthroughEngine")?.value;
939
- if (!engine) {
940
- indicator.style.display = "none";
941
- return;
942
- }
943
- const activeProjectId = resolveVisibleChatProjectId();
944
- const activeProj = activeProjectId && state.projectsData[activeProjectId];
945
- const projectDir = activeProj?.outputDir || null;
946
- const sessionScope = getChatSessionId() || "owner";
947
- try {
948
- const data = await getJSON("/api/passthrough-sessions");
949
- const sessions = data.sessions || {};
950
- // Backend uses engine:projectDir:sessionScope; when no project, backend falls back to config/cwd
951
- const key = projectDir ? `${engine}:${projectDir}:${sessionScope}` : null;
952
- // Also check legacy key format (engine:projectDir) for backward compat
953
- const hasSession =
954
- key && (sessions[key] || sessions[`${engine}:${projectDir}`]);
955
- indicator.style.display = hasSession ? "inline-block" : "none";
956
- indicator.title = hasSession
957
- ? `Session active for ${activeProj?.name || projectDir?.split("/").pop() || "this project"} — click to clear`
958
- : "";
959
- } catch {
960
- indicator.style.display = "none";
961
- }
962
- }
963
-
964
- async function clearPassthroughSession() {
965
- const engine = document.getElementById("passthroughEngine")?.value;
966
- if (!engine) return;
967
- const activeProjectId = resolveVisibleChatProjectId();
968
- const activeProj = activeProjectId && state.projectsData[activeProjectId];
969
- const projectDir = activeProj?.outputDir || null;
970
- if (!projectDir) return;
971
- const sessionScope = getChatSessionId() || "owner";
972
- const key = `${engine}:${projectDir}:${sessionScope}`;
973
- const legacyKey = `${engine}:${projectDir}`;
974
- try {
975
- // Try full key first (backend format), then legacy
976
- await fetch(`/api/passthrough-sessions?key=${encodeURIComponent(key)}`, {
977
- method: "DELETE",
978
- });
979
- await fetch(
980
- `/api/passthrough-sessions?key=${encodeURIComponent(legacyKey)}`,
981
- { method: "DELETE" },
982
- );
983
- showNotification("Session cleared — next message starts fresh");
984
- refreshSessionIndicator();
985
- } catch (e) {
986
- showNotification("Failed: " + e.message, true);
987
- }
988
- }
989
-
990
- // Helper to reset send button to default state
991
- function resetSendButton() {
992
- const sendBtn = document.querySelector('[data-action="sendChat"]');
993
- if (sendBtn) {
994
- sendBtn.textContent = "Send";
995
- sendBtn.className = "btn-green";
996
- sendBtn.disabled = false;
997
- }
998
- }
999
-
1000
- async function sendPassthrough(text, engine) {
1001
- const input = document.getElementById("chatInput");
1002
- const sendBtn = document.querySelector('[data-action="sendChat"]');
1003
- const stopBtn = document.querySelector('[data-action="stopPassthrough"]');
1004
- const modelSelect = document.getElementById("passthroughModel");
1005
- const engineLabels = {
1006
- claude: "Claude Code",
1007
- cursor: "Cursor CLI",
1008
- opencode: "OpenCode",
1009
- codex: "Codex CLI",
1010
- gemini: "Gemini CLI",
1011
- "gemini-cli": "Gemini CLI",
1012
- "docker-sandbox": "Docker Sandbox",
1013
- "crew-cli": "Crew CLI",
1014
- };
1015
-
1016
- // Legacy single-task abort (kept for backward compatibility)
1017
- if (_passthroughAbort) {
1018
- _passthroughAbort.abort();
1019
- _passthroughAbort = null;
1020
- input.disabled = false;
1021
- if (sendBtn) {
1022
- sendBtn.disabled = false;
1023
- sendBtn.textContent = "Send";
1024
- sendBtn.className = "btn-green";
1025
- }
1026
- if (stopBtn) stopBtn.style.display = "none";
1027
- input.focus();
1028
- return;
1029
- }
1030
-
1031
- input.value = "";
1032
- // DON'T disable input - allow concurrent operations
1033
- // input.disabled = true;
1034
- if (sendBtn) {
1035
- sendBtn.disabled = false;
1036
- sendBtn.textContent = "Send";
1037
- sendBtn.className = "btn-green";
1038
- }
1039
- // Hide the separate kill button since we're using task manager
1040
- if (stopBtn) {
1041
- stopBtn.style.display = "none";
1042
- }
1043
-
1044
- appendChatBubble("user", text);
1045
- const box = document.getElementById("chatMessages");
1046
- const bubble = document.createElement("div");
1047
- bubble.className = "chat-bubble assistant";
1048
- bubble.style.cssText =
1049
- "background:var(--surface-2);border-radius:10px;padding:12px 14px;font-size:14px;line-height:1.6;white-space:pre-wrap;word-break:break-word;font-family:monospace;font-size:12px;color:var(--text-2);";
1050
- const label = document.createElement("div");
1051
- label.style.cssText =
1052
- "font-size:11px;font-weight:700;color:var(--text-3);margin-bottom:6px;";
1053
- const activeProjectId = resolveVisibleChatProjectId();
1054
- const activeProj = activeProjectId && state.projectsData[activeProjectId];
1055
- const selectedModel = modelSelect?.value || "";
1056
- const modelLabel = selectedModel ? ` [${selectedModel}]` : "";
1057
- label.textContent =
1058
- (engineLabels[engine] || engine) +
1059
- modelLabel +
1060
- " · direct passthrough" +
1061
- (activeProj?.outputDir
1062
- ? " @ " + activeProj.outputDir.split("/").pop()
1063
- : "");
1064
- const content = document.createElement("div");
1065
- bubble.appendChild(label);
1066
- bubble.appendChild(content);
1067
- box.appendChild(bubble);
1068
- box.scrollTop = box.scrollHeight;
1069
-
1070
- const controller = new AbortController();
1071
- const taskId = "passthrough-" + engine + "-" + Date.now();
1072
- const stderrFilter = createPassthroughStderrLineFilter(engine);
1073
- let stderrFilteredAccum = "";
1074
- let sawAssistantChunk = false;
1075
-
1076
- // DON'T register passthrough/CLI messages as tasks
1077
- // Only actual agent dispatches should show in tasks panel
1078
- // taskManager.registerTask(taskId, {
1079
- // agent: engineLabels[engine] || engine,
1080
- // type: 'passthrough',
1081
- // description: text.slice(0, 60) + (text.length > 60 ? '...' : ''),
1082
- // controller,
1083
- // });
1084
-
1085
- try {
1086
- const projectDir = activeProj?.outputDir || undefined;
1087
- const injectHistory =
1088
- document.getElementById("passthroughInjectHistory")?.checked || false;
1089
- const payload = { engine, message: text };
1090
- if (projectDir) payload.projectDir = projectDir;
1091
- payload.projectId = activeProjectId || "general";
1092
- payload.sessionId = getChatSessionId(); // Add session ID for proper isolation
1093
- if (injectHistory) payload.injectHistory = true;
1094
- if (selectedModel) payload.model = selectedModel;
1095
- const resp = await fetch("/api/chat/unified", {
1096
- method: "POST",
1097
- headers: { "content-type": "application/json" },
1098
- body: JSON.stringify({ mode: "cli", ...payload }),
1099
- signal: controller.signal,
1100
- });
1101
- if (!resp.ok) {
1102
- content.textContent = `Error ${resp.status}: ${await resp.text()}`;
1103
- // Don't fail task since we didn't register it
1104
- // taskManager.failTask(taskId, `HTTP ${resp.status}`);
1105
- return;
1106
- }
1107
-
1108
- const reader = resp.body.getReader();
1109
- const decoder = new TextDecoder();
1110
- let buf = "";
1111
- while (true) {
1112
- const { done, value } = await reader.read();
1113
- if (done) break;
1114
- buf += decoder.decode(value, { stream: true });
1115
- const lines = buf.split("\n");
1116
- buf = lines.pop() || "";
1117
- for (const line of lines) {
1118
- if (!line.startsWith("data: ")) continue;
1119
- try {
1120
- const ev = JSON.parse(line.slice(6));
1121
- if (ev.type === "chunk" && ev.text) {
1122
- let piece = filterOpenCodePassthroughTextChunk(engine, ev.text);
1123
- piece = filterGeminiPassthroughTextChunk(engine, piece);
1124
- if (piece) {
1125
- sawAssistantChunk = true;
1126
- content.textContent += piece;
1127
- box.scrollTop = box.scrollHeight;
1128
- }
1129
- } else if (ev.type === "stderr" && ev.text) {
1130
- const cleaned = stderrFilter.push(ev.text);
1131
- if (cleaned) {
1132
- stderrFilteredAccum += cleaned;
1133
- let stderrPiece = filterOpenCodePassthroughTextChunk(engine, cleaned);
1134
- stderrPiece = filterGeminiPassthroughTextChunk(engine, stderrPiece);
1135
- const inkEngines = engine === "opencode" || engine === "antigravity";
1136
- // Match Vibe for OpenCode: Ink status lines often on stderr; don't spam main bubble
1137
- // after assistant text (no separate trace panel in dashboard passthrough bubble).
1138
- const appendStderr =
1139
- stderrPiece &&
1140
- (!inkEngines || !sawAssistantChunk);
1141
- if (appendStderr) {
1142
- content.textContent += stderrPiece;
1143
- box.scrollTop = box.scrollHeight;
1144
- }
1145
- }
1146
- } else if (ev.type === "done") {
1147
- const tail = stderrFilter.flush();
1148
- if (tail) {
1149
- stderrFilteredAccum += tail;
1150
- let tailPiece = filterOpenCodePassthroughTextChunk(engine, tail);
1151
- tailPiece = filterGeminiPassthroughTextChunk(engine, tailPiece);
1152
- const inkEngines = engine === "opencode" || engine === "antigravity";
1153
- if (
1154
- tailPiece &&
1155
- (!inkEngines || !sawAssistantChunk)
1156
- ) {
1157
- content.textContent += tailPiece;
1158
- box.scrollTop = box.scrollHeight;
1159
- }
1160
- }
1161
- const exitCode = ev.exitCode ?? 0;
1162
- const ok = exitCode === 0;
1163
- label.textContent += ` ${ok ? "✓" : "⚠"} (exit ${exitCode})`;
1164
- const topErr = summarizePassthroughTopErrorLine(
1165
- stderrFilteredAccum,
1166
- engine,
1167
- );
1168
- if (!ok && topErr && !content.textContent.includes(topErr)) {
1169
- const hintEl = document.createElement("div");
1170
- hintEl.style.cssText =
1171
- "font-size:11px;font-weight:600;color:var(--danger, #f87171);margin-top:8px;white-space:pre-wrap;word-break:break-word;";
1172
- hintEl.textContent = `↳ ${topErr}`;
1173
- bubble.appendChild(hintEl);
1174
- box.scrollTop = box.scrollHeight;
1175
- }
1176
- savePassthroughMsg("user", engine, text, null);
1177
- savePassthroughMsg(
1178
- "engine",
1179
- engine,
1180
- content.textContent,
1181
- exitCode,
1182
- );
1183
- // Don't complete task since we didn't register it
1184
- // taskManager.completeTask(taskId);
1185
- }
1186
- } catch {}
1187
- }
1188
- }
1189
- const strayStderr = stderrFilter.flush();
1190
- if (strayStderr) {
1191
- stderrFilteredAccum += strayStderr;
1192
- let stray = filterOpenCodePassthroughTextChunk(engine, strayStderr);
1193
- stray = filterGeminiPassthroughTextChunk(engine, stray);
1194
- const inkEngines = engine === "opencode" || engine === "antigravity";
1195
- if (stray && (!inkEngines || !sawAssistantChunk)) {
1196
- content.textContent += stray;
1197
- box.scrollTop = box.scrollHeight;
1198
- }
1199
- }
1200
- } catch (e) {
1201
- if (e.name === "AbortError") {
1202
- label.textContent += " ✗ (killed)";
1203
- content.textContent += content.textContent
1204
- ? "\n\n[stopped]"
1205
- : "[stopped]";
1206
- // Don't stop task since we didn't register it
1207
- // taskManager.stopTask(taskId);
1208
- } else {
1209
- content.textContent = "Error: " + e.message;
1210
- // Don't fail task since we didn't register it
1211
- // taskManager.failTask(taskId, e.message);
1212
- }
1213
- } finally {
1214
- _passthroughAbort = null;
1215
- if (stopBtn) {
1216
- stopBtn.style.display = "none";
1217
- }
1218
- // input.disabled = false; // Already enabled for concurrent mode
1219
- if (sendBtn) {
1220
- sendBtn.disabled = false;
1221
- sendBtn.textContent = "Send";
1222
- sendBtn.className = "btn-green";
1223
- }
1224
- input.focus();
1225
- // Update session badge after run completes (Gemini/Codex may now have a session)
1226
- refreshSessionIndicator();
1227
- }
1228
- }
1229
-
1230
- function killPassthrough() {
1231
- if (_passthroughAbort) {
1232
- _passthroughAbort.abort();
1233
- _passthroughAbort = null;
1234
- }
1235
- }
1236
-
1237
- async function stopAll() {
1238
- if (!confirm("Stop all running pipelines?")) return;
1239
- try {
1240
- await postJSON("/api/crew-lead/chat", {
1241
- message: "@@STOP",
1242
- sessionId: getChatSessionId(),
1243
- });
1244
- showNotification("⏹ Stop signal sent");
1245
- } catch (e) {
1246
- showNotification("Failed: " + e.message, true);
1247
- }
1248
- }
1249
-
1250
- async function killAll() {
1251
- if (!confirm("Kill all agents? Bridges must be restarted after.")) return;
1252
- try {
1253
- await postJSON("/api/crew-lead/chat", {
1254
- message: "@@KILL",
1255
- sessionId: getChatSessionId(),
1256
- });
1257
- showNotification("☠️ Kill signal sent");
1258
- } catch (e) {
1259
- showNotification("Failed: " + e.message, true);
1260
- }
1261
- }
1262
-
1263
- // ── Multimodal Functions ─────────────────────────────────────────────────
1264
-
1265
- let mediaRecorder = null;
1266
- let audioChunks = [];
1267
-
1268
- function fileToBase64(file) {
1269
- return new Promise((resolve, reject) => {
1270
- const reader = new FileReader();
1271
- reader.onload = () => resolve(reader.result);
1272
- reader.onerror = reject;
1273
- reader.readAsDataURL(file);
1274
- });
1275
- }
1276
-
1277
- async function handleImageUpload(file, customPrompt) {
1278
- let fileToProcess;
1279
-
1280
- if (file instanceof File) {
1281
- fileToProcess = file;
1282
- } else {
1283
- const fileInput = document.getElementById("imageUpload");
1284
- if (!fileInput.files || !fileInput.files[0]) return;
1285
- fileToProcess = fileInput.files[0];
1286
- fileInput.value = ""; // Reset for next upload
1287
- }
1288
-
1289
- const fileName = fileToProcess.name;
1290
- const fileType = fileToProcess.type;
1291
- const fileSize = (fileToProcess.size / 1024).toFixed(1);
1292
-
1293
- // Check file type and handle accordingly
1294
- const isImage = fileType.startsWith("image/");
1295
- const isPDF = fileType === "application/pdf";
1296
- const isExcel =
1297
- fileType.includes("spreadsheet") ||
1298
- fileType.includes("excel") ||
1299
- fileName.match(/\.(xlsx?|csv)$/i);
1300
- const isDoc =
1301
- fileType.includes("document") || fileName.match(/\.(docx?|txt|md)$/i);
1302
-
1303
- let fileIcon = "📎";
1304
- if (isImage) fileIcon = "📷";
1305
- else if (isPDF) fileIcon = "📄";
1306
- else if (isExcel) fileIcon = "📊";
1307
- else if (isDoc) fileIcon = "📝";
1308
-
1309
- // Get any text from input to send with the file
1310
- const chatInput = document.getElementById("chatInput");
1311
- const userText = chatInput ? chatInput.value.trim() : "";
1312
- const promptToUse =
1313
- customPrompt ||
1314
- userText ||
1315
- (isImage
1316
- ? "Describe this image in detail. What do you see?"
1317
- : `Analyze this ${fileName} file`);
1318
-
1319
- appendChatBubble(
1320
- "user",
1321
- `${fileIcon} [Attached: ${fileName}] ${userText ? `\n\n${userText}` : ""}`,
1322
- );
1323
- appendChatBubble(
1324
- "assistant",
1325
- `🔍 Analyzing ${isImage ? "image" : "file"}...`,
1326
- );
1327
-
1328
- try {
1329
- const base64 = await fileToBase64(fileToProcess);
1330
-
1331
- const result = await postJSON("/api/analyze-image", {
1332
- image: base64,
1333
- prompt: promptToUse,
1334
- fileName: fileName,
1335
- fileType: fileType,
1336
- });
1337
-
1338
- if (result.ok) {
1339
- appendChatBubble(
1340
- "assistant",
1341
- `**${isImage ? "Image" : "File"} Analysis:**\n\n${result.result}`,
1342
- );
1343
-
1344
- // Put analysis in input for user to follow up
1345
- if (chatInput) {
1346
- chatInput.value = `[Attached: ${fileName}]\n\n${result.result}\n\n`;
1347
- chatInput.focus();
1348
- }
1349
- } else {
1350
- appendChatBubble("assistant", `⚠️ Analysis failed: ${result.error}`);
1351
- }
1352
- } catch (err) {
1353
- appendChatBubble("assistant", `⚠️ Analysis error: ${err.message}`);
1354
- }
1355
-
1356
- // Clear input after sending
1357
- if (chatInput && userText) {
1358
- chatInput.value = "";
1359
- }
1360
- }
1361
-
1362
- async function toggleVoiceRecording() {
1363
- const btn = document.getElementById("recordVoiceBtn");
1364
-
1365
- if (!mediaRecorder || mediaRecorder.state === "inactive") {
1366
- try {
1367
- const stream = await navigator.mediaDevices.getUserMedia({
1368
- audio: true,
1369
- });
1370
- audioChunks = [];
1371
-
1372
- mediaRecorder = new MediaRecorder(stream, { mimeType: "audio/webm" });
1373
-
1374
- mediaRecorder.ondataavailable = (e) => {
1375
- if (e.data.size > 0) audioChunks.push(e.data);
1376
- };
1377
-
1378
- mediaRecorder.onstop = async () => {
1379
- const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
1380
- stream.getTracks().forEach((track) => track.stop()); // Stop mic access
1381
-
1382
- appendChatBubble(
1383
- "user",
1384
- `🎤 [Voice message recorded - ${(audioBlob.size / 1024).toFixed(0)} KB]`,
1385
- );
1386
- appendChatBubble("assistant", "🎤 Transcribing voice...");
1387
-
1388
- try {
1389
- const formData = new FormData();
1390
- formData.append("audio", audioBlob, "voice.webm");
1391
-
1392
- const controller = new AbortController();
1393
- const timeoutId = setTimeout(() => controller.abort(), 60000); // 60s for Groq
1394
- const response = await fetch("/api/transcribe-audio", {
1395
- method: "POST",
1396
- body: formData,
1397
- signal: controller.signal,
1398
- });
1399
- clearTimeout(timeoutId);
1400
-
1401
- let result;
1402
- try {
1403
- result = await response.json();
1404
- } catch (parseErr) {
1405
- appendChatBubble(
1406
- "assistant",
1407
- `⚠️ Transcription error: Server returned invalid response (${response.status})`,
1408
- );
1409
- audioChunks = [];
1410
- return;
1411
- }
1412
-
1413
- if (result.ok && result.transcription) {
1414
- appendChatBubble(
1415
- "assistant",
1416
- `**Transcription:**\n\n"${result.transcription}"`,
1417
- );
1418
-
1419
- // Put transcription in input for user to send
1420
- const chatInput = document.getElementById("chatInput");
1421
- chatInput.value = result.transcription;
1422
- chatInput.focus();
1423
- } else {
1424
- appendChatBubble(
1425
- "assistant",
1426
- `⚠️ Transcription failed: ${result.error || "No result"}`,
1427
- );
1428
- }
1429
- } catch (err) {
1430
- const msg = err.message || String(err);
1431
- const hint = msg === "Failed to fetch"
1432
- ? " (Is the dashboard running on port 4319? Try: npm run restart-dashboard)"
1433
- : "";
1434
- appendChatBubble(
1435
- "assistant",
1436
- `⚠️ Transcription error: ${msg}${hint}`,
1437
- );
1438
- }
1439
-
1440
- audioChunks = [];
1441
- };
1442
-
1443
- mediaRecorder.start();
1444
- btn.textContent = "⏹️";
1445
- btn.style.background = "var(--red, #ef4444)";
1446
- showNotification("🎤 Recording... Click again to stop");
1447
- } catch (err) {
1448
- showNotification("⚠️ Microphone access denied: " + err.message, true);
1449
- }
1450
- } else {
1451
- // Stop recording
1452
- mediaRecorder.stop();
1453
- btn.textContent = "🎤";
1454
- btn.style.background = "";
1455
- }
1456
- }
1457
-
1458
- // Setup drag-and-drop for images/files
1459
- function setupDragAndDrop() {
1460
- const chatInput = document.getElementById("chatInput");
1461
- const chatMessages = document.getElementById("chatMessages");
1462
-
1463
- [chatInput, chatMessages].forEach((el) => {
1464
- if (!el) return;
1465
-
1466
- el.addEventListener("dragover", (e) => {
1467
- e.preventDefault();
1468
- e.stopPropagation();
1469
- el.style.outline = "2px dashed var(--accent, #3b82f6)";
1470
- });
1471
-
1472
- el.addEventListener("dragleave", (e) => {
1473
- e.preventDefault();
1474
- e.stopPropagation();
1475
- el.style.outline = "";
1476
- });
1477
-
1478
- el.addEventListener("drop", async (e) => {
1479
- e.preventDefault();
1480
- e.stopPropagation();
1481
- el.style.outline = "";
1482
-
1483
- const files = e.dataTransfer.files;
1484
- if (files && files.length > 0) {
1485
- const file = files[0];
1486
- await handleImageUpload(file);
1487
- }
1488
- });
1489
- });
1490
- }
1491
-
1492
- // Initialize drag-and-drop on module load
1493
- setupDragAndDrop();
1494
-
1495
- // ── Direct Agent Chat (Dashboard Chat Bridge) ─────────────────────────────────
1496
- // Works like TG/WA bridges: direct LLM call with @@CLI support
1497
-
1498
- async function sendDirectAgent(text, agentId) {
1499
- const input = document.getElementById("chatInput");
1500
- const sendBtn = document.querySelector('[data-action="sendChat"]');
1501
- const box = document.getElementById("chatMessages");
1502
-
1503
- input.value = "";
1504
- appendChatBubble("user", text);
1505
- setLastAppendedUserContent(text);
1506
- setLastSentContent(text);
1507
-
1508
- // Fetch agent info for display
1509
- let agentInfo = { emoji: "🤖", name: agentId, model: "" };
1510
- try {
1511
- const agentsData = await getJSON("/api/agents-config");
1512
- const agent = (agentsData.agents || []).find((a) => a.id === agentId);
1513
- if (agent) {
1514
- agentInfo = {
1515
- emoji: agent.emoji || "🤖",
1516
- name: agent.name || agentId,
1517
- model: formatAgentModelLabel(agent),
1518
- };
1519
- }
1520
- } catch (err) {
1521
- console.warn("Could not fetch agent info:", err);
1522
- }
1523
-
1524
- // Show typing indicator with agent identity
1525
- const typingId = "typing-" + Date.now();
1526
- const typingDiv = document.createElement("div");
1527
- typingDiv.id = typingId;
1528
- typingDiv.style.cssText =
1529
- "font-size:12px;color:var(--text-3);padding:4px 6px;";
1530
- typingDiv.textContent = `${agentInfo.emoji} ${agentInfo.name} is thinking...`;
1531
- box.appendChild(typingDiv);
1532
- box.scrollTop = box.scrollHeight;
1533
-
1534
- try {
1535
- const activeProjectId = resolveVisibleChatProjectId();
1536
- const response = await postJSON("/api/chat/unified", {
1537
- mode: "agent",
1538
- agentId,
1539
- message: text,
1540
- sessionId: `dashboard-chat-${agentId}-${getChatSessionId()}`,
1541
- projectId: activeProjectId || "general",
1542
- });
1543
-
1544
- // Remove typing indicator
1545
- document.querySelectorAll('[id^="typing-"]').forEach((el) => el.remove());
1546
-
1547
- if (response.error) {
1548
- // Create custom error bubble with agent identity
1549
- appendCustomAgentBubble(agentInfo, "⚠️ " + response.error, box);
1550
- setLastAppendedAssistantContent("");
1551
- return;
1552
- }
1553
-
1554
- if (response.reply) {
1555
- // Create custom reply bubble with agent identity and model
1556
- appendCustomAgentBubble(agentInfo, response.reply, box);
1557
- setLastAppendedAssistantContent(response.reply);
1558
- }
1559
-
1560
- // Show CLI execution status
1561
- if (response.cliInvoked) {
1562
- const cliNote = document.createElement("div");
1563
- cliNote.style.cssText =
1564
- "font-size:11px;color:var(--text-3);text-align:center;padding:4px;";
1565
- cliNote.textContent = `⚡ Executing ${response.cliInvoked}... (check process status)`;
1566
- box.appendChild(cliNote);
1567
- }
1568
-
1569
- box.scrollTop = box.scrollHeight;
1570
- } catch (err) {
1571
- document.querySelectorAll('[id^="typing-"]').forEach((el) => el.remove());
1572
- appendCustomAgentBubble(agentInfo, "⚠️ Error: " + err.message, box);
1573
- setLastAppendedAssistantContent("");
1574
- }
1575
- }
1576
-
1577
- // Helper to create chat bubble with specific agent identity
1578
- function appendCustomAgentBubble(agentInfo, text, box) {
1579
- const div = document.createElement("div");
1580
- div.style.cssText =
1581
- "display:flex;flex-direction:column;align-items:flex-start;gap:4px;";
1582
-
1583
- const labelEl = document.createElement("div");
1584
- labelEl.style.cssText =
1585
- "font-size:11px;color:var(--text-3);padding:0 6px;display:flex;align-items:center;gap:6px;";
1586
- labelEl.textContent = `${agentInfo.emoji} ${agentInfo.name}`;
1587
-
1588
- // Show model badge
1589
- if (agentInfo.model) {
1590
- const badge = document.createElement("span");
1591
- badge.title = "Primary model";
1592
- badge.style.cssText =
1593
- "font-size:10px;padding:1px 6px;border-radius:999px;background:rgba(52,211,153,0.1);color:#34d399;border:1px solid rgba(52,211,153,0.2);cursor:default;";
1594
- const [provider, ...modelParts] = agentInfo.model.split("/");
1595
- badge.textContent = modelParts.join("/") || agentInfo.model;
1596
- labelEl.appendChild(badge);
1597
- }
1598
-
1599
- const bubble = document.createElement("div");
1600
- bubble.style.cssText =
1601
- "max-width:80%;padding:10px 14px;border-radius:14px 14px 14px 4px;background:var(--surface-2);color:var(--text-2);white-space:pre-wrap;word-break:break-word;line-height:1.5;border:1px solid var(--border);";
1602
- bubble.textContent = text;
1603
-
1604
- div.appendChild(labelEl);
1605
- div.appendChild(bubble);
1606
- box.appendChild(div);
1607
- }
1608
-
1609
- function getAgentRouteAndModel(agent) {
1610
- if (agent.useCursorCli) {
1611
- return { route: "cursor", model: agent.cursorCliModel || "auto" };
1612
- }
1613
- if (agent.useClaudeCode) {
1614
- return { route: "claude", model: agent.claudeCodeModel || "auto" };
1615
- }
1616
- if (agent.useCodex) {
1617
- return { route: "codex", model: agent.codexModel || "auto" };
1618
- }
1619
- if (agent.useGeminiCli) {
1620
- return { route: "gemini", model: agent.geminiCliModel || "auto" };
1621
- }
1622
- if (agent.useCrewCLI) {
1623
- return { route: "crew-cli", model: agent.crewCliModel || "auto" };
1624
- }
1625
- if (agent.useOpenCode === true) {
1626
- return {
1627
- route: "opencode",
1628
- model: agent.opencodeModel || agent.model || "default",
1629
- };
1630
- }
1631
- return { route: "llm", model: agent.model || "no model" };
1632
- }
1633
-
1634
- function formatAgentModelLabel(agent) {
1635
- const { route, model } = getAgentRouteAndModel(agent);
1636
- if (route === "llm") return model;
1637
- return `${route}:${model}`;
1638
- }
1639
-
1640
- let lastAgentSelectorRefreshAt = 0;
1641
-
1642
- // Load agent list into unified selector
1643
- async function loadChatAgentSelector(force = false) {
1644
- if (!force && Date.now() - lastAgentSelectorRefreshAt < 5000) return;
1645
-
1646
- // NEW: Load agents into the unified chatModeSelector
1647
- const modeSelector = document.getElementById("chatModeSelector");
1648
- const agentsOptgroup = document.getElementById("agentsOptgroup");
1649
-
1650
- if (modeSelector && agentsOptgroup) {
1651
- try {
1652
- const data = await getJSON("/api/agents-config");
1653
- const agents = data.agents || [];
1654
-
1655
- // Filter out coordinators
1656
- const excludeAgents = new Set([
1657
- "crew-lead",
1658
- "orchestrator",
1659
- "crew-orchestrator",
1660
- "crew-pm-cli",
1661
- "crew-pm-frontend",
1662
- "crew-pm-core",
1663
- ]);
1664
-
1665
- // Clear and repopulate agents optgroup
1666
- agentsOptgroup.innerHTML = "";
1667
- agents
1668
- .filter((a) => !excludeAgents.has(a.id))
1669
- .sort((a, b) => a.id.localeCompare(b.id))
1670
- .forEach((agent) => {
1671
- const opt = document.createElement("option");
1672
- opt.value = agent.id;
1673
- const emoji = agent.emoji || "🤖";
1674
- const modelName = formatAgentModelLabel(agent);
1675
- opt.textContent = `${emoji} ${agent.id} — ${modelName}`;
1676
- agentsOptgroup.appendChild(opt);
1677
- });
1678
- lastAgentSelectorRefreshAt = Date.now();
1679
- } catch (err) {
1680
- console.error("Failed to load agents for unified mode selector:", err);
1681
- }
1682
- }
1683
-
1684
- // LEGACY: Also populate old chatAgentSelector if it exists
1685
- const selector = document.getElementById("chatAgentSelector");
1686
- if (!selector) return;
1687
-
1688
- try {
1689
- const data = await getJSON("/api/agents-config");
1690
- const agents = data.agents || [];
1691
-
1692
- // Clear existing options (keep default)
1693
- selector.innerHTML = '<option value="">🧠 Crew Lead (default)</option>';
1694
-
1695
- // Add agents (exclude crew-lead and coordinators)
1696
- const excludeAgents = new Set([
1697
- "crew-lead",
1698
- "orchestrator",
1699
- "crew-orchestrator",
1700
- ]);
1701
-
1702
- agents
1703
- .filter((a) => !excludeAgents.has(a.id))
1704
- .sort((a, b) => a.id.localeCompare(b.id))
1705
- .forEach((agent) => {
1706
- const opt = document.createElement("option");
1707
- opt.value = agent.id;
1708
- const modelName = formatAgentModelLabel(agent);
1709
- opt.textContent = `${agent.id} — ${modelName}`;
1710
- selector.appendChild(opt);
1711
- });
1712
- } catch (err) {
1713
- console.error("Failed to load agents for chat selector:", err);
1714
- }
1715
- }
1716
-
1717
- // Load agents on init
1718
- loadChatAgentSelector();
1719
-
1720
- // Keep model labels fresh when opening/focusing the selector.
1721
- document.getElementById("chatModeSelector")?.addEventListener("focus", () => {
1722
- loadChatAgentSelector(true);
1723
- });
1724
-
1725
- // Poll for CLI process status when agent is selected
1726
- let processStatusInterval = null;
1727
-
1728
- function startCLIProcessMonitoring() {
1729
- if (processStatusInterval) clearInterval(processStatusInterval);
1730
-
1731
- processStatusInterval = setInterval(async () => {
1732
- // NEW: Check unified selector
1733
- const modeSelector = document.getElementById("chatModeSelector");
1734
- const selectedMode = modeSelector?.value || "crew-lead";
1735
-
1736
- // Extract agent ID (handle both 'agent-id' and 'cli:name' formats)
1737
- let selectedAgent = null;
1738
- if (selectedMode.startsWith("cli:")) {
1739
- // For CLI mode, no specific agent - hide status
1740
- const statusPanel = document.getElementById("chatCLIProcessStatus");
1741
- if (statusPanel) statusPanel.style.display = "none";
1742
- return;
1743
- } else if (selectedMode !== "crew-lead") {
1744
- selectedAgent = selectedMode;
1745
- }
1746
-
1747
- // LEGACY: fallback to old selector
1748
- if (!selectedAgent) {
1749
- selectedAgent = document.getElementById("chatAgentSelector")?.value;
1750
- }
1751
-
1752
- if (!selectedAgent) {
1753
- // No agent selected - hide status panel
1754
- const statusPanel = document.getElementById("chatCLIProcessStatus");
1755
- if (statusPanel) statusPanel.style.display = "none";
1756
- return;
1757
- }
1758
-
1759
- try {
1760
- const data = await getJSON(`/api/cli-processes?agent=${selectedAgent}`);
1761
- const processes = data.processes || [];
1762
- updateCLIProcessStatus(processes);
1763
- } catch (err) {
1764
- console.error("Failed to load CLI process status:", err);
1765
- }
1766
- }, 3000); // Poll every 3 seconds
1767
- }
1768
-
1769
- function updateCLIProcessStatus(processes) {
1770
- const statusPanel = document.getElementById("chatCLIProcessStatus");
1771
- if (!statusPanel) return;
1772
-
1773
- if (processes.length === 0) {
1774
- statusPanel.style.display = "none";
1775
- return;
1776
- }
1777
-
1778
- statusPanel.style.display = "block";
1779
- statusPanel.innerHTML = processes
1780
- .map((proc) => {
1781
- const duration = formatDuration(proc.duration);
1782
- const idleFor = formatDuration(proc.idleFor);
1783
- const statusColor = proc.status === "running" ? "#22c55e" : "#f59e0b";
1784
- const statusIcon = proc.status === "running" ? "⚡" : "⏸️";
1785
-
1786
- return `
1787
- <div style="border-left:3px solid ${statusColor};padding:8px 12px;background:var(--bg-card2);border-radius:6px;margin-bottom:8px;">
1788
- <div style="display:flex;justify-content:space-between;margin-bottom:6px;">
1789
- <span style="font-weight:600;font-family:monospace;font-size:13px;">${statusIcon} ${proc.cli}</span>
1790
- <span style="text-transform:uppercase;font-size:11px;font-weight:700;color:var(--text-3);">${proc.status}</span>
1791
- </div>
1792
- <div style="font-size:12px;color:var(--text-2);line-height:1.5;">
1793
- <div>Task: ${(proc.task || "unknown").slice(0, 80)}</div>
1794
- <div>Duration: ${duration} | Idle: ${idleFor} | Lines: ${proc.outputLines || 0}</div>
1795
- </div>
1796
- </div>
1797
- `;
1798
- })
1799
- .join("");
1800
- }
1801
-
1802
- function formatDuration(ms) {
1803
- const seconds = Math.floor(ms / 1000);
1804
- if (seconds < 60) return `${seconds}s`;
1805
- const minutes = Math.floor(seconds / 60);
1806
- if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
1807
- const hours = Math.floor(minutes / 60);
1808
- return `${hours}h ${minutes % 60}m`;
1809
- }
1810
-
1811
- // Start monitoring
1812
- startCLIProcessMonitoring();
1813
-
1814
- // Re-load agents when switching back to chat view
1815
- document
1816
- .getElementById("chatAgentSelector")
1817
- ?.addEventListener("change", () => {
1818
- const agentId = document.getElementById("chatAgentSelector")?.value;
1819
- if (agentId) {
1820
- showNotification(
1821
- `Switched to ${agentId} - messages go directly to this agent's LLM`,
1822
- "success",
1823
- );
1824
- }
1825
- });
1826
-
1827
- return {
1828
- loadChatHistory,
1829
- waitForChatHistoryIdle,
1830
- chatAtAtInput,
1831
- chatKeydown,
1832
- sendChat,
1833
- sendDirectAgent,
1834
- loadChatAgentSelector,
1835
- clearChatHistory,
1836
- restorePassthroughLog,
1837
- sendPassthrough,
1838
- stopAll,
1839
- killAll,
1840
- killPassthrough,
1841
- refreshSessionIndicator,
1842
- clearPassthroughSession,
1843
- resetSendButton, // Export for use in app.js
1844
- handleImageUpload,
1845
- toggleVoiceRecording,
1846
- };
1847
- }