@yemi33/minions 0.1.2448 → 0.1.2449

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 (315) hide show
  1. package/bin/cli-api-client.js +1 -1
  2. package/bin/install-internal-minions.js +1382 -44
  3. package/bin/install-layout.js +150 -0
  4. package/bin/minions.js +460 -167
  5. package/dashboard/docs/typography.md +65 -12
  6. package/dashboard/js/command-center.js +58 -4
  7. package/dashboard/js/detail-panel.js +36 -0
  8. package/dashboard/js/memory-panel.js +59 -12
  9. package/dashboard/js/qa.js +179 -20
  10. package/dashboard/js/refresh.js +148 -12
  11. package/dashboard/js/render-dispatch.js +3 -4
  12. package/dashboard/js/render-inbox.js +2 -2
  13. package/dashboard/js/render-other.js +3 -3
  14. package/dashboard/js/render-pipelines.js +14 -0
  15. package/dashboard/js/render-plans.js +57 -9
  16. package/dashboard/js/render-prd.js +132 -23
  17. package/dashboard/js/render-prs.js +195 -166
  18. package/dashboard/js/render-schedules.js +63 -3
  19. package/dashboard/js/render-utils.js +3 -3
  20. package/dashboard/js/render-watches.js +19 -3
  21. package/dashboard/js/render-work-items.js +238 -30
  22. package/dashboard/js/settings.js +205 -54
  23. package/dashboard/js/utils.js +51 -1
  24. package/dashboard/pages/home.html +1 -1
  25. package/dashboard/pages/qa.html +1 -16
  26. package/dashboard/pages/work.html +40 -0
  27. package/dashboard/shared/cc-limits.js +79 -0
  28. package/dashboard/shared/pr-filters.js +21 -38
  29. package/dashboard/shared/project-git-summary.js +1 -1
  30. package/dashboard/shared/record-filters.js +169 -0
  31. package/dashboard/shared/watches-source.js +1 -1
  32. package/dashboard/shared/welcome-popup.js +1 -1
  33. package/dashboard/shared/wi-filters.js +302 -0
  34. package/dashboard/slim/body.html +1 -0
  35. package/dashboard/slim/js/command-send.js +26 -0
  36. package/dashboard/slim/js/modals-tiles.js +380 -39
  37. package/dashboard/slim/js/status.js +13 -21
  38. package/dashboard/slim/layout.html +1 -0
  39. package/dashboard/slim/panel-bootstrap.js +6 -2
  40. package/dashboard/slim/styles.css +38 -0
  41. package/dashboard/styles.css +159 -55
  42. package/dashboard-build.js +13 -2
  43. package/dashboard.js +956 -423
  44. package/docs/README.md +11 -6
  45. package/docs/api-errors.md +2 -2
  46. package/docs/architecture-review-2026-07-09.md +1 -1
  47. package/docs/architecture.excalidraw +2 -2
  48. package/docs/auto-discovery.md +18 -9
  49. package/docs/branch-derivation.md +4 -4
  50. package/docs/capture-demos.js +39 -2
  51. package/docs/ci-runner-canary.md +123 -0
  52. package/docs/claude-md-propagation.md +2 -2
  53. package/docs/cloud-agent-dispatch.md +204 -0
  54. package/docs/command-center.md +7 -7
  55. package/docs/completion-reports.md +43 -20
  56. package/docs/constants.md +10 -3
  57. package/docs/constellation-bridge.md +134 -6
  58. package/docs/constellation-style-telemetry.md +4 -4
  59. package/docs/contracts/capability-protocol.v1.json +165 -0
  60. package/docs/cooldown-merge-semantics.md +12 -12
  61. package/docs/copilot-cli-schema.md +7 -7
  62. package/docs/cross-repo-plans.md +10 -10
  63. package/docs/dead-code-audit-retractions.md +5 -5
  64. package/docs/default-branch-ci.md +173 -0
  65. package/docs/deprecated.json +31 -31
  66. package/docs/design-inbox-entries-schema.md +3 -3
  67. package/docs/design-language.md +1051 -0
  68. package/docs/design-state-storage.md +11 -11
  69. package/docs/diagnostics-crash-reports.md +9 -9
  70. package/docs/diagnostics-memory.md +5 -5
  71. package/docs/documentation-audit-2026-07-09.md +7 -7
  72. package/docs/engine-restart.md +90 -5
  73. package/docs/harness-mode.md +1 -1
  74. package/docs/internal-install.md +338 -39
  75. package/docs/kb-dedup-duplicate-pair-investigation.md +5 -5
  76. package/docs/kb-pr3223-cascade-archiving.md +1 -1
  77. package/docs/kb-pr696-merge-conflict-docs.md +6 -6
  78. package/docs/kb-sweep.md +35 -35
  79. package/docs/keep-processes.md +1 -1
  80. package/docs/live-checkout-mode.md +30 -30
  81. package/docs/managed-spawn.md +18 -14
  82. package/docs/named-agents.md +7 -7
  83. package/docs/plan-lifecycle.md +69 -2
  84. package/docs/pr-author-identity.md +114 -0
  85. package/docs/pr-auto-fix-dispatch.md +19 -4
  86. package/docs/pr-comment-followup.md +6 -6
  87. package/docs/pr-review-fix-loop.md +59 -10
  88. package/docs/process-termination.md +40 -0
  89. package/docs/proposals/repo-pool-for-live-checkout.md +13 -13
  90. package/docs/qa-runbook-lifecycle.md +367 -17
  91. package/docs/qa-runbooks.md +3 -3
  92. package/docs/rfc-completion-json.md +18 -18
  93. package/docs/runtime-adapters.md +26 -21
  94. package/docs/security.md +6 -6
  95. package/docs/self-improvement.md +4 -4
  96. package/docs/shared-lifecycle-module-map.md +473 -472
  97. package/docs/skills.md +52 -3
  98. package/docs/slim-ux/concepts.md +121 -116
  99. package/docs/specs/agent-configurability.md +18 -18
  100. package/docs/specs/agent-rename.md +18 -18
  101. package/docs/team-memory.md +38 -21
  102. package/docs/timeouts-and-liveness.md +118 -10
  103. package/docs/tutorials/01-install-and-connect.md +1 -1
  104. package/docs/watches.md +40 -39
  105. package/docs/workspace-manifests.md +4 -4
  106. package/docs/worktree-lifecycle.md +293 -14
  107. package/engine/README.md +46 -0
  108. package/engine/{ado-comment.js → ado/comment.js} +8 -8
  109. package/engine/{ado-git-auth.js → ado/git-auth.js} +4 -4
  110. package/engine/{ado.js → ado/index.js} +417 -63
  111. package/engine/{ado-status.js → ado/status.js} +6 -8
  112. package/engine/{ado-token.js → ado/token.js} +1 -1
  113. package/engine/{acp-transport.js → agents/acp-transport.js} +62 -22
  114. package/engine/{agent-worker-pool.js → agents/agent-worker-pool.js} +17 -8
  115. package/engine/{cc-worker-pool.js → agents/cc-worker-pool.js} +16 -6
  116. package/engine/{claude-md-context.js → agents/claude-md-context.js} +5 -5
  117. package/engine/{harness-context.js → agents/harness-context.js} +5 -5
  118. package/engine/{harness.js → agents/harness.js} +3 -3
  119. package/engine/{llm.js → agents/llm.js} +18 -14
  120. package/engine/{model-discovery.js → agents/model-discovery.js} +2 -2
  121. package/engine/{playbook.js → agents/playbook.js} +155 -22
  122. package/engine/{pooled-agent-process.js → agents/pooled-agent-process.js} +14 -12
  123. package/engine/{preflight.js → agents/preflight.js} +29 -10
  124. package/engine/{spawn-agent.js → agents/spawn-agent.js} +25 -14
  125. package/engine/{spawn-phase-watchdog.js → agents/spawn-phase-watchdog.js} +16 -7
  126. package/engine/{steering.js → agents/steering.js} +5 -5
  127. package/engine/{tools-inventory.js → agents/tools-inventory.js} +2 -2
  128. package/engine/{agent-api-validation.js → api/agent-api-validation.js} +2 -2
  129. package/engine/{api-validation.js → api/api-validation.js} +1 -1
  130. package/engine/api/bridge.js +787 -0
  131. package/engine/{cc-api-validation.js → api/cc-api-validation.js} +1 -1
  132. package/engine/api/companion.js +560 -0
  133. package/engine/{content-api-validation.js → api/content-api-validation.js} +2 -2
  134. package/engine/{pr-issue-validation.js → api/pr-issue-validation.js} +33 -6
  135. package/engine/{settings-validation.js → api/settings-validation.js} +32 -4
  136. package/engine/api-contracts/agent-content.js +4 -4
  137. package/engine/api-contracts/capability-manifest.js +236 -0
  138. package/engine/api-contracts/capability-protocol.js +333 -0
  139. package/engine/api-contracts/cc-ops.js +1 -1
  140. package/engine/api-contracts/config-runtime.js +5 -0
  141. package/engine/api-contracts/core.js +28 -1
  142. package/engine/api-contracts/index.js +100 -0
  143. package/engine/api-contracts/orchestration.js +18 -5
  144. package/engine/api-contracts/pull-requests.js +37 -6
  145. package/engine/api-contracts/qa-process.js +29 -6
  146. package/engine/api-contracts/work-plan-prd.js +21 -1
  147. package/engine/cloud/contract.js +212 -0
  148. package/engine/cloud/index.js +159 -0
  149. package/engine/{execution-model.js → core/execution-model.js} +1 -1
  150. package/engine/{features.js → core/features.js} +4 -4
  151. package/engine/{operator-identity.js → core/operator-identity.js} +1 -1
  152. package/engine/{queries.js → core/queries.js} +201 -36
  153. package/engine/{safe-expr.js → core/safe-expr.js} +1 -1
  154. package/engine/{shared.js → core/shared.js} +1637 -175
  155. package/engine/{stdio-timestamps.js → core/stdio-timestamps.js} +1 -1
  156. package/engine/{untrusted-fence.js → core/untrusted-fence.js} +3 -3
  157. package/engine/db/index.js +11 -2
  158. package/engine/db/migrations/002-dispatches.js +3 -3
  159. package/engine/db/migrations/003-work-items.js +1 -1
  160. package/engine/db/migrations/004-pull-requests.js +1 -1
  161. package/engine/db/migrations/006-metrics.js +1 -1
  162. package/engine/db/migrations/007-watches.js +2 -2
  163. package/engine/db/migrations/008-small-state.js +1 -1
  164. package/engine/db/migrations/009-qa.js +1 -1
  165. package/engine/db/migrations/010-pr-links.js +1 -1
  166. package/engine/db/migrations/011-remaining-state.js +1 -1
  167. package/engine/db/migrations/012-steering-deliveries.js +2 -2
  168. package/engine/db/migrations/013-backfill-broken-note-links.js +1 -1
  169. package/engine/db/migrations/014-pr-fix-target-prefs.js +2 -2
  170. package/engine/db/migrations/015-plans-prds.js +0 -0
  171. package/engine/db/migrations/018-sql-only-cutover.js +2 -2
  172. package/engine/db/migrations/021-archived-work-items.js +1 -1
  173. package/engine/db/migrations/022-global-cc-session.js +1 -1
  174. package/engine/db/migrations/023-engine-state.js +1 -1
  175. package/engine/db/migrations/025-malformed-work-item-phantoms.js +1 -1
  176. package/engine/db/migrations/027-review-learning-lifecycle.js +1 -1
  177. package/engine/db/migrations/029-repair-reused-versions.js +20 -0
  178. package/engine/db/migrations/031-pr-author-identity.js +137 -0
  179. package/engine/{consolidation.js → memory/consolidation.js} +6 -6
  180. package/engine/{kb-sweep-runner.js → memory/kb-sweep-runner.js} +2 -2
  181. package/engine/{kb-sweep.js → memory/kb-sweep.js} +9 -7
  182. package/engine/{memory-retrieval.js → memory/memory-retrieval.js} +46 -4
  183. package/engine/{memory-store.js → memory/memory-store.js} +3 -3
  184. package/engine/{promotion.js → memory/promotion.js} +3 -3
  185. package/engine/{review-learning-backfill.js → memory/review-learning-backfill.js} +6 -6
  186. package/engine/{review-learning.js → memory/review-learning.js} +10 -5
  187. package/engine/{diagnostics-memory.js → observability/diagnostics-memory.js} +1 -1
  188. package/engine/{logs-store.js → observability/logs-store.js} +5 -5
  189. package/engine/{metrics-store.js → observability/metrics-store.js} +4 -4
  190. package/engine/{check-status.js → operations/check-status.js} +3 -3
  191. package/engine/{cli.js → operations/cli.js} +271 -113
  192. package/engine/{distribution.js → operations/distribution.js} +5 -6
  193. package/engine/{cleanup.js → orchestration/cleanup.js} +72 -45
  194. package/engine/{cooldown.js → orchestration/cooldown.js} +5 -5
  195. package/engine/{dispatch-events.js → orchestration/dispatch-events.js} +2 -2
  196. package/engine/{dispatch.js → orchestration/dispatch.js} +129 -36
  197. package/engine/orchestration/failed-scheduled-cleanup.js +274 -0
  198. package/engine/{lifecycle.js → orchestration/lifecycle.js} +198 -90
  199. package/engine/{meeting.js → orchestration/meeting.js} +6 -16
  200. package/engine/{pipeline.js → orchestration/pipeline.js} +12 -12
  201. package/engine/{pre-dispatch-eval.js → orchestration/pre-dispatch-eval.js} +10 -9
  202. package/engine/{routing.js → orchestration/routing.js} +3 -3
  203. package/engine/{schedule-bootstrap.js → orchestration/schedule-bootstrap.js} +4 -4
  204. package/engine/{scheduler.js → orchestration/scheduler.js} +38 -8
  205. package/engine/{timeout.js → orchestration/timeout.js} +158 -109
  206. package/engine/{db-events.js → persistence/db-events.js} +2 -2
  207. package/engine/{dispatch-store.js → persistence/dispatch-store.js} +7 -7
  208. package/engine/{inbox-store.js → persistence/inbox-store.js} +2 -2
  209. package/engine/{note-link-backfill.js → persistence/note-link-backfill.js} +4 -4
  210. package/engine/{pr-fix-target-store.js → persistence/pr-fix-target-store.js} +8 -8
  211. package/engine/{pull-requests-store.js → persistence/pull-requests-store.js} +21 -7
  212. package/engine/{small-state-store.js → persistence/small-state-store.js} +31 -31
  213. package/engine/persistence/state-operations.js +350 -0
  214. package/engine/{steering-store.js → persistence/steering-store.js} +6 -6
  215. package/engine/{issues.js → planning/issues.js} +2 -2
  216. package/engine/{plan-prd-validation.js → planning/plan-prd-validation.js} +8 -2
  217. package/engine/planning/prd-result-sidecar.js +190 -0
  218. package/engine/{prd-store.js → planning/prd-store.js} +17 -17
  219. package/engine/{project-discovery.js → planning/project-discovery.js} +5 -5
  220. package/engine/{projects.js → planning/projects.js} +10 -10
  221. package/engine/{resolve-area.js → planning/resolve-area.js} +1 -1
  222. package/engine/{work-item-validation.js → planning/work-item-validation.js} +39 -3
  223. package/engine/{work-items-store.js → planning/work-items-store.js} +29 -21
  224. package/engine/{keep-process-sweep.js → processes/keep-process-sweep.js} +57 -17
  225. package/engine/{managed-spawn-launcher.js → processes/managed-spawn-launcher.js} +3 -3
  226. package/engine/{managed-spawn.js → processes/managed-spawn.js} +97 -46
  227. package/engine/{process-utils.js → processes/process-utils.js} +599 -55
  228. package/engine/{abandoned-pr-reconciliation.js → providers/abandoned-pr-reconciliation.js} +17 -7
  229. package/engine/{comment-classifier.js → providers/comment-classifier.js} +85 -17
  230. package/engine/{comment-format.js → providers/comment-format.js} +5 -5
  231. package/engine/{gh-comment.js → providers/gh-comment.js} +15 -15
  232. package/engine/{gh-token.js → providers/gh-token.js} +4 -4
  233. package/engine/{github.js → providers/github.js} +131 -54
  234. package/engine/{pr-action.js → providers/pr-action.js} +13 -12
  235. package/engine/{pr-clone-keep.js → providers/pr-clone-keep.js} +7 -7
  236. package/engine/{pr-devbox.js → providers/pr-devbox.js} +6 -6
  237. package/engine/{pr-fix-target.js → providers/pr-fix-target.js} +13 -13
  238. package/engine/{pr-remote-patch.js → providers/pr-remote-patch.js} +4 -4
  239. package/engine/{pr-resolve.js → providers/pr-resolve.js} +7 -7
  240. package/engine/{pr-temp-clone.js → providers/pr-temp-clone.js} +5 -5
  241. package/engine/{pr-track.js → providers/pr-track.js} +11 -13
  242. package/engine/{shared-branch-pr-reconcile.js → providers/shared-branch-pr-reconcile.js} +4 -4
  243. package/engine/qa/auto-prd-qa.js +313 -0
  244. package/engine/{qa-from-prd.js → qa/from-prd.js} +42 -12
  245. package/engine/qa/prd-session.js +240 -0
  246. package/engine/{qa-process-validation.js → qa/process-validation.js} +14 -9
  247. package/engine/{qa-runbooks.js → qa/runbooks.js} +1 -1
  248. package/engine/{qa-runs.js → qa/runs.js} +286 -15
  249. package/engine/{qa-sessions.js → qa/sessions.js} +595 -49
  250. package/engine/qa/visual-journey.js +654 -0
  251. package/engine/{qa-runners.js → qa-runners/index.js} +7 -7
  252. package/engine/qa-runners/maestro.js +3 -3
  253. package/engine/qa-runners/playwright.js +2 -2
  254. package/engine/{restart-health.js → recovery/restart-health.js} +48 -4
  255. package/engine/recovery/stop-stack.js +607 -0
  256. package/engine/{supervisor.js → recovery/supervisor.js} +105 -175
  257. package/engine/{watchdog.js → recovery/watchdog.js} +136 -13
  258. package/engine/runtimes/claude.js +14 -12
  259. package/engine/runtimes/codex.js +8 -6
  260. package/engine/runtimes/copilot.js +17 -16
  261. package/engine/{watch-actions.js → watches/actions.js} +13 -13
  262. package/engine/{watches.js → watches/index.js} +43 -32
  263. package/engine/{watches-store.js → watches/store.js} +4 -4
  264. package/engine/{create-pr-worktree.js → worktrees/create-pr.js} +1 -1
  265. package/engine/{worktree-gc.js → worktrees/gc.js} +70 -22
  266. package/engine/worktrees/inventory.js +671 -0
  267. package/engine/{live-checkout.js → worktrees/live-checkout.js} +4 -4
  268. package/engine/{worktree-pool.js → worktrees/pool.js} +2 -2
  269. package/engine/{worktree-preflight.js → worktrees/preflight.js} +1 -0
  270. package/engine/worktrees/quarantine-refs.js +173 -0
  271. package/engine.js +1137 -208
  272. package/minions.js +147 -77
  273. package/package.json +10 -6
  274. package/playbooks/_pr-description-audit.md +110 -78
  275. package/playbooks/build-fix-complex.md +2 -0
  276. package/playbooks/fix.md +16 -12
  277. package/playbooks/implement-shared.md +2 -0
  278. package/playbooks/implement.md +19 -20
  279. package/playbooks/plan-to-prd.md +18 -3
  280. package/playbooks/qa-session-draft.md +136 -1
  281. package/playbooks/qa-session-execute.md +80 -2
  282. package/playbooks/qa-session-setup.md +17 -1
  283. package/playbooks/qa-validate.md +1 -1
  284. package/playbooks/setup.md +2 -0
  285. package/playbooks/shared-rules.md +25 -32
  286. package/playbooks/templates/followup-dispatch.md +4 -3
  287. package/playbooks/verify.md +1 -1
  288. package/prompts/cc-system.md +19 -27
  289. package/watch-plugins/README.md +92 -0
  290. package/watch-plugins/ado-author-prs.js +336 -0
  291. package/watch-plugins/gh-author-prs.js +375 -0
  292. package/watch-plugins/http.js +474 -0
  293. package/watch-plugins/teams-channel.js +869 -0
  294. package/docs/dev-composite-workflow.md +0 -101
  295. package/docs/pr-screenshots/pr-886/after-single-header.png +0 -0
  296. package/docs/pr-screenshots/pr-886/before-duplicate-header.png +0 -0
  297. package/docs/pr-screenshots/pr-895/01-cancellation-reason-detail.png +0 -0
  298. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
  299. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
  300. package/docs/pr-screenshots/pr-901/projects-tab-default.png +0 -0
  301. package/docs/pr-screenshots/pr-901/projects-tab-fmf-selected.png +0 -0
  302. package/docs/pr-screenshots/pr-916/model-picker-AFTER-crop.png +0 -0
  303. package/docs/pr-screenshots/pr-916/model-picker-AFTER.png +0 -0
  304. package/docs/pr-screenshots/pr-916/model-picker-BEFORE-crop.png +0 -0
  305. package/docs/pr-screenshots/pr-916/model-picker-BEFORE.png +0 -0
  306. package/docs/pr-screenshots/pr-916/model-picker-dropdown-AFTER.png +0 -0
  307. package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
  308. package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
  309. package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
  310. package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
  311. package/docs/visual-evidence-ci.md +0 -103
  312. package/engine/bridge.js +0 -379
  313. package/engine/quarantine-refs.js +0 -103
  314. package/engine/state-operations.js +0 -178
  315. /package/engine/{steering-constraints.js → agents/steering-constraints.js} +0 -0
