@nac3/forge-cli 0.2.0-alpha.59z.123 → 0.2.0-alpha.59z.158

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/agents/bridge.d.ts +69 -0
  2. package/dist/agents/bridge.d.ts.map +1 -0
  3. package/dist/agents/bridge.js +295 -0
  4. package/dist/agents/bridge.js.map +1 -0
  5. package/dist/agents/cli_real.d.ts +38 -0
  6. package/dist/agents/cli_real.d.ts.map +1 -0
  7. package/dist/agents/cli_real.js +46 -0
  8. package/dist/agents/cli_real.js.map +1 -0
  9. package/dist/agents/config_schema.d.ts +1 -1
  10. package/dist/agents/config_schema.d.ts.map +1 -1
  11. package/dist/agents/config_schema.js +16 -0
  12. package/dist/agents/config_schema.js.map +1 -1
  13. package/dist/agents/defaults.d.ts.map +1 -1
  14. package/dist/agents/defaults.js.map +1 -1
  15. package/dist/agents/delegate.d.ts +36 -0
  16. package/dist/agents/delegate.d.ts.map +1 -0
  17. package/dist/agents/delegate.js +68 -0
  18. package/dist/agents/delegate.js.map +1 -0
  19. package/dist/agents/iterative_step.d.ts +95 -0
  20. package/dist/agents/iterative_step.d.ts.map +1 -0
  21. package/dist/agents/iterative_step.js +249 -0
  22. package/dist/agents/iterative_step.js.map +1 -0
  23. package/dist/agents/llm_step.d.ts +50 -0
  24. package/dist/agents/llm_step.d.ts.map +1 -0
  25. package/dist/agents/llm_step.js +28 -0
  26. package/dist/agents/llm_step.js.map +1 -0
  27. package/dist/agents/real_step_factory.d.ts +95 -0
  28. package/dist/agents/real_step_factory.d.ts.map +1 -0
  29. package/dist/agents/real_step_factory.js +51 -0
  30. package/dist/agents/real_step_factory.js.map +1 -0
  31. package/dist/agents/runner.d.ts +14 -0
  32. package/dist/agents/runner.d.ts.map +1 -1
  33. package/dist/agents/runner.js +10 -1
  34. package/dist/agents/runner.js.map +1 -1
  35. package/dist/agents/runtime.d.ts +115 -0
  36. package/dist/agents/runtime.d.ts.map +1 -0
  37. package/dist/agents/runtime.js +429 -0
  38. package/dist/agents/runtime.js.map +1 -0
  39. package/dist/agents/system_prompt.d.ts +36 -0
  40. package/dist/agents/system_prompt.d.ts.map +1 -0
  41. package/dist/agents/system_prompt.js +101 -0
  42. package/dist/agents/system_prompt.js.map +1 -0
  43. package/dist/agents/types.d.ts +12 -1
  44. package/dist/agents/types.d.ts.map +1 -1
  45. package/dist/agents/types.js.map +1 -1
  46. package/dist/bin/yf.d.ts.map +1 -1
  47. package/dist/bin/yf.js +9 -0
  48. package/dist/bin/yf.js.map +1 -1
  49. package/dist/chat/claude_cli.d.ts +42 -0
  50. package/dist/chat/claude_cli.d.ts.map +1 -0
  51. package/dist/chat/claude_cli.js +299 -0
  52. package/dist/chat/claude_cli.js.map +1 -0
  53. package/dist/chat/graph_snapshot.d.ts +38 -0
  54. package/dist/chat/graph_snapshot.d.ts.map +1 -0
  55. package/dist/chat/graph_snapshot.js +159 -0
  56. package/dist/chat/graph_snapshot.js.map +1 -0
  57. package/dist/chat/ingest_session.d.ts +24 -4
  58. package/dist/chat/ingest_session.d.ts.map +1 -1
  59. package/dist/chat/ingest_session.js +139 -4
  60. package/dist/chat/ingest_session.js.map +1 -1
  61. package/dist/chat/panel.d.ts.map +1 -1
  62. package/dist/chat/panel.js +1358 -0
  63. package/dist/chat/panel.js.map +1 -1
  64. package/dist/chat/server.d.ts +21 -0
  65. package/dist/chat/server.d.ts.map +1 -1
  66. package/dist/chat/server.js +904 -4
  67. package/dist/chat/server.js.map +1 -1
  68. package/dist/chat/tools/nac3_invoke.d.ts +5 -0
  69. package/dist/chat/tools/nac3_invoke.d.ts.map +1 -1
  70. package/dist/chat/tools/nac3_invoke.js +1 -0
  71. package/dist/chat/tools/nac3_invoke.js.map +1 -1
  72. package/dist/chat/tools.d.ts.map +1 -1
  73. package/dist/chat/tools.js +12 -0
  74. package/dist/chat/tools.js.map +1 -1
  75. package/dist/commands/agent.d.ts +12 -1
  76. package/dist/commands/agent.d.ts.map +1 -1
  77. package/dist/commands/agent.js +92 -1
  78. package/dist/commands/agent.js.map +1 -1
  79. package/dist/commands/chat.d.ts +10 -0
  80. package/dist/commands/chat.d.ts.map +1 -1
  81. package/dist/commands/chat.js +12 -1
  82. package/dist/commands/chat.js.map +1 -1
  83. package/dist/commands/director.d.ts +37 -0
  84. package/dist/commands/director.d.ts.map +1 -0
  85. package/dist/commands/director.js +290 -0
  86. package/dist/commands/director.js.map +1 -0
  87. package/dist/core/pilot_setup.d.ts +14 -1
  88. package/dist/core/pilot_setup.d.ts.map +1 -1
  89. package/dist/core/pilot_setup.js +62 -1
  90. package/dist/core/pilot_setup.js.map +1 -1
  91. package/dist/design/yujin_design_resolver.d.ts +77 -0
  92. package/dist/design/yujin_design_resolver.d.ts.map +1 -0
  93. package/dist/design/yujin_design_resolver.js +113 -0
  94. package/dist/design/yujin_design_resolver.js.map +1 -0
  95. package/dist/docs/doctrine/default-design-system.md +120 -0
  96. package/dist/docs/doctrine/language-sovereignty.md +143 -0
  97. package/dist/docs/doctrine/testing-policy.md +211 -0
  98. package/dist/nac3/approval_queue.d.ts +8 -0
  99. package/dist/nac3/approval_queue.d.ts.map +1 -1
  100. package/dist/nac3/approval_queue.js.map +1 -1
  101. package/dist/nac3/internal_manifest.d.ts.map +1 -1
  102. package/dist/nac3/internal_manifest.js +178 -0
  103. package/dist/nac3/internal_manifest.js.map +1 -1
  104. package/dist/office/excel.d.ts +22 -0
  105. package/dist/office/excel.d.ts.map +1 -1
  106. package/dist/office/excel.js +82 -0
  107. package/dist/office/excel.js.map +1 -1
  108. package/dist/qa/brain_matrix/fixtures/scenario_a11y_focus_after_open.json +22 -0
  109. package/dist/qa/brain_matrix/fixtures/scenario_a11y_label_unlabeled_button.json +26 -0
  110. package/dist/qa/brain_matrix/fixtures/scenario_agent_director_consults_workflow_before_dispatch.json +42 -0
  111. package/dist/qa/brain_matrix/fixtures/scenario_analyst_extracts_then_returns.json +28 -0
  112. package/dist/qa/brain_matrix/fixtures/scenario_coder_consults_nac_spec_before_writing.json +28 -0
  113. package/dist/qa/brain_matrix/fixtures/scenario_default_design_system_override_shadcn.json +45 -0
  114. package/dist/qa/brain_matrix/fixtures/scenario_default_design_system_use_card_template.json +44 -0
  115. package/dist/qa/brain_matrix/fixtures/scenario_director_delegates_to_analyst.json +26 -0
  116. package/dist/qa/brain_matrix/fixtures/scenario_director_no_self_delegation.json +20 -0
  117. package/dist/qa/brain_matrix/fixtures/scenario_director_terminal_done_marker.json +19 -0
  118. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_default_design_system.json +50 -0
  119. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_language_sovereignty.json +53 -0
  120. package/dist/qa/brain_matrix/fixtures/scenario_doctrine_discover_testing_policy.json +55 -0
  121. package/dist/qa/brain_matrix/fixtures/scenario_language_sovereignty_not_conducive_spa_python.json +61 -0
  122. package/dist/qa/brain_matrix/fixtures/scenario_language_sovereignty_user_picks_python.json +48 -0
  123. package/dist/qa/brain_matrix/fixtures/scenario_mcp_invoke_destructive_pauses_for_r8.json +42 -0
  124. package/dist/qa/brain_matrix/fixtures/scenario_mcp_invoke_happy_path_writes_pizarron.json +76 -0
  125. package/dist/qa/brain_matrix/fixtures/scenario_mcp_invoke_network_timeout_graceful.json +41 -0
  126. package/dist/qa/brain_matrix/fixtures/scenario_no_hardcoded_color_use_token.json +47 -0
  127. package/dist/qa/brain_matrix/fixtures/scenario_office_excel_update_cell_picks_right_verb.json +26 -0
  128. package/dist/qa/brain_matrix/fixtures/scenario_scaffolder_destructive_pauses_for_approval.json +23 -0
  129. package/dist/qa/brain_matrix/fixtures/scenario_sumie_icon_pick_from_catalog.json +56 -0
  130. package/dist/qa/brain_matrix/fixtures/scenario_tester_uses_shell_exec_only.json +27 -0
  131. package/dist/version.d.ts +1 -1
  132. package/dist/version.js +1 -1
  133. package/dist/workflow/graph.d.ts +59 -0
  134. package/dist/workflow/graph.d.ts.map +1 -0
  135. package/dist/workflow/graph.js +137 -0
  136. package/dist/workflow/graph.js.map +1 -0
  137. package/dist/workflow/state.d.ts +34 -0
  138. package/dist/workflow/state.d.ts.map +1 -1
  139. package/dist/workflow/state.js.map +1 -1
  140. package/docs/doctrine/default-design-system.md +120 -0
  141. package/docs/doctrine/language-sovereignty.md +143 -0
  142. package/docs/doctrine/testing-policy.md +211 -0
  143. package/package.json +102 -100
  144. package/src/i18n/catalogs/ar.json +20 -1
  145. package/src/i18n/catalogs/de.json +20 -1
  146. package/src/i18n/catalogs/en.json +20 -21
  147. package/src/i18n/catalogs/es.json +20 -21
  148. package/src/i18n/catalogs/fr.json +20 -1
  149. package/src/i18n/catalogs/hi.json +20 -1
  150. package/src/i18n/catalogs/it.json +20 -1
  151. package/src/i18n/catalogs/ja.json +20 -1
  152. package/src/i18n/catalogs/pt.json +20 -1
  153. package/src/i18n/catalogs/zh.json +20 -1
  154. package/templates/sumi-context.template.md +57 -0
