@proflandrigan/shards 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +475 -0
- package/package.json +37 -0
- package/src/agents/academic.md +276 -0
- package/src/agents/ai-engineer.md +377 -0
- package/src/agents/analytics-engineer.md +364 -0
- package/src/agents/applied-ml-scientist.md +410 -0
- package/src/agents/backend-engineer.md +255 -0
- package/src/agents/bi-engineer.md +333 -0
- package/src/agents/data-analyst.md +343 -0
- package/src/agents/data-engineer.md +260 -0
- package/src/agents/data-modeller.md +386 -0
- package/src/agents/data-scientist.md +366 -0
- package/src/agents/deep-learning-engineer.md +389 -0
- package/src/agents/ml-engineer.md +424 -0
- package/src/agents/mlops-engineer.md +339 -0
- package/src/agents/researcher.md +187 -0
- package/src/agents/specific_instructions/academic/critical_review.md +263 -0
- package/src/agents/specific_instructions/academic/report.md +113 -0
- package/src/agents/specific_instructions/ai_engineer/advise.md +162 -0
- package/src/agents/specific_instructions/ai_engineer/bi_engineer_handoff.md +86 -0
- package/src/agents/specific_instructions/ai_engineer/experiment.md +471 -0
- package/src/agents/specific_instructions/ai_engineer/experiment_ui_mode.md +44 -0
- package/src/agents/specific_instructions/ai_engineer/phases/index.md +45 -0
- package/src/agents/specific_instructions/ai_engineer/phases/phase-1.md +55 -0
- package/src/agents/specific_instructions/ai_engineer/phases/phase-2.md +86 -0
- package/src/agents/specific_instructions/ai_engineer/phases/phase-3.md +96 -0
- package/src/agents/specific_instructions/ai_engineer/phases/phase-4.md +138 -0
- package/src/agents/specific_instructions/ai_engineer/phases/phase-5.md +157 -0
- package/src/agents/specific_instructions/ai_engineer/phases/phase-6.md +196 -0
- package/src/agents/specific_instructions/ai_engineer/phases/phase-7.md +313 -0
- package/src/agents/specific_instructions/ai_engineer/phases.md +1011 -0
- package/src/agents/specific_instructions/ai_engineer/prompt_lab.md +161 -0
- package/src/agents/specific_instructions/ai_engineer/prompt_lab_ui_mode.md +28 -0
- package/src/agents/specific_instructions/ai_engineer/research.md +393 -0
- package/src/agents/specific_instructions/ai_engineer/research_ui_mode.md +66 -0
- package/src/agents/specific_instructions/ai_engineer/review.md +159 -0
- package/src/agents/specific_instructions/ai_engineer/validation_checklist.md +182 -0
- package/src/agents/specific_instructions/analytics_engineer/advise.md +155 -0
- package/src/agents/specific_instructions/analytics_engineer/bi_engineer_handoff.md +91 -0
- package/src/agents/specific_instructions/analytics_engineer/data_analyst_handoff.md +84 -0
- package/src/agents/specific_instructions/analytics_engineer/deep_phases.md +818 -0
- package/src/agents/specific_instructions/analytics_engineer/phases_deep/index.md +24 -0
- package/src/agents/specific_instructions/analytics_engineer/phases_deep/phase-1.md +77 -0
- package/src/agents/specific_instructions/analytics_engineer/phases_deep/phase-2.md +106 -0
- package/src/agents/specific_instructions/analytics_engineer/phases_deep/phase-3.md +93 -0
- package/src/agents/specific_instructions/analytics_engineer/phases_deep/phase-4.md +79 -0
- package/src/agents/specific_instructions/analytics_engineer/phases_deep/phase-5.md +61 -0
- package/src/agents/specific_instructions/analytics_engineer/phases_deep/phase-6.md +45 -0
- package/src/agents/specific_instructions/analytics_engineer/phases_deep/phase-7.md +235 -0
- package/src/agents/specific_instructions/analytics_engineer/phases_deep/phase-8.md +221 -0
- package/src/agents/specific_instructions/analytics_engineer/phases_quick/index.md +19 -0
- package/src/agents/specific_instructions/analytics_engineer/phases_quick/phase-1.md +47 -0
- package/src/agents/specific_instructions/analytics_engineer/phases_quick/phase-2.md +78 -0
- package/src/agents/specific_instructions/analytics_engineer/quick_phases.md +112 -0
- package/src/agents/specific_instructions/analytics_engineer/review.md +167 -0
- package/src/agents/specific_instructions/analytics_engineer/service_mode.md +369 -0
- package/src/agents/specific_instructions/analytics_engineer/ui_mode.md +45 -0
- package/src/agents/specific_instructions/analytics_engineer/update.md +162 -0
- package/src/agents/specific_instructions/analytics_engineer/validation_checklist.md +121 -0
- package/src/agents/specific_instructions/applied_ml_scientist/advise.md +143 -0
- package/src/agents/specific_instructions/applied_ml_scientist/phases/index.md +21 -0
- package/src/agents/specific_instructions/applied_ml_scientist/phases/phase-1.md +51 -0
- package/src/agents/specific_instructions/applied_ml_scientist/phases/phase-2.md +66 -0
- package/src/agents/specific_instructions/applied_ml_scientist/phases/phase-3.md +113 -0
- package/src/agents/specific_instructions/applied_ml_scientist/phases/phase-4.md +104 -0
- package/src/agents/specific_instructions/applied_ml_scientist/phases/phase-5.md +156 -0
- package/src/agents/specific_instructions/applied_ml_scientist/phases.md +428 -0
- package/src/agents/specific_instructions/applied_ml_scientist/research.md +379 -0
- package/src/agents/specific_instructions/applied_ml_scientist/review.md +142 -0
- package/src/agents/specific_instructions/applied_ml_scientist/validation_checklist.md +136 -0
- package/src/agents/specific_instructions/backend_engineer/clean.md +149 -0
- package/src/agents/specific_instructions/backend_engineer/review.md +91 -0
- package/src/agents/specific_instructions/backend_engineer/review_checklist.md +54 -0
- package/src/agents/specific_instructions/backend_engineer/service_mode.md +67 -0
- package/src/agents/specific_instructions/bi_engineer/advise.md +137 -0
- package/src/agents/specific_instructions/bi_engineer/data_analyst_handoff.md +77 -0
- package/src/agents/specific_instructions/bi_engineer/incoming_handoff.md +45 -0
- package/src/agents/specific_instructions/bi_engineer/phases/index.md +20 -0
- package/src/agents/specific_instructions/bi_engineer/phases/phase-1.md +164 -0
- package/src/agents/specific_instructions/bi_engineer/phases/phase-2.md +92 -0
- package/src/agents/specific_instructions/bi_engineer/phases/phase-3.md +121 -0
- package/src/agents/specific_instructions/bi_engineer/phases/phase-4.md +106 -0
- package/src/agents/specific_instructions/bi_engineer/phases.md +451 -0
- package/src/agents/specific_instructions/bi_engineer/review.md +166 -0
- package/src/agents/specific_instructions/bi_engineer/update.md +147 -0
- package/src/agents/specific_instructions/bi_engineer/validation_checklist.md +124 -0
- package/src/agents/specific_instructions/data_analyst/advise.md +138 -0
- package/src/agents/specific_instructions/data_analyst/explain.md +221 -0
- package/src/agents/specific_instructions/data_analyst/incoming_handoff.md +40 -0
- package/src/agents/specific_instructions/data_analyst/phases/index.md +20 -0
- package/src/agents/specific_instructions/data_analyst/phases/phase-1.md +159 -0
- package/src/agents/specific_instructions/data_analyst/phases/phase-2.md +112 -0
- package/src/agents/specific_instructions/data_analyst/phases/phase-3.md +265 -0
- package/src/agents/specific_instructions/data_analyst/phases/phase-4.md +100 -0
- package/src/agents/specific_instructions/data_analyst/phases.md +501 -0
- package/src/agents/specific_instructions/data_analyst/review.md +138 -0
- package/src/agents/specific_instructions/data_analyst/ui_mode.md +26 -0
- package/src/agents/specific_instructions/data_analyst/update.md +144 -0
- package/src/agents/specific_instructions/data_analyst/validation_checklist.md +95 -0
- package/src/agents/specific_instructions/data_engineer/advise.md +137 -0
- package/src/agents/specific_instructions/data_engineer/phases.md +466 -0
- package/src/agents/specific_instructions/data_engineer/phases_deep/index.md +23 -0
- package/src/agents/specific_instructions/data_engineer/phases_deep/phase-1.md +49 -0
- package/src/agents/specific_instructions/data_engineer/phases_deep/phase-2.md +93 -0
- package/src/agents/specific_instructions/data_engineer/phases_deep/phase-3.md +55 -0
- package/src/agents/specific_instructions/data_engineer/phases_deep/phase-4.md +48 -0
- package/src/agents/specific_instructions/data_engineer/phases_deep/phase-5.md +40 -0
- package/src/agents/specific_instructions/data_engineer/phases_deep/phase-6.md +102 -0
- package/src/agents/specific_instructions/data_engineer/phases_deep/phase-7.md +87 -0
- package/src/agents/specific_instructions/data_engineer/phases_quick/index.md +19 -0
- package/src/agents/specific_instructions/data_engineer/phases_quick/phase-1.md +45 -0
- package/src/agents/specific_instructions/data_engineer/phases_quick/phase-2.md +54 -0
- package/src/agents/specific_instructions/data_engineer/review.md +135 -0
- package/src/agents/specific_instructions/data_engineer/validation_checklist.md +136 -0
- package/src/agents/specific_instructions/data_modeller/advise.md +137 -0
- package/src/agents/specific_instructions/data_modeller/phases.md +581 -0
- package/src/agents/specific_instructions/data_modeller/phases_deep/index.md +23 -0
- package/src/agents/specific_instructions/data_modeller/phases_deep/phase-1.md +52 -0
- package/src/agents/specific_instructions/data_modeller/phases_deep/phase-2.md +113 -0
- package/src/agents/specific_instructions/data_modeller/phases_deep/phase-3.md +47 -0
- package/src/agents/specific_instructions/data_modeller/phases_deep/phase-4.md +51 -0
- package/src/agents/specific_instructions/data_modeller/phases_deep/phase-5.md +45 -0
- package/src/agents/specific_instructions/data_modeller/phases_deep/phase-6.md +105 -0
- package/src/agents/specific_instructions/data_modeller/phases_deep/phase-7.md +136 -0
- package/src/agents/specific_instructions/data_modeller/phases_quick/index.md +19 -0
- package/src/agents/specific_instructions/data_modeller/phases_quick/phase-1.md +47 -0
- package/src/agents/specific_instructions/data_modeller/phases_quick/phase-2.md +65 -0
- package/src/agents/specific_instructions/data_modeller/review.md +141 -0
- package/src/agents/specific_instructions/data_modeller/service_mode.md +218 -0
- package/src/agents/specific_instructions/data_modeller/validation_checklist.md +125 -0
- package/src/agents/specific_instructions/data_scientist/advise.md +158 -0
- package/src/agents/specific_instructions/data_scientist/bi_engineer_handoff.md +63 -0
- package/src/agents/specific_instructions/data_scientist/experiment.md +482 -0
- package/src/agents/specific_instructions/data_scientist/experiment_ui_mode.md +44 -0
- package/src/agents/specific_instructions/data_scientist/explain.md +247 -0
- package/src/agents/specific_instructions/data_scientist/greenfield_data.md +35 -0
- package/src/agents/specific_instructions/data_scientist/ml_engineer_handoff.md +52 -0
- package/src/agents/specific_instructions/data_scientist/notebook_walkthrough.md +76 -0
- package/src/agents/specific_instructions/data_scientist/phases/index.md +24 -0
- package/src/agents/specific_instructions/data_scientist/phases/phase-1.md +45 -0
- package/src/agents/specific_instructions/data_scientist/phases/phase-2.md +67 -0
- package/src/agents/specific_instructions/data_scientist/phases/phase-3.md +89 -0
- package/src/agents/specific_instructions/data_scientist/phases/phase-4.md +143 -0
- package/src/agents/specific_instructions/data_scientist/phases/phase-5.md +71 -0
- package/src/agents/specific_instructions/data_scientist/phases/phase-6.md +239 -0
- package/src/agents/specific_instructions/data_scientist/phases/phase-7.md +207 -0
- package/src/agents/specific_instructions/data_scientist/phases.md +651 -0
- package/src/agents/specific_instructions/data_scientist/research.md +345 -0
- package/src/agents/specific_instructions/data_scientist/research_ui_mode.md +52 -0
- package/src/agents/specific_instructions/data_scientist/review.md +136 -0
- package/src/agents/specific_instructions/data_scientist/service_mode.md +247 -0
- package/src/agents/specific_instructions/data_scientist/validation_checklist.md +183 -0
- package/src/agents/specific_instructions/deep_learning_engineer/advise.md +145 -0
- package/src/agents/specific_instructions/deep_learning_engineer/phases/index.md +21 -0
- package/src/agents/specific_instructions/deep_learning_engineer/phases/phase-1.md +74 -0
- package/src/agents/specific_instructions/deep_learning_engineer/phases/phase-2.md +98 -0
- package/src/agents/specific_instructions/deep_learning_engineer/phases/phase-3.md +76 -0
- package/src/agents/specific_instructions/deep_learning_engineer/phases/phase-4.md +128 -0
- package/src/agents/specific_instructions/deep_learning_engineer/phases/phase-5.md +292 -0
- package/src/agents/specific_instructions/deep_learning_engineer/phases.md +567 -0
- package/src/agents/specific_instructions/deep_learning_engineer/research.md +389 -0
- package/src/agents/specific_instructions/deep_learning_engineer/review.md +155 -0
- package/src/agents/specific_instructions/deep_learning_engineer/validation_checklist.md +147 -0
- package/src/agents/specific_instructions/ml_engineer/advise.md +174 -0
- package/src/agents/specific_instructions/ml_engineer/bi_engineer_handoff.md +71 -0
- package/src/agents/specific_instructions/ml_engineer/experiment.md +474 -0
- package/src/agents/specific_instructions/ml_engineer/experiment_ui_mode.md +44 -0
- package/src/agents/specific_instructions/ml_engineer/notebook_walkthrough.md +75 -0
- package/src/agents/specific_instructions/ml_engineer/phases/index.md +25 -0
- package/src/agents/specific_instructions/ml_engineer/phases/phase-1.md +49 -0
- package/src/agents/specific_instructions/ml_engineer/phases/phase-2.md +75 -0
- package/src/agents/specific_instructions/ml_engineer/phases/phase-3.md +124 -0
- package/src/agents/specific_instructions/ml_engineer/phases/phase-4.md +279 -0
- package/src/agents/specific_instructions/ml_engineer/phases/phase-5.md +160 -0
- package/src/agents/specific_instructions/ml_engineer/phases/phase-6-5.md +170 -0
- package/src/agents/specific_instructions/ml_engineer/phases/phase-6.md +295 -0
- package/src/agents/specific_instructions/ml_engineer/phases/phase-7.md +337 -0
- package/src/agents/specific_instructions/ml_engineer/phases.md +1068 -0
- package/src/agents/specific_instructions/ml_engineer/research.md +437 -0
- package/src/agents/specific_instructions/ml_engineer/research_ui_mode.md +71 -0
- package/src/agents/specific_instructions/ml_engineer/review.md +187 -0
- package/src/agents/specific_instructions/ml_engineer/service_mode.md +273 -0
- package/src/agents/specific_instructions/ml_engineer/validation_checklist.md +185 -0
- package/src/agents/specific_instructions/mlops_engineer/advise.md +139 -0
- package/src/agents/specific_instructions/mlops_engineer/phases/index.md +23 -0
- package/src/agents/specific_instructions/mlops_engineer/phases/phase-1.md +52 -0
- package/src/agents/specific_instructions/mlops_engineer/phases/phase-2.md +86 -0
- package/src/agents/specific_instructions/mlops_engineer/phases/phase-3.md +105 -0
- package/src/agents/specific_instructions/mlops_engineer/phases/phase-4.md +128 -0
- package/src/agents/specific_instructions/mlops_engineer/phases/phase-5.md +106 -0
- package/src/agents/specific_instructions/mlops_engineer/phases/phase-6.md +128 -0
- package/src/agents/specific_instructions/mlops_engineer/phases/phase-7.md +144 -0
- package/src/agents/specific_instructions/mlops_engineer/phases.md +671 -0
- package/src/agents/specific_instructions/mlops_engineer/review.md +164 -0
- package/src/agents/specific_instructions/mlops_engineer/service_mode.md +81 -0
- package/src/agents/specific_instructions/mlops_engineer/validation_checklist.md +151 -0
- package/src/agents/specific_instructions/researcher/critical_review.md +292 -0
- package/src/agents/specific_instructions/researcher/review_checklist.md +67 -0
- package/src/agents/specific_instructions/researcher/service_mode.md +224 -0
- package/src/agents/specific_instructions/shared/auto_verify_mode.md +141 -0
- package/src/agents/specific_instructions/shared/autonomous_research.md +1289 -0
- package/src/agents/specific_instructions/shared/behavioral_rules.md +36 -0
- package/src/agents/specific_instructions/shared/diverge_protocol.md +387 -0
- package/src/agents/specific_instructions/shared/engineering_guidelines.md +136 -0
- package/src/agents/specific_instructions/shared/experiment_versioning.md +184 -0
- package/src/agents/specific_instructions/shared/goal_mode.md +187 -0
- package/src/agents/specific_instructions/shared/incremental_testing.md +139 -0
- package/src/agents/specific_instructions/shared/intent_discovery.md +223 -0
- package/src/agents/specific_instructions/shared/join_path_protocol.md +168 -0
- package/src/agents/specific_instructions/shared/knowledge_checkpoint.md +83 -0
- package/src/agents/specific_instructions/shared/knowledge_harvest.md +220 -0
- package/src/agents/specific_instructions/shared/knowledge_retrieval.md +100 -0
- package/src/agents/specific_instructions/shared/notebook_walkthrough_protocol.md +367 -0
- package/src/agents/specific_instructions/shared/reviewer_verdict_protocol.md +74 -0
- package/src/agents/specific_instructions/shared/swarm_protocol.md +97 -0
- package/src/agents/specific_instructions/shared/validation_protocol.md +139 -0
- package/src/agents/specific_instructions/syn/arbiter.md +140 -0
- package/src/agents/specific_instructions/syn/brainstorm.md +550 -0
- package/src/agents/specific_instructions/syn/code_review.md +232 -0
- package/src/agents/specific_instructions/syn/diff.md +239 -0
- package/src/agents/specific_instructions/syn/final_review.md +65 -0
- package/src/agents/specific_instructions/syn/fixer.md +240 -0
- package/src/agents/specific_instructions/syn/free_form.md +130 -0
- package/src/agents/specific_instructions/syn/knowledge.md +468 -0
- package/src/agents/specific_instructions/syn/notebook_walkthrough.md +78 -0
- package/src/agents/specific_instructions/syn/panel_review.md +634 -0
- package/src/agents/specific_instructions/syn/pm.md +453 -0
- package/src/agents/specific_instructions/syn/pr_review.md +255 -0
- package/src/agents/specific_instructions/syn/slides.md +417 -0
- package/src/agents/syn.md +729 -0
- package/src/commands/academic.md +41 -0
- package/src/commands/ai-engineer.md +45 -0
- package/src/commands/analytics-engineer.md +48 -0
- package/src/commands/applied-ml-scientist.md +45 -0
- package/src/commands/backend-engineer.md +35 -0
- package/src/commands/bi-engineer.md +40 -0
- package/src/commands/brainstorm.md +24 -0
- package/src/commands/data-analyst.md +38 -0
- package/src/commands/data-engineer.md +37 -0
- package/src/commands/data-modeller.md +38 -0
- package/src/commands/data-scientist.md +38 -0
- package/src/commands/deep-learning-engineer.md +47 -0
- package/src/commands/end.md +49 -0
- package/src/commands/knowledge.md +24 -0
- package/src/commands/ml-engineer.md +42 -0
- package/src/commands/mlops-engineer.md +47 -0
- package/src/commands/notebook-walkthrough.md +58 -0
- package/src/commands/researcher.md +40 -0
- package/src/commands/resume.md +57 -0
- package/src/commands/review-pr.md +26 -0
- package/src/commands/shards-guide.md +41 -0
- package/src/commands/shards-ui.md +32 -0
- package/src/commands/shards.md +41 -0
- package/src/docs/01-getting-started/concepts.md +109 -0
- package/src/docs/01-getting-started/first-session.md +79 -0
- package/src/docs/01-getting-started/install.md +61 -0
- package/src/docs/02-agents/academic.md +71 -0
- package/src/docs/02-agents/ai-engineer.md +78 -0
- package/src/docs/02-agents/analytics-engineer.md +58 -0
- package/src/docs/02-agents/applied-ml-scientist.md +59 -0
- package/src/docs/02-agents/backend-engineer.md +58 -0
- package/src/docs/02-agents/bi-engineer.md +65 -0
- package/src/docs/02-agents/data-analyst.md +67 -0
- package/src/docs/02-agents/data-engineer.md +57 -0
- package/src/docs/02-agents/data-modeller.md +51 -0
- package/src/docs/02-agents/data-scientist.md +78 -0
- package/src/docs/02-agents/deep-learning-engineer.md +64 -0
- package/src/docs/02-agents/ml-engineer.md +80 -0
- package/src/docs/02-agents/mlops-engineer.md +59 -0
- package/src/docs/02-agents/overview.md +62 -0
- package/src/docs/02-agents/researcher.md +73 -0
- package/src/docs/02-agents/syn.md +88 -0
- package/src/docs/03-protocols/auto-verify.md +82 -0
- package/src/docs/03-protocols/autonomous-research.md +59 -0
- package/src/docs/03-protocols/behavioral-rules.md +35 -0
- package/src/docs/03-protocols/diverge.md +50 -0
- package/src/docs/03-protocols/engineering-guidelines.md +56 -0
- package/src/docs/03-protocols/experiment-versioning.md +38 -0
- package/src/docs/03-protocols/gate-pattern.md +65 -0
- package/src/docs/03-protocols/incremental-testing.md +68 -0
- package/src/docs/03-protocols/join-path.md +46 -0
- package/src/docs/03-protocols/knowledge-ledger.md +70 -0
- package/src/docs/03-protocols/reviewer-verdicts.md +39 -0
- package/src/docs/03-protocols/swarm.md +40 -0
- package/src/docs/03-protocols/validation.md +174 -0
- package/src/docs/04-ui/activity-bar.md +70 -0
- package/src/docs/04-ui/chat-pane.md +80 -0
- package/src/docs/04-ui/code-intel.md +62 -0
- package/src/docs/04-ui/file-editing.md +61 -0
- package/src/docs/04-ui/git.md +54 -0
- package/src/docs/04-ui/keybindings.md +79 -0
- package/src/docs/04-ui/knowledge-map.md +76 -0
- package/src/docs/04-ui/overview.md +93 -0
- package/src/docs/04-ui/panels.md +49 -0
- package/src/docs/04-ui/pinboard-selection.md +66 -0
- package/src/docs/04-ui/quick-open-palette.md +56 -0
- package/src/docs/04-ui/sessions.md +81 -0
- package/src/docs/04-ui/settings-permissions.md +56 -0
- package/src/docs/05-commands/reference.md +59 -0
- package/src/docs/06-outputs/directory-map.md +116 -0
- package/src/docs/07-workflows/ai-eval-first.md +57 -0
- package/src/docs/07-workflows/deep-study-to-production.md +76 -0
- package/src/docs/07-workflows/diverge-exploration.md +77 -0
- package/src/docs/07-workflows/quick-analysis.md +45 -0
- package/src/docs/08-integrations/claude-code-auto-mode.md +191 -0
- package/src/docs/08-integrations/google-slides.md +175 -0
- package/src/docs/README.md +30 -0
- package/src/docs/manifest.json +108 -0
- package/src/templates/analysis-template.md +20 -0
- package/src/templates/branch-report.md +46 -0
- package/src/templates/diff-report.md +88 -0
- package/src/templates/knowledge-index.md +7 -0
- package/src/templates/model-card-schema.json +186 -0
- package/src/templates/model-card-schema.md +88 -0
- package/src/templates/model-card.md +124 -0
- package/src/templates/project-plan.md +47 -0
- package/src/templates/project-specs.md +81 -0
- package/src/templates/report-template.md +43 -0
- package/src/templates/study-template.md +25 -0
- package/src/ui/cc-readonly.js +181 -0
- package/src/ui/chat-session.js +466 -0
- package/src/ui/css/base.css +136 -0
- package/src/ui/css/brainstorm.css +525 -0
- package/src/ui/css/chat.css +1405 -0
- package/src/ui/css/editor.css +546 -0
- package/src/ui/css/eval-dashboard.css +157 -0
- package/src/ui/css/experiment.css +237 -0
- package/src/ui/css/guide.css +186 -0
- package/src/ui/css/knowledge-map.css +383 -0
- package/src/ui/css/layout.css +431 -0
- package/src/ui/css/model-card.css +161 -0
- package/src/ui/css/notebook-walkthrough.css +271 -0
- package/src/ui/css/pr-review.css +403 -0
- package/src/ui/css/prompt-lab.css +325 -0
- package/src/ui/css/sessions.css +258 -0
- package/src/ui/css/sidebar.css +661 -0
- package/src/ui/css/terminal.css +113 -0
- package/src/ui/css/theme-light.css +542 -0
- package/src/ui/index.html +389 -0
- package/src/ui/js/agents.js +32 -0
- package/src/ui/js/bookmarks.js +230 -0
- package/src/ui/js/chat.js +1776 -0
- package/src/ui/js/code-intel.js +328 -0
- package/src/ui/js/command-palette.js +142 -0
- package/src/ui/js/events.js +591 -0
- package/src/ui/js/explorer.js +317 -0
- package/src/ui/js/file-view.js +477 -0
- package/src/ui/js/git.js +536 -0
- package/src/ui/js/guide.js +198 -0
- package/src/ui/js/hud.js +75 -0
- package/src/ui/js/init.js +351 -0
- package/src/ui/js/knowledge-map.js +906 -0
- package/src/ui/js/markdown.js +114 -0
- package/src/ui/js/monaco.js +164 -0
- package/src/ui/js/notebook-walkthrough.js +272 -0
- package/src/ui/js/notebook.js +448 -0
- package/src/ui/js/panels.js +2681 -0
- package/src/ui/js/pinboard.js +186 -0
- package/src/ui/js/quick-open.js +164 -0
- package/src/ui/js/selection-context.js +131 -0
- package/src/ui/js/sessions.js +256 -0
- package/src/ui/js/settings.js +476 -0
- package/src/ui/js/split-view.js +82 -0
- package/src/ui/js/state.js +343 -0
- package/src/ui/js/table.js +161 -0
- package/src/ui/js/tabs.js +284 -0
- package/src/ui/js/tabular.js +125 -0
- package/src/ui/js/terminal.js +354 -0
- package/src/ui/js/timeline.js +137 -0
- package/src/ui/js/utils.js +293 -0
- package/src/ui/notebook-kernel.py +790 -0
- package/src/ui/open-browser.js +55 -0
- package/src/ui/permission-pattern.js +42 -0
- package/src/ui/relay.js +513 -0
- package/src/ui/server.js +3072 -0
- package/src/ui/session-index.js +225 -0
- package/src/ui/shards_icon.png +0 -0
- package/src/ui/spawn-server.js +41 -0
- package/src/ui/symbol-index.js +813 -0
- package/src/ui/ui-push.js +177 -0
- package/tools/gate-hook/VALIDATION_SPEC.md +273 -0
- package/tools/gate-hook/__tests__/auto-verify.test.js +343 -0
- package/tools/gate-hook/auto-allowlist.js +179 -0
- package/tools/gate-hook/auto-state.js +68 -0
- package/tools/gate-hook/classify.js +21 -0
- package/tools/gate-hook/log.js +57 -0
- package/tools/gate-hook/parser.js +205 -0
- package/tools/gate-hook/sql-guard.js +230 -0
- package/tools/gate-hook/state.js +170 -0
- package/tools/gate-hook/sweep.js +139 -0
- package/tools/gate-hook/transcript.js +45 -0
- package/tools/gate-hook/validation.js +321 -0
- package/tools/gate-hook.js +475 -0
- package/tools/install.js +914 -0
- package/tools/shards-gates.js +311 -0
- package/tools/shards-sessions.js +261 -0
- package/tools/shards-ui.js +377 -0
|
@@ -0,0 +1,1776 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// Image attachments (paste / drag-and-drop)
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
var ATTACHMENT_MAX_COUNT = 5;
|
|
6
|
+
var ATTACHMENT_MAX_BYTES = 5 * 1024 * 1024; // 5MB decoded
|
|
7
|
+
var ATTACHMENT_ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
8
|
+
|
|
9
|
+
function _attachmentSession() {
|
|
10
|
+
// Attachments live on the active session so they don't bleed across tabs.
|
|
11
|
+
// Fall back to a global staging slot when no session has been started yet
|
|
12
|
+
// (e.g., user pastes into the agent-picker search box) — currently unused
|
|
13
|
+
// since the picker doesn't accept paste, but guards against null deref.
|
|
14
|
+
var s = (typeof getActiveSession === 'function') ? getActiveSession() : null;
|
|
15
|
+
if (s && !s.pendingAttachments) s.pendingAttachments = [];
|
|
16
|
+
return s;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function _formatBytes(n) {
|
|
20
|
+
if (n < 1024) return n + ' B';
|
|
21
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(0) + ' KB';
|
|
22
|
+
return (n / (1024 * 1024)).toFixed(1) + ' MB';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderAttachmentChips() {
|
|
26
|
+
var wrap = document.getElementById('chat-attachments');
|
|
27
|
+
if (!wrap) return;
|
|
28
|
+
var session = _attachmentSession();
|
|
29
|
+
var items = (session && session.pendingAttachments) ? session.pendingAttachments : [];
|
|
30
|
+
|
|
31
|
+
if (items.length === 0) {
|
|
32
|
+
wrap.style.display = 'none';
|
|
33
|
+
wrap.innerHTML = '';
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
wrap.style.display = 'flex';
|
|
38
|
+
wrap.innerHTML = '';
|
|
39
|
+
for (var i = 0; i < items.length; i++) {
|
|
40
|
+
var a = items[i];
|
|
41
|
+
var chip = document.createElement('div');
|
|
42
|
+
chip.className = 'attachment-chip';
|
|
43
|
+
chip.innerHTML =
|
|
44
|
+
'<img class="attachment-chip-thumb" src="' + a.previewUrl + '" alt="' + esc(a.name || 'image') + '">' +
|
|
45
|
+
'<div class="attachment-chip-meta">' +
|
|
46
|
+
'<span class="attachment-chip-name">' + esc(a.name || 'pasted image') + '</span>' +
|
|
47
|
+
'<span class="attachment-chip-size">' + _formatBytes(a.sizeBytes) + '</span>' +
|
|
48
|
+
'</div>' +
|
|
49
|
+
'<button class="attachment-chip-remove" title="Remove" data-id="' + a.id + '">×</button>';
|
|
50
|
+
chip.querySelector('.attachment-chip-remove').addEventListener('click', (function(id) {
|
|
51
|
+
return function() { removeAttachment(id); };
|
|
52
|
+
})(a.id));
|
|
53
|
+
wrap.appendChild(chip);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function removeAttachment(id) {
|
|
58
|
+
var session = _attachmentSession();
|
|
59
|
+
if (!session) return;
|
|
60
|
+
session.pendingAttachments = (session.pendingAttachments || []).filter(function(a) { return a.id !== id; });
|
|
61
|
+
renderAttachmentChips();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function clearAttachments() {
|
|
65
|
+
var session = _attachmentSession();
|
|
66
|
+
if (!session) return;
|
|
67
|
+
session.pendingAttachments = [];
|
|
68
|
+
renderAttachmentChips();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _addAttachmentFromFile(file) {
|
|
72
|
+
var session = _attachmentSession();
|
|
73
|
+
if (!session) {
|
|
74
|
+
addSystemNotice('Start a chat session before attaching images.');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!file || !file.type || ATTACHMENT_ALLOWED_MIME.indexOf(file.type) === -1) {
|
|
78
|
+
addSystemNotice('Only JPEG, PNG, GIF, and WebP images are supported.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (file.size > ATTACHMENT_MAX_BYTES) {
|
|
82
|
+
addSystemNotice('Image exceeds the 5MB limit.');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if ((session.pendingAttachments || []).length >= ATTACHMENT_MAX_COUNT) {
|
|
86
|
+
addSystemNotice('Attachment limit reached (' + ATTACHMENT_MAX_COUNT + ').');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
var reader = new FileReader();
|
|
90
|
+
reader.onload = function(ev) {
|
|
91
|
+
var dataUrl = ev.target.result || '';
|
|
92
|
+
var commaIdx = dataUrl.indexOf(',');
|
|
93
|
+
if (commaIdx === -1) {
|
|
94
|
+
addSystemNotice('Failed to read pasted image.');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
var dataBase64 = dataUrl.slice(commaIdx + 1);
|
|
98
|
+
var entry = {
|
|
99
|
+
id: 'att_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8),
|
|
100
|
+
mediaType: file.type,
|
|
101
|
+
dataBase64: dataBase64,
|
|
102
|
+
sizeBytes: file.size,
|
|
103
|
+
name: file.name || ('pasted-' + new Date().toISOString().replace(/[:.]/g, '-') + '.' + (file.type.split('/')[1] || 'png')),
|
|
104
|
+
previewUrl: dataUrl,
|
|
105
|
+
};
|
|
106
|
+
if (!session.pendingAttachments) session.pendingAttachments = [];
|
|
107
|
+
session.pendingAttachments.push(entry);
|
|
108
|
+
renderAttachmentChips();
|
|
109
|
+
};
|
|
110
|
+
reader.onerror = function() {
|
|
111
|
+
addSystemNotice('Failed to read pasted image.');
|
|
112
|
+
};
|
|
113
|
+
reader.readAsDataURL(file);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function handleChatInputPaste(e) {
|
|
117
|
+
var items = (e.clipboardData && e.clipboardData.items) || [];
|
|
118
|
+
var imageFiles = [];
|
|
119
|
+
for (var i = 0; i < items.length; i++) {
|
|
120
|
+
var it = items[i];
|
|
121
|
+
if (it.kind === 'file' && it.type && it.type.indexOf('image/') === 0) {
|
|
122
|
+
var f = it.getAsFile();
|
|
123
|
+
if (f) imageFiles.push(f);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (imageFiles.length === 0) return; // let the browser handle text paste normally
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
for (var j = 0; j < imageFiles.length; j++) {
|
|
129
|
+
_addAttachmentFromFile(imageFiles[j]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function handleChatInputDragOver(e) {
|
|
134
|
+
if (!e.dataTransfer || !e.dataTransfer.types) return;
|
|
135
|
+
if (Array.prototype.indexOf.call(e.dataTransfer.types, 'Files') === -1) return;
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
var wrap = document.getElementById('chat-attachments');
|
|
138
|
+
if (wrap) wrap.classList.add('drag-over');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function handleChatInputDragLeave() {
|
|
142
|
+
var wrap = document.getElementById('chat-attachments');
|
|
143
|
+
if (wrap) wrap.classList.remove('drag-over');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function handleChatInputDrop(e) {
|
|
147
|
+
if (!e.dataTransfer) return;
|
|
148
|
+
var files = e.dataTransfer.files || [];
|
|
149
|
+
var imageFiles = [];
|
|
150
|
+
for (var i = 0; i < files.length; i++) {
|
|
151
|
+
if (files[i].type && files[i].type.indexOf('image/') === 0) imageFiles.push(files[i]);
|
|
152
|
+
}
|
|
153
|
+
if (imageFiles.length === 0) return;
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
var wrap = document.getElementById('chat-attachments');
|
|
156
|
+
if (wrap) wrap.classList.remove('drag-over');
|
|
157
|
+
for (var k = 0; k < imageFiles.length; k++) {
|
|
158
|
+
_addAttachmentFromFile(imageFiles[k]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ═══════════════════════════════════════════════════════════════
|
|
163
|
+
// Permission mode cycling (Shift+Tab)
|
|
164
|
+
// ═══════════════════════════════════════════════════════════════
|
|
165
|
+
|
|
166
|
+
var PERMISSION_MODES = [
|
|
167
|
+
{ key: 'acceptEdits', label: 'Accept Edits', shortLabel: 'Edits', color: '#4CAF50', desc: 'Read and edit files' },
|
|
168
|
+
{ key: 'plan', label: 'Plan', shortLabel: 'Plan', color: '#2196F3', desc: 'Explore and plan, no edits' },
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
var currentPermissionMode = 'acceptEdits';
|
|
172
|
+
|
|
173
|
+
function cyclePermissionMode(direction) {
|
|
174
|
+
var idx = PERMISSION_MODES.findIndex(function(m) { return m.key === currentPermissionMode; });
|
|
175
|
+
if (idx === -1) idx = 0;
|
|
176
|
+
idx = (idx + (direction || 1) + PERMISSION_MODES.length) % PERMISSION_MODES.length;
|
|
177
|
+
var newMode = PERMISSION_MODES[idx].key;
|
|
178
|
+
if (newMode === currentPermissionMode) return;
|
|
179
|
+
currentPermissionMode = newMode;
|
|
180
|
+
renderModeIndicator();
|
|
181
|
+
applyModeToSession();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function renderModeIndicator() {
|
|
185
|
+
var btn = document.getElementById('mode-indicator');
|
|
186
|
+
if (!btn) return;
|
|
187
|
+
var mode = PERMISSION_MODES.find(function(m) { return m.key === currentPermissionMode; });
|
|
188
|
+
if (!mode) return;
|
|
189
|
+
btn.textContent = mode.label;
|
|
190
|
+
btn.style.borderColor = mode.color;
|
|
191
|
+
btn.style.color = mode.color;
|
|
192
|
+
btn.title = mode.desc + ' (Shift+Tab to cycle)';
|
|
193
|
+
btn.className = 'mode-' + mode.key;
|
|
194
|
+
// Pulse animation on change
|
|
195
|
+
btn.classList.remove('mode-flash');
|
|
196
|
+
void btn.offsetWidth; // reflow
|
|
197
|
+
btn.classList.add('mode-flash');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function applyModeToSession() {
|
|
201
|
+
var session = getActiveSession();
|
|
202
|
+
if (!session || !activeSessionId) return;
|
|
203
|
+
// Post mode change to server — restarts session with new permission mode
|
|
204
|
+
authFetch('/chat/mode', {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
headers: { 'Content-Type': 'application/json' },
|
|
207
|
+
body: JSON.stringify({ sessionId: activeSessionId, mode: currentPermissionMode }),
|
|
208
|
+
}).then(function(res) { return res.json(); }).then(function(data) {
|
|
209
|
+
if (data.switched && data.sessionId) {
|
|
210
|
+
replaceSessionInTab(activeSessionId, data.sessionId, data.agent);
|
|
211
|
+
}
|
|
212
|
+
}).catch(function() {
|
|
213
|
+
addSystemNotice('Failed to switch permission mode — server unreachable.');
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ═══════════════════════════════════════════════════════════════
|
|
218
|
+
// Agent picker
|
|
219
|
+
// ═══════════════════════════════════════════════════════════════
|
|
220
|
+
|
|
221
|
+
async function loadAgentPicker() {
|
|
222
|
+
var picker = document.getElementById('agent-picker');
|
|
223
|
+
picker.classList.add('visible');
|
|
224
|
+
|
|
225
|
+
if (!agentList) {
|
|
226
|
+
try {
|
|
227
|
+
var res = await authFetch('/agents');
|
|
228
|
+
agentList = await res.json();
|
|
229
|
+
} catch(e) {
|
|
230
|
+
picker.innerHTML = '<div class="empty-state">Failed to load agents</div>';
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
picker.innerHTML = '';
|
|
236
|
+
|
|
237
|
+
// Search input
|
|
238
|
+
var searchWrap = document.createElement('div');
|
|
239
|
+
searchWrap.className = 'agent-picker-search-wrap';
|
|
240
|
+
var searchInput = document.createElement('input');
|
|
241
|
+
searchInput.type = 'text';
|
|
242
|
+
searchInput.className = 'agent-picker-search';
|
|
243
|
+
searchInput.placeholder = 'What do you need help with?';
|
|
244
|
+
searchWrap.appendChild(searchInput);
|
|
245
|
+
picker.appendChild(searchWrap);
|
|
246
|
+
|
|
247
|
+
// Syn hero card (pinned, unaffected by search)
|
|
248
|
+
var synAgent = agentList.find(function(a) { return a.name === 'syn'; });
|
|
249
|
+
if (synAgent) {
|
|
250
|
+
var synInfo = { color: synAgent.color || '#FFD700', label: synAgent.label || 'Syn (Orchestrator)' };
|
|
251
|
+
var hero = document.createElement('div');
|
|
252
|
+
hero.className = 'agent-card agent-card-hero';
|
|
253
|
+
hero.innerHTML =
|
|
254
|
+
'<span class="agent-card-dot" style="background:' + synInfo.color + '"></span>' +
|
|
255
|
+
'<div class="agent-card-hero-body">' +
|
|
256
|
+
'<span class="agent-card-name">' + esc(synInfo.label) + '</span>' +
|
|
257
|
+
'<span class="agent-card-hero-tagline">Describe what you need above and press Enter — Syn will route you to the right specialist.</span>' +
|
|
258
|
+
'</div>';
|
|
259
|
+
hero.addEventListener('click', function() { startNewSession('syn'); });
|
|
260
|
+
picker.appendChild(hero);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Non-Syn agents grouped by category
|
|
264
|
+
var CATEGORY_ORDER = ['data', 'analytics', 'mlai', 'review'];
|
|
265
|
+
var CATEGORY_LABELS = { data: 'DATA', analytics: 'ANALYTICS', mlai: 'ML / AI', review: 'REVIEW' };
|
|
266
|
+
|
|
267
|
+
var grouped = {};
|
|
268
|
+
CATEGORY_ORDER.forEach(function(cat) { grouped[cat] = []; });
|
|
269
|
+
for (var i = 0; i < agentList.length; i++) {
|
|
270
|
+
var agent = agentList[i];
|
|
271
|
+
if (agent.name === 'syn') continue;
|
|
272
|
+
var cat = agent.category || 'review';
|
|
273
|
+
if (grouped[cat]) grouped[cat].push(agent);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
CATEGORY_ORDER.forEach(function(cat) {
|
|
277
|
+
var agents = grouped[cat];
|
|
278
|
+
if (!agents || !agents.length) return;
|
|
279
|
+
|
|
280
|
+
var header = document.createElement('div');
|
|
281
|
+
header.className = 'agent-picker-section-header';
|
|
282
|
+
header.textContent = CATEGORY_LABELS[cat];
|
|
283
|
+
picker.appendChild(header);
|
|
284
|
+
|
|
285
|
+
agents.forEach(function(agent) {
|
|
286
|
+
var card = document.createElement('div');
|
|
287
|
+
card.className = 'agent-card';
|
|
288
|
+
card.innerHTML =
|
|
289
|
+
'<span class="agent-card-dot" style="background:' + (agent.color || '#3860c0') + '"></span>' +
|
|
290
|
+
'<span class="agent-card-name">' + esc(agent.label || agent.name) + '</span>' +
|
|
291
|
+
'<span class="agent-card-desc">' + esc(agent.description || '') + '</span>';
|
|
292
|
+
card.addEventListener('click', (function(name) { return function() { startNewSession(name); }; })(agent.name));
|
|
293
|
+
picker.appendChild(card);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Prompt → Syn auto-start
|
|
298
|
+
searchInput.addEventListener('keydown', function(e) {
|
|
299
|
+
if (e.key === 'Enter') {
|
|
300
|
+
var q = searchInput.value.trim();
|
|
301
|
+
startNewSession('syn', q || undefined);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
setTimeout(function() { searchInput.focus(); }, 50);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function showAgentPicker() {
|
|
309
|
+
document.getElementById('messages').style.display = 'none';
|
|
310
|
+
document.getElementById('chat-input-area').classList.remove('visible');
|
|
311
|
+
loadAgentPicker();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function showChatView() {
|
|
315
|
+
document.getElementById('agent-picker').classList.remove('visible');
|
|
316
|
+
document.getElementById('messages').style.display = 'flex';
|
|
317
|
+
document.getElementById('chat-input-area').classList.add('visible');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ═══════════════════════════════════════════════════════════════
|
|
321
|
+
// Chat session control
|
|
322
|
+
// ═══════════════════════════════════════════════════════════════
|
|
323
|
+
|
|
324
|
+
async function startNewSession(agentName, initialMessage, options) {
|
|
325
|
+
options = options || {};
|
|
326
|
+
try {
|
|
327
|
+
var body = { agent: agentName || null, permissionMode: currentPermissionMode };
|
|
328
|
+
if (initialMessage) body.initialMessage = initialMessage;
|
|
329
|
+
if (options.resumeSessionId) body.resumeSessionId = options.resumeSessionId;
|
|
330
|
+
if (options.model) body.model = options.model;
|
|
331
|
+
var res = await authFetch('/chat/start', {
|
|
332
|
+
method: 'POST',
|
|
333
|
+
headers: { 'Content-Type': 'application/json' },
|
|
334
|
+
body: JSON.stringify(body),
|
|
335
|
+
});
|
|
336
|
+
var data = await res.json();
|
|
337
|
+
if (data.error) {
|
|
338
|
+
addSystemNotice(data.error);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Save current session (chat DOM + workspace)
|
|
343
|
+
var oldSession = getActiveSession();
|
|
344
|
+
if (oldSession) {
|
|
345
|
+
var messagesEl = document.getElementById('messages');
|
|
346
|
+
oldSession.scrollTop = messagesEl.scrollTop;
|
|
347
|
+
var frag = document.createDocumentFragment();
|
|
348
|
+
while (messagesEl.firstChild) {
|
|
349
|
+
frag.appendChild(messagesEl.firstChild);
|
|
350
|
+
}
|
|
351
|
+
oldSession.domFragment = frag;
|
|
352
|
+
saveSessionWorkspace(oldSession);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Dispose DOM-bound instances before swapping workspace
|
|
356
|
+
disposeMonacoInstance();
|
|
357
|
+
disposeNotebookCellMonaco();
|
|
358
|
+
destroyTabulator();
|
|
359
|
+
|
|
360
|
+
var session = createSessionState(data.sessionId, agentName);
|
|
361
|
+
activeSessionId = data.sessionId;
|
|
362
|
+
loadSessionWorkspace(session);
|
|
363
|
+
|
|
364
|
+
showChatView();
|
|
365
|
+
document.getElementById('messages').innerHTML = '';
|
|
366
|
+
session.hasMessages = false;
|
|
367
|
+
|
|
368
|
+
activateAgent(agentName);
|
|
369
|
+
renderSessionTabs();
|
|
370
|
+
|
|
371
|
+
// Render fresh workspace (no files yet)
|
|
372
|
+
renderWsTabs();
|
|
373
|
+
showActiveContent();
|
|
374
|
+
|
|
375
|
+
var chatInput = document.getElementById('chat-input');
|
|
376
|
+
chatInput.focus();
|
|
377
|
+
} catch (err) {
|
|
378
|
+
addSystemNotice('Failed to start session — server unreachable.');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function sendChatMessage() {
|
|
383
|
+
var session = getActiveSession();
|
|
384
|
+
var input = document.getElementById('chat-input');
|
|
385
|
+
var message = input.value.trim();
|
|
386
|
+
var attachments = (session && session.pendingAttachments) ? session.pendingAttachments.slice() : [];
|
|
387
|
+
if ((!message && attachments.length === 0) || !session || session.chatResponding) return;
|
|
388
|
+
|
|
389
|
+
input.value = '';
|
|
390
|
+
input.style.height = 'auto';
|
|
391
|
+
|
|
392
|
+
// Slash commands take the text path and ignore attachments (drop them silently)
|
|
393
|
+
var lower = message.toLowerCase();
|
|
394
|
+
var isSlash = (lower === '/clear' || lower === '/config' || lower === '/mode' || lower === '/exit');
|
|
395
|
+
|
|
396
|
+
// Client-side /clear — no server round-trip needed
|
|
397
|
+
if (lower === '/clear') {
|
|
398
|
+
document.getElementById('messages').innerHTML = '';
|
|
399
|
+
session.messages = [];
|
|
400
|
+
session.hasMessages = false;
|
|
401
|
+
clearSelectionContext();
|
|
402
|
+
clearAttachments();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Client-side /config — open settings panel, no server round-trip
|
|
407
|
+
if (lower === '/config') {
|
|
408
|
+
toggleSettings();
|
|
409
|
+
clearAttachments();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Client-side /mode — cycle permission mode
|
|
414
|
+
if (lower === '/mode') {
|
|
415
|
+
cyclePermissionMode(1);
|
|
416
|
+
clearAttachments();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Client-side /exit — stop session and close tab
|
|
421
|
+
if (lower === '/exit') {
|
|
422
|
+
if (session && activeSessionId) {
|
|
423
|
+
closeSessionTab(activeSessionId);
|
|
424
|
+
}
|
|
425
|
+
clearAttachments();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (isSlash) return;
|
|
430
|
+
|
|
431
|
+
// Augment message with selection context if present
|
|
432
|
+
var fullMessage = formatSelectionContextForMessage(message);
|
|
433
|
+
clearSelectionContext();
|
|
434
|
+
|
|
435
|
+
// Prepend pinned context (fetches file contents for pinned files)
|
|
436
|
+
fullMessage = await formatPinnedContextForMessage(fullMessage);
|
|
437
|
+
|
|
438
|
+
// Clear staged attachments locally now that we've captured them for this send
|
|
439
|
+
if (attachments.length > 0) {
|
|
440
|
+
session.pendingAttachments = [];
|
|
441
|
+
renderAttachmentChips();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Wire format sent to the server: only id/mediaType/dataBase64 are required
|
|
445
|
+
// server-side. previewUrl/name/sizeBytes ride along for the transcript so the
|
|
446
|
+
// chat history can render thumbnails on reload.
|
|
447
|
+
var wireAttachments = attachments.map(function(a) {
|
|
448
|
+
return {
|
|
449
|
+
mediaType: a.mediaType,
|
|
450
|
+
dataBase64: a.dataBase64,
|
|
451
|
+
name: a.name,
|
|
452
|
+
sizeBytes: a.sizeBytes,
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
var res = await authFetch('/chat/send', {
|
|
458
|
+
method: 'POST',
|
|
459
|
+
headers: { 'Content-Type': 'application/json' },
|
|
460
|
+
body: JSON.stringify({
|
|
461
|
+
message: fullMessage,
|
|
462
|
+
sessionId: activeSessionId,
|
|
463
|
+
attachments: wireAttachments,
|
|
464
|
+
}),
|
|
465
|
+
});
|
|
466
|
+
var data = await res.json();
|
|
467
|
+
|
|
468
|
+
// Update client state for agent switches
|
|
469
|
+
if (data.switched && data.agent) {
|
|
470
|
+
replaceSessionInTab(activeSessionId, data.sessionId, data.agent);
|
|
471
|
+
}
|
|
472
|
+
} catch (err) {
|
|
473
|
+
addSystemNotice('Message failed to send — server unreachable.');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function stopChat() {
|
|
478
|
+
if (!activeSessionId) return;
|
|
479
|
+
var sid = activeSessionId;
|
|
480
|
+
try {
|
|
481
|
+
await authFetch('/chat/stop', {
|
|
482
|
+
method: 'POST',
|
|
483
|
+
headers: { 'Content-Type': 'application/json' },
|
|
484
|
+
body: JSON.stringify({ sessionId: sid }),
|
|
485
|
+
});
|
|
486
|
+
} catch(e) {
|
|
487
|
+
console.warn('[shards] stop request failed:', e);
|
|
488
|
+
}
|
|
489
|
+
endSessionTab(sid);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function endSessionTab(sessionId) {
|
|
493
|
+
var wasActive = (activeSessionId === sessionId);
|
|
494
|
+
removeSessionState(sessionId);
|
|
495
|
+
|
|
496
|
+
if (wasActive) {
|
|
497
|
+
// Dispose DOM-bound instances
|
|
498
|
+
disposeMonacoInstance();
|
|
499
|
+
disposeNotebookCellMonaco();
|
|
500
|
+
destroyTabulator();
|
|
501
|
+
|
|
502
|
+
if (sessionOrder.length > 0) {
|
|
503
|
+
// Switch to adjacent session (handles workspace load + renderSessionTabs)
|
|
504
|
+
var targetSid = sessionOrder[sessionOrder.length - 1];
|
|
505
|
+
activeSessionId = targetSid;
|
|
506
|
+
loadSessionWorkspace(chatSessions[targetSid]);
|
|
507
|
+
|
|
508
|
+
// Restore chat
|
|
509
|
+
var newSession = chatSessions[targetSid];
|
|
510
|
+
var messagesEl = document.getElementById('messages');
|
|
511
|
+
messagesEl.innerHTML = '';
|
|
512
|
+
if (newSession.domFragment) {
|
|
513
|
+
messagesEl.appendChild(newSession.domFragment);
|
|
514
|
+
newSession.domFragment = null;
|
|
515
|
+
messagesEl.scrollTop = newSession.scrollTop;
|
|
516
|
+
} else {
|
|
517
|
+
newSession.hasMessages = false;
|
|
518
|
+
rebuildMessages(newSession.messages);
|
|
519
|
+
}
|
|
520
|
+
showChatView();
|
|
521
|
+
activateAgent(newSession.agent);
|
|
522
|
+
setChatInputEnabled(!newSession.chatResponding);
|
|
523
|
+
applySplitLayout();
|
|
524
|
+
renderWsTabs();
|
|
525
|
+
showActiveContent();
|
|
526
|
+
renderSessionTabs();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// No sessions left — reset workspace to defaults
|
|
531
|
+
activeSessionId = null;
|
|
532
|
+
loadSessionWorkspace(null);
|
|
533
|
+
setChatInputEnabled(true);
|
|
534
|
+
applySplitLayout();
|
|
535
|
+
renderWsTabs();
|
|
536
|
+
showActiveContent();
|
|
537
|
+
showAgentPicker();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
renderSessionTabs();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function closeSessionTab(sessionId) {
|
|
544
|
+
try {
|
|
545
|
+
await authFetch('/chat/stop', {
|
|
546
|
+
method: 'POST',
|
|
547
|
+
headers: { 'Content-Type': 'application/json' },
|
|
548
|
+
body: JSON.stringify({ sessionId: sessionId }),
|
|
549
|
+
});
|
|
550
|
+
} catch(e) {
|
|
551
|
+
console.warn('[shards] close session request failed:', e);
|
|
552
|
+
}
|
|
553
|
+
endSessionTab(sessionId);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function replaceSessionInTab(oldSessionId, newSessionId, agent) {
|
|
557
|
+
var oldSession = getSessionState(oldSessionId);
|
|
558
|
+
var title = oldSession ? oldSession.title : null;
|
|
559
|
+
|
|
560
|
+
// Preserve workspace from old session
|
|
561
|
+
var preservedWorkspace = null;
|
|
562
|
+
if (oldSession) {
|
|
563
|
+
preservedWorkspace = {
|
|
564
|
+
openFiles: oldSession.openFiles,
|
|
565
|
+
fileTabOrder: oldSession.fileTabOrder,
|
|
566
|
+
activeTabId: oldSession.activeTabId,
|
|
567
|
+
splitMode: oldSession.splitMode,
|
|
568
|
+
currentFileInPane: oldSession.currentFileInPane,
|
|
569
|
+
openPanels: oldSession.openPanels,
|
|
570
|
+
panelTabOrder: oldSession.panelTabOrder,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Remove old session from map (but preserve position in order)
|
|
575
|
+
delete chatSessions[oldSessionId];
|
|
576
|
+
var idx = sessionOrder.indexOf(oldSessionId);
|
|
577
|
+
|
|
578
|
+
// Create new session state
|
|
579
|
+
var newSession = createSessionState(newSessionId, agent);
|
|
580
|
+
newSession.title = title;
|
|
581
|
+
|
|
582
|
+
// Restore workspace into new session
|
|
583
|
+
if (preservedWorkspace) {
|
|
584
|
+
newSession.openFiles = preservedWorkspace.openFiles;
|
|
585
|
+
newSession.fileTabOrder = preservedWorkspace.fileTabOrder;
|
|
586
|
+
newSession.activeTabId = preservedWorkspace.activeTabId;
|
|
587
|
+
newSession.splitMode = preservedWorkspace.splitMode;
|
|
588
|
+
newSession.currentFileInPane = preservedWorkspace.currentFileInPane;
|
|
589
|
+
newSession.openPanels = preservedWorkspace.openPanels;
|
|
590
|
+
newSession.panelTabOrder = preservedWorkspace.panelTabOrder;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Fix position: createSessionState pushes to end, move to old position
|
|
594
|
+
if (idx !== -1) {
|
|
595
|
+
var endIdx = sessionOrder.indexOf(newSessionId);
|
|
596
|
+
if (endIdx !== -1) sessionOrder.splice(endIdx, 1);
|
|
597
|
+
var oldIdx = sessionOrder.indexOf(oldSessionId);
|
|
598
|
+
if (oldIdx !== -1) {
|
|
599
|
+
sessionOrder.splice(oldIdx, 1, newSessionId);
|
|
600
|
+
} else {
|
|
601
|
+
sessionOrder.splice(idx, 0, newSessionId);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (activeSessionId === oldSessionId) {
|
|
606
|
+
activeSessionId = newSessionId;
|
|
607
|
+
// Reload workspace globals from new session
|
|
608
|
+
loadSessionWorkspace(newSession);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Reset UI for active session
|
|
612
|
+
if (activeSessionId === newSessionId) {
|
|
613
|
+
document.getElementById('messages').innerHTML = '';
|
|
614
|
+
newSession.hasMessages = false;
|
|
615
|
+
activateAgent(agent);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
renderSessionTabs();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function setChatInputEnabled(enabled) {
|
|
622
|
+
var input = document.getElementById('chat-input');
|
|
623
|
+
var btn = document.getElementById('chat-send-btn');
|
|
624
|
+
input.disabled = !enabled;
|
|
625
|
+
btn.disabled = !enabled;
|
|
626
|
+
var session = getActiveSession();
|
|
627
|
+
if (session) session.chatResponding = !enabled;
|
|
628
|
+
if (enabled) {
|
|
629
|
+
removeWorkingIndicator();
|
|
630
|
+
} else {
|
|
631
|
+
showWorkingIndicator();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ═══════════════════════════════════════════════════════════════
|
|
636
|
+
// Session tab bar
|
|
637
|
+
// ═══════════════════════════════════════════════════════════════
|
|
638
|
+
|
|
639
|
+
// R1 — short relative-time string for tooltips ("12s", "4m", "2h")
|
|
640
|
+
function formatRelativeTime(ts) {
|
|
641
|
+
if (!ts) return '';
|
|
642
|
+
var diff = Math.max(0, Date.now() - ts);
|
|
643
|
+
var s = Math.floor(diff / 1000);
|
|
644
|
+
if (s < 60) return s + 's';
|
|
645
|
+
var m = Math.floor(s / 60);
|
|
646
|
+
if (m < 60) return m + 'm';
|
|
647
|
+
var h = Math.floor(m / 60);
|
|
648
|
+
if (h < 24) return h + 'h';
|
|
649
|
+
return Math.floor(h / 24) + 'd';
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// R2 — derive the per-tab subline ("now doing") text.
|
|
653
|
+
// Priority: needsAttention reason > responding state > lastSignal > idle.
|
|
654
|
+
function deriveTabSubline(session) {
|
|
655
|
+
if (!session) return '';
|
|
656
|
+
if (session.needsAttention) {
|
|
657
|
+
if (session.attentionReason === 'gate') return 'gate reached';
|
|
658
|
+
if (session.attentionReason === 'permission') return 'permission required';
|
|
659
|
+
if (session.attentionReason === 'error') return 'error';
|
|
660
|
+
if (session.attentionReason === 'turn-end') return 'awaiting input';
|
|
661
|
+
}
|
|
662
|
+
var sig = session.lastSignal;
|
|
663
|
+
if (session.chatResponding) {
|
|
664
|
+
if (sig && sig.kind === 'tool') return '→ ' + sig.label;
|
|
665
|
+
if (sig && sig.kind === 'consult') return '↘ consulting ' + sig.label;
|
|
666
|
+
return 'responding…';
|
|
667
|
+
}
|
|
668
|
+
if (sig) {
|
|
669
|
+
if (sig.kind === 'gate') return '⏸ ' + sig.label;
|
|
670
|
+
if (sig.kind === 'tool') return '→ ' + sig.label;
|
|
671
|
+
if (sig.kind === 'file') return '✎ ' + sig.label;
|
|
672
|
+
if (sig.kind === 'consult') return '↘ ' + sig.label;
|
|
673
|
+
if (sig.kind === 'error') return '⚠ ' + sig.label;
|
|
674
|
+
}
|
|
675
|
+
if (session.lastActivityAt) return 'idle · ' + formatRelativeTime(session.lastActivityAt);
|
|
676
|
+
return '';
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Truncate a free-form prompt to a short snippet suitable for a tab label.
|
|
680
|
+
// Collapses whitespace, drops trailing punctuation before ellipsis, caps at
|
|
681
|
+
// ~32 chars. Returns '' when the input has no usable text.
|
|
682
|
+
function buildPromptSnippet(text, max) {
|
|
683
|
+
if (typeof text !== 'string') return '';
|
|
684
|
+
var s = text.replace(/\s+/g, ' ').trim();
|
|
685
|
+
if (!s) return '';
|
|
686
|
+
var cap = max || 32;
|
|
687
|
+
if (s.length <= cap) return s;
|
|
688
|
+
var slice = s.slice(0, cap).replace(/[\s.,;:!?\-—]+$/, '');
|
|
689
|
+
return slice + '…';
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Pull the first user prompt from a session and cache the snippet on the
|
|
693
|
+
// session object. Returns '' if no user message has been received yet.
|
|
694
|
+
function deriveFirstPromptSnippet(session) {
|
|
695
|
+
if (!session) return '';
|
|
696
|
+
if (session._promptSnippet != null) return session._promptSnippet;
|
|
697
|
+
if (!session.messages || !session.messages.length) return '';
|
|
698
|
+
for (var i = 0; i < session.messages.length; i++) {
|
|
699
|
+
var m = session.messages[i];
|
|
700
|
+
if (m && m.role === 'user' && typeof m.content === 'string') {
|
|
701
|
+
var snippet = buildPromptSnippet(m.content, 32);
|
|
702
|
+
session._promptSnippet = snippet;
|
|
703
|
+
return snippet;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return '';
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Always-on tab context: prefer auto-detected project name, fall back to a
|
|
710
|
+
// truncated first-prompt snippet (quoted), then to a same-agent ordinal #N
|
|
711
|
+
// only when disambiguation is actually needed.
|
|
712
|
+
function deriveTabContext(session, agentCounts, ordinal) {
|
|
713
|
+
if (!session) return '';
|
|
714
|
+
if (session.projectName) return session.projectName;
|
|
715
|
+
var snippet = deriveFirstPromptSnippet(session);
|
|
716
|
+
if (snippet) return '"' + snippet + '"';
|
|
717
|
+
if (agentCounts && agentCounts[session.agent] > 1) return '#' + ordinal;
|
|
718
|
+
return '';
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function renderSessionTabs() {
|
|
722
|
+
var bar = document.getElementById('header-session-bar');
|
|
723
|
+
if (!bar) return;
|
|
724
|
+
|
|
725
|
+
bar.innerHTML = '';
|
|
726
|
+
|
|
727
|
+
// Pre-pass: count sessions per agent so we know whether to disambiguate.
|
|
728
|
+
var agentCounts = {};
|
|
729
|
+
for (var p = 0; p < sessionOrder.length; p++) {
|
|
730
|
+
var ps = chatSessions[sessionOrder[p]];
|
|
731
|
+
if (ps) agentCounts[ps.agent] = (agentCounts[ps.agent] || 0) + 1;
|
|
732
|
+
}
|
|
733
|
+
var agentOrdinals = {};
|
|
734
|
+
|
|
735
|
+
for (var i = 0; i < sessionOrder.length; i++) {
|
|
736
|
+
var sid = sessionOrder[i];
|
|
737
|
+
var session = chatSessions[sid];
|
|
738
|
+
if (!session) continue;
|
|
739
|
+
|
|
740
|
+
var info = AGENTS[session.agent] || { color: '#666', label: session.agent || 'General Session' };
|
|
741
|
+
agentOrdinals[session.agent] = (agentOrdinals[session.agent] || 0) + 1;
|
|
742
|
+
var ordinal = agentOrdinals[session.agent];
|
|
743
|
+
var suffix = deriveTabContext(session, agentCounts, ordinal);
|
|
744
|
+
var subline = deriveTabSubline(session);
|
|
745
|
+
|
|
746
|
+
var tab = document.createElement('div');
|
|
747
|
+
var tabClass = 'session-tab';
|
|
748
|
+
if (sid === activeSessionId) tabClass += ' active';
|
|
749
|
+
if (session.needsAttention) tabClass += ' needs-attention';
|
|
750
|
+
else if (session.unread) tabClass += ' unread';
|
|
751
|
+
if (session.chatResponding) tabClass += ' responding';
|
|
752
|
+
if (subline) tabClass += ' has-subline';
|
|
753
|
+
tab.className = tabClass;
|
|
754
|
+
tab.style.setProperty('--tab-color', info.color);
|
|
755
|
+
|
|
756
|
+
var titleText = session.title || info.label;
|
|
757
|
+
var suffixHtml = suffix
|
|
758
|
+
? '<span class="session-tab-suffix">· ' + esc(suffix) + '</span>'
|
|
759
|
+
: '';
|
|
760
|
+
var sublineHtml = subline
|
|
761
|
+
? '<span class="session-tab-subline">' + esc(subline) + '</span>'
|
|
762
|
+
: '';
|
|
763
|
+
|
|
764
|
+
tab.innerHTML =
|
|
765
|
+
'<div class="session-tab-row">' +
|
|
766
|
+
'<span class="session-tab-dot" style="background:' + info.color + '"></span>' +
|
|
767
|
+
'<span class="session-tab-title">' + esc(titleText) + '</span>' +
|
|
768
|
+
suffixHtml +
|
|
769
|
+
'<span class="session-tab-close" title="Close session">×</span>' +
|
|
770
|
+
'</div>' +
|
|
771
|
+
sublineHtml;
|
|
772
|
+
|
|
773
|
+
// R1 — rich hover tooltip aggregating activity + cost
|
|
774
|
+
var tipLines = [titleText + (suffix ? ' · ' + suffix : '')];
|
|
775
|
+
var firstPromptFull = '';
|
|
776
|
+
if (session.messages && session.messages.length) {
|
|
777
|
+
for (var mi = 0; mi < session.messages.length; mi++) {
|
|
778
|
+
var fm = session.messages[mi];
|
|
779
|
+
if (fm && fm.role === 'user' && typeof fm.content === 'string') {
|
|
780
|
+
firstPromptFull = fm.content.replace(/\s+/g, ' ').trim();
|
|
781
|
+
if (firstPromptFull.length > 160) firstPromptFull = firstPromptFull.slice(0, 160) + '…';
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (firstPromptFull) tipLines.push('Prompt: ' + firstPromptFull);
|
|
787
|
+
if (session.lastSignal) tipLines.push('Last: ' + session.lastSignal.label);
|
|
788
|
+
if (session.lastActivityAt) tipLines.push('Active ' + formatRelativeTime(session.lastActivityAt) + ' ago');
|
|
789
|
+
if (session.totalCost) tipLines.push('Cost: $' + Number(session.totalCost).toFixed(4));
|
|
790
|
+
tab.title = tipLines.join('\n');
|
|
791
|
+
|
|
792
|
+
tab.addEventListener('click', (function(id) {
|
|
793
|
+
return function(e) {
|
|
794
|
+
if (e.target.classList.contains('session-tab-close')) return;
|
|
795
|
+
switchSession(id);
|
|
796
|
+
};
|
|
797
|
+
})(sid));
|
|
798
|
+
|
|
799
|
+
tab.querySelector('.session-tab-close').addEventListener('click', (function(id) {
|
|
800
|
+
return function(e) {
|
|
801
|
+
e.stopPropagation();
|
|
802
|
+
closeSessionTab(id);
|
|
803
|
+
};
|
|
804
|
+
})(sid));
|
|
805
|
+
|
|
806
|
+
// Double-click title to rename
|
|
807
|
+
tab.querySelector('.session-tab-title').addEventListener('dblclick', (function(id, el) {
|
|
808
|
+
return function(e) {
|
|
809
|
+
e.stopPropagation();
|
|
810
|
+
startTabRename(id, el);
|
|
811
|
+
};
|
|
812
|
+
})(sid, tab.querySelector('.session-tab-title')));
|
|
813
|
+
|
|
814
|
+
bar.appendChild(tab);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Always show the "+ New Chat" button
|
|
818
|
+
var newBtn = document.createElement('button');
|
|
819
|
+
newBtn.id = 'new-session-btn';
|
|
820
|
+
newBtn.title = 'New chat';
|
|
821
|
+
newBtn.textContent = sessionOrder.length > 0 ? '+' : '+ New Chat';
|
|
822
|
+
newBtn.addEventListener('click', function() {
|
|
823
|
+
showAgentPicker();
|
|
824
|
+
});
|
|
825
|
+
bar.appendChild(newBtn);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// ═══════════════════════════════════════════════════════════════
|
|
829
|
+
// Session switching
|
|
830
|
+
// ═══════════════════════════════════════════════════════════════
|
|
831
|
+
|
|
832
|
+
function switchSession(sessionId) {
|
|
833
|
+
if (sessionId === activeSessionId) {
|
|
834
|
+
// If we're on the agent picker, switch back to chat view
|
|
835
|
+
var picker = document.getElementById('agent-picker');
|
|
836
|
+
if (picker.classList.contains('visible')) {
|
|
837
|
+
showChatView();
|
|
838
|
+
}
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
var oldSession = getActiveSession();
|
|
843
|
+
var newSession = getSessionState(sessionId);
|
|
844
|
+
if (!newSession) return;
|
|
845
|
+
|
|
846
|
+
var messagesEl = document.getElementById('messages');
|
|
847
|
+
|
|
848
|
+
// Save current session (chat DOM + workspace)
|
|
849
|
+
if (oldSession) {
|
|
850
|
+
oldSession.scrollTop = messagesEl.scrollTop;
|
|
851
|
+
var frag = document.createDocumentFragment();
|
|
852
|
+
while (messagesEl.firstChild) {
|
|
853
|
+
frag.appendChild(messagesEl.firstChild);
|
|
854
|
+
}
|
|
855
|
+
oldSession.domFragment = frag;
|
|
856
|
+
saveSessionWorkspace(oldSession);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Clear selection context and dispose DOM-bound instances before swapping workspace
|
|
860
|
+
clearSelectionContext();
|
|
861
|
+
disposeMonacoInstance();
|
|
862
|
+
disposeNotebookCellMonaco();
|
|
863
|
+
destroyTabulator();
|
|
864
|
+
|
|
865
|
+
// Switch active session and load its workspace
|
|
866
|
+
activeSessionId = sessionId;
|
|
867
|
+
loadSessionWorkspace(newSession);
|
|
868
|
+
|
|
869
|
+
// Restore target session chat DOM
|
|
870
|
+
messagesEl.innerHTML = '';
|
|
871
|
+
if (newSession.domDirty || !newSession.domFragment) {
|
|
872
|
+
// Rebuild from messages
|
|
873
|
+
newSession.hasMessages = false;
|
|
874
|
+
if (newSession.messages.length === 0) {
|
|
875
|
+
messagesEl.innerHTML = '<div class="empty-state">Waiting for response...</div>';
|
|
876
|
+
} else {
|
|
877
|
+
for (var i = 0; i < newSession.messages.length; i++) {
|
|
878
|
+
addMessageDirect(newSession.messages[i].role, newSession.messages[i].content, newSession.messages[i].agent, true, newSession.messages[i].attachments);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
newSession.domDirty = false;
|
|
882
|
+
newSession.domFragment = null;
|
|
883
|
+
newSession.pendingBubble = null;
|
|
884
|
+
newSession.thinkingIndicatorEl = null;
|
|
885
|
+
newSession.consultingIndicatorEl = null;
|
|
886
|
+
} else {
|
|
887
|
+
// Restore from fragment
|
|
888
|
+
messagesEl.appendChild(newSession.domFragment);
|
|
889
|
+
newSession.domFragment = null;
|
|
890
|
+
messagesEl.scrollTop = newSession.scrollTop;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Show chat view (in case we were on agent picker)
|
|
894
|
+
showChatView();
|
|
895
|
+
activateAgent(newSession.agent);
|
|
896
|
+
setChatInputEnabled(!newSession.chatResponding);
|
|
897
|
+
newSession.unread = false;
|
|
898
|
+
newSession.needsAttention = false;
|
|
899
|
+
newSession.attentionReason = null;
|
|
900
|
+
|
|
901
|
+
// Render any queued permission cards
|
|
902
|
+
var queued = newSession.pendingPermissions;
|
|
903
|
+
if (queued.length > 0) {
|
|
904
|
+
for (var qi = 0; qi < queued.length; qi++) {
|
|
905
|
+
var perm = queued[qi];
|
|
906
|
+
renderPermissionCard(perm.id, perm.tool, perm.command, perm.sessionId);
|
|
907
|
+
}
|
|
908
|
+
newSession.pendingPermissions = [];
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
updateTitleNotification();
|
|
912
|
+
renderSessionTabs();
|
|
913
|
+
renderAttachmentChips();
|
|
914
|
+
|
|
915
|
+
// Rebuild workspace layout for this session
|
|
916
|
+
applySplitLayout();
|
|
917
|
+
renderWsTabs();
|
|
918
|
+
showActiveContent();
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function cycleSessionTab(dir) {
|
|
922
|
+
if (sessionOrder.length < 2) return;
|
|
923
|
+
var idx = sessionOrder.indexOf(activeSessionId);
|
|
924
|
+
if (idx === -1) return;
|
|
925
|
+
var newIdx = (idx + dir + sessionOrder.length) % sessionOrder.length;
|
|
926
|
+
switchSession(sessionOrder[newIdx]);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// ═══════════════════════════════════════════════════════════════
|
|
930
|
+
// Tab rename
|
|
931
|
+
// ═══════════════════════════════════════════════════════════════
|
|
932
|
+
|
|
933
|
+
function startTabRename(sessionId, titleEl) {
|
|
934
|
+
var session = getSessionState(sessionId);
|
|
935
|
+
if (!session) return;
|
|
936
|
+
|
|
937
|
+
var info = AGENTS[session.agent] || { label: session.agent || 'General Session' };
|
|
938
|
+
var currentTitle = session.title || info.label;
|
|
939
|
+
|
|
940
|
+
var input = document.createElement('input');
|
|
941
|
+
input.type = 'text';
|
|
942
|
+
input.className = 'session-tab-rename-input';
|
|
943
|
+
input.value = currentTitle;
|
|
944
|
+
|
|
945
|
+
titleEl.textContent = '';
|
|
946
|
+
titleEl.appendChild(input);
|
|
947
|
+
input.focus();
|
|
948
|
+
input.select();
|
|
949
|
+
|
|
950
|
+
var cancelled = false;
|
|
951
|
+
|
|
952
|
+
function finish() {
|
|
953
|
+
if (cancelled) {
|
|
954
|
+
renderSessionTabs();
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
var newTitle = input.value.trim();
|
|
958
|
+
if (newTitle && newTitle !== info.label) {
|
|
959
|
+
session.title = newTitle;
|
|
960
|
+
authFetch('/chat/rename', {
|
|
961
|
+
method: 'POST',
|
|
962
|
+
headers: { 'Content-Type': 'application/json' },
|
|
963
|
+
body: JSON.stringify({ sessionId: sessionId, title: newTitle }),
|
|
964
|
+
}).catch(function() {
|
|
965
|
+
console.warn('[shards] tab rename failed to persist');
|
|
966
|
+
});
|
|
967
|
+
} else {
|
|
968
|
+
session.title = null;
|
|
969
|
+
}
|
|
970
|
+
renderSessionTabs();
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
input.addEventListener('blur', finish);
|
|
974
|
+
input.addEventListener('keydown', function(e) {
|
|
975
|
+
if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
|
|
976
|
+
if (e.key === 'Escape') { e.preventDefault(); cancelled = true; input.blur(); }
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// ═══════════════════════════════════════════════════════════════
|
|
981
|
+
// Streaming token display
|
|
982
|
+
// ═══════════════════════════════════════════════════════════════
|
|
983
|
+
|
|
984
|
+
function ensurePendingBubble() {
|
|
985
|
+
var session = getActiveSession();
|
|
986
|
+
if (!session || session.pendingBubble) return;
|
|
987
|
+
|
|
988
|
+
var container = document.getElementById('messages');
|
|
989
|
+
if (!session.hasMessages) { container.innerHTML = ''; session.hasMessages = true; }
|
|
990
|
+
|
|
991
|
+
var info = AGENTS[session.agent] || { color: '#666', label: session.agent || 'Agent' };
|
|
992
|
+
var div = document.createElement('div');
|
|
993
|
+
div.className = 'message assistant gathering';
|
|
994
|
+
div.innerHTML =
|
|
995
|
+
'<div class="message-meta">' +
|
|
996
|
+
'<span class="meta-dot" style="background:' + info.color + '; color:' + info.color + '"></span>' +
|
|
997
|
+
esc(info.label) +
|
|
998
|
+
'</div>' +
|
|
999
|
+
'<div class="message-bubble"></div>';
|
|
1000
|
+
container.appendChild(div);
|
|
1001
|
+
|
|
1002
|
+
// Remove gathering class after animation completes
|
|
1003
|
+
setTimeout(function() { div.classList.remove('gathering'); }, 600);
|
|
1004
|
+
session.pendingBubble = div.querySelector('.message-bubble');
|
|
1005
|
+
container.scrollTop = container.scrollHeight;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function appendToken(text) {
|
|
1009
|
+
var session = getActiveSession();
|
|
1010
|
+
if (!session) return;
|
|
1011
|
+
session.tokenBuffer += text;
|
|
1012
|
+
if (!session.tokenFlushPending) {
|
|
1013
|
+
session.tokenFlushPending = true;
|
|
1014
|
+
requestAnimationFrame(flushTokens);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function flushTokens() {
|
|
1019
|
+
var session = getActiveSession();
|
|
1020
|
+
if (!session) return;
|
|
1021
|
+
session.tokenFlushPending = false;
|
|
1022
|
+
if (!session.pendingBubble || !session.tokenBuffer) return;
|
|
1023
|
+
session.pendingBubble.appendChild(document.createTextNode(session.tokenBuffer));
|
|
1024
|
+
session.tokenBuffer = '';
|
|
1025
|
+
var container = document.getElementById('messages');
|
|
1026
|
+
container.scrollTop = container.scrollHeight;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function finalizePendingBubble(markdownContent) {
|
|
1030
|
+
var session = getActiveSession();
|
|
1031
|
+
if (!session) return;
|
|
1032
|
+
flushTokens();
|
|
1033
|
+
if (session.pendingBubble) {
|
|
1034
|
+
var displayContent = stripGateFence(markdownContent);
|
|
1035
|
+
var rendered = renderMarkdown(displayContent);
|
|
1036
|
+
session.pendingBubble.innerHTML = linkifyFilePaths(rendered);
|
|
1037
|
+
session.pendingBubble.setAttribute('data-raw-md', displayContent);
|
|
1038
|
+
// Assign message index for bookmarking
|
|
1039
|
+
var msgEl = session.pendingBubble.parentElement;
|
|
1040
|
+
var msgIdx = session.messages.length - 1;
|
|
1041
|
+
if (msgIdx < 0) msgIdx = 0;
|
|
1042
|
+
msgEl.setAttribute('data-msg-idx', msgIdx);
|
|
1043
|
+
// Add message-level actions (bookmark + copy)
|
|
1044
|
+
var starHtml = typeof bookmarkStarHtml === 'function' ? bookmarkStarHtml(msgIdx) : '';
|
|
1045
|
+
var actions = document.createElement('div');
|
|
1046
|
+
actions.className = 'message-actions';
|
|
1047
|
+
actions.innerHTML = starHtml + '<button class="msg-copy-btn" onclick="copyMessageContent(this)">Copy</button>';
|
|
1048
|
+
msgEl.appendChild(actions);
|
|
1049
|
+
// Auto-collapse long messages
|
|
1050
|
+
if (session.pendingBubble.scrollHeight > 500) {
|
|
1051
|
+
session.pendingBubble.classList.add('collapsed');
|
|
1052
|
+
var showMore = document.createElement('button');
|
|
1053
|
+
showMore.className = 'show-more-btn';
|
|
1054
|
+
showMore.textContent = 'Show more';
|
|
1055
|
+
var bubble = session.pendingBubble;
|
|
1056
|
+
showMore.onclick = function() {
|
|
1057
|
+
bubble.classList.remove('collapsed');
|
|
1058
|
+
showMore.remove();
|
|
1059
|
+
};
|
|
1060
|
+
session.pendingBubble.parentElement.insertBefore(showMore, actions);
|
|
1061
|
+
}
|
|
1062
|
+
// Detect gate pattern and inject confirmation buttons
|
|
1063
|
+
if (isGateMessage(markdownContent)) {
|
|
1064
|
+
injectGateButtons(session.pendingBubble.parentElement);
|
|
1065
|
+
}
|
|
1066
|
+
session.pendingBubble = null;
|
|
1067
|
+
}
|
|
1068
|
+
session.tokenBuffer = '';
|
|
1069
|
+
var container = document.getElementById('messages');
|
|
1070
|
+
container.scrollTop = container.scrollHeight;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// ─── Gate confirmation detection and buttons ────────────────────────────────
|
|
1074
|
+
|
|
1075
|
+
function stripGateFence(content) {
|
|
1076
|
+
if (!content) return content;
|
|
1077
|
+
return content.replace(/::GATE::[\s\S]*?::ENDGATE::\s*/g, '');
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function isGateMessage(content) {
|
|
1081
|
+
if (!content) return false;
|
|
1082
|
+
// Require a real ::GATE:: ... ::ENDGATE:: fence — the same syntax parsed
|
|
1083
|
+
// authoritatively by tools/gate-hook/parser.js and emitted by agents per
|
|
1084
|
+
// src/agents/specific_instructions/shared/behavioral_rules.md. Substring
|
|
1085
|
+
// heuristics on words like "confirm" or "proceed" false-positive on agent
|
|
1086
|
+
// narration (e.g. "Confirmed — X is not present in Y").
|
|
1087
|
+
if (!/::GATE::[\s\S]*?::ENDGATE::/.test(content)) return false;
|
|
1088
|
+
// Suppress the Confirm/Request-Changes injection when enforcement is off
|
|
1089
|
+
// (SHARDS_GATE_ENFORCE=0) — the gate hook will never act on the response,
|
|
1090
|
+
// so showing the buttons as if they meant something is misleading. The
|
|
1091
|
+
// /gate-mode banner already tells the user gates are advisory.
|
|
1092
|
+
if (typeof gateMode !== 'undefined' && gateMode && gateMode.enforce === false) {
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1095
|
+
// Prefer server state — only inject buttons when the gate hook has
|
|
1096
|
+
// actually opened a gate. With SSE in place gateState lands ~immediately;
|
|
1097
|
+
// the fallback (10s polling) can still leave a transient window where
|
|
1098
|
+
// gateState is stale, so we fall back to trusting the fence.
|
|
1099
|
+
if (typeof gateState !== 'undefined' && gateState) {
|
|
1100
|
+
return gateState.open === true;
|
|
1101
|
+
}
|
|
1102
|
+
return true;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function injectGateButtons(messageEl) {
|
|
1106
|
+
if (!messageEl) return;
|
|
1107
|
+
// Don't double-inject
|
|
1108
|
+
if (messageEl.querySelector('.gate-actions')) return;
|
|
1109
|
+
|
|
1110
|
+
var actions = document.createElement('div');
|
|
1111
|
+
actions.className = 'gate-actions';
|
|
1112
|
+
actions.innerHTML =
|
|
1113
|
+
'<span class="gate-label">Gate confirmation</span>' +
|
|
1114
|
+
'<button class="gate-btn gate-btn-confirm">Confirm</button>' +
|
|
1115
|
+
'<button class="gate-btn gate-btn-changes">Request Changes</button>';
|
|
1116
|
+
|
|
1117
|
+
actions.querySelector('.gate-btn-confirm').addEventListener('click', function() {
|
|
1118
|
+
var input = document.getElementById('chat-input');
|
|
1119
|
+
input.value = 'Confirmed. Proceed.';
|
|
1120
|
+
sendChatMessage();
|
|
1121
|
+
actions.classList.add('gate-resolved');
|
|
1122
|
+
setTimeout(function() { actions.remove(); }, 500);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
actions.querySelector('.gate-btn-changes').addEventListener('click', function() {
|
|
1126
|
+
var input = document.getElementById('chat-input');
|
|
1127
|
+
input.value = 'I\'d like to change: ';
|
|
1128
|
+
input.focus();
|
|
1129
|
+
input.setSelectionRange(input.value.length, input.value.length);
|
|
1130
|
+
actions.classList.add('gate-resolved');
|
|
1131
|
+
setTimeout(function() { actions.remove(); }, 300);
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
messageEl.appendChild(actions);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// ─── File path auto-linking ──────────────────────────────────────────────────
|
|
1138
|
+
|
|
1139
|
+
function linkifyFilePaths(html) {
|
|
1140
|
+
// Regex for common file extensions in data projects
|
|
1141
|
+
var pathRegex = /\b([a-zA-Z0-9_\-\./]+\.(?:md|sql|py|ipynb|json|jsonl|csv|tsv|yml|yaml|txt|js|ts|html|css|png|jpg|pdf))\b/g;
|
|
1142
|
+
|
|
1143
|
+
return html.replace(pathRegex, function(match) {
|
|
1144
|
+
// Avoid linking if it's already inside an <a>, <code>, or <pre> tag (basic heuristic)
|
|
1145
|
+
// This is hard in regex on HTML, but we can check if it looks like a URL or is very short
|
|
1146
|
+
if (match.startsWith('http') || match.length < 4) return match;
|
|
1147
|
+
|
|
1148
|
+
return '<span class="file-link" onclick="openFile(\'' + match + '\')" title="Open ' + match + '">' + match + '</span>';
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function openFile(path) {
|
|
1153
|
+
// We call the explorer's open function which handles fetching and rendering
|
|
1154
|
+
if (typeof openFileFromExplorer === 'function') {
|
|
1155
|
+
openFileFromExplorer(path);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function addToolIndicator(toolName) {
|
|
1160
|
+
var session = getActiveSession();
|
|
1161
|
+
var container = document.getElementById('messages');
|
|
1162
|
+
if (session && !session.hasMessages) { container.innerHTML = ''; session.hasMessages = true; }
|
|
1163
|
+
|
|
1164
|
+
// Group consecutive tool calls
|
|
1165
|
+
var last = container.lastElementChild;
|
|
1166
|
+
if (last && last.classList.contains('tool-group')) {
|
|
1167
|
+
var items = last.querySelector('.tool-group-items');
|
|
1168
|
+
var div = document.createElement('div');
|
|
1169
|
+
div.className = 'tool-indicator';
|
|
1170
|
+
div.innerHTML = '<span class="tool-name">' + esc(toolName) + '</span>';
|
|
1171
|
+
items.appendChild(div);
|
|
1172
|
+
// Update header count and tool list
|
|
1173
|
+
var allTools = items.querySelectorAll('.tool-name');
|
|
1174
|
+
var names = [];
|
|
1175
|
+
for (var i = 0; i < allTools.length; i++) {
|
|
1176
|
+
var n = allTools[i].textContent;
|
|
1177
|
+
if (names.indexOf(n) === -1) names.push(n);
|
|
1178
|
+
}
|
|
1179
|
+
last.querySelector('.tool-group-header').innerHTML =
|
|
1180
|
+
'<span class="tool-group-chevron">▶</span> Used ' + allTools.length + ' tool' + (allTools.length > 1 ? 's' : '') + ': ' + names.join(', ');
|
|
1181
|
+
container.scrollTop = container.scrollHeight;
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
var group = document.createElement('div');
|
|
1186
|
+
group.className = 'tool-group';
|
|
1187
|
+
group.innerHTML =
|
|
1188
|
+
'<div class="tool-group-header" onclick="this.parentElement.classList.toggle(\'expanded\')">' +
|
|
1189
|
+
'<span class="tool-group-chevron">▶</span> Used 1 tool: ' + esc(toolName) +
|
|
1190
|
+
'</div>' +
|
|
1191
|
+
'<div class="tool-group-items">' +
|
|
1192
|
+
'<div class="tool-indicator"><span class="tool-name">' + esc(toolName) + '</span></div>' +
|
|
1193
|
+
'</div>';
|
|
1194
|
+
container.appendChild(group);
|
|
1195
|
+
container.scrollTop = container.scrollHeight;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function showThinkingIndicator() {
|
|
1199
|
+
removeThinkingIndicator();
|
|
1200
|
+
var session = getActiveSession();
|
|
1201
|
+
var container = document.getElementById('messages');
|
|
1202
|
+
if (session && !session.hasMessages) { container.innerHTML = ''; session.hasMessages = true; }
|
|
1203
|
+
var info = AGENTS[session ? session.agent : null] || { color: '#4a4a80' };
|
|
1204
|
+
var div = document.createElement('div');
|
|
1205
|
+
div.className = 'thinking-indicator';
|
|
1206
|
+
div.innerHTML =
|
|
1207
|
+
'<div class="thinking-dot" style="background:' + info.color + '"></div>' +
|
|
1208
|
+
'<div class="thinking-dot" style="background:' + info.color + '"></div>' +
|
|
1209
|
+
'<div class="thinking-dot" style="background:' + info.color + '"></div>';
|
|
1210
|
+
container.appendChild(div);
|
|
1211
|
+
if (session) session.thinkingIndicatorEl = div;
|
|
1212
|
+
container.scrollTop = container.scrollHeight;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function removeThinkingIndicator() {
|
|
1216
|
+
var session = getActiveSession();
|
|
1217
|
+
if (session && session.thinkingIndicatorEl) {
|
|
1218
|
+
session.thinkingIndicatorEl.remove();
|
|
1219
|
+
session.thinkingIndicatorEl = null;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function showWorkingIndicator() {
|
|
1224
|
+
removeWorkingIndicator();
|
|
1225
|
+
var session = getActiveSession();
|
|
1226
|
+
var info = AGENTS[session ? session.agent : null] || { color: '#4a4a80' };
|
|
1227
|
+
var inputArea = document.getElementById('chat-input-area');
|
|
1228
|
+
var div = document.createElement('div');
|
|
1229
|
+
div.className = 'working-indicator';
|
|
1230
|
+
div.innerHTML =
|
|
1231
|
+
'<span class="working-indicator-dot" style="background:' + info.color + '"></span>' +
|
|
1232
|
+
'<span class="working-indicator-text">Working...</span>';
|
|
1233
|
+
inputArea.appendChild(div);
|
|
1234
|
+
if (session) session.workingIndicatorEl = div;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function removeWorkingIndicator() {
|
|
1238
|
+
var session = getActiveSession();
|
|
1239
|
+
if (session && session.workingIndicatorEl) {
|
|
1240
|
+
session.workingIndicatorEl.remove();
|
|
1241
|
+
session.workingIndicatorEl = null;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function showConsultingIndicator() {
|
|
1246
|
+
var session = getActiveSession();
|
|
1247
|
+
var container = document.getElementById('messages');
|
|
1248
|
+
if (session && !session.hasMessages) { container.innerHTML = ''; session.hasMessages = true; }
|
|
1249
|
+
var div = document.createElement('div');
|
|
1250
|
+
div.className = 'consulting-indicator';
|
|
1251
|
+
div.innerHTML =
|
|
1252
|
+
'<span class="consulting-glow-dot" style="background:#5050a0"></span>' +
|
|
1253
|
+
'<span class="consulting-label">Consulting agent...</span>';
|
|
1254
|
+
container.appendChild(div);
|
|
1255
|
+
if (session) session.consultingIndicatorEl = div;
|
|
1256
|
+
container.scrollTop = container.scrollHeight;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function updateConsultingIndicator(agentKey) {
|
|
1260
|
+
var session = getActiveSession();
|
|
1261
|
+
var info = AGENTS[agentKey] || { color: '#6060a0', label: agentKey };
|
|
1262
|
+
if (session && session.consultingIndicatorEl) {
|
|
1263
|
+
session.consultingIndicatorEl.innerHTML =
|
|
1264
|
+
'<span class="consulting-glow-dot consulting-glow-dot--done" style="background:' + info.color + '"></span>' +
|
|
1265
|
+
'<span class="consulting-label"><strong style="color:' + info.color + '">' + esc(info.label) + '</strong> consulted</span>';
|
|
1266
|
+
session.consultingIndicatorEl = null;
|
|
1267
|
+
} else {
|
|
1268
|
+
var container = document.getElementById('messages');
|
|
1269
|
+
if (session && !session.hasMessages) { container.innerHTML = ''; session.hasMessages = true; }
|
|
1270
|
+
var div = document.createElement('div');
|
|
1271
|
+
div.className = 'consulting-indicator';
|
|
1272
|
+
div.innerHTML =
|
|
1273
|
+
'<span class="consulting-glow-dot consulting-glow-dot--done" style="background:' + info.color + '"></span>' +
|
|
1274
|
+
'<span class="consulting-label"><strong style="color:' + info.color + '">' + esc(info.label) + '</strong> consulted</span>';
|
|
1275
|
+
container.appendChild(div);
|
|
1276
|
+
container.scrollTop = container.scrollHeight;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1281
|
+
// Conversation panel
|
|
1282
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1283
|
+
|
|
1284
|
+
function addSystemNotice(text, opts) {
|
|
1285
|
+
var session = getActiveSession();
|
|
1286
|
+
var container = document.getElementById('messages');
|
|
1287
|
+
if (session && !session.hasMessages) { container.innerHTML = ''; session.hasMessages = true; }
|
|
1288
|
+
var div = document.createElement('div');
|
|
1289
|
+
div.className = 'message system-notice';
|
|
1290
|
+
// Use markdown rendering if text contains markdown-like formatting, otherwise escape
|
|
1291
|
+
var isMarkdown = text.indexOf('**') !== -1 || text.indexOf('`') !== -1 || text.indexOf('\n') !== -1;
|
|
1292
|
+
var rendered = isMarkdown ? renderMarkdown(text) : esc(text);
|
|
1293
|
+
var color = (opts && opts.color) || '#e05050';
|
|
1294
|
+
var bgColor = (opts && opts.bg) || 'rgba(224,80,80,0.1)';
|
|
1295
|
+
var textColor = (opts && opts.textColor) || '#e08080';
|
|
1296
|
+
div.innerHTML = '<div class="message-bubble" style="border-left: 3px solid ' + color + '; background: ' + bgColor + '; color: ' + textColor + '; font-size: 13px; padding: 8px 12px;">' + rendered + '</div>';
|
|
1297
|
+
container.appendChild(div);
|
|
1298
|
+
container.scrollTop = container.scrollHeight;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function addMessageDirect(role, content, agent, skipAnimation, attachments) {
|
|
1302
|
+
var session = getActiveSession();
|
|
1303
|
+
var container = document.getElementById('messages');
|
|
1304
|
+
if (session && !session.hasMessages) { container.innerHTML = ''; session.hasMessages = true; }
|
|
1305
|
+
|
|
1306
|
+
// Determine message index for bookmarking (count of .message elements already in container)
|
|
1307
|
+
var msgIdx = container.querySelectorAll('.message').length;
|
|
1308
|
+
|
|
1309
|
+
var info = AGENTS[agent] || { color: '#666', label: agent || 'Unknown' };
|
|
1310
|
+
var div = document.createElement('div');
|
|
1311
|
+
div.className = 'message ' + (role === 'user' ? 'user' : ('assistant' + (skipAnimation ? '' : ' gathering')));
|
|
1312
|
+
div.setAttribute('data-msg-idx', msgIdx);
|
|
1313
|
+
|
|
1314
|
+
var starHtml = typeof bookmarkStarHtml === 'function' ? bookmarkStarHtml(msgIdx) : '';
|
|
1315
|
+
var bookmarkedClass = (activeSessionId && typeof isBookmarked === 'function' && isBookmarked(activeSessionId, msgIdx)) ? ' bookmarked' : '';
|
|
1316
|
+
|
|
1317
|
+
if (role === 'user') {
|
|
1318
|
+
var attachmentsHtml = '';
|
|
1319
|
+
if (attachments && attachments.length) {
|
|
1320
|
+
attachmentsHtml = '<div class="message-attachments">';
|
|
1321
|
+
for (var ai = 0; ai < attachments.length; ai++) {
|
|
1322
|
+
var att = attachments[ai];
|
|
1323
|
+
if (!att || !att.dataBase64 || !att.mediaType) continue;
|
|
1324
|
+
var dataUrl = 'data:' + att.mediaType + ';base64,' + att.dataBase64;
|
|
1325
|
+
var altText = esc(att.name || 'attached image');
|
|
1326
|
+
attachmentsHtml +=
|
|
1327
|
+
'<img class="message-attachment-img" src="' + dataUrl + '" alt="' + altText + '" title="' + altText + '" onclick="window.open(this.src, \'_blank\')">';
|
|
1328
|
+
}
|
|
1329
|
+
attachmentsHtml += '</div>';
|
|
1330
|
+
}
|
|
1331
|
+
var textHtml = content ? linkifyFilePaths(esc(content)) : '';
|
|
1332
|
+
div.innerHTML =
|
|
1333
|
+
'<div class="message-meta">You</div>' +
|
|
1334
|
+
'<div class="message-bubble">' + attachmentsHtml + textHtml + '</div>' +
|
|
1335
|
+
'<div class="message-actions">' + starHtml + '</div>';
|
|
1336
|
+
} else {
|
|
1337
|
+
var displayContent = stripGateFence(content);
|
|
1338
|
+
div.innerHTML =
|
|
1339
|
+
'<div class="message-meta">' +
|
|
1340
|
+
'<span class="meta-dot" style="background:' + info.color + '; color:' + info.color + '"></span>' +
|
|
1341
|
+
esc(info.label) +
|
|
1342
|
+
'</div>' +
|
|
1343
|
+
'<div class="message-bubble" data-raw-md="' + esc(displayContent) + '">' + linkifyFilePaths(renderMarkdown(displayContent)) + '</div>' +
|
|
1344
|
+
'<div class="message-actions">' + starHtml + '<button class="msg-copy-btn" onclick="copyMessageContent(this)">Copy</button></div>';
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Apply bookmarked state to star
|
|
1348
|
+
if (bookmarkedClass) {
|
|
1349
|
+
var star = div.querySelector('.bookmark-star');
|
|
1350
|
+
if (star) star.classList.add('bookmarked');
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
container.appendChild(div);
|
|
1354
|
+
|
|
1355
|
+
// Remove gathering class after animation completes
|
|
1356
|
+
if (role !== 'user' && !skipAnimation) {
|
|
1357
|
+
setTimeout(function() { div.classList.remove('gathering'); }, 600);
|
|
1358
|
+
// AI Engineer micro-glitch
|
|
1359
|
+
if (currentAgent === 'ai-engineer' && Math.random() < 0.3) {
|
|
1360
|
+
var bubble = div.querySelector('.message-bubble');
|
|
1361
|
+
if (bubble) {
|
|
1362
|
+
setTimeout(function() {
|
|
1363
|
+
bubble.classList.add('glitch-active');
|
|
1364
|
+
setTimeout(function() { bubble.classList.remove('glitch-active'); }, 300);
|
|
1365
|
+
}, 800 + Math.random() * 2000);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Auto-collapse long assistant messages
|
|
1371
|
+
if (role !== 'user') {
|
|
1372
|
+
var bubble = div.querySelector('.message-bubble');
|
|
1373
|
+
if (bubble && bubble.scrollHeight > 500) {
|
|
1374
|
+
bubble.classList.add('collapsed');
|
|
1375
|
+
var showMore = document.createElement('button');
|
|
1376
|
+
showMore.className = 'show-more-btn';
|
|
1377
|
+
showMore.textContent = 'Show more';
|
|
1378
|
+
showMore.onclick = function() {
|
|
1379
|
+
bubble.classList.remove('collapsed');
|
|
1380
|
+
showMore.remove();
|
|
1381
|
+
};
|
|
1382
|
+
div.querySelector('.message-actions').before(showMore);
|
|
1383
|
+
}
|
|
1384
|
+
// Inject gate buttons if this is the last message and agent is waiting
|
|
1385
|
+
if (isGateMessage(content) && session && !session.chatResponding) {
|
|
1386
|
+
injectGateButtons(div);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
container.scrollTop = container.scrollHeight;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1394
|
+
// Slash command autocomplete
|
|
1395
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1396
|
+
|
|
1397
|
+
var slashSuggestionItems = [];
|
|
1398
|
+
var slashSuggestionIdx = -1;
|
|
1399
|
+
|
|
1400
|
+
var SLASH_UTILITY_CMDS = [
|
|
1401
|
+
{ cmd: 'compact', desc: 'Summarize conversation to free up context' },
|
|
1402
|
+
{ cmd: 'clear', desc: 'Clear the messages panel' },
|
|
1403
|
+
{ cmd: 'help', desc: 'Show available commands' },
|
|
1404
|
+
{ cmd: 'stop', desc: 'Stop the current session' },
|
|
1405
|
+
{ cmd: 'init', desc: 'Initialize a CLAUDE.md file' },
|
|
1406
|
+
{ cmd: 'exit', desc: 'End the current session' },
|
|
1407
|
+
{ cmd: 'context', desc: 'Show current token usage' },
|
|
1408
|
+
{ cmd: 'rewind', desc: 'Restore to a previous point' },
|
|
1409
|
+
{ cmd: 'config', desc: 'Open settings panel' },
|
|
1410
|
+
{ cmd: 'model', desc: 'Change the active LLM model' },
|
|
1411
|
+
{ cmd: 'effort', desc: 'Set compute intensity (low/medium/high)' },
|
|
1412
|
+
{ cmd: 'mode', desc: 'Cycle permission mode (Shift+Tab)' },
|
|
1413
|
+
];
|
|
1414
|
+
|
|
1415
|
+
function buildSlashCommands(prefix) {
|
|
1416
|
+
var all = SLASH_UTILITY_CMDS.slice();
|
|
1417
|
+
if (agentList) {
|
|
1418
|
+
for (var i = 0; i < agentList.length; i++) {
|
|
1419
|
+
var info = AGENTS[agentList[i].name] || {};
|
|
1420
|
+
all.push({ cmd: agentList[i].name, desc: info.label ? 'Switch to ' + info.label : (agentList[i].description || '') });
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
if (prefix) {
|
|
1424
|
+
all = all.filter(function(c) { return c.cmd.indexOf(prefix) === 0; });
|
|
1425
|
+
}
|
|
1426
|
+
return all;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function updateSlashSuggestions() {
|
|
1430
|
+
var input = document.getElementById('chat-input');
|
|
1431
|
+
var val = input.value;
|
|
1432
|
+
var suggestions = document.getElementById('slash-suggestions');
|
|
1433
|
+
if (!val.startsWith('/') || val.indexOf(' ') !== -1) {
|
|
1434
|
+
hideSlashSuggestions();
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
var prefix = val.slice(1).toLowerCase();
|
|
1438
|
+
slashSuggestionItems = buildSlashCommands(prefix);
|
|
1439
|
+
if (slashSuggestionItems.length === 0) {
|
|
1440
|
+
hideSlashSuggestions();
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
slashSuggestionIdx = -1;
|
|
1444
|
+
renderSlashSuggestions(suggestions);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function renderSlashSuggestions(container) {
|
|
1448
|
+
container.innerHTML = '';
|
|
1449
|
+
for (var i = 0; i < slashSuggestionItems.length; i++) {
|
|
1450
|
+
var item = slashSuggestionItems[i];
|
|
1451
|
+
var div = document.createElement('div');
|
|
1452
|
+
div.className = 'slash-suggestion' + (i === slashSuggestionIdx ? ' active' : '');
|
|
1453
|
+
div.innerHTML =
|
|
1454
|
+
'<span class="slash-suggestion-cmd">/' + esc(item.cmd) + '</span>' +
|
|
1455
|
+
'<span class="slash-suggestion-desc">' + esc(item.desc) + '</span>';
|
|
1456
|
+
div.addEventListener('mousedown', (function(cmd) {
|
|
1457
|
+
return function(e) {
|
|
1458
|
+
e.preventDefault();
|
|
1459
|
+
applySlashSuggestion(cmd);
|
|
1460
|
+
};
|
|
1461
|
+
})(item.cmd));
|
|
1462
|
+
container.appendChild(div);
|
|
1463
|
+
}
|
|
1464
|
+
container.classList.add('visible');
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function hideSlashSuggestions() {
|
|
1468
|
+
var suggestions = document.getElementById('slash-suggestions');
|
|
1469
|
+
if (suggestions) {
|
|
1470
|
+
suggestions.classList.remove('visible');
|
|
1471
|
+
suggestions.innerHTML = '';
|
|
1472
|
+
}
|
|
1473
|
+
slashSuggestionItems = [];
|
|
1474
|
+
slashSuggestionIdx = -1;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function applySlashSuggestion(cmd) {
|
|
1478
|
+
var input = document.getElementById('chat-input');
|
|
1479
|
+
// Commands that take no args — send immediately
|
|
1480
|
+
var noArgCmds = ['compact', 'clear', 'stop', 'help', 'init', 'exit', 'context', 'rewind', 'config', 'mode'];
|
|
1481
|
+
var isAgent = agentList && agentList.some(function(a) { return a.name === cmd; });
|
|
1482
|
+
if (noArgCmds.indexOf(cmd) !== -1 || isAgent) {
|
|
1483
|
+
input.value = '/' + cmd;
|
|
1484
|
+
} else {
|
|
1485
|
+
input.value = '/' + cmd + ' ';
|
|
1486
|
+
}
|
|
1487
|
+
input.style.height = 'auto';
|
|
1488
|
+
input.style.height = Math.min(input.scrollHeight, 160) + 'px';
|
|
1489
|
+
input.focus();
|
|
1490
|
+
hideSlashSuggestions();
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function slashSuggestionKeydown(e) {
|
|
1494
|
+
var suggestions = document.getElementById('slash-suggestions');
|
|
1495
|
+
if (!suggestions || !suggestions.classList.contains('visible')) return false;
|
|
1496
|
+
|
|
1497
|
+
if (e.key === 'ArrowDown') {
|
|
1498
|
+
e.preventDefault();
|
|
1499
|
+
slashSuggestionIdx = Math.min(slashSuggestionIdx + 1, slashSuggestionItems.length - 1);
|
|
1500
|
+
renderSlashSuggestions(suggestions);
|
|
1501
|
+
return true;
|
|
1502
|
+
}
|
|
1503
|
+
if (e.key === 'ArrowUp') {
|
|
1504
|
+
e.preventDefault();
|
|
1505
|
+
slashSuggestionIdx = Math.max(slashSuggestionIdx - 1, -1);
|
|
1506
|
+
renderSlashSuggestions(suggestions);
|
|
1507
|
+
return true;
|
|
1508
|
+
}
|
|
1509
|
+
if (e.key === 'Tab') {
|
|
1510
|
+
e.preventDefault();
|
|
1511
|
+
if (slashSuggestionIdx >= 0 && slashSuggestionItems[slashSuggestionIdx]) {
|
|
1512
|
+
applySlashSuggestion(slashSuggestionItems[slashSuggestionIdx].cmd);
|
|
1513
|
+
} else if (slashSuggestionItems.length === 1) {
|
|
1514
|
+
applySlashSuggestion(slashSuggestionItems[0].cmd);
|
|
1515
|
+
}
|
|
1516
|
+
return true;
|
|
1517
|
+
}
|
|
1518
|
+
if (e.key === 'Enter' && slashSuggestionIdx >= 0) {
|
|
1519
|
+
e.preventDefault();
|
|
1520
|
+
applySlashSuggestion(slashSuggestionItems[slashSuggestionIdx].cmd);
|
|
1521
|
+
return true;
|
|
1522
|
+
}
|
|
1523
|
+
if (e.key === 'Escape') {
|
|
1524
|
+
hideSlashSuggestions();
|
|
1525
|
+
return true;
|
|
1526
|
+
}
|
|
1527
|
+
return false;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// Apply split/single layout CSS classes from the current splitMode global
|
|
1531
|
+
function applySplitLayout() {
|
|
1532
|
+
var chatPane = document.getElementById('chat-pane');
|
|
1533
|
+
if (splitMode) {
|
|
1534
|
+
chatPane.classList.add('split-visible');
|
|
1535
|
+
chatPane.style.flex = '1';
|
|
1536
|
+
document.getElementById('file-pane').style.flex = '1';
|
|
1537
|
+
} else {
|
|
1538
|
+
chatPane.classList.remove('split-visible', 'collapsed');
|
|
1539
|
+
chatPane.style.flex = '';
|
|
1540
|
+
chatPane.style.width = '';
|
|
1541
|
+
document.getElementById('file-pane').style.flex = '';
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1546
|
+
// Permission approval cards
|
|
1547
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
function renderPermissionCard(id, tool, command, sessionId) {
|
|
1551
|
+
var session = getActiveSession();
|
|
1552
|
+
var container = document.getElementById('messages');
|
|
1553
|
+
if (session && !session.hasMessages) { container.innerHTML = ''; session.hasMessages = true; }
|
|
1554
|
+
|
|
1555
|
+
var card = document.createElement('div');
|
|
1556
|
+
card.className = 'permission-card';
|
|
1557
|
+
card.setAttribute('data-permission-id', id);
|
|
1558
|
+
|
|
1559
|
+
var displayCmd = esc(command || '');
|
|
1560
|
+
var isLong = command && command.length > 120;
|
|
1561
|
+
var truncated = isLong ? esc(command.slice(0, 120)) + '...' : displayCmd;
|
|
1562
|
+
|
|
1563
|
+
card.innerHTML =
|
|
1564
|
+
'<div class="permission-header">' +
|
|
1565
|
+
'<span class="permission-lock">🔒</span>' +
|
|
1566
|
+
'<span class="permission-title">Permission Required</span>' +
|
|
1567
|
+
'</div>' +
|
|
1568
|
+
'<div class="permission-tool">' + esc(tool || 'Bash') + '</div>' +
|
|
1569
|
+
'<div class="permission-command">' +
|
|
1570
|
+
'<code class="permission-command-text">' + (isLong ? truncated : displayCmd) + '</code>' +
|
|
1571
|
+
(isLong ? '<button class="permission-show-more">show more</button>' : '') +
|
|
1572
|
+
'</div>' +
|
|
1573
|
+
'<div class="permission-actions">' +
|
|
1574
|
+
'<button class="permission-btn permission-btn-allow" data-action="allow">Allow Once</button>' +
|
|
1575
|
+
'<button class="permission-btn permission-btn-always" data-action="allow" data-persist="true">Always Allow</button>' +
|
|
1576
|
+
'<button class="permission-btn permission-btn-deny" data-action="deny">Deny</button>' +
|
|
1577
|
+
'<button class="permission-btn permission-btn-always-deny" data-action="deny" data-persist="true">Always Deny</button>' +
|
|
1578
|
+
'</div>';
|
|
1579
|
+
|
|
1580
|
+
if (isLong) {
|
|
1581
|
+
var cmdEl = card.querySelector('.permission-command');
|
|
1582
|
+
cmdEl.setAttribute('data-full-cmd', command);
|
|
1583
|
+
card.querySelector('.permission-show-more').addEventListener('click', function() {
|
|
1584
|
+
cmdEl.querySelector('.permission-command-text').textContent = command;
|
|
1585
|
+
this.remove();
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// Use addEventListener instead of inline onclick to avoid escaping issues
|
|
1590
|
+
// with double quotes, backslashes, and newlines in command strings
|
|
1591
|
+
var btns = card.querySelectorAll('.permission-btn');
|
|
1592
|
+
for (var i = 0; i < btns.length; i++) {
|
|
1593
|
+
(function(btn) {
|
|
1594
|
+
btn.addEventListener('click', function() {
|
|
1595
|
+
submitPermission(id, btn.getAttribute('data-action'), btn.getAttribute('data-persist') === 'true', command);
|
|
1596
|
+
});
|
|
1597
|
+
})(btns[i]);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
container.appendChild(card);
|
|
1601
|
+
container.scrollTop = container.scrollHeight;
|
|
1602
|
+
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function disablePermissionCard(id) {
|
|
1606
|
+
var card = document.querySelector('[data-permission-id="' + id + '"].permission-card');
|
|
1607
|
+
if (!card) return;
|
|
1608
|
+
var btns = card.querySelectorAll('.permission-btn');
|
|
1609
|
+
for (var i = 0; i < btns.length; i++) {
|
|
1610
|
+
btns[i].disabled = true;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
function enablePermissionCard(id) {
|
|
1615
|
+
var card = document.querySelector('[data-permission-id="' + id + '"].permission-card');
|
|
1616
|
+
if (!card) return;
|
|
1617
|
+
var btns = card.querySelectorAll('.permission-btn');
|
|
1618
|
+
for (var i = 0; i < btns.length; i++) {
|
|
1619
|
+
btns[i].disabled = false;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
async function submitPermission(id, decision, persist, command) {
|
|
1624
|
+
disablePermissionCard(id);
|
|
1625
|
+
try {
|
|
1626
|
+
await authFetch('/chat/permission', {
|
|
1627
|
+
method: 'POST',
|
|
1628
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1629
|
+
body: JSON.stringify({ id: id, decision: decision, persist: persist, command: command }),
|
|
1630
|
+
});
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
showPermissionCardError(id, 'Failed to submit. Try again.');
|
|
1633
|
+
enablePermissionCard(id);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function showPermissionCardError(id, msg) {
|
|
1638
|
+
var card = document.querySelector('[data-permission-id="' + id + '"].permission-card');
|
|
1639
|
+
if (!card) return;
|
|
1640
|
+
var existing = card.querySelector('.permission-error');
|
|
1641
|
+
if (existing) existing.remove();
|
|
1642
|
+
var errDiv = document.createElement('div');
|
|
1643
|
+
errDiv.className = 'permission-error';
|
|
1644
|
+
errDiv.textContent = msg;
|
|
1645
|
+
card.appendChild(errDiv);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function resolvePermissionCard(id, decision) {
|
|
1649
|
+
var card = document.querySelector('[data-permission-id="' + id + '"].permission-card');
|
|
1650
|
+
if (!card) return;
|
|
1651
|
+
|
|
1652
|
+
// Replace card content with resolved state
|
|
1653
|
+
var isAllow = decision === 'allow';
|
|
1654
|
+
card.innerHTML =
|
|
1655
|
+
'<div class="permission-resolved ' + (isAllow ? 'allowed' : 'denied') + '">' +
|
|
1656
|
+
(isAllow ? '✓ Allowed' : '✗ Denied') +
|
|
1657
|
+
'</div>';
|
|
1658
|
+
|
|
1659
|
+
// Fade out after 2s
|
|
1660
|
+
setTimeout(function() {
|
|
1661
|
+
card.classList.add('fade-out');
|
|
1662
|
+
setTimeout(function() { card.remove(); }, 500);
|
|
1663
|
+
}, 2000);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1667
|
+
// Gate block banner (gate hook returned decision:block)
|
|
1668
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1669
|
+
//
|
|
1670
|
+
// Pushed by the server's /gate-state-stream SSE channel when a new entry
|
|
1671
|
+
// appears in .shards/gates/violations.jsonl. Rendered as a chip above the
|
|
1672
|
+
// most recent assistant message so the user can see *why* the turn was
|
|
1673
|
+
// blocked (the agent's text alone doesn't reveal the hook's decision).
|
|
1674
|
+
|
|
1675
|
+
function renderGateBlockBanner(evt) {
|
|
1676
|
+
if (!evt) return;
|
|
1677
|
+
var container = document.getElementById('messages');
|
|
1678
|
+
if (!container) return;
|
|
1679
|
+
|
|
1680
|
+
// Find the last assistant bubble in the active session.
|
|
1681
|
+
var bubbles = container.querySelectorAll('.message.assistant');
|
|
1682
|
+
var anchor = bubbles.length > 0 ? bubbles[bubbles.length - 1] : null;
|
|
1683
|
+
|
|
1684
|
+
var banner = document.createElement('div');
|
|
1685
|
+
banner.className = 'gate-block-banner';
|
|
1686
|
+
var reason = evt.reason || 'Gate violation';
|
|
1687
|
+
var detail = [];
|
|
1688
|
+
if (evt.kind) detail.push(evt.kind);
|
|
1689
|
+
if (evt.gateId) detail.push('gate ' + evt.gateId);
|
|
1690
|
+
if (evt.phase) detail.push('phase ' + evt.phase);
|
|
1691
|
+
var detailStr = detail.length > 0 ? ' (' + detail.join(', ') + ')' : '';
|
|
1692
|
+
banner.innerHTML =
|
|
1693
|
+
'<span class="gate-block-icon">⛔</span>' +
|
|
1694
|
+
'<span class="gate-block-text"><strong>Blocked:</strong> ' +
|
|
1695
|
+
(typeof esc === 'function' ? esc(reason) : reason) +
|
|
1696
|
+
(typeof esc === 'function' ? esc(detailStr) : detailStr) +
|
|
1697
|
+
'</span>' +
|
|
1698
|
+
'<button class="gate-block-dismiss" type="button" aria-label="Dismiss">×</button>';
|
|
1699
|
+
|
|
1700
|
+
banner.querySelector('.gate-block-dismiss').onclick = function() {
|
|
1701
|
+
banner.remove();
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1704
|
+
if (anchor && anchor.parentElement) {
|
|
1705
|
+
anchor.parentElement.insertBefore(banner, anchor);
|
|
1706
|
+
} else {
|
|
1707
|
+
container.appendChild(banner);
|
|
1708
|
+
}
|
|
1709
|
+
container.scrollTop = container.scrollHeight;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1713
|
+
// Gate mode disabled banner
|
|
1714
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1715
|
+
//
|
|
1716
|
+
// Surfaces SHARDS_GATE_ENFORCE=0 / SHARDS_CHECKPOINT_ENFORCE=0 /
|
|
1717
|
+
// SHARDS_AUTO_VERIFY=0 to the user. Without this the Confirm/Request-Changes
|
|
1718
|
+
// buttons render normally even though the hook will ignore the response,
|
|
1719
|
+
// which is misleading (Bug M4 in the audit).
|
|
1720
|
+
|
|
1721
|
+
function renderGateModeBanner(mode) {
|
|
1722
|
+
if (!mode) return;
|
|
1723
|
+
var host = document.getElementById('chat-pane') ||
|
|
1724
|
+
document.getElementById('messages') ||
|
|
1725
|
+
document.body;
|
|
1726
|
+
if (!host) return;
|
|
1727
|
+
|
|
1728
|
+
var existing = document.getElementById('gate-mode-banner');
|
|
1729
|
+
var flags = [];
|
|
1730
|
+
if (mode.enforce === false) flags.push('SHARDS_GATE_ENFORCE=0');
|
|
1731
|
+
if (mode.checkpointEnforce === false) flags.push('SHARDS_CHECKPOINT_ENFORCE=0');
|
|
1732
|
+
if (mode.autoVerify === false) flags.push('SHARDS_AUTO_VERIFY=0');
|
|
1733
|
+
|
|
1734
|
+
if (flags.length === 0) {
|
|
1735
|
+
if (existing) existing.remove();
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
var msg;
|
|
1740
|
+
if (mode.enforce === false) {
|
|
1741
|
+
msg = '⚠ Gate enforcement disabled (' + flags.join(', ') + ') — gates are advisory only.';
|
|
1742
|
+
} else {
|
|
1743
|
+
msg = '⚠ Reduced gate enforcement (' + flags.join(', ') + ').';
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
if (existing) {
|
|
1747
|
+
existing.textContent = msg;
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
var banner = document.createElement('div');
|
|
1752
|
+
banner.id = 'gate-mode-banner';
|
|
1753
|
+
banner.className = 'gate-mode-banner';
|
|
1754
|
+
banner.textContent = msg;
|
|
1755
|
+
|
|
1756
|
+
// Insert at the top of the chat pane so it's always visible.
|
|
1757
|
+
if (host.firstChild) {
|
|
1758
|
+
host.insertBefore(banner, host.firstChild);
|
|
1759
|
+
} else {
|
|
1760
|
+
host.appendChild(banner);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function rebuildMessages(msgArray) {
|
|
1765
|
+
var session = getActiveSession();
|
|
1766
|
+
var container = document.getElementById('messages');
|
|
1767
|
+
container.innerHTML = '';
|
|
1768
|
+
if (session) session.hasMessages = false;
|
|
1769
|
+
if (msgArray.length === 0) {
|
|
1770
|
+
container.innerHTML = '<div class="empty-state">Select an agent to start chatting</div>';
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
for (var i = 0; i < msgArray.length; i++) {
|
|
1774
|
+
addMessageDirect(msgArray[i].role, msgArray[i].content, msgArray[i].agent, true, msgArray[i].attachments);
|
|
1775
|
+
}
|
|
1776
|
+
}
|