@@ -112,6 +112,7 @@
112
112
  <div class="cockpit-detail">no open PRs</div>
113
113
  </div>
114
114
  <div class="cockpit-tile" data-tile="automation">
115
+ <button id="slim-tile-automation-add" class="linkpr-chip on-tile" type="button" title="Add automation" aria-label="Add automation" aria-haspopup="menu" aria-expanded="false">+</button>
115
116
  <div class="cockpit-label"><span class="cockpit-dot"></span> Automation</div>
116
117
  <div class="cockpit-value dim">0</div>
117
118
  <div class="cockpit-detail">no automation set</div>
@@ -30,6 +30,18 @@
30
30
  function sendMessage() {
31
31
  var text = inputEl.value.trim();
32
32
  if (!text) return;
33
+ // Length guard BEFORE clearing the composer / queueing (W-mscjjqdh). An
34
+ // oversized message fails server validation as `body.message has an invalid
35
+ // length`; persisting it would also poison the shared cc-tabs transcript the
36
+ // classic surface carries. Keep the rejected text in the box for editing.
37
+ var lenCheck = window.MinionsCcLimits ? window.MinionsCcLimits.checkMessage(text) : { ok: true };
38
+ if (!lenCheck.ok) {
39
+ appendSystemNotice(window.MinionsCcLimits
40
+ ? window.MinionsCcLimits.messageTooLongText(lenCheck.length, lenCheck.limit)
41
+ : 'Message is too long. Shorten it and send again — your text is still in the box.');
42
+ inputEl.focus();
43
+ return;
44
+ }
33
45
  inputEl.value = '';
34
46
  inputEl.style.height = 'auto';
35
47
  if (_isTabSending(tabId)) {
@@ -91,6 +103,20 @@
91
103
  var turnOutcome = 'done';
92
104
  var live = (turnTabId === tabId);
93
105
 
106
+ // Defensive length guard for paths that bypass sendMessage()'s composer check:
107
+ // queue drain (_drainQueueFor) and any programmatic/retry resend (W-mscjjqdh).
108
+ // Never record (recordTabMessage → shared cc-tabs) or POST an oversized
109
+ // message — drop it with a notice on the visible tab instead of looping.
110
+ var guard = window.MinionsCcLimits ? window.MinionsCcLimits.checkMessage(text) : { ok: true };
111
+ if (!guard.ok) {
112
+ if (live) {
113
+ appendSystemNotice(window.MinionsCcLimits
114
+ ? window.MinionsCcLimits.messageTooLongText(guard.length, guard.limit)
115
+ : 'Message is too long. Shorten it and send again — your text is still in the box.');
116
+ }
117
+ return;
118
+ }
119
+
94
120
  // The tab title is derived from the first user message; only that message
95
121
  // changes the chip, so re-render the bar once instead of on every send.
96
122
  var wasFirstUser = !_tabHasUserMessage(turnTabId);
@@ -21,6 +21,9 @@
21
21
  if (typeof _pauseActiveEmbed === 'function') _pauseActiveEmbed();
22
22
  // The Automation composite is likewise cached + kept mounted (W-ms4r1t5q…);
23
23
  // hide it and suspend its tab iframes' pollers rather than tearing it down.
24
+ // NOTE: the tab buttons tracked in _automationTabBtns stay live across
25
+ // opens now that the composite is never torn down, so they must NOT be
26
+ // cleared here — _applyAutomationTabLabels keeps writing to mounted nodes.
24
27
  if (typeof _hideAutomationComposite === 'function') _hideAutomationComposite();
25
28
  var transient = document.getElementById('slim-tile-transient');
26
29
  if (transient) transient.textContent = '';
@@ -174,6 +177,44 @@
174
177
  return { host: host, transient: transient };
175
178
  }
176
179
 
180
+ // Ensure a lightweight loading affordance element exists inside the embed
181
+ // host (created once). Shown immediately when a not-yet-booted embed iframe is
182
+ // opened so the modal never looks frozen while the heavier /engine SPA boots;
183
+ // hidden the instant that iframe fires 'load'. Built with createElement (no
184
+ // innerHTML) to satisfy the dashboard no-unsanitized lint gate.
185
+ function _ensureEmbedLoadingEl() {
186
+ var containers = _ensureTileContainers();
187
+ if (!containers) return null;
188
+ var el = document.getElementById('slim-tile-embed-loading');
189
+ if (!el) {
190
+ el = document.createElement('div');
191
+ el.id = 'slim-tile-embed-loading';
192
+ el.className = 'slim-tile-embed-loading';
193
+ el.setAttribute('role', 'status');
194
+ el.setAttribute('aria-live', 'polite');
195
+ el.hidden = true;
196
+ var spinner = document.createElement('div');
197
+ spinner.className = 'slim-tile-embed-spinner';
198
+ el.appendChild(spinner);
199
+ var label = document.createElement('div');
200
+ label.className = 'slim-tile-embed-loading-label';
201
+ label.textContent = 'Loading…';
202
+ el.appendChild(label);
203
+ containers.host.appendChild(el);
204
+ }
205
+ return el;
206
+ }
207
+
208
+ function _showEmbedLoading() {
209
+ var el = _ensureEmbedLoadingEl();
210
+ if (el) el.hidden = false;
211
+ }
212
+
213
+ function _hideEmbedLoading() {
214
+ var el = document.getElementById('slim-tile-embed-loading');
215
+ if (el) el.hidden = true;
216
+ }
217
+
177
218
  // Create (once) the cached iframe for an embed tile and return it. Built with
178
219
  // createElement (no innerHTML) to satisfy the dashboard no-unsanitized lint
179
220
  // gate. Starts hidden; a load-time visibility message pauses its poll until
@@ -189,6 +230,15 @@
189
230
  frame.src = spec[0];
190
231
  frame.title = spec[1];
191
232
  frame.hidden = true;
233
+ frame.dataset.embedLoaded = '0';
234
+ frame.addEventListener('load', function() {
235
+ frame.dataset.embedLoaded = '1';
236
+ // If this frame is the one currently shown, drop the loading affordance.
237
+ if (_activeEmbedKey === key) _hideEmbedLoading();
238
+ });
239
+ // Visibility posting is delegated to the shared helper, which evaluates the
240
+ // LIVE visibility at load time (not bind time) so a frame that finished
241
+ // loading while hidden still gets suspended.
192
242
  _bindEmbedVisibilityOnLoad(frame, function() { return _activeEmbedKey === key; });
193
243
  containers.host.appendChild(frame);
194
244
  _embedFrames[key] = frame;
@@ -198,7 +248,7 @@
198
248
  // Show one embed tile's cached iframe and hide the rest, pausing/resuming
199
249
  // each one's embedded poll to match. Reused by every single-iframe embed tile.
200
250
  function _showReusableEmbed(key) {
201
- _getOrCreateEmbedFrame(key);
251
+ var frame = _getOrCreateEmbedFrame(key);
202
252
  Object.keys(_embedFrames).forEach(function(k) {
203
253
  var f = _embedFrames[k];
204
254
  if (!f) return;
@@ -207,6 +257,13 @@
207
257
  _postEmbedVisibility(f, on);
208
258
  });
209
259
  _activeEmbedKey = key;
260
+ // Immediate loading affordance while a not-yet-booted embed finishes: the
261
+ // frame's 'load' handler hides it (or it's hidden now if already booted).
262
+ if (frame && frame.dataset && frame.dataset.embedLoaded !== '1') {
263
+ _showEmbedLoading();
264
+ } else {
265
+ _hideEmbedLoading();
266
+ }
210
267
  }
211
268
 
212
269
  // Hide + pause whichever embed iframe is currently shown (kept mounted for an
@@ -219,6 +276,7 @@
219
276
  f.hidden = true;
220
277
  });