@@ -46,6 +46,20 @@ export function renderPanelHtml(cfg) {
46
46
  streamEmptyBody: tForLang(lang, 'panel.stream.empty.body'),
47
47
  traceEmptyKanji: tForLang(lang, 'panel.trace.empty.kanji'),
48
48
  traceEmptyBody: tForLang(lang, 'panel.trace.empty.body'),
49
+ /* Director sprint slice 6 -- agent overlay strings. */
50
+ agentsButton: tForLang(lang, 'panel.button.agents'),
51
+ agentsTitle: tForLang(lang, 'panel.agents.title'),
52
+ agentsSubtitle: tForLang(lang, 'panel.agents.subtitle'),
53
+ agentsEmptyTitle: tForLang(lang, 'panel.agents.empty.title'),
54
+ agentsEmptyBody: tForLang(lang, 'panel.agents.empty.body'),
55
+ agentsLegendEco: tForLang(lang, 'panel.agents.legend.eco'),
56
+ agentsLegendManaged: tForLang(lang, 'panel.agents.legend.managed'),
57
+ agentsLegendFrontier: tForLang(lang, 'panel.agents.legend.frontier'),
58
+ agentsStatusDisabled: tForLang(lang, 'panel.agents.status.disabled'),
59
+ agentsEventRunning: tForLang(lang, 'panel.agents.event.running'),
60
+ agentsEventIdle: tForLang(lang, 'panel.agents.event.idle'),
61
+ agentsTickLive: tForLang(lang, 'panel.agents.tick.live'),
62
+ agentsTickLost: tForLang(lang, 'panel.agents.tick.lost'),
49
63
  };
