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