221
278
  _activeEmbedKey = null;
279
+ _hideEmbedLoading();
222
280
  }
223
281
 
224
282
  // Prewarm the two hot embed tiles (Queued work + Active PRs) shortly after the
@@ -229,8 +287,35 @@
229
287
  _getOrCreateEmbedFrame('queued');
230
288
  _getOrCreateEmbedFrame('prs');
231
289
  }
290
+ // Engine is heavier than Work/PRs (it cold-boots the full classic SPA plus the
291
+ // engine log, managed-process panel, and memory/diagnostics pollers), so it is
292
+ // NOT folded into the 800ms hot burst — that would regress cockpit first paint.
293
+ // Instead warm it on an idle callback AFTER the hot tiles, so its boot overlaps
294
+ // idle time and its first open is a near-instant show. Predictive prewarm on
295
+ // tile intent (below) is the earlier, finer-grained trigger; this is the
296
+ // guaranteed fallback for users who click without hovering first.
297
+ // (W-ms4odb8h009a5d16)
298
+ function prewarmEngineEmbedIdle() {
299
+ _getOrCreateEmbedFrame('engine');
300
+ }
301
+ // Predictive prewarm: the instant a user shows intent on an embed tile
302
+ // (hover / focus / pointer-down), start booting its cached iframe so the
303
+ // heavier /engine screen overlaps intent instead of blocking the click.
304
+ // _getOrCreateEmbedFrame is create-once/idempotent, so repeat intents are free
305
+ // and the already-prewarmed queued/prs tiles are unaffected.
306
+ function _predictivePrewarmEmbed(key) {
307
+ if (!Object.prototype.hasOwnProperty.call(_EMBED_TILES, key)) return;
308
+ try { _getOrCreateEmbedFrame(key); } catch (e) { /* lazy-create on open instead */ }
309
+ }
232
310
  (function schedulePrewarm() {
233
311
  try { setTimeout(prewarmHotEmbeds, 800); } catch (e) { /* no timers — skip prewarm */ }
312
+ try {
313
+ if (typeof requestIdleCallback === 'function') {
314
+ requestIdleCallback(prewarmEngineEmbedIdle, { timeout: 4000 });
315
+ } else {
316
+ setTimeout(prewarmEngineEmbedIdle, 2500);
317
+ }
318
+ } catch (e) { /* no idle/timers — engine warms on first intent/open instead */ }
234
319
  })();