50
64
  const runtimeConfig = JSON.stringify({
51
65
  projectName: cfg.projectName,
@@ -911,6 +925,297 @@ body {
911
925
  font-size: 11px; opacity: 0.7;
912
926
  }
913
927
 
928
+ /* ---- agents overlay (Director sprint slice 6) ---- */
929
+ .header .actions .agents-btn { font-family: inherit; font-size: 13px; padding: 4px 10px; }
930
+ #yf-agents-modal {
931
+ position: fixed; inset: 0;
932
+ background: rgba(26,26,26,0.55); backdrop-filter: blur(4px);
933
+ display: flex; align-items: center; justify-content: center;
934
+ z-index: 320;
935
+ }
936
+ #yf-agents-modal .agents-card {
937
+ width: 100%; max-width: 760px; max-height: 88vh;
938
+ background: var(--bg-1, #1a1d27); border: 1px solid var(--border);
939
+ border-radius: 12px; box-shadow: 0 12px 36px rgba(0,0,0,0.30);
940
+ display: flex; flex-direction: column; overflow: hidden;
941
+ }
942
+ #yf-agents-modal .a-header {
943
+ display: flex; align-items: flex-start; justify-content: space-between;
944
+ background: var(--indigo); color: white; padding: 12px 16px;
945
+ flex-shrink: 0; gap: 12px;
946
+ }
947
+ #yf-agents-modal .a-header h2 { font-size: 15px; margin: 0; font-weight: 600; }
948
+ #yf-agents-modal .a-header .a-subtitle {
949
+ font-size: 11px; margin: 4px 0 0 0; opacity: 0.85; font-weight: 400;
950
+ }
951
+ #yf-agents-modal .a-header-status { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
952
+ #yf-agents-modal .a-tick {
953
+ display: inline-flex; align-items: center; gap: 6px;
954
+ background: rgba(255,255,255,0.12); padding: 4px 10px; border-radius: 999px;
955
+ font-size: 11px; color: white;
956
+ }
957
+ #yf-agents-modal .a-tick .a-tick-dot {
958
+ width: 8px; height: 8px; border-radius: 50%; background: #ffb84d;
959
+ }
960
+ #yf-agents-modal .a-tick.live .a-tick-dot { background: #4dd28a; box-shadow: 0 0 6px rgba(77,210,138,0.7); }
961
+ #yf-agents-modal .a-header button {
962
+ background: rgba(255,255,255,0.1); border: 0; color: white;
963
+ padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 12px;
964
+ }
965
+ #yf-agents-modal .a-body { flex: 1; overflow: hidden; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
966
+ #yf-agents-modal .a-canvas {
967
+ flex: 1; min-height: 360px; position: relative;
968
+ background: var(--bg-2, #20232f);
969
+ border: 1px solid var(--border-subtle, #2a2e3a);
970
+ border-radius: 8px; overflow: hidden;
971
+ }
972
+ #yf-agents-modal .a-canvas svg { width: 100%; height: 100%; display: block; }
973
+ #yf-agents-modal .a-empty, #yf-agents-modal .a-disabled {
974
+ position: absolute; inset: 0; display: flex; flex-direction: column;
975
+ align-items: center; justify-content: center; gap: 8px; padding: 24px;
976
+ color: var(--ink-muted); text-align: center;
977
+ }
978
+ #yf-agents-modal .a-empty-kanji { font-size: 48px; color: var(--ink-subtle); opacity: 0.5; }
979
+ #yf-agents-modal .a-empty-title { font-size: 15px; font-weight: 600; color: var(--ink); }
980
+ #yf-agents-modal .a-empty-body { font-size: 12px; max-width: 420px; }
981
+ #yf-agents-modal .a-legend { display: flex; gap: 8px; flex-wrap: wrap; padding: 0 4px; }
982
+ #yf-agents-modal .a-chip {
983
+ font-size: 11px; padding: 4px 10px; border-radius: 999px;
984
+ border: 1px solid currentColor; color: var(--ink-muted);
985
+ }
986
+ #yf-agents-modal .a-chip.tier-eco { color: #6c8aa6; }
987
+ #yf-agents-modal .a-chip.tier-managed { color: #7da06d; }
988
+ #yf-agents-modal .a-chip.tier-frontier { color: #c87c4a; }
989
+ /* SVG node + edge styles. */
990
+ .a-node-circle { fill: var(--bg-1, #1a1d27); stroke: var(--ink-muted); stroke-width: 2; transition: stroke 0.18s ease, fill 0.18s ease; }
991
+ .a-node-circle.tier-eco { stroke: #6c8aa6; }
992
+ .a-node-circle.tier-managed { stroke: #7da06d; }
993
+ .a-node-circle.tier-frontier { stroke: #c87c4a; }
994
+ .a-node-circle.status-paused { stroke-dasharray: 4 4; opacity: 0.65; }
995
+ .a-node-circle.status-deleted { opacity: 0.30; }
996
+ .a-node-circle.running { fill: rgba(255,184,77,0.18); stroke-width: 3; animation: a-pulse 1.4s ease-in-out infinite; }
997
+ .a-node-circle.paused-for-approval {
998
+ /* Director closure pendiente 5 -- the agent finished its run
999
+ * but is waiting on a human to resolve the R8 approval modal.
1000
+ * Distinct from .running (active) and from a clean succeeded
1001
+ * idle node: amber stroke + dashed + slow breath. */
1002
+ stroke: #ffb84d; stroke-width: 2.5;
1003
+ stroke-dasharray: 5 3;
1004
+ fill: rgba(255,184,77,0.10);
1005
+ animation: a-breath 2.2s ease-in-out infinite;
1006
+ }
1007
+ .a-node-label {
1008
+ fill: var(--ink, #d4d4d4); font-size: 11px;
1009
+ text-anchor: middle; pointer-events: none;
1010
+ font-family: system-ui, -apple-system, sans-serif;
1011
+ }
1012
+ .a-node-tier {
1013
+ fill: var(--ink-muted, #8a8a8a); font-size: 9px;
1014
+ text-anchor: middle; pointer-events: none;
1015
+ }
1016
+ .a-edge {
1017
+ stroke: var(--ink-subtle, #5a5e6a); stroke-width: 1.2;
1018
+ fill: none; opacity: 0.55;
1019
+ transition: stroke 0.18s ease, opacity 0.18s ease, stroke-width 0.18s ease;
1020
+ }
1021
+ .a-edge.active {
1022
+ stroke: #ffb84d; stroke-width: 2; opacity: 1;
1023
+ animation: a-edge-flash 0.9s ease-out;
1024
+ }
1025
+ @keyframes a-pulse {
1026
+ 0% { stroke-width: 3; }
1027
+ 50% { stroke-width: 5; }
1028
+ 100% { stroke-width: 3; }
1029
+ }
1030
+ @keyframes a-breath {
1031
+ 0% { opacity: 0.55; }
1032
+ 50% { opacity: 1.00; }
1033
+ 100% { opacity: 0.55; }
1034
+ }
1035
+ @keyframes a-edge-flash {
1036
+ 0% { stroke: #ffe6a8; }
1037
+ 100% { stroke: #ffb84d; }
1038
+ }
1039
+
1040
+ /* ---- MCP bridges modal (F101 slice 3) ---- */
1041
+ #yf-mcp-modal {
1042
+ position: fixed; inset: 0; background: rgba(8,10,18,0.78);
1043
+ z-index: 99; display: flex; align-items: center; justify-content: center; padding: 24px;
1044
+ -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
1045
+ }
1046
+ #yf-mcp-modal .mcp-card {
1047
+ background: var(--bg-1, #1a1d27); color: var(--ink, #d4d4d4);
1048
+ border: 1px solid var(--ink-subtle, #5a5e6a); border-radius: 8px;
1049
+ width: 100%; max-width: 720px; max-height: 80vh; display: flex; flex-direction: column;
1050
+ }
1051
+ #yf-mcp-modal .mcp-header {
1052
+ display: flex; align-items: center; justify-content: space-between;
1053
+ padding: 12px 16px; border-bottom: 1px solid var(--ink-subtle, #5a5e6a);
1054
+ }
1055
+ #yf-mcp-modal .mcp-header h2 { font-size: 15px; margin: 0; font-weight: 600; }
1056
+ #yf-mcp-modal .mcp-header .mcp-subtitle { font-size: 11px; color: var(--ink-muted, #8a8a8a); margin: 2px 0 0 0; }
1057
+ #yf-mcp-modal .mcp-header button {
1058
+ background: transparent; color: inherit; border: 1px solid var(--ink-subtle, #5a5e6a);
1059
+ border-radius: 4px; padding: 4px 10px; font-family: inherit; font-size: 13px;
1060
+ }
1061
+ #yf-mcp-modal .mcp-body { flex: 1; overflow: auto; padding: 12px 16px; }
1062
+ #yf-mcp-modal .mcp-empty {
1063
+ text-align: center; padding: 28px 12px; color: var(--ink-muted, #8a8a8a); font-size: 13px;
1064
+ }
1065
+ #yf-mcp-modal .mcp-bridge {
1066
+ display: flex; align-items: center; gap: 10px;
1067
+ padding: 10px 12px; margin-bottom: 8px;
1068
+ background: var(--bg-2, #232631); border: 1px solid var(--ink-subtle, #5a5e6a); border-radius: 6px;
1069
+ }
1070
+ #yf-mcp-modal .mcp-bridge .mcp-meta { flex: 1; min-width: 0; }
1071
+ #yf-mcp-modal .mcp-bridge .mcp-name {
1072
+ font-weight: 600; font-size: 13px;
1073
+ }
1074
+ #yf-mcp-modal .mcp-bridge .mcp-endpoint {
1075
+ font-size: 11px; color: var(--ink-muted, #8a8a8a);
1076
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1077
+ }
1078
+ #yf-mcp-modal .mcp-bridge .mcp-source-chip {
1079
+ font-size: 10px; padding: 2px 6px; border-radius: 3px;
1080
+ background: rgba(108,138,166,0.18); color: #9bb4cc; text-transform: uppercase; letter-spacing: 0.4px;
1081
+ }
1082
+ #yf-mcp-modal .mcp-bridge .mcp-protocol-chip {
1083
+ font-size: 10px; padding: 2px 6px; border-radius: 3px;
1084
+ background: rgba(125,160,109,0.18); color: #a6c592; text-transform: uppercase; letter-spacing: 0.4px;
1085
+ }
1086
+ #yf-mcp-modal .mcp-bridge.disabled { opacity: 0.55; }
1087
+ #yf-mcp-modal .mcp-toggle {
1088
+ background: transparent; color: inherit; border: 1px solid var(--ink-subtle, #5a5e6a);
1089
+ border-radius: 4px; padding: 4px 10px; font-family: inherit; font-size: 12px; cursor: pointer;
1090
+ }
1091
+ #yf-mcp-modal .mcp-toggle:hover { background: rgba(255,255,255,0.05); }
1092
+ #yf-mcp-modal .mcp-tools-btn {
1093
+ background: transparent; color: inherit; border: 1px solid var(--ink-subtle, #5a5e6a);
1094
+ border-radius: 4px; padding: 4px 10px; font-family: inherit; font-size: 12px; cursor: pointer;
1095
+ }
1096
+ #yf-mcp-modal .mcp-tools-list {
1097
+ margin-top: 6px; padding: 8px 12px; background: var(--bg-0, #0f1118); border-radius: 4px;
1098
+ font-size: 12px; line-height: 1.5; max-height: 240px; overflow: auto;
1099
+ }
1100
+ #yf-mcp-modal .mcp-tool {
1101
+ padding: 4px 0; border-bottom: 1px solid var(--ink-subtle, #5a5e6a);
1102
+ }
1103
+ #yf-mcp-modal .mcp-tool:last-child { border-bottom: 0; }
1104
+ #yf-mcp-modal .mcp-tool .mcp-tool-id { font-weight: 600; color: var(--ink, #d4d4d4); }
1105
+ #yf-mcp-modal .mcp-tool .mcp-tool-desc { color: var(--ink-muted, #8a8a8a); }
1106
+ #yf-mcp-modal .mcp-tools-error { color: #ff8b7a; padding: 6px 0; }
1107
+ #yf-mcp-modal .mcp-refresh {
1108
+ margin-bottom: 8px;
1109
+ background: transparent; color: inherit; border: 1px solid var(--ink-subtle, #5a5e6a);
1110
+ border-radius: 4px; padding: 4px 10px; font-family: inherit; font-size: 12px; cursor: pointer;
1111
+ }
1112
+
1113
+ /* ---- Activity overlay (visibilidad sprint -- alpha.59z.153) ---- */
1114
+ #yf-activity-modal {
1115
+ position: fixed; inset: 0; background: rgba(8,10,18,0.78);
1116
+ z-index: 99; display: flex; align-items: center; justify-content: center; padding: 24px;
1117
+ -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
1118
+ }
1119
+ #yf-activity-modal .act-card {
1120
+ background: var(--bg-1, #1a1d27); color: var(--ink, #d4d4d4);
1121
+ border: 1px solid var(--ink-subtle, #5a5e6a); border-radius: 8px;
1122
+ width: 100%; max-width: 920px; max-height: 84vh; display: flex; flex-direction: column;
1123
+ }
1124
+ #yf-activity-modal .act-header {
1125
+ display: flex; align-items: center; justify-content: space-between;
1126
+ padding: 12px 16px; border-bottom: 1px solid var(--ink-subtle, #5a5e6a);
1127
+ }
1128
+ #yf-activity-modal .act-header h2 { font-size: 15px; margin: 0; font-weight: 600; }
1129
+ #yf-activity-modal .act-header .act-subtitle { font-size: 11px; color: var(--ink-muted, #8a8a8a); margin: 2px 0 0 0; }
1130
+ #yf-activity-modal .act-header button {
1131
+ background: transparent; color: inherit; border: 1px solid var(--ink-subtle, #5a5e6a);
1132
+ border-radius: 4px; padding: 4px 10px; font-family: inherit; font-size: 13px; cursor: pointer;
1133
+ }
1134
+ #yf-activity-modal .act-tabs {
1135
+ display: flex; gap: 4px; padding: 8px 16px 0;
1136
+ border-bottom: 1px solid var(--ink-subtle, #5a5e6a);
1137
+ }
1138
+ #yf-activity-modal .act-tab {
1139
+ background: transparent; color: var(--ink-muted, #8a8a8a);
1140
+ border: 1px solid transparent; border-bottom: 0;
1141
+ border-radius: 4px 4px 0 0; padding: 6px 12px;
1142
+ font-family: inherit; font-size: 12px; cursor: pointer;
1143
+ }
1144
+ #yf-activity-modal .act-tab.active {
1145
+ color: var(--ink, #d4d4d4); background: var(--bg-2, #232631);
1146
+ border-color: var(--ink-subtle, #5a5e6a);
1147
+ }
1148
+ #yf-activity-modal .act-body { flex: 1; overflow: auto; padding: 12px 16px; }
1149
+ #yf-activity-modal .act-empty { text-align: center; padding: 24px 12px; color: var(--ink-muted, #8a8a8a); font-size: 13px; }
1150
+ #yf-activity-modal .act-line {
1151
+ display: grid;
1152
+ grid-template-columns: 90px 130px 1fr; gap: 8px;
1153
+ padding: 4px 0; border-bottom: 1px dashed rgba(90,94,106,0.25);
1154
+ font-size: 12px; font-family: ui-monospace, Menlo, Consolas, monospace;
1155
+ }
1156
+ #yf-activity-modal .act-line:last-child { border-bottom: 0; }
1157
+ #yf-activity-modal .act-line .act-ts { color: var(--ink-muted, #8a8a8a); }
1158
+ #yf-activity-modal .act-line .act-kind {
1159
+ color: var(--ink, #d4d4d4); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1160
+ }
1161
+ #yf-activity-modal .act-line.kind-dispatch .act-kind { color: #9bb4cc; }
1162
+ #yf-activity-modal .act-line.kind-agent .act-kind { color: #c87c4a; }
1163
+ #yf-activity-modal .act-line.kind-bridge .act-kind { color: #a6c592; }
1164
+ #yf-activity-modal .act-line.kind-approval .act-kind { color: #ffb84d; }
1165
+ #yf-activity-modal .act-line .act-text { word-break: break-word; }
1166
+ #yf-activity-modal .act-controls {
1167
+ display: flex; gap: 8px; padding: 8px 16px;
1168
+ border-top: 1px solid var(--ink-subtle, #5a5e6a);
1169
+ font-size: 12px; color: var(--ink-muted, #8a8a8a); align-items: center;
1170
+ }
1171
+ #yf-activity-modal .act-controls input[type="checkbox"] { accent-color: #c87c4a; }
1172
+ #yf-activity-modal .act-controls button {
1173
+ background: transparent; color: inherit; border: 1px solid var(--ink-subtle, #5a5e6a);
1174
+ border-radius: 4px; padding: 4px 10px; cursor: pointer;
1175
+ }
1176
+ /* Bridge tab */
1177
+ #yf-activity-modal .act-pair {
1178
+ display: flex; align-items: center; gap: 10px;
1179
+ padding: 8px 12px; margin-bottom: 6px;
1180
+ background: var(--bg-2, #232631); border: 1px solid var(--ink-subtle, #5a5e6a); border-radius: 5px;
1181
+ cursor: pointer;
1182
+ }
1183
+ #yf-activity-modal .act-pair:hover { border-color: #c87c4a; }
1184
+ #yf-activity-modal .act-pair .act-pair-id { flex: 1; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; }
1185
+ #yf-activity-modal .act-pair .act-pair-count {
1186
+ font-size: 11px; padding: 2px 6px; border-radius: 3px;
1187
+ background: rgba(108,138,166,0.2); color: #9bb4cc;
1188
+ }
1189
+ #yf-activity-modal .act-bridge-msg {
1190
+ padding: 6px 10px; margin-bottom: 5px;
1191
+ background: var(--bg-0, #0f1118); border-radius: 4px;
1192
+ font-size: 12px;
1193
+ }
1194
+ #yf-activity-modal .act-bridge-msg .act-bridge-head {
1195
+ font-family: ui-monospace, Menlo, Consolas, monospace; color: var(--ink-muted, #8a8a8a);
1196
+ }
1197
+ #yf-activity-modal .act-bridge-msg .act-bridge-payload { color: var(--ink, #d4d4d4); margin-top: 2px; word-break: break-word; }
1198
+ /* Chat tab */
1199
+ #yf-activity-modal .act-chat-row {
1200
+ display: flex; gap: 8px; margin-bottom: 10px;
1201
+ }
1202
+ #yf-activity-modal .act-chat-row select,
1203
+ #yf-activity-modal .act-chat-row input[type="text"] {
1204
+ background: var(--bg-2, #232631); color: var(--ink, #d4d4d4);
1205
+ border: 1px solid var(--ink-subtle, #5a5e6a); border-radius: 4px;
1206
+ padding: 6px 10px; font-family: inherit; font-size: 13px;
1207
+ }
1208
+ #yf-activity-modal .act-chat-row input[type="text"] { flex: 1; }
1209
+ #yf-activity-modal .act-chat-row button {
1210
+ background: transparent; color: inherit; border: 1px solid var(--ink-subtle, #5a5e6a);
1211
+ border-radius: 4px; padding: 6px 14px; cursor: pointer;
1212
+ }
1213
+ #yf-activity-modal .act-chat-result {
1214
+ padding: 10px; background: var(--bg-0, #0f1118); border-radius: 4px;
1215
+ font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px;
1216
+ white-space: pre-wrap; word-break: break-word;
1217
+ }
1218
+
914
1219
  /* ---- hidden helper ---- */
915
1220
  .hidden { display: none !important; }
916
1221
  </style>
@@ -1000,6 +1305,132 @@ body {
1000
1305
  </div>
1001
1306
  </div>
1002
1307
 
1308
+ <!-- Director sprint slice 6 -- agent graph overlay.
1309
+ Topology: Director (centre) + 4 sub-agents (radial) +
1310
+ auxiliary agents (lower row). Live updates via SSE. -->
1311
+ <div id="yf-agents-modal" data-nac-id="yujin.agents.modal" class="hidden" role="dialog" aria-modal="true" aria-labelledby="agents-title">
1312
+ <div class="agents-card">
1313
+ <div class="a-header">
1314
+ <div>
1315
+ <h2 id="agents-title" data-nac-id="yujin.agents.title">${escapeHtml(tr.agentsTitle)}</h2>
1316
+ <p class="a-subtitle">${escapeHtml(tr.agentsSubtitle)}</p>
1317
+ </div>
1318
+ <div class="a-header-status">
1319
+ <span class="a-tick" data-agents-tick aria-live="polite" title="${escapeHtml(tr.agentsTickLive)}"><span class="a-tick-dot"></span><span class="a-tick-label">${escapeHtml(tr.agentsTickLost)}</span></span>
1320
+ <button data-agents-close aria-label="${escapeHtml(tr.closeButton)}" title="${escapeHtml(tr.closeButton)}">${escapeHtml(tr.closeButton)}</button>
1321
+ </div>
1322
+ </div>
1323
+ <div class="a-body">
1324
+ <div class="a-canvas" data-agents-canvas>
1325
+ <svg id="yf-agents-svg" viewBox="0 0 640 440" preserveAspectRatio="xMidYMid meet" role="img" aria-label="${escapeHtml(tr.agentsTitle)}">
1326
+ <defs>
1327
+ <marker id="a-arrow" viewBox="0 -5 10 10" refX="10" refY="0" markerWidth="6" markerHeight="6" orient="auto">
1328
+ <path d="M0,-4 L8,0 L0,4 Z" fill="currentColor"></path>
1329
+ </marker>
1330
+ </defs>
1331
+ <g data-agents-edges></g>
1332
+ <g data-agents-nodes></g>
1333
+ </svg>
1334
+ <div class="a-empty hidden" data-agents-empty>
1335
+ <div class="a-empty-kanji">空</div>
1336
+ <div class="a-empty-title">${escapeHtml(tr.agentsEmptyTitle)}</div>
1337
+ <div class="a-empty-body">${escapeHtml(tr.agentsEmptyBody)}</div>
1338
+ </div>
1339
+ <div class="a-disabled hidden" data-agents-disabled>
1340
+ ${escapeHtml(tr.agentsStatusDisabled)}
1341
+ </div>
1342
+ </div>
1343
+ <div class="a-legend">
1344
+ <span class="a-chip tier-eco">${escapeHtml(tr.agentsLegendEco)}</span>
1345
+ <span class="a-chip tier-managed">${escapeHtml(tr.agentsLegendManaged)}</span>
1346
+ <span class="a-chip tier-frontier">${escapeHtml(tr.agentsLegendFrontier)}</span>
1347
+ </div>
1348
+ </div>
1349
+ </div>
1350
+ </div>
1351
+
1352
+ <!-- F107 accessibility -- two ARIA live regions that the
1353
+ a11y.announce verb writes into. The polite region is the
1354
+ default; assertive is reserved for errors / urgent. Hidden
1355
+ visually but exposed to screen readers + voice loop. -->
1356
+ <div id="yf-a11y-live-polite" aria-live="polite" aria-atomic="true"
1357
+ style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;"></div>
1358
+ <div id="yf-a11y-live-assertive" aria-live="assertive" aria-atomic="true"
1359
+ style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;"></div>
1360
+
1361
+ <!-- F101 slice 3 -- MCP bridges modal. Lists discovered bridges +
1362
+ per-bridge enable toggle + tool catalog inline panel. -->
1363
+ <div id="yf-mcp-modal" data-nac-id="yujin.mcp.modal" class="hidden" role="dialog" aria-modal="true" aria-labelledby="mcp-title">
1364
+ <div class="mcp-card">
1365
+ <div class="mcp-header">
1366
+ <div>
1367
+ <h2 id="mcp-title">MCP Bridges</h2>
1368
+ <p class="mcp-subtitle">F101 -- Forge as transparent MCP aggregator</p>
1369
+ </div>
1370
+ <button data-mcp-close aria-label="${escapeHtml(tr.closeButton)}" title="${escapeHtml(tr.closeButton)}">${escapeHtml(tr.closeButton)}</button>
1371
+ </div>
1372
+ <div class="mcp-body">
1373
+ <button class="mcp-refresh" data-mcp-refresh title="Refrescar discovery">Refrescar</button>
1374
+ <div data-mcp-content>
1375
+ <div class="mcp-empty">Cargando...</div>
1376
+ </div>
1377
+ </div>
1378
+ </div>
1379
+ </div>
1380
+
1381
+ <!-- Activity overlay -- 3 tabs: feed live timestamped / bridge
1382
+ inspector / chat-per-agent. Alpha.59z.153 antidote anti-ansiedad. -->
1383
+ <div id="yf-activity-modal" data-nac-id="yujin.activity.modal" class="hidden" role="dialog" aria-modal="true" aria-labelledby="act-title">
1384
+ <div class="act-card">
1385
+ <div class="act-header">
1386
+ <div>
1387
+ <h2 id="act-title">Agent Activity</h2>
1388
+ <p class="act-subtitle">Feed timestamped + bridge inspector + chat-per-agent</p>
1389
+ </div>
1390
+ <div style="display:flex;align-items:center;gap:10px;">
1391
+ <select data-act-project-filter aria-label="Filter by project"
1392
+ style="background:var(--bg-2,#232631);color:var(--ink,#d4d4d4);border:1px solid var(--ink-subtle,#5a5e6a);border-radius:4px;padding:4px 8px;font-family:inherit;font-size:12px;">
1393
+ <option value="">All projects</option>
1394
+ </select>
1395
+ <span data-act-concurrency aria-label="Concurrent runs"
1396
+ title="Concurrent runs: global / per-project caps"
1397
+ style="font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11px;color:var(--ink-muted,#8a8a8a);white-space:nowrap;">-/-</span>
1398
+ <button data-activity-close aria-label="${escapeHtml(tr.closeButton)}" title="${escapeHtml(tr.closeButton)}">${escapeHtml(tr.closeButton)}</button>
1399
+ </div>
1400
+ </div>
1401
+ <div class="act-tabs" role="tablist">
1402
+ <button class="act-tab active" data-act-tab="feed" role="tab">Feed</button>
1403
+ <button class="act-tab" data-act-tab="bridge" role="tab">Bridge</button>
1404
+ <button class="act-tab" data-act-tab="chat" role="tab">Chat</button>
1405
+ </div>
1406
+ <div class="act-body">
1407
+ <div data-act-panel="feed">
1408
+ <div class="act-empty">Cargando feed...</div>
1409
+ </div>
1410
+ <div data-act-panel="bridge" class="hidden">
1411
+ <div class="act-empty">Cargando bridge...</div>
1412
+ </div>
1413
+ <div data-act-panel="chat" class="hidden">
1414
+ <div class="act-chat-row">
1415
+ <select data-act-chat-slug aria-label="Agent slug"></select>
1416
+ <input type="text" data-act-chat-text placeholder="Mensaje para el agente..." style="min-width:200px;" />
1417
+ <label style="display:flex;align-items:center;gap:4px;font-size:11px;color:var(--ink-muted,#8a8a8a);">
1418
+ <input type="checkbox" data-act-chat-dispatch checked /> dispatch
1419
+ </label>
1420
+ <button data-act-chat-send>Enviar</button>
1421
+ </div>
1422
+ <p style="font-size:11px;color:var(--ink-muted,#8a8a8a);margin:0 0 8px 0;">El mensaje se enrutara al proyecto seleccionado arriba en el filtro. Default: "self".</p>
1423
+ <div class="act-chat-result" data-act-chat-result>Selecciona un agente + escribe un mensaje + click Enviar.</div>
1424
+ </div>
1425
+ </div>
1426
+ <div class="act-controls">
1427
+ <label><input type="checkbox" data-act-autorefresh checked /> auto-refresh 3s</label>
1428
+ <button data-act-refresh>Refrescar ahora</button>
1429
+ <span data-act-last-update style="margin-left:auto;font-family:ui-monospace,Menlo,Consolas,monospace;"></span>
1430
+ </div>
1431
+ </div>
1432
+ </div>
1433
+
1003
1434
  <div id="yf-full" data-nac-id="yujin.panel.full" class="hidden">
1004
1435
  <div class="panel">
1005
1436
  <div class="header">
@@ -1008,6 +1439,27 @@ body {
1008
1439
  <div class="t"><span class="name">Yujin Forge</span><span class="proj" id="proj-full" data-nac-id="yujin.panel.project-full"></span></div>
1009
1440
  </div>
1010
1441
  <div class="actions">
1442
+ <button class="agents-btn"
1443
+ data-agents-open
1444
+ data-nac-id="yujin.panel.agents"
1445
+ data-nac-role="action"
1446
+ data-nac-action="open_agents"
1447
+ aria-label="${escapeHtml(tr.agentsButton)}"
1448
+ title="${escapeHtml(tr.agentsButton)}">${escapeHtml(tr.agentsButton)}</button>
1449
+ <button class="agents-btn"
1450
+ data-mcp-open
1451
+ data-nac-id="yujin.panel.mcp"
1452
+ data-nac-role="action"
1453
+ data-nac-action="open_mcp"
1454
+ aria-label="MCP"
1455
+ title="MCP Bridges (F101)">MCP</button>
1456
+ <button class="agents-btn"
1457
+ data-activity-open
1458
+ data-nac-id="yujin.panel.activity"
1459
+ data-nac-role="action"
1460
+ data-nac-action="open_activity"
1461
+ aria-label="Activity"
1462
+ title="Agent activity + bridge inspector + chat-per-agent">Activity</button>
1011
1463
  <button class="settings-btn"
1012
1464
  data-settings-toggle
1013
1465
  data-nac-id="yujin.panel.settings"
@@ -3678,6 +4130,31 @@ async function send(text, opts) {
3678
4130
  /* Fire-and-forget; do not block the intercept loop. */
3679
4131
  try { playTtsForText(safeText).catch(() => undefined); } catch (e) { /* ignore */ }
3680
4132
  }
4133
+ } else if (verbId === 'yujin.a11y.announce') {
4134
+ /* F107 -- push text into the live region. Screen readers
4135
+ * pick it up; politeness controls the assertive/polite
4136
+ * mode. When speak=true we also queue TTS so the voice
4137
+ * loop announces it audibly. */
4138
+ a11yAnnounce(String(a.text || ''), {
4139
+ politeness: String(a.politeness || 'polite').toLowerCase() === 'assertive' ? 'assertive' : 'polite',
4140
+ speak: a.speak === true,
4141
+ locale: a.locale ? String(a.locale) : undefined,
4142
+ });
4143
+ } else if (verbId === 'yujin.a11y.focus') {
4144
+ /* F107 -- move keyboard focus to the data-nac-slug
4145
+ * target, optionally scroll into view + announce. */
4146
+ a11yFocus(String(a.slug || ''), {
4147
+ scroll: a.scroll !== false,
4148
+ announce: a.announce !== false,
4149
+ });
4150
+ } else if (verbId === 'yujin.a11y.label') {
4151
+ /* F107 -- set aria-label / aria-describedby on the
4152
+ * NAC-3 element. Optional shortcut hint + role. */
4153
+ a11yLabel(String(a.slug || ''), {
4154
+ description: String(a.description || ''),
4155
+ shortcut: a.shortcut ? String(a.shortcut) : undefined,
4156
+ role: a.role ? String(a.role) : undefined,
4157
+ });
3681
4158
  }
3682
4159
  }
3683
4160
  }
@@ -5875,7 +6352,888 @@ document.addEventListener('DOMContentLoaded', () => {
5875
6352
  document.addEventListener('keydown', (e) => {
5876
6353
  if (e.key === 'Escape' && !$('#yf-vault-modal').classList.contains('hidden')) vaultClose();
5877
6354
  });
6355
+
6356
+ /* ---- Director sprint slice 6 -- agent graph overlay ---- */
6357
+ agentsWireUp();
6358
+ mcpWireUp();
6359
+ activityWireUp();
5878
6360
  });
6361
+
6362
+ /* Agent graph overlay state lives in a module-scoped closure so
6363
+ * SSE subscribes once + drops the connection on close. The wiring
6364
+ * is idempotent (multiple opens/closes are safe). */
6365
+ const yfAgents = {
6366
+ modal: null,
6367
+ svg: null,
6368
+ edgesG: null,
6369
+ nodesG: null,
6370
+ tickEl: null,
6371
+ emptyEl: null,
6372
+ disabledEl: null,
6373
+ evtSource: null,
6374
+ byslug: new Map(),
6375
+ edgeBySlugPair: new Map(),
6376
+ };
6377
+
6378
+ function agentsWireUp() {
6379
+ yfAgents.modal = document.getElementById('yf-agents-modal');
6380
+ yfAgents.svg = document.getElementById('yf-agents-svg');
6381
+ if (!yfAgents.modal || !yfAgents.svg) return;
6382
+ yfAgents.edgesG = yfAgents.svg.querySelector('[data-agents-edges]');
6383
+ yfAgents.nodesG = yfAgents.svg.querySelector('[data-agents-nodes]');
6384
+ yfAgents.tickEl = yfAgents.modal.querySelector('[data-agents-tick]');
6385
+ yfAgents.emptyEl = yfAgents.modal.querySelector('[data-agents-empty]');
6386
+ yfAgents.disabledEl = yfAgents.modal.querySelector('[data-agents-disabled]');
6387
+
6388
+ document.querySelectorAll('[data-agents-open]').forEach((btn) => {
6389
+ btn.addEventListener('click', agentsOpen);
6390
+ });
6391
+ document.querySelectorAll('[data-agents-close]').forEach((btn) => {
6392
+ btn.addEventListener('click', agentsClose);
6393
+ });
6394
+ document.addEventListener('keydown', (e) => {
6395
+ if (e.key === 'Escape' && yfAgents.modal && !yfAgents.modal.classList.contains('hidden')) {
6396
+ agentsClose();
6397
+ }
6398
+ });
6399
+ }
6400
+
6401
+ async function agentsOpen() {
6402
+ if (!yfAgents.modal) return;
6403
+ yfAgents.modal.classList.remove('hidden');
6404
+ try {
6405
+ const r = await fetch('/api/agent/state');
6406
+ if (r.status === 503) {
6407
+ agentsShowDisabled();
6408
+ return;
6409
+ }
6410
+ if (!r.ok) throw new Error('state ' + r.status);
6411
+ const body = await r.json();
6412
+ const agents = Array.isArray(body.agents) ? body.agents : [];
6413
+ if (agents.length === 0) {
6414
+ agentsShowEmpty();
6415
+ return;
6416
+ }
6417
+ agentsShowGraph();
6418
+ agentsRenderGraph(agents);
6419
+ agentsSubscribe();
6420
+ } catch (err) {
6421
+ agentsShowDisabled();
6422
+ }
6423
+ }
6424
+
6425
+ function agentsClose() {
6426
+ if (!yfAgents.modal) return;
6427
+ yfAgents.modal.classList.add('hidden');
6428
+ if (yfAgents.evtSource) {
6429
+ try { yfAgents.evtSource.close(); } catch { /* noop */ }
6430
+ yfAgents.evtSource = null;
6431
+ }
6432
+ agentsSetTick(false);
6433
+ }
6434
+
6435
+ function agentsShowDisabled() {
6436
+ yfAgents.disabledEl.classList.remove('hidden');
6437
+ yfAgents.emptyEl.classList.add('hidden');
6438
+ yfAgents.edgesG.innerHTML = '';
6439
+ yfAgents.nodesG.innerHTML = '';
6440
+ }
6441
+
6442
+ function agentsShowEmpty() {
6443
+ yfAgents.emptyEl.classList.remove('hidden');
6444
+ yfAgents.disabledEl.classList.add('hidden');
6445
+ yfAgents.edgesG.innerHTML = '';
6446
+ yfAgents.nodesG.innerHTML = '';
6447
+ }
6448
+
6449
+ function agentsShowGraph() {
6450
+ yfAgents.emptyEl.classList.add('hidden');
6451
+ yfAgents.disabledEl.classList.add('hidden');
6452
+ }
6453
+
6454
+ function agentsSetTick(live) {
6455
+ if (!yfAgents.tickEl) return;
6456
+ yfAgents.tickEl.classList.toggle('live', !!live);
6457
+ const labelEl = yfAgents.tickEl.querySelector('.a-tick-label');
6458
+ if (labelEl) labelEl.textContent = live ? yfPanelCfg().i18n.agentsTickLive : yfPanelCfg().i18n.agentsTickLost;
6459
+ }
6460
+
6461
+ function yfPanelCfg() {
6462
+ /* runtimeConfig is injected by the server as a const named CONFIG
6463
+ * at the top of this inline script. The try/catch covers the
6464
+ * edge case of the script firing before CONFIG resolves. */
6465
+ try { return CONFIG; } catch { return { i18n: {} }; }
6466
+ }
6467
+
6468
+ /** Pick the layout: agent with empty can_be_called_by AND highest
6469
+ * can_call fan-out is the hub. Others ring around. Auxiliary
6470
+ * agents (web-search etc.) go in a second ring. */
6471
+ function agentsLayout(agents) {
6472
+ let hub = null;
6473
+ let bestFan = -1;
6474
+ for (const a of agents) {
6475
+ if ((a.can_be_called_by || []).length > 0) continue;
6476
+ const fan = (a.can_call || []).length;
6477
+ if (fan > bestFan) { bestFan = fan; hub = a; }
6478
+ }
6479
+ if (!hub) hub = agents[0];
6480
+ const cx = 320, cy = 220;
6481
+ const ring = agents.filter((a) => a.slug !== hub.slug);
6482
+ /* Two-tier layout: first-class sub-agents (called BY the hub)
6483
+ * vs auxiliary (everyone else). */
6484
+ const firstTier = ring.filter((a) => (hub.can_call || []).includes(a.slug) || (hub.can_call || []).includes('*'));
6485
+ const secondTier = ring.filter((a) => !firstTier.includes(a));
6486
+ const positions = new Map();
6487
+ positions.set(hub.slug, { x: cx, y: cy });
6488
+ const r1 = 140;
6489
+ for (let i = 0; i < firstTier.length; i++) {
6490
+ const angle = (i / firstTier.length) * Math.PI * 2 - Math.PI / 2;
6491
+ positions.set(firstTier[i].slug, {
6492
+ x: cx + Math.cos(angle) * r1,
6493
+ y: cy + Math.sin(angle) * r1,
6494
+ });
6495
+ }
6496
+ const r2 = 195;
6497
+ for (let i = 0; i < secondTier.length; i++) {
6498
+ /* Offset by half-step so they sit between first-tier slots. */
6499
+ const angle = ((i + 0.5) / Math.max(1, secondTier.length)) * Math.PI * 2 - Math.PI / 2;
6500
+ positions.set(secondTier[i].slug, {
6501
+ x: cx + Math.cos(angle) * r2,
6502
+ y: cy + Math.sin(angle) * r2,
6503
+ });
6504
+ }
6505
+ return { hub, positions };
6506
+ }
6507
+
6508
+ function agentsRenderGraph(agents) {
6509
+ yfAgents.byslug.clear();
6510
+ yfAgents.edgeBySlugPair.clear();
6511
+ for (const a of agents) yfAgents.byslug.set(a.slug, a);
6512
+ const { hub, positions } = agentsLayout(agents);
6513
+ /* Edges first so they sit under the nodes. */
6514
+ yfAgents.edgesG.innerHTML = '';
6515
+ for (const a of agents) {
6516
+ const calls = a.can_call || [];
6517
+ for (const target of calls) {
6518
+ if (target === '*') continue;
6519
+ if (!positions.has(target)) continue;
6520
+ const from = positions.get(a.slug);
6521
+ const to = positions.get(target);
6522
+ if (!from || !to) continue;
6523
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
6524
+ const d = 'M ' + from.x.toFixed(1) + ' ' + from.y.toFixed(1)
6525
+ + ' L ' + to.x.toFixed(1) + ' ' + to.y.toFixed(1);
6526
+ line.setAttribute('d', d);
6527
+ line.setAttribute('class', 'a-edge');
6528
+ line.setAttribute('marker-end', 'url(#a-arrow)');
6529
+ line.dataset.from = a.slug;
6530
+ line.dataset.to = target;
6531
+ yfAgents.edgesG.appendChild(line);
6532
+ yfAgents.edgeBySlugPair.set(a.slug + '->' + target, line);
6533
+ }
6534
+ }
6535
+ /* Nodes. */
6536
+ yfAgents.nodesG.innerHTML = '';
6537
+ for (const a of agents) {
6538
+ const pos = positions.get(a.slug);
6539
+ if (!pos) continue;
6540
+ const isHub = a.slug === hub.slug;
6541
+ const radius = isHub ? 34 : 26;
6542
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
6543
+ g.setAttribute('transform', 'translate(' + pos.x.toFixed(1) + ',' + pos.y.toFixed(1) + ')');
6544
+ g.dataset.slug = a.slug;
6545
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
6546
+ circle.setAttribute('r', String(radius));
6547
+ circle.setAttribute('class', 'a-node-circle tier-' + (a.brain_tier || 'eco') + ' status-' + (a.status || 'active'));
6548
+ circle.dataset.slug = a.slug;
6549
+ g.appendChild(circle);
6550
+ const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
6551
+ label.setAttribute('y', '4');
6552
+ label.setAttribute('class', 'a-node-label');
6553
+ label.textContent = a.name || a.slug;
6554
+ g.appendChild(label);
6555
+ const tier = document.createElementNS('http://www.w3.org/2000/svg', 'text');
6556
+ tier.setAttribute('y', String(radius + 14));
6557
+ tier.setAttribute('class', 'a-node-tier');
6558
+ tier.textContent = a.brain_tier || '';
6559
+ g.appendChild(tier);
6560
+ g.style.cursor = 'pointer';
6561
+ g.addEventListener('click', () => agentsDispatch(a.slug));
6562
+ yfAgents.nodesG.appendChild(g);
6563
+ }
6564
+ }
6565
+
6566
+ async function agentsDispatch(slug) {
6567
+ try {
6568
+ await fetch('/api/agent/run', {
6569
+ method: 'POST',
6570
+ headers: { 'content-type': 'application/json' },
6571
+ body: JSON.stringify({ slug, trigger_reason: 'panel-click' }),
6572
+ });
6573
+ } catch { /* the SSE will tell the user about failure via the run event. */ }
6574
+ }
6575
+
6576
+ function agentsSubscribe() {
6577
+ if (yfAgents.evtSource) return;
6578
+ try {
6579
+ yfAgents.evtSource = new EventSource('/api/agent/events');
6580
+ yfAgents.evtSource.onopen = () => { agentsSetTick(true); };
6581
+ yfAgents.evtSource.onerror = () => { agentsSetTick(false); };
6582
+ /* Listen by named event. The server sends event: <kind>. */
6583
+ yfAgents.evtSource.addEventListener('agent_run_started', (e) => agentsHandleRunStarted(e));
6584
+ yfAgents.evtSource.addEventListener('agent_run_finished', (e) => agentsHandleRunFinished(e));
6585
+ yfAgents.evtSource.addEventListener('agent_run_skipped', (e) => agentsHandleRunFinished(e));
6586
+ yfAgents.evtSource.addEventListener('bridge_drain', (e) => agentsHandleBridgeDrain(e));
6587
+ /* Director closure pendiente 5 followup -- the server fires
6588
+ * this when /api/agent/approve resolves an R8 pending row
6589
+ * tagged with an agent_slug. The handler clears the persistent
6590
+ * .paused-for-approval CSS marker on the SVG node so the
6591
+ * operator sees a clean idle state without re-dispatching the
6592
+ * agent. */
6593
+ yfAgents.evtSource.addEventListener('agent_run_paused_resolved', (e) => agentsHandlePausedResolved(e));
6594
+ } catch {
6595
+ agentsSetTick(false);
6596
+ }
6597
+ }
6598
+
6599
+ function agentsParseEvent(e) {
6600
+ try { return JSON.parse(e.data); } catch { return null; }
6601
+ }
6602
+ function agentsHandleRunStarted(e) {
6603
+ const ev = agentsParseEvent(e);
6604
+ if (!ev || !ev.slug) return;
6605
+ const circle = yfAgents.nodesG.querySelector('[data-slug="' + cssEscape(ev.slug) + '"] .a-node-circle')
6606
+ || yfAgents.nodesG.querySelector('g[data-slug="' + cssEscape(ev.slug) + '"] .a-node-circle');
6607
+ if (circle) {
6608
+ circle.classList.add('running');
6609
+ /* A new run supersedes any prior paused-for-approval state. */
6610
+ circle.classList.remove('paused-for-approval');
6611
+ }
6612
+ }
6613
+ function agentsHandleRunFinished(e) {
6614
+ const ev = agentsParseEvent(e);
6615
+ if (!ev || !ev.slug) return;
6616
+ const circle = yfAgents.nodesG.querySelector('g[data-slug="' + cssEscape(ev.slug) + '"] .a-node-circle');
6617
+ if (!circle) return;
6618
+ circle.classList.remove('running');
6619
+ /* Director closure pendiente 5 -- when the runner concluded
6620
+ * the agent as paused_for_approval, leave a persistent visual
6621
+ * marker so the operator sees which agent is waiting. A later
6622
+ * /api/agent/approve -> agent_run_paused_resolved event (or a
6623
+ * fresh agent_run_started for the same slug) clears it. */
6624
+ if (ev.status === 'paused_for_approval') {
6625
+ circle.classList.add('paused-for-approval');
6626
+ } else {
6627
+ circle.classList.remove('paused-for-approval');
6628
+ }
6629
+ }
6630
+ function agentsHandlePausedResolved(e) {
6631
+ const ev = agentsParseEvent(e);
6632
+ if (!ev || !ev.slug) return;
6633
+ const circle = yfAgents.nodesG.querySelector('g[data-slug="' + cssEscape(ev.slug) + '"] .a-node-circle');
6634
+ if (circle) circle.classList.remove('paused-for-approval');
6635
+ }
6636
+ function agentsHandleBridgeDrain(e) {
6637
+ const ev = agentsParseEvent(e);
6638
+ if (!ev || !ev.slug) return;
6639
+ /* Highlight every edge that targets this slug -- visualises
6640
+ * the inbound message wave. */
6641
+ const target = ev.slug;
6642
+ for (const [pair, line] of yfAgents.edgeBySlugPair.entries()) {
6643
+ if (pair.endsWith('->' + target)) {
6644
+ line.classList.add('active');
6645
+ /* Drop the highlight after the keyframe animation completes
6646
+ * so a sustained drain shows a regular flash, not a stuck
6647
+ * coloured edge. */
6648
+ setTimeout(() => line.classList.remove('active'), 950);
6649
+ }
6650
+ }
6651
+ }
6652
+
6653
+ /* ------------------------------------------------------------------
6654
+ * F101 slice 3 -- MCP bridges panel UI.
6655
+ * Open: click MCP button OR data-nac-action="open_mcp".
6656
+ * Calls GET /api/forge/mcp/bridges + renders cards.
6657
+ * Per-card: enable/disable toggle + "Ver tools" -> inline catalog.
6658
+ * ----------------------------------------------------------------*/
6659
+ const yfMcp = {
6660
+ modal: null,
6661
+ contentEl: null,
6662
+ };
6663
+
6664
+ function mcpWireUp() {
6665
+ yfMcp.modal = document.getElementById('yf-mcp-modal');
6666
+ yfMcp.contentEl = yfMcp.modal && yfMcp.modal.querySelector('[data-mcp-content]');
6667
+ if (!yfMcp.modal) return;
6668
+ document.querySelectorAll('[data-mcp-open]').forEach((btn) => {
6669
+ btn.addEventListener('click', mcpOpen);
6670
+ });
6671
+ document.querySelectorAll('[data-mcp-close]').forEach((btn) => {
6672
+ btn.addEventListener('click', mcpClose);
6673
+ });
6674
+ const refresh = yfMcp.modal.querySelector('[data-mcp-refresh]');
6675
+ if (refresh) refresh.addEventListener('click', () => mcpLoadBridges(true));
6676
+ document.addEventListener('keydown', (e) => {
6677
+ if (e.key === 'Escape' && yfMcp.modal && !yfMcp.modal.classList.contains('hidden')) {
6678
+ mcpClose();
6679
+ }
6680
+ });
6681
+ }
6682
+
6683
+ async function mcpOpen() {
6684
+ if (!yfMcp.modal) return;
6685
+ yfMcp.modal.classList.remove('hidden');
6686
+ await mcpLoadBridges(false);
6687
+ }
6688
+
6689
+ function mcpClose() {
6690
+ if (!yfMcp.modal) return;
6691
+ yfMcp.modal.classList.add('hidden');
6692
+ }
6693
+
6694
+ async function mcpLoadBridges(/* force */) {
6695
+ if (!yfMcp.contentEl) return;
6696
+ yfMcp.contentEl.innerHTML = '<div class="mcp-empty">Cargando bridges...</div>';
6697
+ try {
6698
+ const r = await fetch('/api/forge/mcp/bridges');
6699
+ const body = await r.json();
6700
+ const bridges = Array.isArray(body.bridges) ? body.bridges : [];
6701
+ if (bridges.length === 0) {
6702
+ yfMcp.contentEl.innerHTML = '<div class="mcp-empty">No hay bridges MCP discovered. Yujin escanea 4 sources al boot (loopback / manifest / file / env).</div>';
6703
+ return;
6704
+ }
6705
+ yfMcp.contentEl.innerHTML = '';
6706
+ for (const b of bridges) {
6707
+ const card = mcpRenderBridge(b);
6708
+ yfMcp.contentEl.appendChild(card);
6709
+ }
6710
+ } catch (err) {
6711
+ yfMcp.contentEl.innerHTML = '<div class="mcp-empty">Error: ' + (err && err.message ? err.message : String(err)) + '</div>';
6712
+ }
6713
+ }
6714
+
6715
+ function mcpRenderBridge(b) {
6716
+ const wrap = document.createElement('div');
6717
+ wrap.className = 'mcp-bridge' + (b.enabled ? '' : ' disabled');
6718
+ wrap.dataset.bridgeId = b.id;
6719
+ const meta = document.createElement('div');
6720
+ meta.className = 'mcp-meta';
6721
+ meta.innerHTML =
6722
+ '<div class="mcp-name">' + mcpEsc(b.display_name || b.id) + '</div>' +
6723
+ '<div class="mcp-endpoint">' + mcpEsc(b.endpoint || '-') + '</div>';
6724
+ const srcChip = document.createElement('span');
6725
+ srcChip.className = 'mcp-source-chip';
6726
+ srcChip.textContent = b.source || '?';
6727
+ const protoChip = document.createElement('span');
6728
+ protoChip.className = 'mcp-protocol-chip';
6729
+ protoChip.textContent = b.protocol || 'http_rest';
6730
+ const toggleBtn = document.createElement('button');
6731
+ toggleBtn.className = 'mcp-toggle';
6732
+ toggleBtn.textContent = b.enabled ? 'Disable' : 'Enable';
6733
+ toggleBtn.addEventListener('click', () => mcpToggleBridge(b.id, !b.enabled));
6734
+ const toolsBtn = document.createElement('button');
6735
+ toolsBtn.className = 'mcp-tools-btn';
6736
+ toolsBtn.textContent = 'Tools';
6737
+ toolsBtn.addEventListener('click', () => mcpLoadTools(b.id, wrap));
6738
+ wrap.appendChild(meta);
6739
+ wrap.appendChild(srcChip);
6740
+ wrap.appendChild(protoChip);
6741
+ wrap.appendChild(toolsBtn);
6742
+ wrap.appendChild(toggleBtn);
6743
+ return wrap;
6744
+ }
6745
+
6746
+ async function mcpToggleBridge(bridgeId, enabled) {
6747
+ try {
6748
+ const r = await fetch('/api/forge/mcp/toggle', {
6749
+ method: 'POST',
6750
+ headers: { 'content-type': 'application/json' },
6751
+ body: JSON.stringify({ bridge_id: bridgeId, enabled }),
6752
+ });
6753
+ if (!r.ok) throw new Error('toggle ' + r.status);
6754
+ } catch (_err) {
6755
+ /* Silent -- the reload below will reflect the persisted state. */
6756
+ }
6757
+ await mcpLoadBridges(true);
6758
+ }
6759
+
6760
+ async function mcpLoadTools(bridgeId, cardEl) {
6761
+ /* Toggle inline panel. */
6762
+ const existing = cardEl.parentElement && cardEl.parentElement.querySelector('[data-mcp-tools-for="' + cssEscape(bridgeId) + '"]');
6763
+ if (existing) {
6764
+ existing.parentElement.removeChild(existing);
6765
+ return;
6766
+ }
6767
+ const panel = document.createElement('div');
6768
+ panel.className = 'mcp-tools-list';
6769
+ panel.dataset.mcpToolsFor = bridgeId;
6770
+ panel.textContent = 'Cargando tools...';
6771
+ cardEl.parentElement.insertBefore(panel, cardEl.nextSibling);
6772
+ try {
6773
+ const r = await fetch('/api/forge/mcp/tools?bridge_id=' + encodeURIComponent(bridgeId));
6774
+ const body = await r.json();
6775
+ if (!body.ok) {
6776
+ panel.innerHTML = '<div class="mcp-tools-error">' + mcpEsc(body.error || 'unreachable') + '</div>';
6777
+ return;
6778
+ }
6779
+ const tools = Array.isArray(body.tools) ? body.tools : [];
6780
+ if (tools.length === 0) {
6781
+ panel.innerHTML = '<div class="mcp-empty">Sin tools.</div>';
6782
+ return;
6783
+ }
6784
+ panel.innerHTML = '';
6785
+ for (const t of tools) {
6786
+ const row = document.createElement('div');
6787
+ row.className = 'mcp-tool';
6788
+ row.innerHTML =
6789
+ '<div class="mcp-tool-id">' + mcpEsc(t.id || t.slug || t.name || '?') + '</div>' +
6790
+ '<div class="mcp-tool-desc">' + mcpEsc(t.description || t.desc || '') + '</div>';
6791
+ panel.appendChild(row);
6792
+ }
6793
+ } catch (err) {
6794
+ panel.innerHTML = '<div class="mcp-tools-error">' + mcpEsc(err && err.message ? err.message : String(err)) + '</div>';
6795
+ }
6796
+ }
6797
+
6798
+ function mcpEsc(s) {
6799
+ return String(s == null ? '' : s)
6800
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
6801
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
6802
+ }
6803
+
6804
+ /* ------------------------------------------------------------------
6805
+ * F107 accessibility -- handlers for the 3 yujin.a11y.* verbs.
6806
+ *
6807
+ * announce -- write text into the matching ARIA live region so
6808
+ * screen readers + the voice loop pick it up. When
6809
+ * speak=true, also queue TTS (locale optional).
6810
+ * focus -- find a [data-nac-slug] element + move focus +
6811
+ * optionally scroll into view + auto-announce the
6812
+ * new focus target.
6813
+ * label -- attach aria-label / aria-describedby + optional
6814
+ * role + shortcut hint to the [data-nac-slug] target.
6815
+ *
6816
+ * All three are idempotent and never throw -- a missing slug is
6817
+ * silent; the brain learns from no visible effect.
6818
+ * ----------------------------------------------------------------*/
6819
+
6820
+ function a11yAnnounce(text, opts) {
6821
+ const txt = String(text || '').slice(0, 1000).trim();
6822
+ if (!txt) return;
6823
+ const politeness = (opts && opts.politeness === 'assertive') ? 'assertive' : 'polite';
6824
+ const region = document.getElementById('yf-a11y-live-' + politeness);
6825
+ if (region) {
6826
+ /* Empty + re-fill forces some screen readers to re-announce
6827
+ * even identical text. */
6828
+ region.textContent = '';
6829
+ setTimeout(() => { region.textContent = txt; }, 30);
6830
+ }
6831
+ if (opts && opts.speak) {
6832
+ try {
6833
+ const locale = opts.locale;
6834
+ if (typeof playTtsForText === 'function') {
6835
+ playTtsForText(txt, locale ? { locale } : undefined).catch(() => undefined);
6836
+ }
6837
+ } catch (_err) { /* silent */ }
6838
+ }
6839
+ }
6840
+
6841
+ function a11yFocus(slug, opts) {
6842
+ const s = String(slug || '').trim();
6843
+ if (!s) return;
6844
+ /* The NAC-3 verb references the data-nac-slug attribute; some
6845
+ * components also expose data-nac-id with the same shape. Try
6846
+ * both for resilience. */
6847
+ const el =
6848
+ document.querySelector('[data-nac-slug="' + cssEscape(s) + '"]') ||
6849
+ document.querySelector('[data-nac-id="' + cssEscape(s) + '"]');
6850
+ if (!el) return;
6851
+ try {
6852
+ if (!opts || opts.scroll !== false) {
6853
+ el.scrollIntoView({ block: 'center', behavior: 'smooth' });
6854
+ }
6855
+ /* Ensure the target accepts focus -- add tabindex=-1 when not
6856
+ * already focusable. This is the same trick screen-reader
6857
+ * patterns use for non-button skip targets. */
6858
+ if (!el.matches('a, button, input, textarea, select, [tabindex]')) {
6859
+ el.setAttribute('tabindex', '-1');
6860
+ }
6861
+ el.focus({ preventScroll: false });
6862
+ if (!opts || opts.announce !== false) {
6863
+ const lbl = el.getAttribute('aria-label')
6864
+ || el.getAttribute('title')
6865
+ || el.textContent || '';
6866
+ a11yAnnounce('Foco: ' + lbl.toString().trim().slice(0, 200), { politeness: 'polite' });
6867
+ }
6868
+ } catch (_err) { /* silent */ }
6869
+ }
6870
+
6871
+ function a11yLabel(slug, opts) {
6872
+ const s = String(slug || '').trim();
6873
+ if (!s) return;
6874
+ const el =
6875
+ document.querySelector('[data-nac-slug="' + cssEscape(s) + '"]') ||
6876
+ document.querySelector('[data-nac-id="' + cssEscape(s) + '"]');
6877
+ if (!el || !opts) return;
6878
+ try {
6879
+ if (opts.description) {
6880
+ el.setAttribute('aria-label', String(opts.description).slice(0, 500));
6881
+ }
6882
+ if (opts.role) {
6883
+ el.setAttribute('role', String(opts.role).slice(0, 40));
6884
+ }
6885
+ if (opts.shortcut) {
6886
+ el.setAttribute('aria-keyshortcuts', String(opts.shortcut).slice(0, 40));
6887
+ }
6888
+ } catch (_err) { /* silent */ }
6889
+ }
6890
+
6891
+ /* ------------------------------------------------------------------
6892
+ * Activity overlay -- 3 tabs: feed timestamped / bridge inspector
6893
+ * / chat-per-agent. Consumes /api/agent/activity, /bridge-tail,
6894
+ * /inject. Polls every 3s when auto-refresh is on.
6895
+ * ----------------------------------------------------------------*/
6896
+ const yfActivity = {
6897
+ modal: null,
6898
+ tabPanels: { feed: null, bridge: null, chat: null },
6899
+ tabBtns: [],
6900
+ refreshTimer: null,
6901
+ activeTab: 'feed',
6902
+ lastEvtTs: '',
6903
+ selectedPair: null,
6904
+ /* Slice 3 N-projects sprint -- project filter + concurrency
6905
+ * badge. project='' means "all". project='self' is the default
6906
+ * single-project legacy slug. */
6907
+ projectFilter: '',
6908
+ knownProjects: new Set(),
6909
+ };
6910
+
6911
+ function activityWireUp() {
6912
+ yfActivity.modal = document.getElementById('yf-activity-modal');
6913
+ if (!yfActivity.modal) return;
6914
+ yfActivity.tabPanels.feed = yfActivity.modal.querySelector('[data-act-panel="feed"]');
6915
+ yfActivity.tabPanels.bridge = yfActivity.modal.querySelector('[data-act-panel="bridge"]');
6916
+ yfActivity.tabPanels.chat = yfActivity.modal.querySelector('[data-act-panel="chat"]');
6917
+ yfActivity.tabBtns = Array.from(yfActivity.modal.querySelectorAll('[data-act-tab]'));
6918
+ document.querySelectorAll('[data-activity-open]').forEach((b) => b.addEventListener('click', activityOpen));
6919
+ document.querySelectorAll('[data-activity-close]').forEach((b) => b.addEventListener('click', activityClose));
6920
+ yfActivity.tabBtns.forEach((b) => b.addEventListener('click', () => activitySwitchTab(b.dataset.actTab)));
6921
+ const refreshBtn = yfActivity.modal.querySelector('[data-act-refresh]');
6922
+ if (refreshBtn) refreshBtn.addEventListener('click', () => activityRefresh(true));
6923
+ const autoBtn = yfActivity.modal.querySelector('[data-act-autorefresh]');
6924
+ if (autoBtn) autoBtn.addEventListener('change', activityToggleAuto);
6925
+ const sendBtn = yfActivity.modal.querySelector('[data-act-chat-send]');
6926
+ if (sendBtn) sendBtn.addEventListener('click', activityChatSend);
6927
+ const projSel = yfActivity.modal.querySelector('[data-act-project-filter]');
6928
+ if (projSel) projSel.addEventListener('change', (e) => {
6929
+ yfActivity.projectFilter = String((e.target && e.target.value) || '');
6930
+ yfActivity.lastEvtTs = '';
6931
+ activityRefresh(true);
6932
+ });
6933
+ document.addEventListener('keydown', (e) => {
6934
+ if (e.key === 'Escape' && yfActivity.modal && !yfActivity.modal.classList.contains('hidden')) {
6935
+ activityClose();
6936
+ }
6937
+ });
6938
+ }
6939
+
6940
+ async function activityOpen() {
6941
+ if (!yfActivity.modal) return;
6942
+ yfActivity.modal.classList.remove('hidden');
6943
+ yfActivity.lastEvtTs = '';
6944
+ await activityRefresh(true);
6945
+ activityPopulateChatSlugs();
6946
+ activityStartAutoRefresh();
6947
+ }
6948
+
6949
+ function activityClose() {
6950
+ if (!yfActivity.modal) return;
6951
+ yfActivity.modal.classList.add('hidden');
6952
+ activityStopAutoRefresh();
6953
+ }
6954
+
6955
+ function activitySwitchTab(tab) {
6956
+ if (!tab) return;
6957
+ yfActivity.activeTab = tab;
6958
+ yfActivity.tabBtns.forEach((b) => {
6959
+ if (b.dataset.actTab === tab) b.classList.add('active');
6960
+ else b.classList.remove('active');
6961
+ });
6962
+ for (const key of Object.keys(yfActivity.tabPanels)) {
6963
+ const p = yfActivity.tabPanels[key];
6964
+ if (!p) continue;
6965
+ if (key === tab) p.classList.remove('hidden');
6966
+ else p.classList.add('hidden');
6967
+ }
6968
+ activityRefresh(true);
6969
+ }
6970
+
6971
+ function activityStartAutoRefresh() {
6972
+ activityStopAutoRefresh();
6973
+ const auto = yfActivity.modal && yfActivity.modal.querySelector('[data-act-autorefresh]');
6974
+ if (auto && !auto.checked) return;
6975
+ yfActivity.refreshTimer = setInterval(() => activityRefresh(false), 3000);
6976
+ }
6977
+ function activityStopAutoRefresh() {
6978
+ if (yfActivity.refreshTimer) {
6979
+ clearInterval(yfActivity.refreshTimer);
6980
+ yfActivity.refreshTimer = null;
6981
+ }
6982
+ }
6983
+ function activityToggleAuto(e) {
6984
+ if (e.target && e.target.checked) activityStartAutoRefresh();
6985
+ else activityStopAutoRefresh();
6986
+ }
6987
+
6988
+ async function activityRefresh(force) {
6989
+ if (!yfActivity.modal) return;
6990
+ if (yfActivity.activeTab === 'feed') {
6991
+ await activityLoadFeed(force);
6992
+ } else if (yfActivity.activeTab === 'bridge') {
6993
+ if (yfActivity.selectedPair) await activityLoadBridgeMessages(yfActivity.selectedPair);
6994
+ else await activityLoadBridgePairs();
6995
+ }
6996
+ /* Concurrency badge -- always refreshed via /api/agent/state. */
6997
+ await activityLoadConcurrency();
6998
+ /* Project dropdown -- newly seen projects from the feed / bridge
6999
+ * get appended. */
7000
+ activitySyncProjectDropdown();
7001
+ const stamp = yfActivity.modal.querySelector('[data-act-last-update]');
7002
+ if (stamp) stamp.textContent = new Date().toLocaleTimeString();
7003
+ }
7004
+
7005
+ async function activityLoadConcurrency() {
7006
+ const badge = yfActivity.modal.querySelector('[data-act-concurrency]');
7007
+ if (!badge) return;
7008
+ try {
7009
+ const r = await fetch('/api/agent/state');
7010
+ if (r.status === 503) { badge.textContent = 'runtime off'; return; }
7011
+ if (!r.ok) return;
7012
+ const body = await r.json();
7013
+ const c = body && body.runtime && body.runtime.concurrency;
7014
+ if (!c) { badge.textContent = '0/?'; return; }
7015
+ badge.textContent = c.global_running + '/' + c.global_max + ' (cap ' + c.per_project_max + '/proj)';
7016
+ } catch { /* silent */ }
7017
+ }
7018
+
7019
+ function activitySyncProjectDropdown() {
7020
+ const sel = yfActivity.modal.querySelector('[data-act-project-filter]');
7021
+ if (!sel) return;
7022
+ /* Read current options into a set so we only append new ones. */
7023
+ const existing = new Set();
7024
+ for (const opt of sel.options) existing.add(opt.value);
7025
+ let appended = false;
7026
+ for (const slug of yfActivity.knownProjects) {
7027
+ if (!existing.has(slug)) {
7028
+ const opt = document.createElement('option');
7029
+ opt.value = slug;
7030
+ opt.textContent = slug;
7031
+ sel.appendChild(opt);
7032
+ appended = true;
7033
+ }
7034
+ }
7035
+ /* Preserve user's current selection across appends. */
7036
+ if (appended && yfActivity.projectFilter) sel.value = yfActivity.projectFilter;
7037
+ }
7038
+
7039
+ async function activityLoadFeed(force) {
7040
+ const panel = yfActivity.tabPanels.feed;
7041
+ if (!panel) return;
7042
+ try {
7043
+ let url = '/api/agent/activity?limit=200';
7044
+ if (yfActivity.lastEvtTs && !force) url += '&since=' + encodeURIComponent(yfActivity.lastEvtTs);
7045
+ if (yfActivity.projectFilter) url += '&project_slug=' + encodeURIComponent(yfActivity.projectFilter);
7046
+ const r = await fetch(url);
7047
+ const body = await r.json();
7048
+ const events = Array.isArray(body.events) ? body.events : [];
7049
+ if (events.length === 0 && force) {
7050
+ panel.innerHTML = '<div class="act-empty">Sin actividad todavia. Disparate un agente o un chat y vuelve.</div>';
7051
+ return;
7052
+ }
7053
+ if (force) panel.innerHTML = '';
7054
+ /* Newest at top. */
7055
+ for (const ev of events) {
7056
+ const line = document.createElement('div');
7057
+ const family = String(ev.kind || '').split('.')[0] || 'misc';
7058
+ line.className = 'act-line kind-' + family;
7059
+ const ts = activityFormatTs(ev.ts);
7060
+ line.innerHTML =
7061
+ '<div class="act-ts">' + actEsc(ts) + '</div>' +
7062
+ '<div class="act-kind">' + actEsc(ev.kind || '') + '</div>' +
7063
+ '<div class="act-text">' + actEsc(ev.text || '') + '</div>';
7064
+ /* Prepend so newest stays on top. */
7065
+ panel.insertBefore(line, panel.firstChild);
7066
+ if (ev.ts > yfActivity.lastEvtTs) yfActivity.lastEvtTs = ev.ts;
7067
+ /* Track project_slugs we have seen so the dropdown can list
7068
+ * them as filter options. */
7069
+ if (ev.project_slug && typeof ev.project_slug === 'string') {
7070
+ yfActivity.knownProjects.add(ev.project_slug);
7071
+ }
7072
+ }
7073
+ /* Cap DOM at 500 lines. */
7074
+ while (panel.childNodes.length > 500) panel.removeChild(panel.lastChild);
7075
+ } catch (err) {
7076
+ if (force) panel.innerHTML = '<div class="act-empty">Error cargando feed: ' + actEsc(err.message || String(err)) + '</div>';
7077
+ }
7078
+ }
7079
+
7080
+ async function activityLoadBridgePairs() {
7081
+ const panel = yfActivity.tabPanels.bridge;
7082
+ if (!panel) return;
7083
+ try {
7084
+ let url = '/api/agent/bridge-tail';
7085
+ if (yfActivity.projectFilter) url += '?project_slug=' + encodeURIComponent(yfActivity.projectFilter);
7086
+ const r = await fetch(url);
7087
+ const body = await r.json();
7088
+ const pairs = Array.isArray(body.pairs) ? body.pairs : [];
7089
+ for (const p of pairs) {
7090
+ if (p.project_slug) yfActivity.knownProjects.add(p.project_slug);
7091
+ }
7092
+ if (pairs.length === 0) {
7093
+ panel.innerHTML = '<div class="act-empty">No hay mensajes en el bridge todavia.</div>';
7094
+ return;
7095
+ }
7096
+ panel.innerHTML = '';
7097
+ for (const p of pairs) {
7098
+ const card = document.createElement('div');
7099
+ card.className = 'act-pair';
7100
+ const projBadge = p.project_slug && p.project_slug !== 'self'
7101
+ ? '<span class="act-pair-count" style="background:rgba(200,124,74,0.2);color:#c87c4a;">' + actEsc(p.project_slug) + '</span>'
7102
+ : '';
7103
+ card.innerHTML =
7104
+ '<div class="act-pair-id">' + actEsc(p.from) + ' &rarr; ' + actEsc(p.to) + '</div>' +
7105
+ projBadge +
7106
+ '<span class="act-pair-count">' + actEsc(String(p.count)) + ' msg</span>' +
7107
+ '<span class="act-ts" style="font-size:11px;color:var(--ink-muted,#8a8a8a);">' + actEsc(activityFormatTs(p.last_ts)) + '</span>';
7108
+ card.addEventListener('click', () => {
7109
+ yfActivity.selectedPair = p.pair_id;
7110
+ activityLoadBridgeMessages(p.pair_id);
7111
+ });
7112
+ panel.appendChild(card);
7113
+ }
7114
+ } catch (err) {
7115
+ panel.innerHTML = '<div class="act-empty">Error bridge: ' + actEsc(err.message || String(err)) + '</div>';
7116
+ }
7117
+ }
7118
+
7119
+ async function activityLoadBridgeMessages(pairId) {
7120
+ const panel = yfActivity.tabPanels.bridge;
7121
+ if (!panel) return;
7122
+ try {
7123
+ const r = await fetch('/api/agent/bridge-tail?limit=200&pair_id=' + encodeURIComponent(pairId));
7124
+ const body = await r.json();
7125
+ const msgs = Array.isArray(body.messages) ? body.messages : [];
7126
+ panel.innerHTML = '';
7127
+ const back = document.createElement('button');
7128
+ back.textContent = '< Pares';
7129
+ back.style.cssText = 'background:transparent;color:inherit;border:1px solid var(--ink-subtle,#5a5e6a);border-radius:4px;padding:4px 10px;font-family:inherit;font-size:12px;cursor:pointer;margin-bottom:10px;';
7130
+ back.addEventListener('click', () => { yfActivity.selectedPair = null; activityLoadBridgePairs(); });
7131
+ panel.appendChild(back);
7132
+ const header = document.createElement('div');
7133
+ header.style.cssText = 'font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px;color:var(--ink-muted,#8a8a8a);margin-bottom:8px;';
7134
+ header.textContent = pairId + ' -- ' + msgs.length + ' msg';
7135
+ panel.appendChild(header);
7136
+ for (const m of msgs) {
7137
+ const w = document.createElement('div');
7138
+ w.className = 'act-bridge-msg';
7139
+ const ts = activityFormatTs(m.sent_at ? new Date(Number(m.sent_at)).toISOString() : '');
7140
+ w.innerHTML =
7141
+ '<div class="act-bridge-head">' + actEsc(ts) + ' #' + actEsc(String(m.id || '?'))
7142
+ + ' (' + actEsc(String(m.status || '?')) + ', ' + actEsc(String(m.tokens_used || 0)) + ' tok)</div>' +
7143
+ '<div class="act-bridge-payload">' + actEsc(String(m.payload || '').slice(0, 1200)) + '</div>';
7144
+ panel.appendChild(w);
7145
+ }
7146
+ if (msgs.length === 0) {
7147
+ const empty = document.createElement('div');
7148
+ empty.className = 'act-empty';
7149
+ empty.textContent = 'Sin mensajes en este par.';
7150
+ panel.appendChild(empty);
7151
+ }
7152
+ } catch (err) {
7153
+ panel.innerHTML = '<div class="act-empty">Error: ' + actEsc(err.message || String(err)) + '</div>';
7154
+ }
7155
+ }
7156
+
7157
+ async function activityPopulateChatSlugs() {
7158
+ const select = yfActivity.modal && yfActivity.modal.querySelector('[data-act-chat-slug]');
7159
+ if (!select) return;
7160
+ try {
7161
+ const r = await fetch('/api/agent/state');
7162
+ if (!r.ok) return;
7163
+ const body = await r.json();
7164
+ const agents = Array.isArray(body.agents) ? body.agents : [];
7165
+ select.innerHTML = '';
7166
+ for (const a of agents) {
7167
+ const opt = document.createElement('option');
7168
+ opt.value = a.slug;
7169
+ opt.textContent = (a.name || a.slug) + ' (' + (a.brain_tier || '?') + ')';
7170
+ select.appendChild(opt);
7171
+ }
7172
+ if (agents.length === 0) {
7173
+ const opt = document.createElement('option');
7174
+ opt.value = '';
7175
+ opt.textContent = 'no agents';
7176
+ select.appendChild(opt);
7177
+ }
7178
+ } catch (_err) { /* silent */ }
7179
+ }
7180
+
7181
+ async function activityChatSend() {
7182
+ const select = yfActivity.modal.querySelector('[data-act-chat-slug]');
7183
+ const input = yfActivity.modal.querySelector('[data-act-chat-text]');
7184
+ const dispBox = yfActivity.modal.querySelector('[data-act-chat-dispatch]');
7185
+ const result = yfActivity.modal.querySelector('[data-act-chat-result]');
7186
+ if (!select || !input || !result) return;
7187
+ const slug = String(select.value || '').trim();
7188
+ const text = String(input.value || '').trim();
7189
+ if (!slug || !text) { result.textContent = 'Elegi un agente + escribe texto antes de enviar.'; return; }
7190
+ result.textContent = 'Enviando...';
7191
+ try {
7192
+ const r = await fetch('/api/agent/inject', {
7193
+ method: 'POST',
7194
+ headers: { 'content-type': 'application/json' },
7195
+ body: JSON.stringify({
7196
+ slug, text,
7197
+ dispatch: dispBox ? dispBox.checked : true,
7198
+ /* Slice 3 N-projects -- enrutar al proyecto seleccionado
7199
+ * (default 'self'). */
7200
+ project_slug: yfActivity.projectFilter || 'self',
7201
+ }),
7202
+ });
7203
+ const body = await r.json();
7204
+ result.textContent = JSON.stringify(body, null, 2);
7205
+ input.value = '';
7206
+ /* Trigger a feed refresh so the user sees the new run. */
7207
+ if (yfActivity.activeTab === 'feed') setTimeout(() => activityRefresh(true), 800);
7208
+ } catch (err) {
7209
+ result.textContent = 'Error: ' + (err.message || String(err));
7210
+ }
7211
+ }
7212
+
7213
+ function activityFormatTs(ts) {
7214
+ if (!ts) return '';
7215
+ try {
7216
+ const d = new Date(ts);
7217
+ if (isNaN(d.getTime())) return String(ts);
7218
+ const hh = String(d.getHours()).padStart(2, '0');
7219
+ const mm = String(d.getMinutes()).padStart(2, '0');
7220
+ const ss = String(d.getSeconds()).padStart(2, '0');
7221
+ return hh + ':' + mm + ':' + ss;
7222
+ } catch { return String(ts); }
7223
+ }
7224
+
7225
+ function actEsc(s) {
7226
+ return String(s == null ? '' : s)
7227
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
7228
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
7229
+ }
7230
+
7231
+ /* CSS.escape polyfill -- IE/legacy safe; modern browsers have it
7232
+ * native. Kebab-case slugs are safe under both. */
7233
+ function cssEscape(s) {
7234
+ if (typeof CSS !== 'undefined' && CSS && typeof CSS.escape === 'function') return CSS.escape(s);
7235
+ return String(s).replace(/[^a-z0-9_-]/gi, '\\$&');
7236
+ }
5879
7237
  </script>
5880
7238
  </body>
5881
7239
  </html>`;