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.
- package/README.md +22 -9
- package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js +1 -0
- package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -0
- package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js +1 -0
- package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js.br +0 -0
- package/apps/dashboard/dist/assets/index-BeVllEj_.js +2 -0
- package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
- package/apps/dashboard/dist/assets/{index-CF0aJRtC.css → index-D-sRshvg.css} +1 -1
- package/apps/dashboard/dist/assets/index-D-sRshvg.css.br +0 -0
- package/apps/dashboard/dist/assets/tab-benchmarks-tab-BHjKCPm3.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js +1 -0
- package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-pm-loop-tab-Bfd449B4.js → tab-pm-loop-tab-DiAPTJXu.js} +1 -1
- package/apps/dashboard/dist/assets/tab-pm-loop-tab-DiAPTJXu.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-projects-tab-DhNWnlzt.js → tab-projects-tab-SFH4E--a.js} +1 -1
- package/apps/dashboard/dist/assets/tab-projects-tab-SFH4E--a.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js +1 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js +1 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js +1 -0
- package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js.br +0 -0
- package/apps/dashboard/dist/index.html +135 -15
- package/apps/dashboard/dist/index.html.br +0 -0
- package/apps/dashboard/dist/index.html.gz +0 -0
- package/apps/vibe/README.md +2 -2
- package/apps/vibe/package.json +1 -1
- package/apps/vibe/server.mjs +101 -56
- package/crew-lead.mjs +34 -4
- package/lib/bridges/cli-executor.mjs +1 -1
- package/lib/bridges/gateway-ws.mjs +4 -0
- package/lib/browser/passthrough-stderr.js +1 -0
- package/lib/chat/project-messages.mjs +3 -5
- package/lib/cli-process-tracker.mjs +3 -2
- package/lib/contacts/identity-linker.mjs +1 -0
- package/lib/crew-judge/judge.mjs +19 -18
- package/lib/crew-lead/agent-manager.mjs +1 -1
- package/lib/crew-lead/background.mjs +14 -1
- package/lib/crew-lead/chat-handler.mjs +38 -1
- package/lib/crew-lead/http-server.mjs +106 -57
- package/lib/crew-lead/llm-caller.mjs +24 -8
- package/lib/crew-lead/prompts.mjs +14 -1
- package/lib/crew-lead/tools.mjs +3 -2
- package/lib/crew-lead/wave-dispatcher.mjs +19 -5
- package/lib/crew-lead/ws-router.mjs +219 -27
- package/lib/engines/crew-cli.mjs +1 -1
- package/lib/engines/engine-registry.mjs +14 -3
- package/lib/engines/rt-envelope.mjs +1 -0
- package/lib/engines/runners.mjs +28 -4
- package/lib/gemini-cli-passthrough-noise.mjs +1 -1
- package/lib/integrations/code-search.mjs +4 -3
- package/lib/memory/shared-adapter.mjs +23 -10
- package/lib/pipeline/manager.mjs +2 -1
- package/lib/runtime/config.mjs +1 -1
- package/lib/runtime/paths.mjs +12 -8
- package/lib/runtime/spending.mjs +2 -1
- package/package.json +42 -14
- package/scripts/capture-build-flow.mjs +118 -0
- package/scripts/coverage-report.mjs +209 -0
- package/scripts/coverage-summary.mjs +47 -0
- package/scripts/dashboard-validation.mjs +76 -0
- package/scripts/dashboard.mjs +1667 -551
- package/scripts/generate-openapi.mjs +683 -277
- package/scripts/live-bridge-matrix.mjs +79 -0
- package/scripts/live-cli-matrix.mjs +166 -0
- package/scripts/live-crewchat-check.mjs +42 -0
- package/scripts/live-engine-matrix.mjs +50 -0
- package/scripts/live-provider-failover-matrix.mjs +107 -0
- package/scripts/live-provider-matrix.mjs +228 -0
- package/scripts/restart-all-from-repo.sh +4 -4
- package/scripts/restart-service.sh +12 -9
- package/scripts/smoke-dispatch.mjs +4 -1
- package/scripts/test-blast-radius.mjs +204 -0
- package/scripts/test-report-summary.mjs +88 -0
- package/scripts/test-reporter.mjs +651 -0
- package/scripts/test-rerun.mjs +136 -0
- package/scripts/tmux-bridge +130 -0
- package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js +0 -1
- package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js +0 -1
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
- package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
- package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
- package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js +0 -1
- package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
- package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js +0 -1
- package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
- package/apps/dashboard/index.html +0 -6529
- package/apps/dashboard/package.json +0 -15
- package/apps/dashboard/src/app.js +0 -2828
- package/apps/dashboard/src/app.js.br +0 -0
- package/apps/dashboard/src/app.js.gz +0 -0
- package/apps/dashboard/src/chat/chat-actions.js +0 -1847
- package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
- package/apps/dashboard/src/chat/unified-messages.js +0 -327
- package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
- package/apps/dashboard/src/cli-process.js +0 -208
- package/apps/dashboard/src/cli-process.js.br +0 -0
- package/apps/dashboard/src/cli-process.js.gz +0 -0
- package/apps/dashboard/src/components/active-tasks-panel.js +0 -175
- package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
- package/apps/dashboard/src/core/api.js +0 -18
- package/apps/dashboard/src/core/api.js.br +0 -0
- package/apps/dashboard/src/core/dom.js +0 -228
- package/apps/dashboard/src/core/dom.js.br +0 -0
- package/apps/dashboard/src/core/state.js +0 -91
- package/apps/dashboard/src/core/state.js.br +0 -0
- package/apps/dashboard/src/core/task-manager.js +0 -134
- package/apps/dashboard/src/core/task-manager.js.br +0 -0
- package/apps/dashboard/src/orchestration-status.js +0 -127
- package/apps/dashboard/src/orchestration-status.js.br +0 -0
- package/apps/dashboard/src/setup-wizard.js +0 -562
- package/apps/dashboard/src/setup-wizard.js.br +0 -0
- package/apps/dashboard/src/styles.css +0 -2085
- package/apps/dashboard/src/styles.css.br +0 -0
- package/apps/dashboard/src/styles.css.gz +0 -0
- package/apps/dashboard/src/tabs/agents-tab.js +0 -2237
- package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
- package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/comms-tab.js +0 -955
- package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
- package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/engines-tab.js +0 -175
- package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/memory-tab.js +0 -182
- package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/models-tab.js +0 -450
- package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
- package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/projects-tab.js +0 -663
- package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
- package/apps/dashboard/src/tabs/prompts-tab.js +0 -160
- package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/services-tab.js +0 -202
- package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/settings-tab.js +0 -861
- package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/skills-tab.js +0 -284
- package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/spending-tab.js +0 -173
- package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
- package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
- package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/usage-tab.js +0 -390
- package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/waves-tab.js +0 -238
- package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
- package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
- package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
- package/apps/vibe/.crew/cost.json +0 -17
- package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
- package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
- package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
- package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
- package/apps/vibe/.crew/sandbox.json +0 -7
- package/apps/vibe/.crew/session.json +0 -330
- package/apps/vibe/.crew/training-data.jsonl +0 -0
- package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
- package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
- package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
- package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
- package/apps/vibe/ARCHITECTURE.md +0 -3393
- package/apps/vibe/QUICK-REFERENCE.md +0 -211
- package/apps/vibe/ROADMAP.md +0 -41
- package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
- package/apps/vibe/VISUAL-GUIDE.md +0 -378
- package/apps/vibe/capture-demo.mjs +0 -160
- package/apps/vibe/capture-full-demo.mjs +0 -255
- package/apps/vibe/capture-quickstart.mjs +0 -256
- package/apps/vibe/capture-vibe-assets.mjs +0 -71
- package/apps/vibe/capture-vibe-video.mjs +0 -260
- package/apps/vibe/check-buttons.js +0 -41
- package/apps/vibe/diagnose.html +0 -106
- package/apps/vibe/fix-buttons.js +0 -103
- package/apps/vibe/index.html +0 -3404
- package/apps/vibe/package-lock.json +0 -920
- package/apps/vibe/scripts/studio-pty-host.py +0 -117
- package/apps/vibe/src/main.js +0 -2940
- package/apps/vibe/src/register-all-languages.js +0 -98
- package/apps/vibe/start-studio.sh +0 -11
- package/apps/vibe/test/accessibility-tests.js +0 -77
- package/apps/vibe/test/browser-performance-audit.mjs +0 -205
- package/apps/vibe/test/performance-tests.js +0 -120
- package/apps/vibe/test/security-tests.js +0 -213
- package/apps/vibe/tests/e2e.local.mjs +0 -54
- package/apps/vibe/tests/server.smoke.mjs +0 -106
- package/apps/vibe/update_website.mjs +0 -74
- package/apps/vibe/vite.config.js +0 -19
- package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
- 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
|
-
}
|