235
320
 
236
321
  // Per-tile body renderers for the detail modal. Each reads the latest status
@@ -312,12 +397,107 @@
312
397
  _showReusableEmbed('prs');
313
398
  }
314
399
 
315
- // Persistent Automation composite (W-ms4r1t5q003h341a): the tabbed
316
- // Watches·Schedules·Pipelines composite + its lazy iframes are built ONCE into
317
- // #slim-tile-automation-host and shown/hidden across opens — mirroring the
318
- // _embedFrames cache used by the single-iframe tiles — so reopening the
319
- // Automation tile is an instant show, NOT a rebuild that cold-reloads all three
320
- // classic screens into the wiped transient container. { activate, frames,
400
+ // ── Automation tile counts (Watches · Scheduled Tasks · Pipelines) ────
401
+ // The cockpit tile headline is the SUM of all three automation surfaces.
402
+ // Watches ride the live 5s /api/status poll (renderAutomationTile is fed the
403
+ // live watches count by applyStatus). Schedules + pipelines are NOT carried by
404
+ // the status snapshot, so they are fetched lazily off the hot poll path — once
405
+ // shortly after first paint and again whenever the Automation modal opens —
406
+ // mirroring knowledge.js#loadKnowledgeCounts / plans.js#loadPlansCounts. The
407
+ // same cached counts drive the "(N)" suffix on each modal tab label.
408
+ var _automationCounts = { watches: 0, schedules: null, pipelines: null };
409
+ // Live tab buttons ({ key: {el, base} }) so loadAutomationCounts can refresh
410
+ // their "(N)" labels. The composite is built once and kept mounted, so these
411
+ // stay valid across opens and are never cleared.
412
+ var _automationTabBtns = null;
413
+ // "+ Add" picker target: { tab, create } consumed by renderAutomationTileBody.
414
+ var _automationInitial = null;
415
+ // Last (count|detail|lit) signature painted onto the Automation cockpit tile,
416
+ // so the 5s status poll can skip a redundant updateTile when nothing changed
417
+ // (W-ms4r1t5q003h341a). Lives here because the painted value is the SUM, which
418
+ // also moves when the lazy schedule/pipeline counts land.
419
+ var _automationTileSig = null;
420
+ // Monotonic counter making each "+ Add" deep-link URL unique, so re-requesting
421
+ // Create on an ALREADY-MOUNTED cached tab frame is a real navigation (a bare
422
+ // "#create" hash change on the same URL would not reload the classic screen,
423
+ // and its boot hook only runs on load).
424
+ var _automationCreateSeq = 0;
425
+
426
+ function _automationTabCount(key) {
427
+ if (key === 'watches') return _automationCounts.watches;
428
+ if (key === 'schedule') return _automationCounts.schedules;
429
+ if (key === 'pipelines') return _automationCounts.pipelines;
430
+ return null;
431
+ }
432
+
433
+ function _automationTabLabel(base, count) {
434
+ return base + ' (' + (count == null ? '…' : count) + ')';
435
+ }
436
+
437
+ function _applyAutomationTabLabels() {
438
+ if (!_automationTabBtns) return;
439
+ Object.keys(_automationTabBtns).forEach(function(key) {
440
+ var b = _automationTabBtns[key];
441
+ if (b && b.el) b.el.textContent = _automationTabLabel(b.base, _automationTabCount(key));
442
+ });
443
+ }
444
+
445
+ // Repaint the cockpit tile from the cached sum. Called each 5s poll from
446
+ // applyStatus (with the live watches count) and after the lazy count fetch.
447
+ // Change-gated: the poll runs every 5s, so skip the updateTile DOM work
448
+ // (querySelectors + textContent + classList churn) when nothing moved.
449
+ function renderAutomationTile(watchesCount) {
450
+ if (typeof watchesCount === 'number') _automationCounts.watches = watchesCount;
451
+ var w = _automationCounts.watches || 0;
452
+ var s = _automationCounts.schedules || 0;
453
+ var p = _automationCounts.pipelines || 0;
454
+ var total = w + s + p;
455
+ var autoDetail = total ? 'monitoring' : 'no automation set';
456
+ var autoLit = total ? 'blue' : null;
457
+ var autoSig = total + '|' + autoDetail + '|' + (autoLit || '');
458
+ if (_automationTileSig !== autoSig) {
459
+ _automationTileSig = autoSig;
460
+ updateTile('automation', total, autoDetail, autoLit);
461
+ }
462
+ }
463
+
464
+ // Best-effort lazy fetch of the three automation-surface counts. Runs off the
465
+ // hot 5s status poll. Failures leave the cached value untouched so a transient
466
+ // error doesn't blank the tile / tab labels. /api/watches responds
467
+ // { watches: [...] } (unwrapped via the shared source); /api/schedules and
468
+ // /api/pipelines respond bare arrays.
469
+ async function loadAutomationCounts() {
470
+ try {
471
+ var wres = await fetch('/api/watches', { headers: { 'Accept': 'application/json' } });
472
+ if (wres.ok) {
473
+ var wlist = (typeof MinionsWatchesSource !== 'undefined' && MinionsWatchesSource.unwrap)
474
+ ? MinionsWatchesSource.unwrap(await wres.json())
475
+ : null;
476
+ if (Array.isArray(wlist)) _automationCounts.watches = wlist.length;
477
+ }
478
+ } catch (e) { /* keep last-known */ }
479
+ try {
480
+ var sres = await fetch('/api/schedules', { headers: { 'Accept': 'application/json' } });
481
+ if (sres.ok) { var sdata = await sres.json(); _automationCounts.schedules = Array.isArray(sdata) ? sdata.length : 0; }
482
+ } catch (e) { /* keep last-known */ }
483
+ try {
484
+ var pres = await fetch('/api/pipelines', { headers: { 'Accept': 'application/json' } });
485
+ if (pres.ok) { var pdata = await pres.json(); _automationCounts.pipelines = Array.isArray(pdata) ? pdata.length : 0; }
486
+ } catch (e) { /* keep last-known */ }
487
+ renderAutomationTile();
488
+ _applyAutomationTabLabels();
489
+ }
490
+
491
+ // Automation tile body — a tabbed composite (Watches · Scheduled Tasks ·
492
+ // Pipelines) that reuses the LITERAL classic dashboard screens (/watches,
493
+ // /schedule, /pipelines) instead of a slim-specific list, so there is one
494
+ // Automation UI, not two (W-mrz0x2a400030f90 / "reuse, don't fork").
495
+ //
496
+ // Persistent Automation composite (W-ms4r1t5q003h341a): the composite + its
497
+ // lazy iframes are built ONCE into #slim-tile-automation-host and shown/hidden
498
+ // across opens — mirroring the _embedFrames cache used by the single-iframe
499
+ // tiles — so reopening the Automation tile is an instant show, NOT a rebuild
500
+ // that cold-reloads all three classic screens. { activate, openCreate, frames,
321
501
  // activeKey } or null until first open.
322
502
  var _automationComposite = null;
323
503
 
@@ -354,21 +534,23 @@
354
534
  }
355
535
  }
356
536
 
357
- // Build the tabbed Watches·Schedules·Pipelines composite ONCE into the
358
- // persistent host. Returns { activate, frames, activeKey }. Each tab's iframe
359
- // is LAZY (mounted only on first activation) and CACHED (kept mounted so
360
- // reopening never rebuilds/reloads it). Hidden tabs' pollers are suspended via
361
- // the same visibility message the single-iframe embeds use, so a cached-but-
362
- // inactive tab isn't polling in the background. Built with createElement (no
537
+ // Build the tabbed Watches·Scheduled Tasks·Pipelines composite ONCE into the
538
+ // persistent host. Returns { activate, openCreate, frames, activeKey }. Each
539
+ // tab's iframe is LAZY (mounted only on first activation) and CACHED (kept
540
+ // mounted so reopening never rebuilds/reloads it). Hidden tabs' pollers are
541
+ // suspended via the same visibility message the single-iframe embeds use, so a
542
+ // cached-but-inactive tab isn't polling in the background. Built with
543
+ // createElement (no
363
544
  // innerHTML) to satisfy the dashboard no-unsanitized lint gate; tab buttons
364
545
  // reuse the shared .kn-tab pill primitive and carry role="tab"/aria-selected
365
546
  // for keyboard/AT parity with the Knowledge modal tabs.
366
547
  function _buildAutomationComposite(host) {
367
548
  var TABS = [
368
- { key: 'watches', label: 'Watches', src: '/watches?embed=1' },
369
- { key: 'schedule', label: 'Schedules', src: '/schedule?embed=1' },
370
- { key: 'pipelines', label: 'Pipelines', src: '/pipelines?embed=1' },
549
+ { key: 'watches', base: 'Watches', src: '/watches?embed=1' },
550
+ { key: 'schedule', base: 'Scheduled Tasks', src: '/schedule?embed=1' },
551
+ { key: 'pipelines', base: 'Pipelines', src: '/pipelines?embed=1' },
371
552
  ];
553
+
372
554
  var tablist = document.createElement('div');
373
555
  tablist.className = 'kn-tabs slim-automation-tabs';
374
556
  tablist.setAttribute('role', 'tablist');
@@ -380,9 +562,15 @@
380
562
  var btns = {};
381
563
  var panels = {};
382
564
  var frames = {};
383
- var composite = { activate: null, frames: frames, activeKey: null };
565
+ var composite = { activate: null, openCreate: null, frames: frames, activeKey: null };
566
+ // Track the live tab buttons so loadAutomationCounts can refresh the "(N)"
567
+ // labels once the list endpoints resolve. The composite is built once and
568
+ // kept mounted, so these stay valid for the life of the page.
569
+ _automationTabBtns = {};
384
570
 
385
- function activate(key) {
571
+ // `createSrc`, when given, is the deep-link URL the ACTIVE tab's iframe must
572
+ // load instead of its plain src (see openCreate).
573
+ function activate(key, createSrc) {
386
574
  TABS.forEach(function(t) {
387
575
  var isActive = t.key === key;
388
576
  var btn = btns[t.key];
@@ -395,8 +583,10 @@
395
583
  if (isActive && !frames[t.key]) {
396
584
  var frame = document.createElement('iframe');
397
585
  frame.className = 'slim-automation-embed';
398
- frame.src = t.src;
399
- frame.title = t.label;
586
+ // Mount straight at the deep-link when one was requested, so the
587
+ // create flow costs one load, not a load + immediate re-navigate.
588
+ frame.src = createSrc || t.src;
589
+ frame.title = t.base;
400
590
  // A visibility message posted before this frame navigates is dropped
401
591
  // on about:blank, so re-post the live state once it has loaded —
402
592
  // otherwise a tab hidden mid-load becomes a permanent background
@@ -406,6 +596,9 @@
406
596
  });
407
597
  panel.appendChild(frame);
408
598
  frames[t.key] = frame;
599
+ } else if (isActive && createSrc) {
600
+ // Already-cached frame: re-navigate it to run the deep-link boot hook.
601
+ frames[t.key].src = createSrc;
409
602
  }
410
603
  // Suspend hidden cached frames' pollers; resume the shown one.
411
604
  if (frames[t.key] && typeof _postEmbedVisibility === 'function') {
@@ -416,16 +609,32 @@
416
609
  }
417
610
  composite.activate = activate;
418
611
 
612
+ // Activate `key` AND make its embedded classic screen auto-open its own
613
+ // Create dialog via the ?embed=1#create deep-link (the classic create flow is
614
+ // reused, not forked — CLAUDE.md slim-vs-classic). The tab's iframe is cached
615
+ // across opens, so a bare "#create" hash on an already-loaded frame would be
616
+ // a same-document fragment change that never reloads the page and never runs
617
+ // its boot hook — hence the unique `&create=<seq>` cache-buster, which the
618
+ // classic screen ignores.
619
+ function openCreate(key) {
620
+ var tab = null;
621
+ TABS.forEach(function(t) { if (t.key === key) tab = t; });
622
+ if (!tab) { activate(key); return; }
623
+ activate(key, tab.src + '&create=' + (++_automationCreateSeq) + '#create');
624
+ }
625
+ composite.openCreate = openCreate;
626
+
419
627
  TABS.forEach(function(t) {
420
628
  var btn = document.createElement('button');
421
629
  btn.type = 'button';
422
630
  btn.className = 'kn-tab';
423
631
  btn.id = 'slim-automation-tab-' + t.key;
424
- btn.textContent = t.label;
632
+ btn.textContent = _automationTabLabel(t.base, _automationTabCount(t.key));
425
633
  btn.setAttribute('role', 'tab');
426
634
  btn.setAttribute('aria-controls', 'slim-automation-panel-' + t.key);
427
635
  btn.addEventListener('click', function() { activate(t.key); });
428
636
  btns[t.key] = btn;
637
+ _automationTabBtns[t.key] = { el: btn, base: t.base };
429
638
  tablist.appendChild(btn);
430
639
 
431
640
  var panel = document.createElement('div');
@@ -443,22 +652,22 @@
443
652
  return composite;
444
653
  }
445
654
 
446
- // Automation tile body — a tabbed composite (Watches · Schedules · Pipelines)
447
- // that reuses the LITERAL classic dashboard screens (/watches, /schedule,
448
- // /pipelines) instead of a slim-specific list, so there is one Automation UI,
449
- // not two (W-mrz0x2a400030f90 / "reuse, don't fork" — mirrors renderPlansBody
450
- // for the Plans tile and renderPrsBody for the PRs tile). Each tab embeds the
451
- // real classic screen in an iframe with the chrome-off ?embed=1 mode; the
452
- // iframed page IS the classic screen — full create/edit/pause/run + row
453
- // actions, backed by the same /api/{watches,schedules,pipelines} endpoints —
454
- // with zero duplicated rendering logic. Classic is reachable at each route even
455
- // with slim-ux ON (only / is taken over). The composite + its lazy iframes are
456
- // CACHED in the persistent #slim-tile-automation-host and SHOWN/HIDDEN across
457
- // opens (W-ms4r1t5q003h341a) rather than rebuilt into the wiped transient
458
- // container — so reopening the tile is an instant show, not a rebuild that
459
- // cold-reloads all three classic screens. Mirrors the _embedFrames reuse the
460
- // single-iframe tiles already use. The `body` arg (transient) is intentionally
461
- // ignored; the composite lives in its own persistent host.
655
+ // Automation tile body — a tabbed composite (Watches · Scheduled Tasks ·
656
+ // Pipelines) that reuses the LITERAL classic dashboard screens (/watches,
657
+ // /schedule, /pipelines) instead of a slim-specific list, so there is one
658
+ // Automation UI, not two (W-mrz0x2a400030f90 / "reuse, don't fork" — mirrors
659
+ // renderPlansBody for the Plans tile and renderPrsBody for the PRs tile). Each
660
+ // tab embeds the real classic screen in an iframe with the chrome-off ?embed=1
661
+ // mode; the iframed page IS the classic screen — full create/edit/pause/run +
662
+ // row actions, backed by the same /api/{watches,schedules,pipelines} endpoints
663
+ // — with zero duplicated rendering logic. Classic is reachable at each route
664
+ // even with slim-ux ON (only / is taken over). The composite + its lazy iframes
665
+ // are CACHED in the persistent #slim-tile-automation-host and SHOWN/HIDDEN
666
+ // across opens (W-ms4r1t5q003h341a) rather than rebuilt into the wiped
667
+ // transient container — so reopening the tile is an instant show, not a rebuild
668
+ // that cold-reloads all three classic screens. Mirrors the _embedFrames reuse
669
+ // the single-iframe tiles already use. The `body` arg (transient) is
670
+ // intentionally ignored; the composite lives in its own persistent host.
462
671
  function renderAutomationTileBody(/* body */) {
463
672
  var host = _ensureAutomationHost();
464
673
  if (!host) return;
@@ -466,8 +675,23 @@
466
675
  _automationComposite = _buildAutomationComposite(host);
467
676
  }
468
677
  host.hidden = false;
469
- // Reopen on the last-active tab (default Watches on first open).
470
- _automationComposite.activate(_automationComposite.activeKey || 'watches');
678
+ // Slim "+ Add" picker → open on a specific tab with that tab's classic
679
+ // create dialog auto-opened. Consumed HERE (every open) rather than inside
680
+ // _buildAutomationComposite, which only runs once now that the composite is
681
+ // cached — otherwise "+ Add" would work only on the very first open.
682
+ var initial = _automationInitial;
683
+ _automationInitial = null;
684
+ if (initial && initial.create) {
685
+ _automationComposite.openCreate(initial.tab || 'watches');
686
+ } else {
687
+ // Reopen on the last-active tab (default Watches on first open).
688
+ _automationComposite.activate(initial && initial.tab
689
+ ? initial.tab
690
+ : (_automationComposite.activeKey || 'watches'));
691
+ }
692
+ // Refresh the live "(N)" counts (and the cockpit tile sum) whenever the
693
+ // modal opens — these are off the hot 5s status poll (loadAutomationCounts).
694
+ if (typeof loadAutomationCounts === 'function') loadAutomationCounts();
471
695
  }
472
696
 
473
697
  // tile key -> { title, render }. Adding a tile is a one-line table edit.
@@ -542,6 +766,12 @@
542
766
  if (ev.target !== tile) return; // ignore keys from inner controls (e.g. the Link-PR chip)
543
767
  if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openTileModal(key); }
544
768
  });
769
+ // Predictive prewarm: start booting the embed the instant the user shows
770
+ // intent (hover / focus / pointer-down) so the click lands on an
771
+ // already-booting iframe rather than a cold SPA boot (esp. Engine).
772
+ tile.addEventListener('pointerenter', function() { _predictivePrewarmEmbed(key); });
773
+ tile.addEventListener('pointerdown', function() { _predictivePrewarmEmbed(key); });
774
+ tile.addEventListener('focus', function() { _predictivePrewarmEmbed(key); });
545
775
  })(tiles[i]);
546
776
  }
547
777
  })();
@@ -556,3 +786,114 @@
556
786
  (function bindPlansTileCount() {
557
787
  if (typeof loadPlansCounts === 'function') setTimeout(loadPlansCounts, 1500);
558
788
  })();
789
+
790
+ // ── Automation tile wiring ─────────────────────────────────────────
791
+ // (1) Lazy-seed the summed cockpit count (watches + scheduled tasks +
792
+ // pipelines) shortly after first paint, mirroring the Plans/Knowledge
793
+ // tiles — the schedule/pipeline counts are not on the hot status poll.
794
+ // (2) The on-tile "+ Add" chip opens a small picker (Watch / Scheduled Task /
795
+ // Pipeline); picking one opens the Automation modal on the matching tab and
796
+ // auto-opens that tab's classic Create dialog via openAutomationCreate.
797
+ (function bindAutomationTileCount() {
798
+ if (typeof loadAutomationCounts === 'function') setTimeout(loadAutomationCounts, 1500);
799
+ })();
800
+
801
+ // Open the Automation modal on `tabKey` with the embedded classic screen's
802
+ // Create dialog auto-opened (?embed=1#create). tabKey is a TABS key
803
+ // ('watches' | 'schedule' | 'pipelines').
804
+ function openAutomationCreate(tabKey) {
805
+ _automationInitial = { tab: tabKey, create: true };
806
+ openTileModal('automation');
807
+ // Single-call handoff: the automation view consumes (and nulls) the flag
808
+ // synchronously during openTileModal's render. Clearing here covers the
809
+ // path where openTileModal early-returns because the modal shell is
810
+ // missing — otherwise the flag would survive and a later plain tile click
811
+ // would silently open in create mode.
812
+ _automationInitial = null;
813
+ }
814
+
815
+ var _automationAddMenu = null;
816
+ function closeAutomationAddPicker() {
817
+ var menu = _automationAddMenu;
818
+ // openAutomationAddPicker moves focus INTO the menu, so removing that node
819
+ // would drop focus to <body> and lose a keyboard user's place. Reclaim it
820
+ // for the chip (WAI-ARIA menu-button pattern) — but ONLY when focus is
821
+ // still inside the menu we're removing, so the open-path reset call and an
822
+ // outside click that already landed on another control keep their focus.
823
+ // Must be sampled BEFORE removeChild: once detached, activeElement is body.
824
+ var reclaimFocus = !!(menu && document.activeElement
825
+ && (menu === document.activeElement
826
+ || (menu.contains && menu.contains(document.activeElement))));
827
+ if (menu && menu.parentNode) {
828
+ menu.parentNode.removeChild(menu);
829
+ }
830
+ _automationAddMenu = null;
831
+ document.removeEventListener('click', _automationAddOutside, true);
832
+ document.removeEventListener('keydown', _automationAddKeydown, true);
833
+ var chip = document.getElementById('slim-tile-automation-add');
834
+ if (chip) {
835
+ chip.setAttribute('aria-expanded', 'false');
836
+ if (reclaimFocus && typeof chip.focus === 'function') chip.focus();
837
+ }
838
+ }
839
+ function _automationAddOutside(ev) {
840
+ if (_automationAddMenu && !_automationAddMenu.contains(ev.target)
841
+ && ev.target.id !== 'slim-tile-automation-add') {
842
+ closeAutomationAddPicker();
843
+ }
844
+ }
845
+ function _automationAddKeydown(ev) {
846
+ if (ev.key === 'Escape') { ev.stopPropagation(); closeAutomationAddPicker(); }
847
+ }
848
+ function openAutomationAddPicker(anchorEl) {
849
+ closeAutomationAddPicker();
850
+ var menu = document.createElement('div');
851
+ menu.className = 'slim-add-menu';
852
+ menu.id = 'slim-automation-add-menu';
853
+ menu.setAttribute('role', 'menu');
854
+ menu.setAttribute('aria-label', 'Add automation');
855
+ [
856
+ { key: 'watches', label: 'Watch' },
857
+ { key: 'schedule', label: 'Scheduled Task' },
858
+ { key: 'pipelines', label: 'Pipeline' },
859
+ ].forEach(function(o) {
860
+ var item = document.createElement('button');
861
+ item.type = 'button';
862
+ item.className = 'slim-add-menu-item';
863
+ item.setAttribute('role', 'menuitem');
864
+ item.textContent = o.label;
865
+ item.addEventListener('click', function(ev) {
866
+ ev.stopPropagation();
867
+ closeAutomationAddPicker();
868
+ openAutomationCreate(o.key);
869
+ });
870
+ menu.appendChild(item);
871
+ });
872
+ document.body.appendChild(menu);
873
+ _automationAddMenu = menu;
874
+ // Anchor under the chip (fixed, so tile overflow can't clip it).
875
+ var r = anchorEl.getBoundingClientRect();
876
+ menu.style.top = (r.bottom + 4) + 'px';
877
+ menu.style.left = Math.max(8, r.right - menu.offsetWidth) + 'px';
878
+ if (anchorEl.setAttribute) anchorEl.setAttribute('aria-expanded', 'true');
879
+ var first = menu.querySelector('.slim-add-menu-item');
880
+ if (first) first.focus();
881
+ // Defer listener attach so the opening click doesn't immediately dismiss.
882
+ setTimeout(function() {
883
+ document.addEventListener('click', _automationAddOutside, true);
884
+ document.addEventListener('keydown', _automationAddKeydown, true);
885
+ }, 0);
886
+ }
887
+ (function bindAutomationAddUi() {
888
+ var chip = document.getElementById('slim-tile-automation-add');
889
+ if (!chip) return;
890
+ chip.addEventListener('click', function(ev) {
891
+ ev.stopPropagation(); // don't trigger the cockpit tile's own click
892
+ if (_automationAddMenu) { closeAutomationAddPicker(); return; }
893
+ openAutomationAddPicker(chip);
894
+ });
895
+ // Keyboard activation on the chip shouldn't bubble to the tile handler.
896
+ chip.addEventListener('keydown', function(ev) {
897
+ if (ev.key === 'Enter' || ev.key === ' ') { ev.stopPropagation(); }
898
+ });
899
+ })();
@@ -21,10 +21,6 @@
21
21
  // — only deltas from a previously-seen number trigger the fade, so opening
22
22
  // the dashboard doesn't animate every tile.
23
23
  var lastTileValues = Object.create(null);
24
- // Last (count|detail|lit) signature painted onto the Automation cockpit tile,
25
- // so applyStatus can skip a redundant updateTile on every 5s status poll when
26
- // nothing changed (W-ms4r1t5q003h341a).
27
- var _automationTileSig = null;
28
24
 
29
25
  // Apply lit-state and value/detail text to a tile.
30
26
  function updateTile(key, value, detail, lit) {
@@ -229,24 +225,20 @@
229
225
  activePrs.length === 0 ? null : (failingPrs.length ? 'red' : 'blue')
230
226
  );
231
227
 
232
- // ── Automation tile (Watches · Schedules · Pipelines) ─────────
233
- // The at-a-glance indicator stays watches-driven: schedule/pipeline
234
- // counts are not carried by the status poll, and the task explicitly
235
- // says not to invent new API calls just for the tile count. The detail
236
- // modal (renderAutomationTileBody) embeds all three classic screens.
237
- // This runs on EVERY 5s status poll, so gate the updateTile DOM work
238
- // (querySelectors + textContent + classList churn) behind a change-check —
239
- // repaint only when the count / detail / lit-state actually changed
240
- // (render-cache-busting parity with the classic dashboard's _changed()).
228
+ // ── Automation tile (Watches · Scheduled Tasks · Pipelines) ───
229
+ // The cockpit headline is the SUM of all three automation surfaces. Watches
230
+ // ride the live 5s status poll (fed here). Schedules + pipelines are NOT in
231
+ // the /api/status snapshot, so modals-tiles.js#loadAutomationCounts fetches
232
+ // them lazily off the hot poll path (mirroring the Knowledge + Plans tiles)
233
+ // and caches them; renderAutomationTile combines this live watches total with
234
+ // those cached counts. Total watches (not just active) is used to match the
235
+ // "sum of all 3 set" summed-count contract.
236
+ // This runs on EVERY 5s status poll, so the change-check that gates the
237
+ // updateTile DOM work (querySelectors + textContent + classList churn) lives
238
+ // inside renderAutomationTile — it has to compare the SUM, which also moves
239
+ // when the lazy schedule/pipeline counts land (W-ms4r1t5q003h341a).
241
240
  var watches = Array.isArray(data.watches) ? data.watches : [];
242
- var activeWatches = watches.filter(function(w) { return w && w.status === 'active'; });
243
- var autoDetail = activeWatches.length ? 'monitoring' : 'no automation set';
244
- var autoLit = activeWatches.length ? 'blue' : null;
245
- var autoSig = activeWatches.length + '|' + autoDetail + '|' + (autoLit || '');
246
- if (_automationTileSig !== autoSig) {
247
- _automationTileSig = autoSig;
248
- updateTile('automation', activeWatches.length, autoDetail, autoLit);
249
- }
241
+ if (typeof renderAutomationTile === 'function') renderAutomationTile(watches.length);
250
242
 
251
243
  // ── Knowledge tile (Pinned Context + Notes + KB) ──────────────
252
244
  // Pinned count is live from the poll; notes/KB counts are fetched lazily