create-walle 0.10.1 → 0.10.3

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 (681) hide show
  1. package/README.md +7 -1
  2. package/lib/owner-write-safety.js +135 -0
  3. package/lib/public-runtime.js +13 -5
  4. package/lib/release-schema.json +3 -2
  5. package/lib/runtime-closure-policy.json +11 -8
  6. package/package.json +1 -1
  7. package/template/bin/deps-doctor.js +1 -1
  8. package/template/bin/node-bin.sh +11 -0
  9. package/template/coding-task-manager/api-prompts.js +55 -29
  10. package/template/coding-task-manager/bin/claude-hook.js +62 -4
  11. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  12. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  13. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  14. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  16. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  17. package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
  18. package/template/coding-task-manager/bin/land.js +2069 -0
  19. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  20. package/template/coding-task-manager/bin/memory-census.js +4 -2
  21. package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
  22. package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
  23. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  24. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  25. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  26. package/template/coding-task-manager/db.js +860 -317
  27. package/template/coding-task-manager/git-utils.js +44 -0
  28. package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
  29. package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
  30. package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
  31. package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
  32. package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
  33. package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
  34. package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
  35. package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
  36. package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
  37. package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
  38. package/template/coding-task-manager/lib/adopted-host-redial.js +47 -7
  39. package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
  40. package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
  41. package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
  42. package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
  43. package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
  44. package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
  45. package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
  46. package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
  47. package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
  48. package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
  49. package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
  50. package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
  51. package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
  52. package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
  53. package/template/coding-task-manager/lib/agent-definitions.js +19 -0
  54. package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
  55. package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
  56. package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
  57. package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
  58. package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
  59. package/template/coding-task-manager/lib/agent-presets.js +99 -1
  60. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  61. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  62. package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
  63. package/template/coding-task-manager/lib/agent-version-service.js +74 -11
  64. package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
  65. package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
  66. package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
  67. package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
  68. package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
  69. package/template/coding-task-manager/lib/api/fleet.js +118 -0
  70. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  71. package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
  72. package/template/coding-task-manager/lib/api/ingress.js +43 -12
  73. package/template/coding-task-manager/lib/api/landing.js +403 -0
  74. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  75. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  76. package/template/coding-task-manager/lib/api/machines.js +1150 -0
  77. package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
  78. package/template/coding-task-manager/lib/api/misc-ops.js +25 -5
  79. package/template/coding-task-manager/lib/api/providers.js +53 -6
  80. package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
  81. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  82. package/template/coding-task-manager/lib/api/session-attach.js +623 -0
  83. package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
  84. package/template/coding-task-manager/lib/api/sessions.js +237 -59
  85. package/template/coding-task-manager/lib/api/setup-network.js +544 -16
  86. package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
  87. package/template/coding-task-manager/lib/api/setup.js +829 -10
  88. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  89. package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
  90. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  91. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  92. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  93. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  94. package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
  95. package/template/coding-task-manager/lib/auth-context.js +32 -2
  96. package/template/coding-task-manager/lib/auth-rate-limit.js +64 -1
  97. package/template/coding-task-manager/lib/auth-rules.js +474 -62
  98. package/template/coding-task-manager/lib/background-stop.js +369 -0
  99. package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
  100. package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
  101. package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
  102. package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
  103. package/template/coding-task-manager/lib/broadcast-serialize.js +7 -1
  104. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  105. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  106. package/template/coding-task-manager/lib/build-info.js +4 -2
  107. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  108. package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
  109. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  110. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  111. package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
  112. package/template/coding-task-manager/lib/client-diag-handler.js +69 -2
  113. package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
  114. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  115. package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
  116. package/template/coding-task-manager/lib/codex-harness-qualification.js +55 -20
  117. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  118. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  119. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  120. package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
  121. package/template/coding-task-manager/lib/coding-agent-models.js +48 -42
  122. package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
  123. package/template/coding-task-manager/lib/coding-session-model-adapters.js +70 -4
  124. package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
  125. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  126. package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
  127. package/template/coding-task-manager/lib/coding-session-model-runtime.js +24 -6
  128. package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
  129. package/template/coding-task-manager/lib/coding-session-model-state.js +17 -0
  130. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  131. package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
  132. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  133. package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
  134. package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
  135. package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
  136. package/template/coding-task-manager/lib/conversation-sync.js +355 -0
  137. package/template/coding-task-manager/lib/create/resolve-launch-params.js +104 -13
  138. package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
  139. package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
  140. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  141. package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
  142. package/template/coding-task-manager/lib/db-domain-tables.js +8 -0
  143. package/template/coding-task-manager/lib/db-domains.js +22 -2
  144. package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
  145. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  146. package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
  147. package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
  148. package/template/coding-task-manager/lib/db-schema-source-files.js +6 -0
  149. package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
  150. package/template/coding-task-manager/lib/feature-manifest.js +5 -1
  151. package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
  152. package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
  153. package/template/coding-task-manager/lib/fleet-hub.js +762 -0
  154. package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
  155. package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
  156. package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
  157. package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
  158. package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
  159. package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
  160. package/template/coding-task-manager/lib/fleet-update.js +798 -0
  161. package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
  162. package/template/coding-task-manager/lib/fs-watcher.js +61 -44
  163. package/template/coding-task-manager/lib/harness-history-transition.js +52 -11
  164. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  165. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  166. package/template/coding-task-manager/lib/headless-term-service.js +139 -25
  167. package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
  168. package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
  169. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  170. package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
  171. package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
  172. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  173. package/template/coding-task-manager/lib/host-handoff.js +50 -15
  174. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  175. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  176. package/template/coding-task-manager/lib/host-stats.js +475 -0
  177. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  178. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  179. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  180. package/template/coding-task-manager/lib/hosted-sync-worker.js +141 -310
  181. package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
  182. package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
  183. package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
  184. package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
  185. package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
  186. package/template/coding-task-manager/lib/hub-bridge.js +398 -0
  187. package/template/coding-task-manager/lib/idle-notify.js +491 -0
  188. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +12 -0
  189. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  190. package/template/coding-task-manager/lib/instance-manifest.js +18 -2
  191. package/template/coding-task-manager/lib/instance-sync-status.js +33 -8
  192. package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
  193. package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
  194. package/template/coding-task-manager/lib/landing/config.js +536 -0
  195. package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
  196. package/template/coding-task-manager/lib/landing/gate.js +360 -0
  197. package/template/coding-task-manager/lib/landing/git.js +344 -0
  198. package/template/coding-task-manager/lib/landing/lease.js +247 -0
  199. package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
  200. package/template/coding-task-manager/lib/landing/proof.js +223 -0
  201. package/template/coding-task-manager/lib/landing/queue.js +998 -0
  202. package/template/coding-task-manager/lib/landing/radar.js +489 -0
  203. package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
  204. package/template/coding-task-manager/lib/landing/state.js +263 -0
  205. package/template/coding-task-manager/lib/landing/stop.js +49 -0
  206. package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
  207. package/template/coding-task-manager/lib/landing/sync.js +456 -0
  208. package/template/coding-task-manager/lib/landing/train.js +885 -0
  209. package/template/coding-task-manager/lib/landing/window.js +57 -0
  210. package/template/coding-task-manager/lib/launch-environment.js +121 -0
  211. package/template/coding-task-manager/lib/launch-model-override.js +193 -8
  212. package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
  213. package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
  214. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  215. package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
  216. package/template/coding-task-manager/lib/lead-model-options.js +160 -3
  217. package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
  218. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +409 -72
  219. package/template/coding-task-manager/lib/lead-model-switch.js +115 -10
  220. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
  221. package/template/coding-task-manager/lib/lead-model-transition-store.js +85 -9
  222. package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
  223. package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
  224. package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
  225. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  226. package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
  227. package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
  228. package/template/coding-task-manager/lib/login-tripwire.js +1 -0
  229. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  230. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  231. package/template/coding-task-manager/lib/machine-registry.js +1494 -0
  232. package/template/coding-task-manager/lib/machine-self.js +66 -0
  233. package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
  234. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +485 -57
  235. package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
  236. package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
  237. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  238. package/template/coding-task-manager/lib/media-journal.js +84 -27
  239. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  240. package/template/coding-task-manager/lib/memory-join.js +337 -0
  241. package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
  242. package/template/coding-task-manager/lib/memory-sync.js +565 -0
  243. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  244. package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
  245. package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
  246. package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
  247. package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
  248. package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
  249. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  250. package/template/coding-task-manager/lib/model-availability.js +727 -0
  251. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
  252. package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
  253. package/template/coding-task-manager/lib/model-id-rule.js +19 -0
  254. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +8 -0
  255. package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
  256. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +6 -0
  257. package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
  258. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  259. package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
  260. package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
  261. package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
  262. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  263. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  264. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  265. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  266. package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
  267. package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
  268. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  269. package/template/coding-task-manager/lib/model-proxy/tokens.js +147 -5
  270. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +18 -0
  271. package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
  272. package/template/coding-task-manager/lib/native-history-control.js +7 -5
  273. package/template/coding-task-manager/lib/native-lead-settings-client.js +46 -5
  274. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  275. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +776 -75
  276. package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
  277. package/template/coding-task-manager/lib/opencode-cli.js +480 -0
  278. package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
  279. package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
  280. package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
  281. package/template/coding-task-manager/lib/owner-action-manifest-data.json +15 -1
  282. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  283. package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
  284. package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
  285. package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
  286. package/template/coding-task-manager/lib/perf-summary.js +7 -0
  287. package/template/coding-task-manager/lib/perf-tracker.js +63 -2
  288. package/template/coding-task-manager/lib/project-config.js +7 -0
  289. package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
  290. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  291. package/template/coding-task-manager/lib/provider-login-broker.js +264 -24
  292. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  293. package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
  294. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  295. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  296. package/template/coding-task-manager/lib/read-pool-client.js +94 -4
  297. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  298. package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
  299. package/template/coding-task-manager/lib/recent-catalog-query.js +41 -0
  300. package/template/coding-task-manager/lib/recent-session-catalog-response.js +76 -10
  301. package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
  302. package/template/coding-task-manager/lib/relay-client.js +57 -3
  303. package/template/coding-task-manager/lib/relink-audit.js +34 -5
  304. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  305. package/template/coding-task-manager/lib/remote-command-runtime.js +13 -1
  306. package/template/coding-task-manager/lib/remote-machine-catalog.js +1357 -0
  307. package/template/coding-task-manager/lib/remote-machine-create.js +1233 -0
  308. package/template/coding-task-manager/lib/remote-machine-link.js +1426 -0
  309. package/template/coding-task-manager/lib/remote-machine-proxy.js +654 -0
  310. package/template/coding-task-manager/lib/remote-session-forward.js +364 -0
  311. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  312. package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
  313. package/template/coding-task-manager/lib/restart-guard.js +18 -6
  314. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  315. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  316. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  317. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  318. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  319. package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
  320. package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
  321. package/template/coding-task-manager/lib/resume-admission.js +17 -3
  322. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  323. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  324. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  325. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  326. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  327. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  328. package/template/coding-task-manager/lib/review-repo-registry.js +74 -18
  329. package/template/coding-task-manager/lib/root-delivery-runtime.js +74 -17
  330. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  331. package/template/coding-task-manager/lib/root-prompt-admission.js +33 -2
  332. package/template/coding-task-manager/lib/root-prompt-flow.js +168 -34
  333. package/template/coding-task-manager/lib/runtime-contract.js +3 -1
  334. package/template/coding-task-manager/lib/schema-indexes.js +1 -0
  335. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  336. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  337. package/template/coding-task-manager/lib/session-context-locator.js +742 -0
  338. package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
  339. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  340. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  341. package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
  342. package/template/coding-task-manager/lib/session-create/delegate-worker.js +179 -20
  343. package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
  344. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  345. package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
  346. package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
  347. package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
  348. package/template/coding-task-manager/lib/session-create/prepared.js +201 -9
  349. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  350. package/template/coding-task-manager/lib/session-create/provider-process.js +193 -64
  351. package/template/coding-task-manager/lib/session-create/response.js +37 -19
  352. package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
  353. package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
  354. package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
  355. package/template/coding-task-manager/lib/session-create/transcript.js +14 -5
  356. package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
  357. package/template/coding-task-manager/lib/session-create/worker-continuation.js +50 -2
  358. package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
  359. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  360. package/template/coding-task-manager/lib/session-create/worker-observability.js +101 -9
  361. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  362. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  363. package/template/coding-task-manager/lib/session-empty.js +13 -10
  364. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  365. package/template/coding-task-manager/lib/session-host-manager.js +212 -206
  366. package/template/coding-task-manager/lib/session-jobs.js +20 -10
  367. package/template/coding-task-manager/lib/session-lifecycle.js +28 -4
  368. package/template/coding-task-manager/lib/session-model-action-preconditions.js +15 -2
  369. package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
  370. package/template/coding-task-manager/lib/session-park.js +738 -0
  371. package/template/coding-task-manager/lib/session-path-grant.js +13 -4
  372. package/template/coding-task-manager/lib/session-resource-sampler.js +19 -9
  373. package/template/coding-task-manager/lib/session-standup.js +67 -4
  374. package/template/coding-task-manager/lib/session-status-ledger.js +147 -6
  375. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  376. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  377. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  378. package/template/coding-task-manager/lib/session-transport.js +17 -0
  379. package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
  380. package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
  381. package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
  382. package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
  383. package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
  384. package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
  385. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  386. package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
  387. package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
  388. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  389. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +25 -23
  390. package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
  391. package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
  392. package/template/coding-task-manager/lib/submission-ack.js +90 -2
  393. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  394. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  395. package/template/coding-task-manager/lib/systemd-scope.js +115 -0
  396. package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
  397. package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
  398. package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
  399. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  400. package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
  401. package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
  402. package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
  403. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  404. package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
  405. package/template/coding-task-manager/lib/transcript-frame-cache.js +20 -5
  406. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  407. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  408. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  409. package/template/coding-task-manager/lib/transcript-source.js +44 -7
  410. package/template/coding-task-manager/lib/transcript-store.js +49 -0
  411. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  412. package/template/coding-task-manager/lib/typing-interaction-trace.js +198 -3
  413. package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
  414. package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
  415. package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
  416. package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
  417. package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
  418. package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
  419. package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
  420. package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
  421. package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
  422. package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
  423. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  424. package/template/coding-task-manager/lib/wake-skip-reasons.js +47 -0
  425. package/template/coding-task-manager/lib/walle/turn.js +1 -1
  426. package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
  427. package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
  428. package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
  429. package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
  430. package/template/coding-task-manager/lib/walle-row-history.js +495 -0
  431. package/template/coding-task-manager/lib/walle-supervisor.js +130 -27
  432. package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
  433. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  434. package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
  435. package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
  436. package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
  437. package/template/coding-task-manager/lib/ws/misc.js +1 -1
  438. package/template/coding-task-manager/lib/ws/state-sync.js +125 -6
  439. package/template/coding-task-manager/lib/ws/terminal-attach.js +123 -1
  440. package/template/coding-task-manager/lib/ws-router.js +68 -1
  441. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  442. package/template/coding-task-manager/oauth-proxy.js +211 -170
  443. package/template/coding-task-manager/package-lock.json +34 -94
  444. package/template/coding-task-manager/package.json +7 -3
  445. package/template/coding-task-manager/protocol/message-table.json +10 -0
  446. package/template/coding-task-manager/protocol/messages.js +10 -0
  447. package/template/coding-task-manager/protocol/ws-schema.json +20 -0
  448. package/template/coding-task-manager/protocol/ws-schema.md +2 -0
  449. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  450. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  451. package/template/coding-task-manager/public/css/app.css +449 -1
  452. package/template/coding-task-manager/public/css/reviews.css +4 -0
  453. package/template/coding-task-manager/public/css/walle-session.css +4 -0
  454. package/template/coding-task-manager/public/index.html +766 -465
  455. package/template/coding-task-manager/public/js/agent-command.js +86 -0
  456. package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
  457. package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
  458. package/template/coding-task-manager/public/js/app/machines-page.js +2572 -0
  459. package/template/coding-task-manager/public/js/app/models-tab.js +658 -73
  460. package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
  461. package/template/coding-task-manager/public/js/app/worktrees-panel.js +191 -7
  462. package/template/coding-task-manager/public/js/background-work.js +203 -28
  463. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +181 -13
  464. package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
  465. package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
  466. package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
  467. package/template/coding-task-manager/public/js/core/session-commands.js +847 -5
  468. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  469. package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
  470. package/template/coding-task-manager/public/js/document-review-links.js +9 -1
  471. package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
  472. package/template/coding-task-manager/public/js/history-app-seam.js +48 -4
  473. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  474. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  475. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  476. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  477. package/template/coding-task-manager/public/js/machine-pool-shared.js +991 -0
  478. package/template/coding-task-manager/public/js/message-renderer.js +4 -3
  479. package/template/coding-task-manager/public/js/model-picker.js +3 -1
  480. package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
  481. package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
  482. package/template/coding-task-manager/public/js/pasted-content.js +236 -0
  483. package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
  484. package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
  485. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  486. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -8
  487. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  488. package/template/coding-task-manager/public/js/prompts.js +11 -10
  489. package/template/coding-task-manager/public/js/protocol.js +10 -0
  490. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  491. package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
  492. package/template/coding-task-manager/public/js/reviews.js +7 -3
  493. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  494. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  495. package/template/coding-task-manager/public/js/row-join.js +4 -2
  496. package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
  497. package/template/coding-task-manager/public/js/session-search-utils.js +64 -21
  498. package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
  499. package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
  500. package/template/coding-task-manager/public/js/state-sync-client.js +14 -1
  501. package/template/coding-task-manager/public/js/stream-view.js +6 -5
  502. package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
  503. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  504. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  505. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  506. package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
  507. package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
  508. package/template/coding-task-manager/public/js/terminal/runtime-12.js +60 -3
  509. package/template/coding-task-manager/public/js/terminal/runtime-16.js +16 -12
  510. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  511. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  512. package/template/coding-task-manager/public/js/terminal/runtime-20.js +211 -74
  513. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  514. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  515. package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
  516. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -1
  517. package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
  518. package/template/coding-task-manager/public/js/terminal-wheel.js +62 -5
  519. package/template/coding-task-manager/public/js/walle/chat-conversation.js +15 -7
  520. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  521. package/template/coding-task-manager/public/js/walle.js +6 -0
  522. package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
  523. package/template/coding-task-manager/public/m/app.css +366 -8
  524. package/template/coding-task-manager/public/m/app.js +1006 -74
  525. package/template/coding-task-manager/public/m/index.html +55 -1
  526. package/template/coding-task-manager/public/m/machines.js +2147 -0
  527. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  528. package/template/coding-task-manager/public/m/private-api.js +31 -1
  529. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  530. package/template/coding-task-manager/public/m/sw.js +5 -0
  531. package/template/coding-task-manager/public/usage.html +1 -0
  532. package/template/coding-task-manager/queue-engine.js +13 -0
  533. package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
  534. package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
  535. package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
  536. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
  537. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
  538. package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
  539. package/template/coding-task-manager/server/approvals.js +5 -6
  540. package/template/coding-task-manager/server/boot.js +136 -17
  541. package/template/coding-task-manager/server.js +2819 -11885
  542. package/template/coding-task-manager/session-integrity.js +71 -13
  543. package/template/coding-task-manager/workers/db-owner-worker.js +72 -9
  544. package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
  545. package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
  546. package/template/coding-task-manager/workers/read-pool-worker.js +93 -12
  547. package/template/coding-task-manager/workers/session-host-pool-process.js +24 -10
  548. package/template/coding-task-manager/workers/session-host-process.js +19 -8
  549. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  550. package/template/coding-task-manager/workers/watch-worker.js +105 -0
  551. package/template/coding-task-manager/workers/worktree-worker.js +202 -33
  552. package/template/ctm-relay/lib/protocol.js +14 -1
  553. package/template/package-lock.json +2 -2
  554. package/template/package.json +1 -1
  555. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  556. package/template/shared/backup-retention.js +82 -0
  557. package/template/shared/conversation-admission.js +2 -0
  558. package/template/shared/conversation-provenance.js +3 -7
  559. package/template/shared/conversation-storage-v2-contract.js +47 -6
  560. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  561. package/template/shared/git-remote-key.js +83 -0
  562. package/template/shared/host-identity.js +98 -2
  563. package/template/shared/linked-worktree.js +51 -0
  564. package/template/shared/macos-fda-target.js +57 -0
  565. package/template/shared/orphan-parents.js +55 -0
  566. package/template/shared/owner-write-safety.js +135 -0
  567. package/template/shared/public-runtime.js +13 -5
  568. package/template/shared/release-schema.json +3 -2
  569. package/template/shared/report-citations.js +14 -0
  570. package/template/shared/sqlite-write-lock.js +361 -223
  571. package/template/shared/storage/contract.js +38 -3
  572. package/template/wall-e/adapters/imessage.js +12 -0
  573. package/template/wall-e/agent.js +92 -4
  574. package/template/wall-e/api-walle.js +26 -1
  575. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  576. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
  577. package/template/wall-e/bin/walle-mcp-stdio.js +14 -8
  578. package/template/wall-e/brain/brain-join.js +859 -0
  579. package/template/wall-e/brain/brain-sync-membership.js +20 -0
  580. package/template/wall-e/brain/brain-sync-store.js +131 -77
  581. package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
  582. package/template/wall-e/brain/brain-sync-v2.js +578 -0
  583. package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
  584. package/template/wall-e/brain/deterministic-id.js +51 -0
  585. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  586. package/template/wall-e/brain/memories.js +2 -2
  587. package/template/wall-e/brain/migrations.js +91 -28
  588. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  589. package/template/wall-e/brain/model-identity-migration.js +1 -1
  590. package/template/wall-e/brain/owner-call-whitelist.js +39 -7
  591. package/template/wall-e/brain/schema-ensures.js +22 -0
  592. package/template/wall-e/brain/schema.js +45 -85
  593. package/template/wall-e/brain.js +172 -36
  594. package/template/wall-e/chat.js +3 -3
  595. package/template/wall-e/coding/agent-shell-env.js +7 -2
  596. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  597. package/template/wall-e/coding/prompt-bundle.js +66 -21
  598. package/template/wall-e/coding/session-journal.js +28 -40
  599. package/template/wall-e/coding-orchestrator.js +23 -26
  600. package/template/wall-e/config/main-loop-exemptions.json +2 -2
  601. package/template/wall-e/http/admin-api.js +37 -0
  602. package/template/wall-e/http/auth.js +18 -1
  603. package/template/wall-e/http/blog-views-api.js +318 -0
  604. package/template/wall-e/http/gws-api.js +4 -0
  605. package/template/wall-e/http/helper-rules-api.js +327 -0
  606. package/template/wall-e/http/model-admin.js +8 -1
  607. package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
  608. package/template/wall-e/http/model-proxy-providers.js +177 -2
  609. package/template/wall-e/http/model-route-health.js +157 -0
  610. package/template/wall-e/http/sync-api.js +278 -464
  611. package/template/wall-e/http/telemetry-api.js +12 -0
  612. package/template/wall-e/jobs/job-runner.js +28 -1
  613. package/template/wall-e/lib/blog-view-policy.js +88 -0
  614. package/template/wall-e/lib/blog-view-store.js +169 -0
  615. package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
  616. package/template/wall-e/lib/client-config-lock.js +2 -1
  617. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  618. package/template/wall-e/lib/ctm-data-dir.js +26 -0
  619. package/template/wall-e/lib/derivation-home.js +147 -0
  620. package/template/wall-e/lib/error-kinds.js +10 -1
  621. package/template/wall-e/lib/feature-manifest.js +1 -1
  622. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  623. package/template/wall-e/lib/helper-rules/evaluate.js +398 -0
  624. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  625. package/template/wall-e/lib/helper-rules/grade.js +320 -0
  626. package/template/wall-e/lib/helper-rules/label.js +247 -0
  627. package/template/wall-e/lib/helper-rules/lead-call-hook.js +555 -0
  628. package/template/wall-e/lib/helper-rules/store.js +669 -0
  629. package/template/wall-e/lib/landing-mcp-tools.js +170 -0
  630. package/template/wall-e/lib/mcp-client-config.js +133 -14
  631. package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
  632. package/template/wall-e/lib/mcp-integration.js +63 -19
  633. package/template/wall-e/lib/parent-death.js +7 -1
  634. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  635. package/template/wall-e/lib/scheduler-worker-jobs.js +12 -4
  636. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  637. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  638. package/template/wall-e/llm/default-fallback.js +4 -0
  639. package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
  640. package/template/wall-e/llm/delegate-vendor-default.js +12 -5
  641. package/template/wall-e/llm/model-catalog.js +13 -4
  642. package/template/wall-e/llm/model-roles.js +7 -1
  643. package/template/wall-e/llm/native-delegate-selection.js +56 -8
  644. package/template/wall-e/llm/portkey-http.js +150 -0
  645. package/template/wall-e/llm/portkey-sync.js +15 -145
  646. package/template/wall-e/llm/worker-delegate-args.js +72 -7
  647. package/template/wall-e/llm/worker-mcp-routing.js +117 -7
  648. package/template/wall-e/llm/worker-route.js +43 -1
  649. package/template/wall-e/loops/brain-custom-writes.js +64 -0
  650. package/template/wall-e/loops/ingest.js +17 -1
  651. package/template/wall-e/loops/think.js +19 -4
  652. package/template/wall-e/mcp-contract.js +222 -6
  653. package/template/wall-e/mcp-protocol-contract.js +77 -2
  654. package/template/wall-e/mcp-server.js +141 -31
  655. package/template/wall-e/mcp-worker-schemas.js +25 -7
  656. package/template/wall-e/memory/ctm-context-client.js +110 -5
  657. package/template/wall-e/memory/ctm-session-context.js +9 -3
  658. package/template/wall-e/memory/hub-bridge-client.js +388 -0
  659. package/template/wall-e/memory/replica-origin.js +56 -0
  660. package/template/wall-e/memory/session-pool.js +135 -0
  661. package/template/wall-e/runtime/live-capabilities.js +8 -3
  662. package/template/wall-e/server.js +4 -2
  663. package/template/wall-e/shared/release-schema.json +3 -2
  664. package/template/wall-e/shared/sqlite-file-set.js +39 -0
  665. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  666. package/template/wall-e/shared/storage/contract.js +38 -3
  667. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  668. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +13 -1
  669. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +11 -6
  670. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +13 -8
  671. package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
  672. package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
  673. package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
  674. package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
  675. package/template/wall-e/tools/chrome-automation.js +6 -0
  676. package/template/wall-e/tools/local-tools.js +6 -4
  677. package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
  678. package/template/wall-e/workers/brain-owner-worker.js +27 -4
  679. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  680. package/template/wall-e/workers/mcp-gateway-process.js +5 -5
  681. package/template/wall-e/workers/telemetry-worker.js +8 -0
@@ -0,0 +1,2147 @@
1
+ /* ===================================================================
2
+ CtmPhoneMachines — the phone's machine pool surfaces (WP-6).
3
+
4
+ Design: docs/machine-pool-picker-pairing-dashboard-2026-10-24.html §5 D-1/D-2/D-3/D-4/D-9/D-12,
5
+ P-7, P-8; §6.4 client block; §8 WP-6. Ruling in force: PHONE = DESKTOP PARITY.
6
+
7
+ Two surfaces, both the phone twin of a desktop one:
8
+ 1. the Machines page inside More — twin of public/js/app/machines-page.js (This Mac + every
9
+ registry member: same groups, same badges, same sparklines, same actions, same Add dialog,
10
+ Show a code, Leave pool, Join pool, New session here, "Sign in to <member> too");
11
+ 2. the picker search sheet for N > 4 — twin of the desktop "All machines (N)…" search card
12
+ (D-1/D-2/D-3: Favorites / Recent / All / Offline, search over displayName/hostname/os/arch/
13
+ url/gpu.name, offline rows disabled with "last seen …", hidden rows never rendered).
14
+
15
+ One copy, not two: every DOM-free presenter (order, search text, groups, load line, sparkline,
16
+ pairing container, card facts) comes from public/js/machine-pool-shared.js, which the phone loads
17
+ before this file. Settings are MACHINE-OWNED (D-4 rev 2): rename / favorite / hide POST
18
+ /api/machine-self on the machine they describe (`?machine=<id>` for a member). Nothing here writes
19
+ localStorage — the only client-side machine state is the P-7 dial cache (public/m/pool-failover.js),
20
+ plus the code being typed into an open sign-in's box, which is held in this module's own memory
21
+ (state.signInDrafts, per machine+agent) because the More page rebuilds the card under it.
22
+
23
+ Classic script, like public/m/model-sheet.js: the app creates it with DOM/transport/clock/timer
24
+ injected, so every presenter is provable in a vm without a browser. It owns exactly ONE timer: the
25
+ bounded sign-in re-read (SIGN_IN_POLL_MS, at most SIGN_IN_POLL_MAX_TICKS ticks) started by a
26
+ sign-in on a machine, which stops as soon as that flow closes, its block leaves the page, or the
27
+ ten-minute cap is reached. Nothing else here schedules work.
28
+ =================================================================== */
29
+ (function (root) {
30
+ 'use strict';
31
+
32
+ var STALE_NOTE = 'stats stale';
33
+ var STATUS_ID = 'phone-machines-status';
34
+ // D-12: the same 24 h window the desktop page asks for, so a spark is the same spark on both.
35
+ var HISTORY_MS = 24 * 60 * 60 * 1000;
36
+ // WP-3 fix (C): the box prints its sign-in link ~1 s AFTER the POST answers, so one read is not
37
+ // enough — the block would sit on "Waiting for the sign-in page…" forever. One bounded re-read.
38
+ var SIGN_IN_POLL_MS = 2000;
39
+ var SIGN_IN_POLL_MAX_TICKS = 300;
40
+ var SIGN_IN_OPEN_STATUSES = ['starting', 'running', 'awaiting_user'];
41
+
42
+ function defaultEsc(value) {
43
+ return String(value == null ? '' : value).replace(/[&<>"']/g, function (ch) {
44
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch];
45
+ });
46
+ }
47
+
48
+ function normId(value) {
49
+ return String(value == null ? '' : value).trim();
50
+ }
51
+
52
+ function machineName(machine) {
53
+ var m = machine || {};
54
+ return String(m.displayName || m.hostname || m.url || m.instanceId || '');
55
+ }
56
+
57
+ // P-8: the credential the hub dialled the row with. `env` rows are one-way members and carry the
58
+ // "Join pool" action, exactly as on the desktop page.
59
+ function credentialBadge(machine, isSelf) {
60
+ var credential = String((machine && machine.peerCredential) || (isSelf ? 'self' : 'missing'));
61
+ if (isSelf) return { text: 'This Mac', cred: 'self', title: 'This machine runs its own sessions', action: '' };
62
+ if (credential === 'pool') return { text: 'Pool', cred: 'pool', title: 'Dialled with the shared pool secret', action: '' };
63
+ if (credential === 'env') return { text: 'Env token', cred: 'env', title: 'Legacy env token — a one-way member', action: 'join-pool' };
64
+ return { text: 'No credential', cred: 'missing', title: 'No credential this hub can dial it with', action: '' };
65
+ }
66
+
67
+
68
+ // ── D-1 picker: <select> at N <= 4, search sheet above it ──────────────────────────────────────
69
+ // D-1 counts N = 1 (This Mac) + the visible registry rows. At or below MACHINE_PICKER_CARD_LIMIT the
70
+ // phone keeps the one-tap <select>; above it the picker is the search sheet.
71
+ function pickerMode(machines, selfIncluded) {
72
+ var visible = (Array.isArray(machines) ? machines : []).filter(function (m) {
73
+ return m && m.instanceId && !machineIsHidden(m);
74
+ });
75
+ var n = visible.length + (selfIncluded === false ? 0 : 1);
76
+ return n > MACHINE_PICKER_CARD_LIMIT ? 'search' : 'select';
77
+ }
78
+
79
+ // D-2/D-3 for the search sheet: the grouped rows a query leaves, in the shared order, with `hidden`
80
+ // rows already dropped by machinePickerGroupRows (so a hidden box can never surface here).
81
+ function sheetGroups(machines, query, now) {
82
+ var needle = String(query == null ? '' : query).trim().toLowerCase();
83
+ var groups = machinePickerGroupRows(machines, now);
84
+ if (!needle) return groups;
85
+ return groups.map(function (group) {
86
+ return {
87
+ id: group.id,
88
+ label: group.label,
89
+ rows: group.rows.filter(function (machine) { return machineSearchText(machine).indexOf(needle) !== -1; }),
90
+ };
91
+ }).filter(function (group) { return group.rows.length > 0; });
92
+ }
93
+
94
+ // A sheet row. D-3: the sheet lists EVERY row, including the unreachable ones HUB AC-5a keeps out
95
+ // of the picker CARD row — "where is my machine" is exactly the question a search answers, so the
96
+ // offline group is the point of the group set, not an afterthought. A non-reachable row is DISABLED
97
+ // and says so with its last-seen stamp; a reachable one is tappable and carries the D-12 load line.
98
+ // `now` is passed in, so staleness is never judged inside a tested path.
99
+ function sheetRowHtml(machine, selectedId, now) {
100
+ var id = normId(machine && machine.instanceId);
101
+ if (!id) return '';
102
+ var disabled = machineIsOffline(machine);
103
+ var meta = disabled
104
+ ? [machineHealthLabel(machine).toLowerCase(), machineLastSeenText(machine)].join(' · ')
105
+ : machineLoadLine(machine, now);
106
+ var selected = id === selectedId;
107
+ return '<button type="button" class="model-picker-option machine-sheet-row' + (selected ? ' is-selected' : '') + '"'
108
+ + ' role="option" aria-selected="' + (selected ? 'true' : 'false') + '"'
109
+ + ' data-machine-pick="' + defaultEsc(id) + '"' + (disabled ? ' disabled aria-disabled="true"' : '') + '>'
110
+ + '<span class="machine-sheet-row-name">' + defaultEsc(machineName(machine)) + '</span>'
111
+ + (meta ? '<span class="machine-sheet-row-meta">' + defaultEsc(meta) + '</span>' : '')
112
+ + '</button>';
113
+ }
114
+
115
+ function sheetListHtml(machines, query, selectedId, now) {
116
+ var groups = sheetGroups(machines, query, now);
117
+ if (!groups.length) return '<p class="machine-sheet-empty">No machine matches that search.</p>';
118
+ return groups.map(function (group) {
119
+ return '<div class="machine-sheet-group" data-group="' + defaultEsc(group.id) + '">'
120
+ + '<p class="machine-sheet-group-label">' + defaultEsc(group.label) + '</p>'
121
+ + group.rows.map(function (machine) { return sheetRowHtml(machine, selectedId, now); }).join('')
122
+ + '</div>';
123
+ }).join('');
124
+ }
125
+
126
+
127
+ // ── WP-3 models helpers, hoisted to module scope (box-owned sessions WP-3, docs/remote-session-
128
+ // child-transcript-model-truth-and-restart-2026-10-27.html §5 D-7): pure functions of the registry
129
+ // row, used by the card/sheet below AND exported as `modelsDelegateWarnings` so the shared
130
+ // Delegate pane (public/js/session-model-picker.js) words a box's reach verdict with THIS page's
131
+ // copy on the phone, exactly as it uses machinesPageModelsDelegateWarnings on the desktop.
132
+ function modelsBlock(machine) {
133
+ var block = machine && machine.models;
134
+ return block && typeof block === 'object' ? block : null;
135
+ }
136
+
137
+ function modelsDelegate(machine) {
138
+ var block = modelsBlock(machine);
139
+ return block && Array.isArray(block.delegate) ? block.delegate : [];
140
+ }
141
+
142
+ function modelsDelegateWarnings(machine) {
143
+ return modelsDelegate(machine).filter(function (entry) {
144
+ var reach = String((entry && entry.reach) || '');
145
+ return reach === 'unreachable' || reach === 'no_server';
146
+ }).map(function (entry) {
147
+ return '/delegate on ' + machineName(machine) + ' uses ' + String((entry && entry.model) || '')
148
+ + ' through a route ' + machineName(machine) + " can't reach.";
149
+ });
150
+ }
151
+
152
+ function create(deps) {
153
+ var d = deps || {};
154
+ var $ = d.$ || function () { return null; };
155
+ var esc = d.esc || defaultEsc;
156
+ var api = d.api || function () { return Promise.reject(new Error('no api')); };
157
+ // hub bridge (R-13/AC-4d): the host re-mounts its per-card Access block after EVERY paint, because
158
+ // render() rebuilds the card markup (a load/refresh would otherwise wipe it).
159
+ var afterRender = typeof d.afterRender === 'function' ? d.afterRender : null;
160
+ var toast = d.toast || function () {};
161
+ var host = d.document || (typeof document !== 'undefined' ? document : null);
162
+ var openNewSession = d.openNewSessionSheet || function () {};
163
+ var confirmImpl = d.confirm || (typeof confirm === 'function' ? confirm : null);
164
+ var clock = d.now || function () { return Date.now(); };
165
+ var at = function () { var value = Number(clock()); return Number.isFinite(value) ? value : 0; };
166
+ // The ONE timer seam (WP-3 fix C). Injected like every other dependency, so a vm spec drives the
167
+ // sign-in re-read tick by tick instead of waiting on a real clock.
168
+ var timers = d.timers || {
169
+ set: function (fn, ms) { return setTimeout(fn, ms); },
170
+ clear: function (id) { if (id != null) clearTimeout(id); },
171
+ };
172
+
173
+ var state = {
174
+ machines: [],
175
+ histories: {},
176
+ self: null, // /api/host/stats for This Mac: { latest, samples, instanceId, urls }
177
+ selfSettings: null, // /api/machine-self: { displayName, favorite, hidden }
178
+ selfInstanceId: '',
179
+ origins: [], // the P-7 dial cache, read through the failover module
180
+ // WP-3: the last machine-scoped login-state entry per '<instanceId>-<agent>', so an open
181
+ // sign-in survives the app's re-renders.
182
+ signIn: {},
183
+ // WP-3 fix round 3 (G): the draft the user has typed into a block's own code box, per the same
184
+ // '<instanceId>-<agent>' key — kept HERE, not only in the block, because the More page rewrites
185
+ // #more-content wholesale BEFORE this module renders into it, so by then the old input (and its
186
+ // draft) is gone; this state is the only record the new card can be painted from.
187
+ signInDrafts: {},
188
+ // WP-3 fix (C): at most ONE re-read handle per '<instanceId>-<agent>'.
189
+ signInTimers: {},
190
+ loading: false,
191
+ loaded: false,
192
+ error: '',
193
+ };
194
+ var dialog = null;
195
+
196
+ function failoverModule() {
197
+ return root.CtmPoolFailover || null;
198
+ }
199
+
200
+ // The P-7 dial cache is the ONLY client-side machine state. A blocked or corrupted cache reads as
201
+ // "this device has reached nothing", which is exactly the safe default for the sign-in row.
202
+ function poolOrigins() {
203
+ var failover = failoverModule();
204
+ if (!failover || typeof failover.readPoolOrigins !== 'function') return [];
205
+ try {
206
+ return failover.readPoolOrigins(d.storage);
207
+ } catch (error) {
208
+ return []; // HANDLE: a blocked dial cache is an empty roster; the page still renders
209
+ }
210
+ }
211
+
212
+ // ── Machines page markup ───────────────────────────────────────────────────────────────────
213
+ // P-7: a member whose public https origin this device has never reached offers the one-tap
214
+ // sign-in — passkeys are pinned to the exact hostname, so the member's own /m/ runs its flow.
215
+ function signInRowHtml(machine) {
216
+ var origin = machinePoolSignInOrigin(machine, state.origins);
217
+ if (!origin) return '';
218
+ return '<button type="button" class="mp-btn mp-signin-row"'
219
+ + ' title="' + esc('Opens ' + origin + '/m/ — a passkey is pinned to that hostname') + '"'
220
+ + ' data-machine-signin="' + esc(origin) + '">'
221
+ + esc('Sign in to ' + machineName(machine) + ' too')
222
+ + '<span class="mp-signin-hint">' + esc(origin + '/m/') + '</span>'
223
+ + '</button>';
224
+ }
225
+
226
+ function factRow(label, value) {
227
+ if (!value) return '';
228
+ return '<div class="mp-fact"><span class="mp-fact-label">' + esc(label) + '</span>'
229
+ + '<span class="mp-fact-value">' + value + '</span></div>';
230
+ }
231
+
232
+ function sparkBlock(cpuSeries, memSeries, cpuNow, memNow) {
233
+ return '<div class="mp-sparks">'
234
+ + '<div class="mp-spark"><span class="mp-spark-label">CPU</span>' + sparklineSvg(cpuSeries)
235
+ + '<span class="mp-spark-val">' + esc(machineFormatPct(cpuNow)) + '</span></div>'
236
+ + '<div class="mp-spark"><span class="mp-spark-label">MEM</span>' + sparklineSvg(memSeries)
237
+ + '<span class="mp-spark-val">' + esc(machineFormatPct(memNow)) + '</span></div>'
238
+ + '</div>';
239
+ }
240
+
241
+ function machineFactsHtml(sample, agents, history, presence) {
242
+ return ''
243
+ + factRow('Load', sample && sample.load1 != null ? esc(Number(sample.load1).toFixed(2)) : '')
244
+ + factRow('Cores', sample && sample.cores != null ? esc(String(sample.cores)) : '')
245
+ + factRow('Disk free', sample ? esc(machineFormatBytes(sample.diskFreeBytes)) : '')
246
+ + factRow('GPU', esc(machineGpuText(sample && sample.gpu)))
247
+ + factRow('Power', esc(machinePowerText(sample, machineHistoryRows(history), at())))
248
+ + factRow('Tasks', esc(machineTaskLine(sample && sample.tasks)))
249
+ + factRow('Agents', esc(machineAgentText(agents, presence)));
250
+ }
251
+
252
+ // ── WP-3 sign-in: a coding agent ON a pooled machine, signed in from this phone ───────────────
253
+ // docs/remote-agent-sign-in-from-hub-browser-2026-10-25.html §5 D-7/D-9, §6, §9 S-13 — PHONE =
254
+ // DESKTOP PARITY, so the markup carries the SAME `data-*` names as the desktop card
255
+ // (data-machine-sign-in / data-machine-agent / data-machine-sign-in-block /
256
+ // data-open-sign-in-page / data-sign-in-code / data-sign-in-code-submit) and the three provider
257
+ // routes ride the same `?machine=<id>` proxy. Fix (C): the POST's own follow-up read is NOT
258
+ // enough — the box prints its link about a second later — so one bounded re-read per machine (see
259
+ // SIGN_IN_POLL_MS) runs until the flow closes; the entry also persists in `state.signIn` across
260
+ // the app's re-renders, so a repaint never loses the flow it already has.
261
+ var SIGN_IN_AGENTS = ['claude', 'codex'];
262
+
263
+ // The desktop's mapped copy, verbatim (models-tab.js `_modelsProviderLoginErrorText`): the phone
264
+ // must not print a raw refusal id where the card prints what the user can do about it.
265
+ function signInErrorText(errorId) {
266
+ var id = String(errorId === null || errorId === undefined ? '' : errorId);
267
+ if (id === 'code_format_invalid') return "That doesn't look like a sign-in code. Paste the code, or the page address, from the sign-in page.";
268
+ if (id === 'code_state_mismatch') return 'That code is from a different sign-in. Use the link above and try again.';
269
+ if (id === 'code_from_loopback_redirect') return 'That page came from a localhost link. Use the Open sign-in page link above.';
270
+ if (id === 'login_tab_gone' || id === 'provider_login_not_open') return 'The sign-in tab closed. Start Sign in again.';
271
+ if (id === 'code_entry_not_supported') return 'This sign-in does not take a code here.';
272
+ return 'Could not send the code (' + id + ').';
273
+ }
274
+
275
+ // The clipboard is the browser's, and its refusal is a label, never an exception escaping to the
276
+ // console (the desktop card's own rule). No timer: the label holds until the next render.
277
+ function copySignInCode(code, btn) {
278
+ var label = function (text) { if (btn) btn.textContent = text; };
279
+ try {
280
+ navigator.clipboard.writeText(String(code || '')).then(
281
+ function () { label('Copied'); },
282
+ function () { label('Copy failed'); },
283
+ );
284
+ } catch (error) {
285
+ // HANDLE: no clipboard (or no navigator) on this surface — the button says so and stops.
286
+ label('Copy failed');
287
+ }
288
+ }
289
+
290
+ // `signed_out` only — never `signed_in`, never `unknown` (P3: a box that never answered is not
291
+ // told it is signed out).
292
+ function signedOutAgents(machine) {
293
+ var agents = machine && machine.agents;
294
+ if (!agents || typeof agents !== 'object') return [];
295
+ return SIGN_IN_AGENTS.filter(function (agent) { return String(agents[agent] || '') === 'signed_out'; });
296
+ }
297
+
298
+ // The Sign in button plus the block it fills, per signed-out agent, under the Agents fact.
299
+ // F.1 (fix round 2): while an entry is open, the row button renders as the disabled "Opening…"
300
+ // the POST set — a More-page re-render runs after every render of the container, and an enabled
301
+ // "Sign in" there invites a second tap that just gets the 409.
302
+ function signInBlockHtml(machine) {
303
+ var id = normId(machine && machine.instanceId);
304
+ if (!id) return '';
305
+ return signedOutAgents(machine).map(function (agent) {
306
+ var key = id + '-' + agent;
307
+ var opening = signInEntryOpen(state.signIn[key]);
308
+ return '<div class="mp-fact mp-signin" data-machine-signin-for="' + esc(key) + '">'
309
+ + '<button type="button" class="mp-btn mp-signin-btn" data-machine-sign-in="' + esc(id) + '"'
310
+ + ' data-machine-agent="' + esc(agent) + '"'
311
+ + (opening ? ' disabled' : '') + '>'
312
+ + (opening ? 'Opening…' : 'Sign in') + '</button>'
313
+ + '<div class="mp-signin-block" data-machine-agent="' + esc(agent) + '"'
314
+ + ' data-machine-sign-in-block="' + esc(key) + '">'
315
+ + signInFlowHtml(agent, state.signIn[key]) + '</div>'
316
+ + '</div>';
317
+ }).join('');
318
+ }
319
+
320
+ // D-1 defence in depth, the same rule the card applies: only an https URL whose host (and whose
321
+ // redirect_uri host) is NOT loopback may become a link — a loopback link is the BOX's own URL,
322
+ // which a phone is not on.
323
+ function signInUrl(value) {
324
+ var loopback = ['localhost', '127.0.0.1', '::1', '[::1]'];
325
+ var parsed = null;
326
+ try { parsed = new URL(String(value || '')); } catch (error) { return ''; }
327
+ if (parsed.protocol !== 'https:') return '';
328
+ if (loopback.indexOf(String(parsed.hostname).toLowerCase()) >= 0) return '';
329
+ var redirect = parsed.searchParams.get('redirect_uri');
330
+ if (redirect) {
331
+ var host = '';
332
+ try { host = new URL(redirect).hostname; } catch (error) { host = ''; }
333
+ if (loopback.indexOf(String(host).toLowerCase()) >= 0) return '';
334
+ }
335
+ return parsed.href;
336
+ }
337
+
338
+ function providerIdForAgent(agent) {
339
+ return String(agent) === 'codex' ? 'codex' : 'claude';
340
+ }
341
+
342
+ // The flow's block, in the card's own markup and labels (desktop parity). 'Show sign-in tab' is
343
+ // deliberately absent: the sign-in tab lives on the box, not on this phone.
344
+ function signInFlowHtml(agent, entry) {
345
+ var flow = entry && entry.flow;
346
+ if (!flow || ['starting', 'running', 'awaiting_user'].indexOf(String(flow.status || '')) === -1) return '';
347
+ var url = signInUrl(flow.verificationUrl);
348
+ var parts = [];
349
+ if (url) {
350
+ parts.push('<a class="mp-btn" data-open-sign-in-page href="' + esc(url) + '" target="_blank" rel="noopener">Open sign-in page</a>');
351
+ }
352
+ if (String(flow.codeEntry || '') === 'browser') {
353
+ parts.push('<code data-sign-in-user-code>' + esc(flow.userCode || '') + '</code>');
354
+ parts.push('<span data-sign-in-hint>Enter this code on the sign-in page.</span>');
355
+ } else if (String(flow.codeEntry || '') === 'paste') {
356
+ parts.push('<input data-sign-in-code type="text" inputmode="text" autocomplete="off" spellcheck="false"'
357
+ + ' aria-label="Sign-in code" placeholder="Paste the code from the sign-in page" />');
358
+ parts.push('<button type="button" class="mp-btn" data-sign-in-code-submit>Submit code</button>');
359
+ parts.push('<span data-sign-in-hint>After you sign in, the page shows a code. Paste it here.</span>');
360
+ parts.push('<span data-sign-in-error></span>');
361
+ } else {
362
+ parts.push('<span data-sign-in-hint>Waiting for the sign-in page…</span>');
363
+ }
364
+ return parts.join('');
365
+ }
366
+
367
+ function signInBlockNode(instanceId, agent) {
368
+ var doc = host || (typeof document !== 'undefined' ? document : null);
369
+ if (!doc || typeof doc.querySelector !== 'function') return null;
370
+ return doc.querySelector('[data-machine-sign-in-block="' + normId(instanceId) + '-' + String(agent) + '"]');
371
+ }
372
+
373
+ function signInKey(instanceId, agent) {
374
+ return normId(instanceId) + '-' + String(agent);
375
+ }
376
+
377
+ function signInEntryOpen(entry) {
378
+ var flow = entry && entry.flow;
379
+ return !!flow && SIGN_IN_OPEN_STATUSES.indexOf(String(flow.status || '')) >= 0;
380
+ }
381
+
382
+ // The desktop's render key, over ONE machine-scoped entry: everything the block renders, with the
383
+ // probe's own timestamp dropped (it moves between ticks and is painted nowhere). Same defect as
384
+ // WP-2 on the card — a repaint on every tick replaces the code box mid-typing.
385
+ function signInRenderKey(entry) {
386
+ if (!entry || typeof entry !== 'object') return JSON.stringify(entry === undefined ? null : entry);
387
+ var copy = {};
388
+ Object.keys(entry).forEach(function (k) { copy[k] = entry[k]; });
389
+ if (copy.account && typeof copy.account === 'object') {
390
+ var account = {};
391
+ Object.keys(copy.account).forEach(function (k) { if (k !== 'checkedAt') account[k] = copy.account[k]; });
392
+ copy.account = account;
393
+ }
394
+ return JSON.stringify(copy);
395
+ }
396
+
397
+ // The draft a repaint must not destroy: the typed code (caret and focus included) and the refusal.
398
+ function captureSignInDraft(node) {
399
+ var code = node && typeof node.querySelector === 'function' ? node.querySelector('[data-sign-in-code]') : null;
400
+ var error = node && typeof node.querySelector === 'function' ? node.querySelector('[data-sign-in-error]') : null;
401
+ var doc = host || (typeof document !== 'undefined' ? document : null);
402
+ var draft = {
403
+ value: code && code.value !== undefined && code.value !== null ? String(code.value) : null,
404
+ selectionStart: code && typeof code.selectionStart === 'number' ? code.selectionStart : null,
405
+ selectionEnd: code && typeof code.selectionEnd === 'number' ? code.selectionEnd : null,
406
+ focused: !!(doc && doc.activeElement && doc.activeElement === code),
407
+ error: error ? String(error.textContent == null ? '' : error.textContent) : null,
408
+ };
409
+ // Fix round 3 (G): the carry (signInBlocksIn → adoptSignInBlocks) can only reach a block that is
410
+ // still on the page, and a More-page rebuild has already thrown that one away — so the same
411
+ // capture is kept in state as well, and the carry and the state can never disagree.
412
+ var key = node && typeof node.getAttribute === 'function'
413
+ ? String(node.getAttribute('data-machine-sign-in-block') || '')
414
+ : '';
415
+ if (key && code) state.signInDrafts[key] = draftFromSignInInput(code, draft.focused);
416
+ return draft;
417
+ }
418
+
419
+ // The draft of ONE code input, in the shape state.signInDrafts holds it.
420
+ function draftFromSignInInput(code, focused) {
421
+ return {
422
+ value: code && code.value !== undefined && code.value !== null ? String(code.value) : null,
423
+ // Guarded like captureSignInDraft: a fake or exotic node has no numeric caret at all.
424
+ selectionStart: code && typeof code.selectionStart === 'number' ? code.selectionStart : null,
425
+ selectionEnd: code && typeof code.selectionEnd === 'number' ? code.selectionEnd : null,
426
+ focused: !!focused,
427
+ };
428
+ }
429
+
430
+ function restoreSignInDraft(node, draft) {
431
+ if (!node || !draft || typeof node.querySelector !== 'function') return false;
432
+ var code = node.querySelector('[data-sign-in-code]');
433
+ if (code) {
434
+ if (draft.value !== null && code.value !== undefined) code.value = draft.value;
435
+ if (draft.focused) {
436
+ if (typeof code.focus === 'function') code.focus();
437
+ if (typeof code.setSelectionRange === 'function' && draft.selectionStart !== null && draft.selectionEnd !== null) {
438
+ code.setSelectionRange(draft.selectionStart, draft.selectionEnd);
439
+ }
440
+ }
441
+ }
442
+ // `undefined` is "this draft says nothing about the refusal" — a state draft carries no copy, and
443
+ // writing undefined into the node would print the word "undefined" in the block.
444
+ if (draft.error !== null && draft.error !== undefined) {
445
+ var error = node.querySelector('[data-sign-in-error]');
446
+ if (error) error.textContent = draft.error;
447
+ }
448
+ return true;
449
+ }
450
+
451
+ // F.2 (fix round 2): render() replaces the card's markup wholesale on every More-page render, so
452
+ // an open block would lose its draft (the value being typed, the caret, the focus, the refusal
453
+ // copy) and its renderKey (one wasted write on the next tick). Capture before the write, adopt the
454
+ // same-keyed new node after it. No timer: the phone's ONE bounded re-read is unaffected.
455
+ function signInBlocksIn(root) {
456
+ var out = [];
457
+ var walk = function (node) {
458
+ var kids = node && node.children;
459
+ if (!kids) return;
460
+ for (var i = 0; i < kids.length; i += 1) {
461
+ var child = kids[i];
462
+ var key = child && typeof child.getAttribute === 'function'
463
+ ? String(child.getAttribute('data-machine-sign-in-block') || '')
464
+ : '';
465
+ if (key) out.push({ key: key, draft: captureSignInDraft(child) });
466
+ walk(child);
467
+ }
468
+ };
469
+ walk(root);
470
+ return out;
471
+ }
472
+
473
+ function adoptSignInBlocks(root, carried, adoptedKeys) {
474
+ if (!carried || !carried.length) return 0;
475
+ var byKey = {};
476
+ carried.forEach(function (item) { byKey[item.key] = item; });
477
+ var adopted = 0;
478
+ var walk = function (node) {
479
+ var kids = node && node.children;
480
+ if (!kids) return;
481
+ for (var i = 0; i < kids.length; i += 1) {
482
+ var child = kids[i];
483
+ var key = child && typeof child.getAttribute === 'function'
484
+ ? String(child.getAttribute('data-machine-sign-in-block') || '')
485
+ : '';
486
+ var item = key ? byKey[key] : null;
487
+ if (item) {
488
+ restoreSignInDraft(child, item.draft);
489
+ // The content was just painted from state.signIn[key], so the next identical tick must
490
+ // write nothing.
491
+ if (child.dataset && state.signIn[key] !== undefined) {
492
+ child.dataset.renderKey = signInRenderKey(state.signIn[key]);
493
+ }
494
+ if (adoptedKeys) adoptedKeys[key] = true;
495
+ adopted += 1;
496
+ }
497
+ walk(child);
498
+ }
499
+ };
500
+ walk(root);
501
+ return adopted;
502
+ }
503
+
504
+ // Fix round 3 (G): the More page rebuilds #more-content wholesale and only THEN calls render(),
505
+ // so the block that was on the page is gone before the carry can look at it — the draft the code
506
+ // box reported into state.signInDrafts is the only record left. Every block the new card paints
507
+ // gets that draft back here; a block that WAS carried keeps its own (never overwritten).
508
+ function restoreSignInDraftsFromState(card, adoptedKeys) {
509
+ var restored = 0;
510
+ var walk = function (node) {
511
+ var kids = node && node.children;
512
+ if (!kids) return;
513
+ for (var i = 0; i < kids.length; i += 1) {
514
+ var child = kids[i];
515
+ var key = child && typeof child.getAttribute === 'function'
516
+ ? String(child.getAttribute('data-machine-sign-in-block') || '')
517
+ : '';
518
+ if (key && !(adoptedKeys && adoptedKeys[key]) && state.signInDrafts[key]) {
519
+ restoreSignInDraft(child, state.signInDrafts[key]);
520
+ restored += 1;
521
+ }
522
+ walk(child);
523
+ }
524
+ };
525
+ walk(card);
526
+ return restored;
527
+ }
528
+
529
+ // Fill ONE machine's block from a machine-scoped login-state payload, and remember the entry so
530
+ // the next page render paints the same flow. Gated on the fingerprint (see signInRenderKey), so a
531
+ // checkedAt-only tick leaves the DOM — and the code being typed into it — untouched.
532
+ // false = the block is not on the page (the caller stops that machine's re-read).
533
+ function renderSignInBlock(instanceId, agent, payload) {
534
+ var id = normId(instanceId);
535
+ var key = signInKey(id, agent);
536
+ var entry = ((payload && payload.providers) || {})[providerIdForAgent(agent)] || null;
537
+ state.signIn[key] = entry;
538
+ var node = signInBlockNode(id, agent);
539
+ if (!node) return false;
540
+ var nextKey = signInRenderKey(entry);
541
+ if (node.dataset && node.dataset.renderKey === nextKey) return true;
542
+ var draft = captureSignInDraft(node);
543
+ node.innerHTML = signInFlowHtml(agent, entry);
544
+ restoreSignInDraft(node, draft);
545
+ if (node.dataset) node.dataset.renderKey = nextKey;
546
+ // Fix round 3 (G): a closed entry has no box to type into, so the draft kept for it is stale —
547
+ // dropped AFTER the capture above, which would otherwise just have written it back.
548
+ if (!signInEntryOpen(entry)) delete state.signInDrafts[key];
549
+ return true;
550
+ }
551
+
552
+ // ── the ONE bounded re-read (fix C) ─────────────────────────────────────────────────────────
553
+ // One handle per '<id>-<agent>', never two. It stops when the flow is no longer open (then it
554
+ // re-reads the page once, silently), when the block has left the page, or at the tick cap.
555
+ function stopSignInPoll(key) {
556
+ var handle = state.signInTimers[key];
557
+ if (!handle) return false;
558
+ if (handle.id != null) timers.clear(handle.id);
559
+ delete state.signInTimers[key];
560
+ return true;
561
+ }
562
+
563
+ function stopSignInPolls() {
564
+ Object.keys(state.signInTimers).forEach(stopSignInPoll);
565
+ return true;
566
+ }
567
+
568
+ function armSignInPoll(key, id, agent) {
569
+ var handle = state.signInTimers[key];
570
+ if (!handle) return false;
571
+ handle.id = timers.set(function () {
572
+ handle.id = null;
573
+ if (!state.signInTimers[key]) return; // stopped while we waited
574
+ handle.ticks += 1;
575
+ if (handle.ticks > SIGN_IN_POLL_MAX_TICKS) { stopSignInPoll(key); return; }
576
+ readSignInState(id, agent).then(function () {
577
+ if (!state.signInTimers[key]) return;
578
+ if (!signInEntryOpen(state.signIn[key])) { stopSignInPoll(key); load({ silent: true }); return; }
579
+ armSignInPoll(key, id, agent);
580
+ });
581
+ }, SIGN_IN_POLL_MS);
582
+ return true;
583
+ }
584
+
585
+ function startSignInPoll(instanceId, agent) {
586
+ var id = normId(instanceId);
587
+ if (!id) return false;
588
+ var key = signInKey(id, agent);
589
+ if (state.signInTimers[key]) return false; // one handle per machine+agent
590
+ state.signInTimers[key] = { id: null, ticks: 0 };
591
+ return armSignInPoll(key, id, agent);
592
+ }
593
+
594
+ function readSignInState(instanceId, agent) {
595
+ var id = normId(instanceId);
596
+ return api('/api/providers/login-state?machine=' + encodeURIComponent(id))
597
+ .then(function (payload) {
598
+ // No block on the page any more: the row is gone, so is the reason to re-read.
599
+ if (!renderSignInBlock(id, agent, payload)) stopSignInPoll(signInKey(id, agent));
600
+ return payload;
601
+ })
602
+ .catch(function () {
603
+ // HANDLE: a box that does not answer leaves the block empty; the button stays tappable.
604
+ return null;
605
+ });
606
+ }
607
+
608
+ // The server's refusal id, read the way serverError reads it.
609
+ function errorIdOf(failure) {
610
+ var payload = failure && failure.payload;
611
+ if (payload && typeof payload === 'object') {
612
+ if (payload.error) return String(payload.error);
613
+ if (payload.code) return String(payload.code);
614
+ }
615
+ return String((failure && (failure.code || failure.message)) || '');
616
+ }
617
+
618
+ // The click: POST the box's login route through the proxy, read that box's state, then keep
619
+ // re-reading until the flow closes (see the block's header).
620
+ function machineSignIn(instanceId, agent, btn) {
621
+ var id = normId(instanceId);
622
+ if (!id) return Promise.resolve(false);
623
+ if (btn) { btn.disabled = true; btn.textContent = 'Opening…'; }
624
+ return api('/api/providers/' + providerIdForAgent(agent) + '/login?machine=' + encodeURIComponent(id), { method: 'POST', body: {} })
625
+ .then(function () { return readSignInState(id, agent); })
626
+ .then(function (payload) { startSignInPoll(id, agent); return payload; })
627
+ .catch(function (error) {
628
+ // A second tap while the box is already opening this flow is NOT an error: adopt the flow
629
+ // the box already has and keep reading it (the alternative is a dead-end 409 on the row).
630
+ if (errorIdOf(error) === 'provider_login_already_running') {
631
+ return readSignInState(id, agent).then(function (payload) { startSignInPoll(id, agent); return payload; });
632
+ }
633
+ // REPORT: the server's own code is the message; the button is restored for a retry.
634
+ if (btn) { btn.disabled = false; btn.textContent = 'Sign in'; }
635
+ setStatus(serverError(error));
636
+ return null;
637
+ });
638
+ }
639
+
640
+ // The code the user pasted goes to the flow's own tab on the box (one POST, one meaning), then
641
+ // the block re-reads that box's state and keeps re-reading while the flow is open.
642
+ function submitSignInCode(instanceId, agent, node) {
643
+ var id = normId(instanceId);
644
+ var block = node && typeof node.closest === 'function' ? node.closest('[data-machine-sign-in-block]') : null;
645
+ var input = node && node.hasAttribute && node.hasAttribute('data-sign-in-code')
646
+ ? node
647
+ : (block && typeof block.querySelector === 'function' ? block.querySelector('[data-sign-in-code]') : null);
648
+ var error = block && typeof block.querySelector === 'function' ? block.querySelector('[data-sign-in-error]') : null;
649
+ var submitBtn = node && node.hasAttribute && node.hasAttribute('data-sign-in-code-submit')
650
+ ? node
651
+ : (block && typeof block.querySelector === 'function' ? block.querySelector('[data-sign-in-code-submit]') : null);
652
+ if (!id || !input) return Promise.resolve(false);
653
+ return api('/api/providers/' + providerIdForAgent(agent) + '/login/code?machine=' + encodeURIComponent(id), {
654
+ method: 'POST',
655
+ body: { code: String(input.value || '') },
656
+ })
657
+ .then(function () {
658
+ if (error) error.textContent = '';
659
+ input.value = '';
660
+ // The code is the box's now: the draft is spent, so a later re-render must not paint it back
661
+ // into an empty box. A refused send never reaches here — its typed code stays (see catch).
662
+ delete state.signInDrafts[signInKey(id, agent)];
663
+ setStatus('Code sent to the sign-in tab');
664
+ return readSignInState(id, agent).then(function (payload) { startSignInPoll(id, agent); return payload; });
665
+ })
666
+ .catch(function (failure) {
667
+ // REPORT: the server classifies its own refusal — shown in the card's own words, with the
668
+ // typed code kept and the button re-armed, so the user can fix it and send again.
669
+ if (error) error.textContent = signInErrorText(errorIdOf(failure) || 'unknown_error');
670
+ if (submitBtn) submitBtn.disabled = false;
671
+ return null;
672
+ });
673
+ }
674
+
675
+ // WP-3 (§8, docs/fleet-auto-update-pooled-machines-2026-10-26.html): the box's update status line
676
+ // — the phone twin of the desktop card's "mpc-fleet". The rule is the shared machineFleetLine;
677
+ // this is only its markup, so PHONE = DESKTOP PARITY holds byte for byte.
678
+ function fleetHtml(machine) {
679
+ var fleet = machineFleetLine(machine);
680
+ if (!fleet) return '';
681
+ return '<div class="mp-fleet" data-fleet-tone="' + esc(fleet.tone) + '" title="' + esc(fleet.title) + '">' + esc(fleet.text) + '</div>';
682
+ }
683
+
684
+ function machineCardHtml(machine, history) {
685
+ var m = machine || {};
686
+ var id = normId(m.instanceId);
687
+ var health = String(m.health || 'unknown').toLowerCase();
688
+ var sample = (m.stats && m.stats.sample) || null;
689
+ var statsAt = Number((m.stats && m.stats.checkedAt) || 0);
690
+ var stale = !statsAt || (at() - statsAt) > MACHINE_STATS_STALE_MS;
691
+ var cpuSeries = machineCpuSeries(history);
692
+ var memSeries = machineMemSeries(history);
693
+ var cpuNow = sample && sample.cpuPct != null ? Number(sample.cpuPct) : machineLastValue(cpuSeries);
694
+ var memNow = machineLastValue(memSeries);
695
+ var badge = credentialBadge(m, false);
696
+ var staleNote = stale && (sample || statsAt)
697
+ ? '<span class="mp-stale" title="The hub re-reads a box at most once a minute; this sample is older than three">'
698
+ + esc(STALE_NOTE) + '</span>'
699
+ : '';
700
+ var meta = [machineHealthLabel(m), machineLastSeenText(m), [m.os, m.arch].filter(Boolean).join('/')]
701
+ .filter(Boolean).join(' · ');
702
+ return '<article class="mp-card" data-machine-id="' + esc(id) + '" data-health="' + esc(health) + '">'
703
+ + '<div class="mp-head">'
704
+ + '<span class="mp-dot" data-health="' + esc(health) + '" title="' + esc(health) + '"></span>'
705
+ + '<input class="mp-name" type="text" maxlength="80" spellcheck="false" value="' + esc(machineName(m)) + '"'
706
+ + ' title="' + esc(machineName(m)) + '" aria-label="Machine name" data-machine-rename="' + esc(id) + '">'
707
+ + '<span class="mp-cred" data-cred="' + esc(badge.cred) + '" title="' + esc(badge.title) + '">' + esc(badge.text) + '</span>'
708
+ + '</div>'
709
+ + '<div class="mp-meta">' + esc(meta) + ' ' + staleNote + '</div>'
710
+ + fleetHtml(m)
711
+ + sparkBlock(cpuSeries, memSeries, cpuNow, memNow)
712
+ + '<div class="mp-facts">' + machineFactsHtml(sample, m.agents, history, m.agentPresence) + '</div>'
713
+ + signInBlockHtml(m)
714
+ + skillsRowHtml(m)
715
+ + modelsRowHtml(m)
716
+ + '<div class="mp-actions">'
717
+ + '<button type="button" class="mp-btn mp-fav" aria-pressed="' + (m.favorite ? 'true' : 'false') + '"'
718
+ + ' title="Favorite — shown first in the picker"'
719
+ + ' data-machine-toggle="' + esc(id) + '" data-machine-field="favorite">'
720
+ + esc(m.favorite ? '★ Favorite' : '☆ Favorite') + '</button>'
721
+ + '<button type="button" class="mp-btn" data-machine-new-session="' + esc(id) + '">New session here</button>'
722
+ + '<button type="button" class="mp-btn" data-machine-toggle="' + esc(id) + '" data-machine-field="hidden">'
723
+ + esc(m.hidden ? 'Show in picker' : 'Hide from picker') + '</button>'
724
+ + (badge.action === 'join-pool'
725
+ ? '<button type="button" class="mp-btn" data-machine-join="' + esc(id) + '"'
726
+ + ' data-machine-url="' + esc(m.url || '') + '">Join pool</button>'
727
+ : '')
728
+ + '<button type="button" class="mp-btn mp-danger" data-machine-remove="' + esc(id) + '">Remove</button>'
729
+ + '</div>'
730
+ + signInRowHtml(m)
731
+ + '</article>';
732
+ }
733
+
734
+ // This Mac is not a registry row: its settings are /api/machine-self and its numbers are its own
735
+ // host samples. Same three settings writes as a member, with an empty machine id.
736
+ function selfCardHtml() {
737
+ var settings = state.selfSettings || {};
738
+ var latest = (state.self && state.self.latest) || null;
739
+ var history = state.self || null;
740
+ var id = normId(state.selfInstanceId);
741
+ var name = String(settings.displayName || 'This Mac');
742
+ var cpuSeries = machineCpuSeries(history);
743
+ var memSeries = machineMemSeries(history);
744
+ var cpuNow = latest && latest.cpuPct != null ? Number(latest.cpuPct) : machineLastValue(cpuSeries);
745
+ var memNow = machineLastValue(memSeries);
746
+ var poolNote = state.machines.length
747
+ ? 'In a pool with ' + state.machines.length + ' other machine' + (state.machines.length === 1 ? '' : 's')
748
+ : 'Not in a pool yet';
749
+ var facts = machineFactsHtml(latest, null, history)
750
+ + factRow('Memory', latest ? esc(machineFormatBytes(latest.memUsedBytes) + ' / ' + machineFormatBytes(latest.memTotalBytes)) : '');
751
+ return '<article class="mp-card mp-self" data-machine-id="' + esc(id) + '" data-health="reachable">'
752
+ + '<div class="mp-head">'
753
+ + '<span class="mp-dot" data-health="reachable" title="reachable"></span>'
754
+ + '<input class="mp-name" type="text" maxlength="80" spellcheck="false" value="' + esc(name) + '"'
755
+ + ' title="' + esc(name) + '" aria-label="This Mac name" data-machine-rename="">'
756
+ + '<span class="mp-cred" data-cred="self" title="This machine runs its own sessions">This Mac</span>'
757
+ + '</div>'
758
+ + '<div class="mp-meta">' + esc(poolNote + (id ? ' · ' + id : '')) + '</div>'
759
+ + sparkBlock(cpuSeries, memSeries, cpuNow, memNow)
760
+ + '<div class="mp-facts">' + facts + '</div>'
761
+ + '<div class="mp-actions">'
762
+ + '<button type="button" class="mp-btn mp-fav" aria-pressed="' + (settings.favorite ? 'true' : 'false') + '"'
763
+ + ' title="Favorite — shown first in the picker" data-machine-toggle="" data-machine-field="favorite">'
764
+ + esc(settings.favorite ? '★ Favorite' : '☆ Favorite') + '</button>'
765
+ + '<button type="button" class="mp-btn" data-machine-new-session="">New session here</button>'
766
+ + '<button type="button" class="mp-btn" data-machine-toggle="" data-machine-field="hidden">'
767
+ + esc(settings.hidden ? 'Show in picker' : 'Hide from picker') + '</button>'
768
+ + '</div>'
769
+ + '</article>';
770
+ }
771
+
772
+ function headerHtml() {
773
+ var inPool = state.machines.length > 0;
774
+ return '<div class="more-section-head">'
775
+ + '<span>'
776
+ + '<span class="more-kicker">Machines</span>'
777
+ + '<h3 id="phone-machines-title">Other CTM instances</h3>'
778
+ + '</span>'
779
+ + '<button class="row-action" type="button" data-machines-refresh>Refresh</button>'
780
+ + '</div>'
781
+ + '<div class="mp-toolbar">'
782
+ + '<button type="button" class="mp-btn mp-primary" data-machine-add>+ Add machine</button>'
783
+ + '<button type="button" class="mp-btn" data-machine-show-code>Show a code for this machine</button>'
784
+ + (inPool ? '<button type="button" class="mp-btn mp-danger" data-pool-leave>Leave pool</button>' : '')
785
+ + '</div>';
786
+ }
787
+
788
+ function listHtml() {
789
+ if (state.error) return '<p class="mp-error" role="status">' + esc(state.error) + '</p>';
790
+ var cards = selfCardHtml();
791
+ var rows = state.machines.map(function (machine) {
792
+ return machineCardHtml(machine, state.histories[normId(machine && machine.instanceId)]);
793
+ }).join('');
794
+ var note = rows ? '' : '<p class="more-section-copy">No other machines registered yet — use “Add machine”.</p>';
795
+ return cards + rows + note;
796
+ }
797
+
798
+ function bodyHtml() {
799
+ return headerHtml() + '<div class="mp-body">' + listHtml() + '</div>';
800
+ }
801
+
802
+ // Renders inside #more-content. more.js rewrites that container on every More render, so app.js
803
+ // calls this again after each of its renders and the section node is reused (never duplicated).
804
+ function render(container) {
805
+ var target = container || $('more-content');
806
+ if (!target) return null;
807
+ var card = typeof target.querySelector === 'function' ? target.querySelector('#phone-machines-card') : null;
808
+ var doc = host || (typeof document !== 'undefined' ? document : null);
809
+ if (!card) {
810
+ if (!doc || typeof doc.createElement !== 'function') return null;
811
+ card = doc.createElement('section');
812
+ card.id = 'phone-machines-card';
813
+ card.className = 'more-section-card';
814
+ card.setAttribute('aria-labelledby', 'phone-machines-title');
815
+ target.appendChild(card);
816
+ }
817
+ var carried = signInBlocksIn(card);
818
+ card.innerHTML = bodyHtml() + '<p class="mp-status" id="' + STATUS_ID + '" role="status" aria-live="polite"></p>';
819
+ var adoptedKeys = {};
820
+ adoptSignInBlocks(card, carried, adoptedKeys);
821
+ // The carried blocks are already whole; a block the write just REBUILT (a More-page render threw
822
+ // the old card away) has only state.signInDrafts to be painted from.
823
+ restoreSignInDraftsFromState(card, adoptedKeys);
824
+ bind(card);
825
+ if (afterRender) {
826
+ try { afterRender(card); } catch (error) { if (typeof console !== 'undefined') console.warn('[phone-machines] afterRender failed', error); }
827
+ }
828
+ return card;
829
+ }
830
+
831
+ function setStatus(text) {
832
+ var message = String(text || '');
833
+ var node = $(STATUS_ID);
834
+ if (node) node.textContent = message;
835
+ if (message) toast(message);
836
+ return message;
837
+ }
838
+
839
+ // The server's error code is shown VERBATIM (machine_identity_mismatch, machine_unreachable, …).
840
+ function serverError(err) {
841
+ var payload = err && err.payload;
842
+ if (payload && typeof payload === 'object') {
843
+ if (payload.error) return String(payload.error);
844
+ if (payload.code) return String(payload.code);
845
+ }
846
+ return (err && (err.code || err.message)) || 'machine_request_failed';
847
+ }
848
+
849
+ // ── loading ────────────────────────────────────────────────────────────────────────────────
850
+ function historyQuery(machineId, now) {
851
+ return 'since=' + Math.trunc(Math.max(0, Number(now) - HISTORY_MS))
852
+ + (machineId ? '&machine=' + encodeURIComponent(machineId) : '');
853
+ }
854
+
855
+ function loadHistories(now) {
856
+ var reachable = state.machines.filter(function (machine) {
857
+ return machine && machine.instanceId && String(machine.health || '').toLowerCase() === 'reachable';
858
+ });
859
+ return Promise.all(reachable.map(function (machine) {
860
+ var id = normId(machine.instanceId);
861
+ return api('/api/host/stats?' + historyQuery(id, now))
862
+ .then(function (payload) { state.histories[id] = payload || null; return null; })
863
+ .catch(function () {
864
+ // HANDLE: one box's sparkline is decoration; its card still renders from the hub's sample.
865
+ state.histories[id] = null;
866
+ return null;
867
+ });
868
+ }));
869
+ }
870
+
871
+ // P-7: after every successful /api/machines load, remember who this phone has reached. `self` is
872
+ // this origin; members keep whatever lastSeenAt they already had.
873
+ function rememberOrigins() {
874
+ var failover = failoverModule();
875
+ if (!failover || typeof failover.rememberPoolOrigins !== 'function') return null;
876
+ var selfOrigin = String((root.location && root.location.origin) || '');
877
+ var self = {
878
+ instanceId: state.selfInstanceId,
879
+ displayName: (state.selfSettings && state.selfSettings.displayName) || '',
880
+ urls: (state.self && state.self.urls) || (selfOrigin ? [selfOrigin] : []),
881
+ health: 'reachable',
882
+ };
883
+ try {
884
+ return failover.rememberPoolOrigins(state.machines, self, { storage: d.storage, now: clock });
885
+ } catch (error) {
886
+ return null; // HANDLE: a blocked dial cache must never break the Machines page
887
+ }
888
+ }
889
+
890
+ function load(options) {
891
+ var opts = options || {};
892
+ if (!opts.silent) state.loading = true;
893
+ var now = at();
894
+ return Promise.all([
895
+ api('/api/machines'),
896
+ api('/api/machine-self'),
897
+ api('/api/host/stats?' + historyQuery('', now)),
898
+ ]).then(function (results) {
899
+ var list = results[0] || {};
900
+ var self = results[1] || {};
901
+ state.loading = false;
902
+ state.error = '';
903
+ state.machines = Array.isArray(list.machines) ? list.machines.filter(function (m) { return m && m.instanceId; }) : [];
904
+ state.selfSettings = self.self || null;
905
+ state.self = results[2] || null;
906
+ state.selfInstanceId = normId((state.self && state.self.instanceId) || self.instanceId);
907
+ state.loaded = true;
908
+ rememberOrigins();
909
+ state.origins = poolOrigins();
910
+ return loadHistories(now).then(function () {
911
+ render();
912
+ return true;
913
+ });
914
+ }).catch(function (error) {
915
+ // REPORT: a transport failure is shown on the page, never swallowed into an empty list.
916
+ state.loading = false;
917
+ state.error = 'machines_unavailable: ' + ((error && error.message) || 'fetch failed');
918
+ render();
919
+ return false;
920
+ });
921
+ }
922
+
923
+ // ── writes (machine-owned settings, D-4 rev 2) ─────────────────────────────────────────────
924
+ function selfPath(instanceId) {
925
+ var id = normId(instanceId);
926
+ return '/api/machine-self' + (id ? '?machine=' + encodeURIComponent(id) : '');
927
+ }
928
+
929
+ function applySelfPatch(instanceId, patch) {
930
+ return api(selfPath(instanceId), { method: 'POST', body: patch || {} })
931
+ .then(function () { setStatus(''); return load({ silent: true }); })
932
+ .catch(function (error) {
933
+ // REPORT: the server's refusal (or the transport fault) is what the user needs to see.
934
+ setStatus(serverError(error));
935
+ return false;
936
+ });
937
+ }
938
+
939
+ function toggle(instanceId, field) {
940
+ if (field !== 'favorite' && field !== 'hidden') return Promise.resolve(false);
941
+ var id = normId(instanceId);
942
+ var current = id
943
+ ? state.machines.filter(function (m) { return normId(m && m.instanceId) === id; })[0]
944
+ : state.selfSettings;
945
+ var patch = {};
946
+ patch[field] = (current && (current[field] === 1 || current[field] === true)) ? 0 : 1;
947
+ return applySelfPatch(id, patch);
948
+ }
949
+
950
+ function rename(instanceId, value) {
951
+ var name = String(value == null ? '' : value).trim();
952
+ if (name.length > 80) {
953
+ setStatus('display_name_too_long');
954
+ return Promise.resolve(false);
955
+ }
956
+ return applySelfPatch(normId(instanceId), { displayName: name });
957
+ }
958
+
959
+ function removeMachine(instanceId) {
960
+ var id = normId(instanceId);
961
+ if (!id) return Promise.resolve(false);
962
+ var machine = state.machines.filter(function (row) { return normId(row && row.instanceId) === id; })[0];
963
+ var name = machineName(machine) || id;
964
+ if (confirmImpl && !confirmImpl('Remove ' + name + ' from this machine?')) return Promise.resolve(false);
965
+ return api('/api/machines/' + encodeURIComponent(id) + '/remove', { method: 'POST' })
966
+ .then(function (data) {
967
+ var payload = data || {};
968
+ var rotated = Array.isArray(payload.rotated) ? payload.rotated : [];
969
+ var unreachable = Array.isArray(payload.unreachable) ? payload.unreachable : [];
970
+ var note = 'Removed ' + name + '.';
971
+ if (rotated.length) note += ' Re-keyed: ' + rotated.join(', ') + '.';
972
+ if (unreachable.length) note += ' Unreachable (re-pair after the grace): ' + unreachable.join(', ') + '.';
973
+ setStatus(note);
974
+ return load({ silent: true });
975
+ })
976
+ .catch(function (error) {
977
+ // REPORT: a failed removal must never look like a successful one.
978
+ setStatus(serverError(error));
979
+ return false;
980
+ });
981
+ }
982
+
983
+ function leavePool() {
984
+ if (confirmImpl && !confirmImpl('Leave the machine pool? This phone drops the shared secret and its machine list.')) {
985
+ return Promise.resolve(false);
986
+ }
987
+ return api('/api/pool/leave', { method: 'POST', body: {} })
988
+ .then(function (data) {
989
+ setStatus('Left the pool (' + (Number((data || {}).removed) || 0) + ' rows dropped).');
990
+ return load({ silent: true });
991
+ })
992
+ .catch(function (error) {
993
+ // REPORT: an unreachable hub must not read as "you left the pool".
994
+ setStatus(serverError(error));
995
+ return false;
996
+ });
997
+ }
998
+
999
+ // "New session here": the EXISTING new-session sheet with this machine selected. app.js owns that
1000
+ // modal, so there is no second create path.
1001
+ function newSessionHere(instanceId) {
1002
+ return Promise.resolve(openNewSession(normId(instanceId)));
1003
+ }
1004
+
1005
+ function signInToo(origin) {
1006
+ var target = String(origin || '').trim().replace(/\/+$/, '');
1007
+ if (!/^https:\/\/[^/?#]+$/i.test(target)) {
1008
+ setStatus('sign_in_origin_unavailable');
1009
+ return false;
1010
+ }
1011
+ if (root.location && typeof root.location.assign === 'function') {
1012
+ root.location.assign(target + '/m/');
1013
+ return true;
1014
+ }
1015
+ return false;
1016
+ }
1017
+
1018
+ // ── dialogs (Add machine, Show a code) ─────────────────────────────────────────────────────
1019
+ function mountDialog(html) {
1020
+ var doc = host || (typeof document !== 'undefined' ? document : null);
1021
+ if (!doc || typeof doc.createElement !== 'function') return null;
1022
+ var overlay = doc.createElement('div');
1023
+ overlay.className = 'modal-backdrop';
1024
+ overlay.innerHTML = html;
1025
+ var parent = doc.body || doc.documentElement;
1026
+ if (parent && typeof parent.appendChild === 'function') parent.appendChild(overlay);
1027
+ return overlay;
1028
+ }
1029
+
1030
+ function closeDialog() {
1031
+ if (dialog && dialog.overlay && dialog.overlay.parentNode) dialog.overlay.parentNode.removeChild(dialog.overlay);
1032
+ dialog = null;
1033
+ return true;
1034
+ }
1035
+
1036
+ function previewFields() {
1037
+ return machinesAddPreview(
1038
+ dialog && dialog.pairInput ? dialog.pairInput.value : '',
1039
+ dialog && dialog.codeInput ? dialog.codeInput.value : '',
1040
+ dialog && dialog.urlInput ? dialog.urlInput.value : '',
1041
+ );
1042
+ }
1043
+
1044
+ // RFC 8628 §5.4 parity with the desktop dialog: the box's OWN name is shown BEFORE Confirm is
1045
+ // enabled, and a string that does not decode is refused with the server's own wording.
1046
+ function paintPreview() {
1047
+ if (!dialog) return null;
1048
+ var preview = previewFields();
1049
+ if (dialog.preview) {
1050
+ var text = '';
1051
+ if (preview.message === 'pairing_string_invalid') text = 'pairing_string_invalid — that is not a ctmpair1: string.';
1052
+ else if (preview.message === 'pairing_code_invalid') text = 'pairing_code_invalid — a code is 8 letters (XXXX-XXXX).';
1053
+ else if (preview.message === 'address_required') text = 'Enter the machine address (https://host:port).';
1054
+ else if (preview.ok && preview.name) text = 'Box: ' + preview.name + (preview.url ? ' — ' + preview.url : '');
1055
+ else if (preview.ok) text = 'Box: ' + preview.url;
1056
+ else if (preview.addressOnly) text = preview.url + ' — the name appears after pairing';
1057
+ dialog.preview.textContent = text;
1058
+ }
1059
+ if (dialog.confirm) dialog.confirm.disabled = !preview.ok;
1060
+ return preview;
1061
+ }
1062
+
1063
+ function openAddDialog(options) {
1064
+ var opts = options || {};
1065
+ closeDialog();
1066
+ var overlay = mountDialog('<section class="model-picker-sheet mp-modal" role="dialog" aria-modal="true" aria-labelledby="mp-add-title">'
1067
+ + '<div class="model-picker-head"><div><h2 id="mp-add-title">Add a machine</h2>'
1068
+ + '<p>Paste a pairing string from the other machine, or type its code and address.</p></div>'
1069
+ + '<button type="button" class="model-picker-close" id="mp-add-cancel" aria-label="Close">&times;</button></div>'
1070
+ + '<label class="mp-modal-label" for="mp-pair-input">Pairing string</label>'
1071
+ + '<textarea id="mp-pair-input" class="mp-modal-input" rows="3" spellcheck="false" placeholder="ctmpair1:…"></textarea>'
1072
+ + '<div id="mp-pair-preview" class="mp-pair-preview" role="status" aria-live="polite"></div>'
1073
+ + '<p class="mp-modal-or">or</p>'
1074
+ + '<input id="mp-pair-code" class="mp-modal-input" type="text" autocomplete="off" spellcheck="false" placeholder="XXXX-XXXX" aria-label="Pairing code">'
1075
+ + '<input id="mp-pair-url" class="mp-modal-input" type="text" autocapitalize="off" autocomplete="off" spellcheck="false" placeholder="https://spark.r.walle.sh" aria-label="Machine address">'
1076
+ + '<p id="mp-add-error" class="mp-modal-error" role="status" aria-live="polite"></p>'
1077
+ + '<div class="mp-modal-actions">'
1078
+ + '<button type="button" class="mp-btn mp-primary" id="mp-add-confirm" disabled>Add machine</button>'
1079
+ + '</div>'
1080
+ + '<details class="mp-modal-legacy"><summary>Register by URL (legacy)</summary>'
1081
+ + '<p class="mp-modal-sub">An env-token box (P-8) that has not joined the pool. Its identity is pinned by the manifest it answers.</p>'
1082
+ + '<input id="mp-legacy-url" class="mp-modal-input" type="text" autocomplete="off" spellcheck="false" placeholder="http://192.168.1.20:3456" aria-label="Legacy machine URL">'
1083
+ + '<input id="mp-legacy-name" class="mp-modal-input" type="text" autocomplete="off" spellcheck="false" placeholder="Display name" aria-label="Legacy machine display name">'
1084
+ + '<div class="mp-modal-actions"><button type="button" class="mp-btn" id="mp-legacy-submit">Register by URL</button></div>'
1085
+ + '</details></section>');
1086
+ if (!overlay) return null;
1087
+ dialog = {
1088
+ overlay: overlay,
1089
+ pairInput: $('mp-pair-input'),
1090
+ codeInput: $('mp-pair-code'),
1091
+ urlInput: $('mp-pair-url'),
1092
+ preview: $('mp-pair-preview'),
1093
+ error: $('mp-add-error'),
1094
+ confirm: $('mp-add-confirm'),
1095
+ };
1096
+ // P-8: "Join pool" on an env-token row opens this dialog with the row's address already filled.
1097
+ if (opts.url && dialog.urlInput) dialog.urlInput.value = String(opts.url);
1098
+ ['pairInput', 'codeInput', 'urlInput'].forEach(function (key) {
1099
+ var node = dialog[key];
1100
+ if (node && typeof node.addEventListener === 'function') node.addEventListener('input', paintPreview);
1101
+ });
1102
+ var cancel = $('mp-add-cancel');
1103
+ if (cancel && typeof cancel.addEventListener === 'function') cancel.addEventListener('click', closeDialog);
1104
+ if (dialog.confirm && typeof dialog.confirm.addEventListener === 'function') dialog.confirm.addEventListener('click', submitAddDialog);
1105
+ var legacy = $('mp-legacy-submit');
1106
+ if (legacy && typeof legacy.addEventListener === 'function') legacy.addEventListener('click', registerLegacy);
1107
+ paintPreview();
1108
+ return dialog;
1109
+ }
1110
+
1111
+ function submitAddDialog() {
1112
+ if (!dialog) return Promise.resolve(false);
1113
+ var preview = previewFields();
1114
+ if (!preview.ok) return Promise.resolve(false);
1115
+ var body = preview.mode === 'string'
1116
+ ? { pairingString: preview.pairingString }
1117
+ : { code: machinesFormatCode(preview.code), url: preview.url };
1118
+ if (dialog.error) dialog.error.textContent = '';
1119
+ return api('/api/machines/pair', { method: 'POST', body: body })
1120
+ .then(function () { closeDialog(); setStatus('Machine added.'); return load({ silent: true }); })
1121
+ .catch(function (error) {
1122
+ // REPORT: the server's code is relayed VERBATIM and the dialog stays open for a retry.
1123
+ if (dialog && dialog.error) dialog.error.textContent = serverError(error);
1124
+ return false;
1125
+ });
1126
+ }
1127
+
1128
+ function registerLegacy() {
1129
+ var urlNode = $('mp-legacy-url');
1130
+ var nameNode = $('mp-legacy-name');
1131
+ var url = String((urlNode && urlNode.value) || '').trim();
1132
+ var displayName = String((nameNode && nameNode.value) || '').trim();
1133
+ var errorNode = (dialog && dialog.error) || $('mp-add-error');
1134
+ if (!url) {
1135
+ if (errorNode) errorNode.textContent = 'Enter the machine URL.';
1136
+ return Promise.resolve(false);
1137
+ }
1138
+ return api('/api/machines', { method: 'POST', body: { url: url, displayName: displayName } })
1139
+ .then(function () { closeDialog(); setStatus('Machine registered.'); return load({ silent: true }); })
1140
+ .catch(function (error) {
1141
+ // REPORT: the register refusal is shown verbatim in the dialog.
1142
+ if (errorNode) errorNode.textContent = serverError(error);
1143
+ return false;
1144
+ });
1145
+ }
1146
+
1147
+ function openShowCodeDialog() {
1148
+ closeDialog();
1149
+ var overlay = mountDialog('<section class="model-picker-sheet mp-modal" role="dialog" aria-modal="true" aria-labelledby="mp-code-title">'
1150
+ + '<div class="model-picker-head"><div><h2 id="mp-code-title">Show a code for this machine</h2>'
1151
+ + '<p>On the other machine, choose “Add machine” and paste this string — or type the code.</p></div>'
1152
+ + '<button type="button" class="model-picker-close" id="mp-code-close" aria-label="Close">&times;</button></div>'
1153
+ + '<div id="mp-code-value" class="mp-code-value" role="status" aria-live="polite">…</div>'
1154
+ + '<textarea id="mp-pair-string" class="mp-modal-input" rows="4" readonly spellcheck="false" aria-label="Pairing string"></textarea>'
1155
+ + '<p id="mp-code-error" class="mp-modal-error" role="status" aria-live="polite"></p>'
1156
+ + '</section>');
1157
+ if (!overlay) return null;
1158
+ dialog = { overlay: overlay, codeValue: $('mp-code-value'), pairString: $('mp-pair-string'), error: $('mp-code-error') };
1159
+ var close = $('mp-code-close');
1160
+ if (close && typeof close.addEventListener === 'function') close.addEventListener('click', closeDialog);
1161
+ return api('/api/machine-pairing/offers', { method: 'POST', body: {} })
1162
+ .then(function (data) {
1163
+ var payload = data || {};
1164
+ if (dialog && dialog.codeValue) dialog.codeValue.textContent = String(payload.code || '');
1165
+ if (dialog && dialog.pairString) dialog.pairString.value = String(payload.pairingString || '');
1166
+ return true;
1167
+ })
1168
+ .catch(function (error) {
1169
+ // REPORT: the offer failure stays in the dialog so the user can retry.
1170
+ if (dialog && dialog.error) dialog.error.textContent = serverError(error);
1171
+ return false;
1172
+ });
1173
+ }
1174
+
1175
+ // ── WP-9 (§9 S-9, AC-9a..AC-9d): the row's one Skills line and its Review sheet ─────────────
1176
+ // PHONE = DESKTOP PARITY: the same line, the same three sections, the same actions and the same
1177
+ // wording public/js/app/machines-page.js renders. AC-9c: a row with `skills: null` renders exactly
1178
+ // the card it rendered before this work — the line builder returns '' and nothing else moved.
1179
+ var SKILL_REASONS = {
1180
+ platform: 'macOS-only',
1181
+ hub_home_path: 'points at a path that exists only on this laptop',
1182
+ too_large: 'too large to send',
1183
+ unreadable: 'could not be read',
1184
+ };
1185
+ var SKILL_STATES = {
1186
+ new_on_box: 'new on this machine',
1187
+ same: 'same as the hub',
1188
+ shadows_hub: 'this machine’s copy wins',
1189
+ changed_on_box: 'changed on this machine',
1190
+ conflict: 'both sides changed',
1191
+ };
1192
+ var BOX_LOCAL_STATES = ['new_on_box', 'changed_on_box', 'conflict', 'shadows_hub'];
1193
+ var DIFF_LINE_CAP = 400;
1194
+ var review = { instanceId: '', skipped: {}, fromHub: null };
1195
+
1196
+ function skillsBlock(machine) {
1197
+ var block = machine && machine.skills;
1198
+ return block && typeof block === 'object' ? block : null;
1199
+ }
1200
+
1201
+ function skillReasonLabel(reason) {
1202
+ var key = String(reason || '');
1203
+ return SKILL_REASONS[key] || key || 'held back';
1204
+ }
1205
+
1206
+ function skillStateLabel(value) {
1207
+ var key = String(value || '');
1208
+ return SKILL_STATES[key] || key || 'differs';
1209
+ }
1210
+
1211
+ function skillsHeldBack(machine) {
1212
+ var block = skillsBlock(machine);
1213
+ return block && Array.isArray(block.heldBack) ? block.heldBack : [];
1214
+ }
1215
+
1216
+ function skillsOnBox(machine) {
1217
+ var block = skillsBlock(machine);
1218
+ return block && Array.isArray(block.onBox) ? block.onBox : [];
1219
+ }
1220
+
1221
+ function skillsPlugins(machine) {
1222
+ var block = skillsBlock(machine);
1223
+ return block && Array.isArray(block.plugins) ? block.plugins : [];
1224
+ }
1225
+
1226
+ function skillsExcluded(machine) {
1227
+ var block = skillsBlock(machine);
1228
+ return block && Array.isArray(block.excludePlugins) ? block.excludePlugins : [];
1229
+ }
1230
+
1231
+ function skillsBoxLocalCount(machine) {
1232
+ return skillsOnBox(machine).filter(function (item) {
1233
+ return item && BOX_LOCAL_STATES.indexOf(String(item.state)) !== -1;
1234
+ }).length;
1235
+ }
1236
+
1237
+ function skillsLineText(machine) {
1238
+ var block = skillsBlock(machine);
1239
+ if (!block) return '';
1240
+ var parts = [(Number(block.sentCount) || 0) + ' sent'];
1241
+ var heldBack = skillsHeldBack(machine).length;
1242
+ if (heldBack) parts.push(heldBack + ' held back');
1243
+ var local = skillsBoxLocalCount(machine);
1244
+ if (local) parts.push(local + ' new on ' + machineName(machine));
1245
+ var text = 'Skills: ' + parts.join(' · ');
1246
+ var error = block.pushError;
1247
+ if (error && (error.message || error.code)) text += ' · last push failed: ' + String(error.message || error.code);
1248
+ return text;
1249
+ }
1250
+
1251
+ function skillsNeedsReview(machine) {
1252
+ if (!skillsBlock(machine)) return false;
1253
+ if (skillsHeldBack(machine).length) return true;
1254
+ if (skillsBoxLocalCount(machine)) return true;
1255
+ var excluded = skillsExcluded(machine);
1256
+ return skillsPlugins(machine).some(function (plugin) { return excluded.indexOf(plugin) === -1; });
1257
+ }
1258
+
1259
+ function skillsRowHtml(machine) {
1260
+ if (!skillsBlock(machine)) return ''; // AC-9c: no block, no bytes
1261
+ var id = normId(machine && machine.instanceId);
1262
+ return '<div class="mp-skills"><span class="mp-skills-line">' + esc(skillsLineText(machine)) + '</span>'
1263
+ + (skillsNeedsReview(machine)
1264
+ ? '<button type="button" class="mp-btn mp-skills-review" data-machine-review="' + esc(id) + '">Review</button>'
1265
+ : '')
1266
+ + '</div>';
1267
+ }
1268
+
1269
+ // ── WP-3: the Models line + the Review sheet's Models section (§6.2, §8 WP-3) ────────────────
1270
+ // The phone twin of public/js/app/machines-page.js: the same states, the same strings, from the
1271
+ // same row block (lib/machine-registry.js `_modelsBlockFrom`, read on the 60 s probe tick).
1272
+ // `null` = never read, or a box that predates WP-1 — then this card renders exactly the bytes it
1273
+ // rendered before this work (AC-3a), so every builder below returns '' for null.
1274
+
1275
+ function modelsUnreachable(machine) {
1276
+ var block = modelsBlock(machine);
1277
+ return block && Array.isArray(block.unreachable) ? block.unreachable : [];
1278
+ }
1279
+
1280
+ function modelsUnreachableLabel(machine, item) {
1281
+ if (String((item && item.type) || '').toLowerCase() === 'ollama') return 'nothing is running there';
1282
+ return "can't be reached from " + machineName(machine);
1283
+ }
1284
+
1285
+ function modelsReachLabel(machine, reach) {
1286
+ return (String(reach || '') === 'reachable' ? 'can be reached from ' : "can't be reached from ")
1287
+ + machineName(machine);
1288
+ }
1289
+
1290
+ function modelsLineText(machine) {
1291
+ var block = modelsBlock(machine);
1292
+ if (!block) return '';
1293
+ var parts = [(Number(block.usable) || 0) + ' usable'];
1294
+ var unreachable = modelsUnreachable(machine).length;
1295
+ if (unreachable) parts.push(unreachable + " can't be reached from " + machineName(machine));
1296
+ return 'Models: ' + parts.join(' · ');
1297
+ }
1298
+
1299
+ function modelsNeedsReview(machine) {
1300
+ if (!modelsBlock(machine)) return false;
1301
+ if (modelsUnreachable(machine).length) return true;
1302
+ return modelsDelegateWarnings(machine).length > 0;
1303
+ }
1304
+
1305
+ function modelsRowHtml(machine) {
1306
+ if (!modelsBlock(machine)) return ''; // AC-3a: no block, no bytes
1307
+ var id = normId(machine && machine.instanceId);
1308
+ return '<div class="mp-models"><span class="mp-models-line">' + esc(modelsLineText(machine)) + '</span>'
1309
+ + (modelsNeedsReview(machine)
1310
+ ? '<span class="mp-models-sep">—</span>'
1311
+ + '<button type="button" class="mp-btn mp-models-review" data-machine-review="' + esc(id) + '">Review</button>'
1312
+ : '')
1313
+ + modelsDelegateWarnings(machine).map(function (text) {
1314
+ return '<span class="mp-models-warn">' + esc(text) + '</span>';
1315
+ }).join('')
1316
+ + '</div>';
1317
+ }
1318
+
1319
+ /** WP-9 title, extended by WP-3: the sheet names whichever of the two blocks the row has. */
1320
+ function reviewTitle(machine) {
1321
+ var name = machineName(machine);
1322
+ var hasSkills = !!skillsBlock(machine);
1323
+ var hasModels = !!modelsBlock(machine);
1324
+ if (hasSkills && hasModels) return 'Skills and models on ' + name;
1325
+ if (hasModels) return 'Models on ' + name;
1326
+ return 'Skills on ' + name;
1327
+ }
1328
+
1329
+ /** WP-3: a row is reviewable while EITHER block has something to show; an absent block renders
1330
+ * no section, so a models-only row opens a Models-only sheet. */
1331
+ function reviewOpenable(machine) {
1332
+ return !!skillsBlock(machine) || modelsNeedsReview(machine);
1333
+ }
1334
+
1335
+ /** WP-3 + WP-4 (desktop parity): the sheet's Models section. WP-4 puts ONE control on the
1336
+ * unreachable rows — a checkbox, because the route ids are the box's own — and the matching
1337
+ * "Remove from <name>" button under the list; never on the /delegate rows. Nothing unreachable →
1338
+ * no control at all, and the button stays disabled until a row is ticked. */
1339
+ function reviewModelsHtml(machine) {
1340
+ if (!modelsBlock(machine)) return ''; // no block, no section
1341
+ var name = machineName(machine);
1342
+ var unreachable = modelsUnreachable(machine);
1343
+ var rows = unreachable.map(function (item) {
1344
+ var id = String((item && item.id) || '');
1345
+ return '<li class="mp-review-item" data-route="' + esc(id) + '">'
1346
+ + '<label class="mp-review-pick"><input type="checkbox" data-role="models-pick" value="' + esc(id) + '"'
1347
+ + ' aria-label="' + esc('Remove ' + id + ' from ' + name) + '">'
1348
+ + '<span class="mp-review-name">' + esc(id) + '</span></label>'
1349
+ + '<span class="mp-review-meta">'
1350
+ + esc([String((item && item.type) || ''), String((item && item.origin) || '')].filter(Boolean).join(' · '))
1351
+ + '</span>'
1352
+ + '<span class="mp-review-where">' + esc(modelsUnreachableLabel(machine, item)) + '</span></li>';
1353
+ });
1354
+ if (!rows.length) rows.push('<li class="mp-review-empty">Every route on this machine can be reached.</li>');
1355
+ modelsDelegate(machine).forEach(function (entry) {
1356
+ var route = String((entry && entry.routeId) || '');
1357
+ var meta = [String((entry && entry.model) || ''), route ? 'route ' + route : ''].filter(Boolean).join(' · ');
1358
+ rows.push('<li class="mp-review-item" data-agent="' + esc(String((entry && entry.agent) || '')) + '">'
1359
+ + '<span class="mp-review-name">' + esc(String((entry && entry.agent) || '')) + '</span>'
1360
+ + '<span class="mp-review-meta">' + esc(meta) + '</span>'
1361
+ + '<span class="mp-review-where">' + esc(modelsReachLabel(machine, entry && entry.reach)) + '</span></li>');
1362
+ });
1363
+ var remove = unreachable.length
1364
+ ? '<div class="mp-review-models-remove"><button type="button" class="mp-btn mp-danger" data-role="models-remove"'
1365
+ + ' disabled>Remove from ' + esc(name) + '</button></div>'
1366
+ : '';
1367
+ return '<section class="mp-review-section" data-section="models"><h4>Models on ' + esc(name)
1368
+ + '</h4><ul>' + rows.join('') + '</ul>' + remove + fromHubHtml(machine) + '</section>';
1369
+ }
1370
+
1371
+ /** WP-5 (desktop parity): the "From this hub" subsection — the routes THIS hub holds that the
1372
+ * machine does not, one row each, each row with its OWN "Use on <name>" button. No checkbox and
1373
+ * no bulk control (AC-5c: nothing here can copy more than one route). The list is read ONCE per
1374
+ * open and read again after a copy or a remove. */
1375
+ function fromHubState(machine) {
1376
+ var id = normId(machine && machine.instanceId);
1377
+ var store = review && review.fromHub;
1378
+ if (!store || normId(store.instanceId) !== id) return { status: 'loading' };
1379
+ if (store.error) return { status: 'error', error: String(store.error) };
1380
+ return { status: 'ready', routes: Array.isArray(store.routes) ? store.routes : [] };
1381
+ }
1382
+
1383
+ function fromHubItemsHtml(machine) {
1384
+ if (!modelsBlock(machine)) return ''; // no block, no subsection
1385
+ var name = machineName(machine);
1386
+ var id = normId(machine && machine.instanceId);
1387
+ var view = fromHubState(machine);
1388
+ if (view.status === 'loading') {
1389
+ return '<li class="mp-review-empty">' + esc('Looking for routes this hub has that ' + name + ' lacks…') + '</li>';
1390
+ }
1391
+ if (view.status === 'error') return '<li class="mp-review-note">' + esc(view.error) + '</li>';
1392
+ if (!view.routes.length) {
1393
+ return '<li class="mp-review-empty">' + esc('This hub has no other routes ' + name + ' could use.') + '</li>';
1394
+ }
1395
+ return view.routes.map(function (route) {
1396
+ var routeId = String((route && route.id) || '');
1397
+ var label = String((route && route.name) || '') || routeId;
1398
+ var origin = String((route && route.origin) || '');
1399
+ var meta = [String((route && route.type) || ''), origin].filter(Boolean).join(' · ');
1400
+ return '<li class="mp-review-item" data-hub-route="' + esc(routeId) + '">'
1401
+ + '<span class="mp-review-name">' + esc(label) + '</span>'
1402
+ + '<span class="mp-review-meta">' + esc(meta) + '</span>'
1403
+ + '<span class="mp-review-actions"><button type="button" class="mp-btn mp-primary" data-role="models-copy"'
1404
+ + ' data-route-id="' + esc(routeId) + '" data-origin="' + esc(origin) + '">'
1405
+ + 'Use on ' + esc(name) + '</button></span></li>';
1406
+ }).join('');
1407
+ }
1408
+
1409
+ function fromHubHtml(machine) {
1410
+ if (!modelsBlock(machine)) return '';
1411
+ return '<div class="mp-review-from-hub" data-section="from-hub"><h5>From this hub</h5>'
1412
+ + '<ul class="mp-review-list" data-role="from-hub">' + fromHubItemsHtml(machine) + '</ul></div>';
1413
+ }
1414
+
1415
+ /** Repaint ONLY the list: the rest of the open sheet (and the note an action just wrote) stays. */
1416
+ function fromHubPaint() {
1417
+ var overlay = dialog && dialog.overlay;
1418
+ var machine = reviewMachine();
1419
+ if (!overlay || typeof overlay.querySelector !== 'function' || !modelsBlock(machine)) return false;
1420
+ var node = overlay.querySelector('[data-role="from-hub"]');
1421
+ if (!node) return false;
1422
+ node.innerHTML = fromHubItemsHtml(machine);
1423
+ return true;
1424
+ }
1425
+
1426
+ /** WP-5: ONE read of `models/copyable` per open, stored on the open sheet's own state. A refusal
1427
+ * is the server's own code and is shown, never retried — there is no timer here. */
1428
+ function fetchFromHub(instanceId) {
1429
+ var id = normId(instanceId);
1430
+ if (!id) return Promise.resolve(false);
1431
+ review.fromHub = { instanceId: id, loading: true };
1432
+ fromHubPaint();
1433
+ return api('/api/machines/' + encodeURIComponent(id) + '/models/copyable')
1434
+ .then(function (payload) {
1435
+ if (normId(review.instanceId) !== id) return false; // the sheet moved on meanwhile
1436
+ review.fromHub = {
1437
+ instanceId: id,
1438
+ routes: (payload && Array.isArray(payload.routes)) ? payload.routes : [],
1439
+ };
1440
+ fromHubPaint();
1441
+ return true;
1442
+ })
1443
+ .catch(function (error) {
1444
+ // REPORT: the subsection says why it is empty; the sheet stays open and nothing is retried.
1445
+ if (normId(review.instanceId) !== id) return false;
1446
+ review.fromHub = { instanceId: id, error: serverError(error) };
1447
+ fromHubPaint();
1448
+ return false;
1449
+ });
1450
+ }
1451
+
1452
+ /** The name a row was painted with — the success note says what travelled, in the user's words. */
1453
+ function fromHubRouteName(routeId) {
1454
+ var routes = (review && review.fromHub && Array.isArray(review.fromHub.routes)) ? review.fromHub.routes : [];
1455
+ var match = routes.filter(function (route) {
1456
+ return String((route && route.id) || '') === String(routeId == null ? '' : routeId);
1457
+ })[0];
1458
+ return String((match && match.name) || '') || String(routeId == null ? '' : routeId);
1459
+ }
1460
+
1461
+ /** WP-5: after a copy or a remove, re-read BOTH the list and what this hub could still offer. */
1462
+ function reloadReview() {
1463
+ var id = normId(review && review.instanceId);
1464
+ return reviewRefresh().then(function () {
1465
+ return fetchFromHub(id);
1466
+ });
1467
+ }
1468
+
1469
+ /** WP-5 / AC-5c (desktop parity): "Use on <name>" — ONE route and its key, and nothing else; the
1470
+ * confirm says so before anything is sent, and `expectOrigin` is the origin the row was painted
1471
+ * with, so a route that moved on the hub is refused instead of copied. */
1472
+ function copyModel(instanceId, button) {
1473
+ var id = normId(instanceId);
1474
+ var name = machineName(reviewMachine() || { instanceId: id });
1475
+ var attr = function (key) {
1476
+ return button && typeof button.getAttribute === 'function' ? String(button.getAttribute(key) || '') : '';
1477
+ };
1478
+ var routeId = attr('data-route-id');
1479
+ var origin = attr('data-origin');
1480
+ if (!id || !routeId) return Promise.resolve(false);
1481
+ var label = fromHubRouteName(routeId);
1482
+ if (confirmImpl && !confirmImpl('Copies this route and its key to ' + name + '. Only this route is copied.')) {
1483
+ return Promise.resolve(false);
1484
+ }
1485
+ busy(button, true);
1486
+ return api('/api/machines/' + encodeURIComponent(id) + '/models/copy', {
1487
+ method: 'POST',
1488
+ body: { routeId: routeId, expectOrigin: origin },
1489
+ }).then(function () {
1490
+ busy(button, false);
1491
+ // After the reload, which repaints the sheet (a note set first would be wiped).
1492
+ return reloadReview().then(function (reloaded) {
1493
+ reviewNote('Copied ' + label + ' to ' + name, 'ok');
1494
+ return reloaded;
1495
+ });
1496
+ }).catch(function (error) {
1497
+ // REPORT: the refusal (or the transport fault) stays on the sheet, which is still open.
1498
+ busy(button, false);
1499
+ reviewNote(serverError(error), 'bad');
1500
+ return false;
1501
+ });
1502
+ }
1503
+
1504
+ function reviewItemKey(agent, name) {
1505
+ return String(agent || '') + ':' + String(name || '');
1506
+ }
1507
+
1508
+ function reviewHeldBackHtml(machine) {
1509
+ if (!skillsBlock(machine)) return ''; // no block, no section (§8 WP-3)
1510
+ var items = skillsHeldBack(machine);
1511
+ var body = items.length
1512
+ ? items.map(function (item) {
1513
+ var where = item && item.file
1514
+ ? '<span class="mp-review-where">' + esc(String(item.file) + (item.line ? ':' + item.line : '')) + '</span>'
1515
+ : '';
1516
+ return '<li class="mp-review-item" data-key="' + esc(reviewItemKey(item && item.agent, item && item.name)) + '">'
1517
+ + '<span class="mp-review-name">' + esc(String((item && item.name) || '')) + '</span>'
1518
+ + '<span class="mp-review-meta">' + esc([String((item && item.agent) || ''), skillReasonLabel(item && item.reason)].filter(Boolean).join(' · ')) + '</span>'
1519
+ + where + '</li>';
1520
+ }).join('')
1521
+ : '<li class="mp-review-empty">Nothing was held back.</li>';
1522
+ return '<section class="mp-review-section" data-section="held-back"><h4>Held back</h4><ul>' + body + '</ul></section>';
1523
+ }
1524
+
1525
+ function reviewItemActionsHtml(instanceId, item) {
1526
+ var state = String((item && item.state) || '');
1527
+ var agent = String((item && item.agent) || '');
1528
+ var name = String((item && item.name) || '');
1529
+ var attrs = ' data-review-action data-review-id="' + esc(instanceId) + '" data-review-agent="' + esc(agent)
1530
+ + '" data-review-name="' + esc(name) + '" data-review-box-hash="' + esc(String((item && item.boxHash) || '')) + '"';
1531
+ var out = '';
1532
+ if (state !== 'shadows_hub') out += '<button type="button" class="mp-btn mp-primary"' + attrs + ' data-review-verb="import">Import</button>';
1533
+ if (state !== 'new_on_box') out += '<button type="button" class="mp-btn"' + attrs + ' data-review-verb="retire">Use the laptop’s copy</button>';
1534
+ out += '<button type="button" class="mp-btn"' + attrs + ' data-review-skip>Skip</button>';
1535
+ return out;
1536
+ }
1537
+
1538
+ function reviewOnBoxHtml(machine) {
1539
+ if (!skillsBlock(machine)) return ''; // no block, no section (§8 WP-3)
1540
+ var id = normId(machine && machine.instanceId);
1541
+ var items = skillsOnBox(machine).filter(function (item) { return item && String(item.state) !== 'same'; });
1542
+ var body = items.length
1543
+ ? items.map(function (item) {
1544
+ var name = String((item && item.name) || '');
1545
+ var key = reviewItemKey(item && item.agent, name);
1546
+ return '<li class="mp-review-item" data-key="' + esc(key) + '">'
1547
+ + '<span class="mp-review-name">' + esc(name) + '</span>'
1548
+ + '<span class="mp-review-meta">' + esc([String((item && item.agent) || ''), skillStateLabel(item && item.state)].filter(Boolean).join(' · ')) + '</span>'
1549
+ + '<span class="mp-review-actions">' + reviewItemActionsHtml(id, item)
1550
+ + '<button type="button" class="mp-btn" data-review-diff data-review-id="' + esc(id) + '"'
1551
+ + ' data-review-agent="' + esc(String((item && item.agent) || '')) + '" data-review-name="' + esc(name) + '">Show diff</button>'
1552
+ + '</span><div class="mp-review-diff" hidden></div></li>';
1553
+ }).join('')
1554
+ : '<li class="mp-review-empty">This machine has no skills of its own.</li>';
1555
+ return '<section class="mp-review-section" data-section="on-box"><h4>On ' + esc(machineName(machine)) + '</h4><ul>' + body + '</ul></section>';
1556
+ }
1557
+
1558
+ function reviewPluginsHtml(machine) {
1559
+ if (!skillsBlock(machine)) return ''; // no block, no section (§8 WP-3)
1560
+ var plugins = skillsPlugins(machine);
1561
+ var excluded = skillsExcluded(machine);
1562
+ var body = plugins.length
1563
+ ? plugins.map(function (plugin) {
1564
+ var id = String(plugin);
1565
+ return '<label class="mp-review-plugin"><input type="checkbox" value="' + esc(id) + '"'
1566
+ + (excluded.indexOf(id) !== -1 ? ' checked' : '') + '> ' + esc(id) + '</label>';
1567
+ }).join('')
1568
+ : '<p class="mp-review-empty">The hub sends no plugins to this machine.</p>';
1569
+ return '<section class="mp-review-section" data-section="plugins"><h4>Plugins</h4>'
1570
+ + '<p class="mp-review-note">Checked plugins are not sent to this machine.</p>'
1571
+ + '<div class="mp-review-plugins">' + body + '</div>'
1572
+ + '<div class="mp-review-actions"><button type="button" class="mp-btn mp-primary" data-review-save-plugins'
1573
+ + ' data-review-id="' + esc(normId(machine && machine.instanceId)) + '">Save</button></div></section>';
1574
+ }
1575
+
1576
+ function reviewSheetHtml(machine) {
1577
+ return '<section class="model-picker-sheet mp-modal mp-review-sheet" role="dialog" aria-modal="true" aria-labelledby="mp-review-title">'
1578
+ + '<div class="model-picker-head"><div><h2 id="mp-review-title">' + esc(reviewTitle(machine)) + '</h2>'
1579
+ + '<p>' + esc(skillsLineText(machine) || modelsLineText(machine)) + '</p></div>'
1580
+ + '<button type="button" class="model-picker-close" id="mp-review-close" aria-label="Close">&times;</button></div>'
1581
+ + '<p class="mp-review-note" data-role="review-note" role="status" aria-live="polite"></p>'
1582
+ + reviewHeldBackHtml(machine) + reviewOnBoxHtml(machine) + reviewPluginsHtml(machine)
1583
+ + reviewModelsHtml(machine)
1584
+ + '<div class="mp-modal-actions"><button type="button" class="mp-btn" id="mp-review-done">Close</button></div>'
1585
+ + '</section>';
1586
+ }
1587
+
1588
+ function reviewMachine() {
1589
+ var id = normId(review.instanceId);
1590
+ return state.machines.filter(function (m) { return normId(m && m.instanceId) === id; })[0] || null;
1591
+ }
1592
+
1593
+ function reviewNote(text, kind) {
1594
+ var node = (dialog && dialog.overlay && typeof dialog.overlay.querySelector === 'function')
1595
+ ? dialog.overlay.querySelector('[data-role="review-note"]') : null;
1596
+ if (!node) return;
1597
+ node.textContent = String(text || '');
1598
+ node.className = 'mp-review-note' + (kind === 'bad' ? ' bad' : '');
1599
+ }
1600
+
1601
+ /** Client-only Skip: the item leaves this sheet and stays gone across a repaint — no server call. */
1602
+ function reviewSkip(button) {
1603
+ if (!button || typeof button.getAttribute !== 'function') return false;
1604
+ review.skipped[reviewItemKey(button.getAttribute('data-review-agent'), button.getAttribute('data-review-name'))] = true;
1605
+ var item = typeof button.closest === 'function' ? button.closest('.mp-review-item') : null;
1606
+ if (item) item.hidden = true;
1607
+ return true;
1608
+ }
1609
+
1610
+ function applySkips() {
1611
+ var overlay = dialog && dialog.overlay;
1612
+ if (!overlay || typeof overlay.querySelectorAll !== 'function') return;
1613
+ Array.prototype.slice.call(overlay.querySelectorAll('.mp-review-item[data-key]') || []).forEach(function (item) {
1614
+ var key = typeof item.getAttribute === 'function' ? item.getAttribute('data-key') : null;
1615
+ if (key && review.skipped[key]) item.hidden = true;
1616
+ });
1617
+ }
1618
+
1619
+ /** AC-9a: re-read the list and repaint the OPEN sheet — never a page reload. */
1620
+ function reviewRefresh() {
1621
+ return load({ silent: true }).then(function () {
1622
+ var machine = reviewMachine();
1623
+ if (!(dialog && dialog.overlay && dialog.review)) return true; // the sheet was closed or replaced meanwhile
1624
+ if (machine && reviewOpenable(machine)) {
1625
+ dialog.overlay.innerHTML = reviewSheetHtml(machine);
1626
+ bindSheet();
1627
+ applySkips();
1628
+ } else {
1629
+ closeDialog(); // same as desktop machinesPageReviewPaint: nothing left to review → close
1630
+ }
1631
+ return true;
1632
+ });
1633
+ }
1634
+
1635
+ function openReviewSheet(instanceId) {
1636
+ var id = normId(instanceId);
1637
+ var machine = state.machines.filter(function (m) { return normId(m && m.instanceId) === id; })[0] || null;
1638
+ if (!reviewOpenable(machine)) return null;
1639
+ closeDialog();
1640
+ review = { instanceId: id, skipped: {}, fromHub: null };
1641
+ var overlay = mountDialog(reviewSheetHtml(machine));
1642
+ if (!overlay) return null;
1643
+ dialog = { overlay: overlay, review: true };
1644
+ bindSheet();
1645
+ // WP-5: one read of the hub's copyable routes per open — the sheet is already painted, so the
1646
+ // subsection shows "Looking for…" until this lands.
1647
+ fetchFromHub(id);
1648
+ return dialog;
1649
+ }
1650
+
1651
+ function bindSheet() {
1652
+ var close = $('mp-review-close');
1653
+ if (close && typeof close.addEventListener === 'function') close.addEventListener('click', closeDialog);
1654
+ var done = $('mp-review-done');
1655
+ if (done && typeof done.addEventListener === 'function') done.addEventListener('click', closeDialog);
1656
+ // The sheet mounts on document.body (mountDialog), OUTSIDE the card whose delegated click/change
1657
+ // listeners dispatch every data-review-* control — so a tap inside it never reached them.
1658
+ // Binding the overlay to the same dispatcher (bind() is idempotent per scope) makes the sheet's
1659
+ // Import / Use-the-laptop's-copy / Save / Remove-from-<box> controls work on the phone.
1660
+ if (dialog && dialog.overlay && dialog.overlay.dataset) bind(dialog.overlay);
1661
+ }
1662
+
1663
+ function busy(node, state_) {
1664
+ if (!node || typeof node !== 'object') return;
1665
+ node.disabled = !!state_;
1666
+ if (typeof node.setAttribute === 'function') node.setAttribute('aria-busy', state_ ? 'true' : 'false');
1667
+ }
1668
+
1669
+ function reviewActionFailed(error) {
1670
+ var payload = (error && error.payload) || null;
1671
+ var code = String((payload && (payload.error || payload.code)) || (error && (error.code || error.message)) || '');
1672
+ if (Number(error && error.status) === 409 || code.indexOf('agent_skill_changed') !== -1) {
1673
+ reviewNote('This skill changed on the box since you opened Review — reopen to see the new version.', 'bad');
1674
+ return reviewRefresh();
1675
+ }
1676
+ reviewNote(serverError(error), 'bad');
1677
+ return false;
1678
+ }
1679
+
1680
+ function reviewAction(instanceId, agent, name, boxHash, verb, button) {
1681
+ var action = String(verb) === 'retire' ? 'retire' : 'import';
1682
+ var body = { agent: String(agent || ''), name: String(name || '') };
1683
+ if (boxHash) body.expectBoxHash = String(boxHash);
1684
+ busy(button, true);
1685
+ return api('/api/machines/' + encodeURIComponent(normId(instanceId)) + '/skills/' + action, { method: 'POST', body: body })
1686
+ .then(function () {
1687
+ busy(button, false);
1688
+ setStatus(action === 'import'
1689
+ ? 'Imported ' + String(name || '') + ' from this machine.'
1690
+ : 'This machine now uses the laptop’s copy of ' + String(name || '') + '.');
1691
+ delete review.skipped[reviewItemKey(agent, name)];
1692
+ return reviewRefresh();
1693
+ })
1694
+ .catch(function (error) {
1695
+ // REPORT: the refusal (or the transport fault) stays on the sheet, which is still open.
1696
+ busy(button, false);
1697
+ return reviewActionFailed(error);
1698
+ });
1699
+ }
1700
+
1701
+ function savePlugins(instanceId, button) {
1702
+ var overlay = dialog && dialog.overlay;
1703
+ var excluded = [];
1704
+ if (overlay && typeof overlay.querySelectorAll === 'function') {
1705
+ Array.prototype.slice.call(overlay.querySelectorAll('.mp-review-plugins input') || []).forEach(function (box) {
1706
+ if (box && box.checked) excluded.push(String(box.value == null ? '' : box.value));
1707
+ });
1708
+ }
1709
+ busy(button, true);
1710
+ return api('/api/machines/' + encodeURIComponent(normId(instanceId)) + '/skills/exclude-plugins', {
1711
+ method: 'PUT',
1712
+ body: { plugins: excluded },
1713
+ }).then(function () {
1714
+ busy(button, false);
1715
+ setStatus(excluded.length
1716
+ ? 'Plugins saved — ' + excluded.length + ' not sent to this machine.'
1717
+ : 'Plugins saved — every plugin is sent to this machine.');
1718
+ return reviewRefresh();
1719
+ }).catch(function (error) {
1720
+ busy(button, false);
1721
+ reviewNote(serverError(error), 'bad');
1722
+ return false;
1723
+ });
1724
+ }
1725
+
1726
+ /** WP-4: the route ids ticked in the open sheet — the only truth a Remove POST may be built from. */
1727
+ function modelsChecked() {
1728
+ var overlay = dialog && dialog.overlay;
1729
+ var picked = [];
1730
+ if (overlay && typeof overlay.querySelectorAll === 'function') {
1731
+ Array.prototype.slice.call(overlay.querySelectorAll('[data-role="models-pick"]') || []).forEach(function (box) {
1732
+ if (box && box.checked) picked.push(String(box.value == null ? '' : box.value));
1733
+ });
1734
+ }
1735
+ return picked;
1736
+ }
1737
+
1738
+ /** WP-4: one tick or untick → the button is live exactly while something is ticked. Returns the count. */
1739
+ function modelsPicked() {
1740
+ var overlay = dialog && dialog.overlay;
1741
+ var count = modelsChecked().length;
1742
+ var node = overlay && typeof overlay.querySelector === 'function'
1743
+ ? overlay.querySelector('[data-role="models-remove"]') : null;
1744
+ if (node) node.disabled = count === 0;
1745
+ return count;
1746
+ }
1747
+
1748
+ /** WP-4: "Remove from <name>" — the box deletes these routes and the keys it holds for them; the
1749
+ * hub keeps its own copies, which is what the confirm says before anything is sent. A cancelled
1750
+ * confirm is not a request, only the TICKED ids travel, and the sheet is re-read after. */
1751
+ function removeModels(instanceId, button) {
1752
+ var id = normId(instanceId);
1753
+ var name = machineName(reviewMachine() || { instanceId: id });
1754
+ if (!modelsChecked().length) return Promise.resolve(false);
1755
+ if (confirmImpl && !confirmImpl('Deletes these routes and their keys from ' + name + '. This hub keeps its own copies.')) {
1756
+ return Promise.resolve(false);
1757
+ }
1758
+ var routeIds = modelsChecked();
1759
+ busy(button, true);
1760
+ return api('/api/machines/' + encodeURIComponent(id) + '/models/remove', {
1761
+ method: 'POST',
1762
+ body: { routeIds: routeIds },
1763
+ }).then(function (result) {
1764
+ busy(button, false);
1765
+ var data = (result && typeof result === 'object') ? result : {};
1766
+ var removed = Array.isArray(data.removed) ? data.removed.length : routeIds.length;
1767
+ var missing = Array.isArray(data.missing) ? data.missing.length : 0;
1768
+ // The note goes AFTER the reload, which repaints the sheet. WP-5: the removals also change
1769
+ // what this hub can offer, hence the reload of both lists.
1770
+ return reloadReview().then(function (reloaded) {
1771
+ reviewNote('Removed ' + removed + ' from ' + name + (missing ? ' · ' + missing + ' already gone' : ''), 'ok');
1772
+ return reloaded;
1773
+ });
1774
+ }).catch(function (error) {
1775
+ // REPORT: the refusal (or the transport fault) stays on the sheet, which is still open.
1776
+ busy(button, false);
1777
+ reviewNote(serverError(error), 'bad');
1778
+ return false;
1779
+ });
1780
+ }
1781
+
1782
+ // The diff ALGORITHM is public/js/prompt-diff.js — the same module the desktop page loads in
1783
+ // index.html and the phone does not, so the phone pulls the one copy in on first use (never a
1784
+ // second implementation of the LCS).
1785
+ var promptDiffPromise = null;
1786
+
1787
+ function promptDiffModule() {
1788
+ if (root.PromptDiff && typeof root.PromptDiff.diffLines === 'function') return Promise.resolve(root.PromptDiff);
1789
+ if (d.promptDiff && typeof d.promptDiff.diffLines === 'function') return Promise.resolve(d.promptDiff);
1790
+ if (promptDiffPromise) return promptDiffPromise;
1791
+ var doc = host || (typeof document !== 'undefined' ? document : null);
1792
+ if (!doc || typeof doc.createElement !== 'function') return Promise.resolve(null);
1793
+ var src = typeof d.promptDiffSrc === 'string' ? d.promptDiffSrc : '/js/prompt-diff.js';
1794
+ promptDiffPromise = new Promise(function (resolve) {
1795
+ var script = doc.createElement('script');
1796
+ script.src = src;
1797
+ script.onload = function () {
1798
+ resolve(root.PromptDiff && typeof root.PromptDiff.diffLines === 'function' ? root.PromptDiff : null);
1799
+ };
1800
+ script.onerror = function () { resolve(null); };
1801
+ var parent = doc.head || doc.body || doc.documentElement;
1802
+ if (parent && typeof parent.appendChild === 'function') parent.appendChild(script);
1803
+ else resolve(null);
1804
+ });
1805
+ return promptDiffPromise;
1806
+ }
1807
+
1808
+ function diffFileHtml(module, boxFile, hubFile) {
1809
+ var filePath = String((boxFile && boxFile.path) || (hubFile && hubFile.path) || '');
1810
+ var size = Number((boxFile && boxFile.size) || (hubFile && hubFile.size) || 0);
1811
+ var head = '<p class="mp-diff-file">' + esc(filePath) + '</p>';
1812
+ if ((boxFile && boxFile.binary) || (hubFile && hubFile.binary)) {
1813
+ return head + '<p class="mp-diff-binary">' + esc('binary file, ' + machineFormatBytes(size)) + '</p>';
1814
+ }
1815
+ var ops = module ? module.diffLines(hubFile ? String(hubFile.text || '') : '', boxFile ? String(boxFile.text || '') : '') : null;
1816
+ if (!Array.isArray(ops)) return head + '<p class="mp-diff-binary">' + esc('binary file, ' + machineFormatBytes(size)) + '</p>';
1817
+ var shown = ops.slice(0, DIFF_LINE_CAP);
1818
+ return head + '<div class="mp-diff">' + shown.map(function (op) {
1819
+ var cls = op.type === 'add' ? 'mp-diff-add' : (op.type === 'del' ? 'mp-diff-del' : 'mp-diff-eq');
1820
+ var sign = op.type === 'add' ? '+' : (op.type === 'del' ? '-' : ' ');
1821
+ return '<span class="' + cls + '">' + esc(sign + ' ' + op.text) + '</span>';
1822
+ }).join('') + '</div>'
1823
+ + (ops.length > shown.length ? '<p class="mp-review-note">' + esc('… ' + (ops.length - shown.length) + ' more lines') + '</p>' : '');
1824
+ }
1825
+
1826
+ /** A preview payload + a diff module → the per-file markup. Pure, so the sheet is provable. */
1827
+ function diffHtml(module, preview) {
1828
+ var boxFiles = (preview && preview.box && Array.isArray(preview.box.files)) ? preview.box.files : [];
1829
+ var hubFiles = (preview && preview.hub && Array.isArray(preview.hub.files)) ? preview.hub.files : [];
1830
+ var hubByName = {};
1831
+ hubFiles.forEach(function (file) { if (file && file.path) hubByName[String(file.path)] = file; });
1832
+ var seen = {};
1833
+ var out = boxFiles.map(function (file) {
1834
+ var key = String((file && file.path) || '');
1835
+ seen[key] = true;
1836
+ return diffFileHtml(module, file, hubByName[key] || null);
1837
+ });
1838
+ hubFiles.forEach(function (file) {
1839
+ var key = String((file && file.path) || '');
1840
+ if (seen[key]) return;
1841
+ out.push('<p class="mp-diff-file">' + esc(key) + '</p><p class="mp-diff-binary">'
1842
+ + esc('only on this hub — importing replaces it (a backup is kept)') + '</p>');
1843
+ });
1844
+ return out.join('') || '<p class="mp-review-empty">No files on either side.</p>';
1845
+ }
1846
+
1847
+ function diffNode(agent, name) {
1848
+ var overlay = dialog && dialog.overlay;
1849
+ if (!overlay || typeof overlay.querySelector !== 'function') return null;
1850
+ var key = reviewItemKey(agent, name).replace(/"/g, '\\"');
1851
+ var item = overlay.querySelector('.mp-review-item[data-key="' + key + '"]');
1852
+ return item && typeof item.querySelector === 'function' ? item.querySelector('.mp-review-diff') : null;
1853
+ }
1854
+
1855
+ /** AC-9b: the hub's copy vs the box's, through the ONE diff module (lazily loaded on the phone). */
1856
+ function showDiff(instanceId, agent, name, button) {
1857
+ var node = diffNode(agent, name);
1858
+ if (node && !node.hidden) { node.hidden = true; return Promise.resolve(true); }
1859
+ busy(button, true);
1860
+ return api('/api/machines/' + encodeURIComponent(normId(instanceId)) + '/skills/preview?agent='
1861
+ + encodeURIComponent(String(agent || '')) + '&name=' + encodeURIComponent(String(name || '')))
1862
+ .then(function (payload) {
1863
+ return promptDiffModule().then(function (module) {
1864
+ busy(button, false);
1865
+ node = diffNode(agent, name);
1866
+ if (!node) return false;
1867
+ node.hidden = false;
1868
+ node.innerHTML = diffHtml(module, payload);
1869
+ if (!module) node.innerHTML += '<p class="mp-review-note">' + esc('The diff module did not load — the file list above is still the truth.') + '</p>';
1870
+ return true;
1871
+ });
1872
+ })
1873
+ .catch(function (error) {
1874
+ busy(button, false);
1875
+ node = diffNode(agent, name);
1876
+ if (node) {
1877
+ node.hidden = false;
1878
+ node.innerHTML = '<p class="mp-review-note">' + esc(serverError(error)) + '</p>';
1879
+ }
1880
+ return false;
1881
+ });
1882
+ }
1883
+
1884
+ // ── wiring ─────────────────────────────────────────────────────────────────────────────────
1885
+ // ONE delegated click listener plus one change listener on the page, so a re-render never loses a
1886
+ // binding and a rename input only writes on commit (desktop parity: rename on `change`).
1887
+ // Fix round 3 (G): the code box reports itself the same way — the draft is kept in
1888
+ // state.signInDrafts as the user types (and as focus moves), because the More page may replace the
1889
+ // whole card between two keystrokes and the input's own value goes with it.
1890
+ function bind(scope) {
1891
+ if (!scope || typeof scope.addEventListener !== 'function' || scope.dataset.mpBound === '1') return false;
1892
+ scope.dataset.mpBound = '1';
1893
+ // The event's code input and its block key. The key is READ from the block's own attribute,
1894
+ // never rebuilt by splitting: an instance id is uuid-like and contains hyphens itself.
1895
+ function draftTargetOf(event) {
1896
+ var target = event && event.target;
1897
+ var code = target && typeof target.closest === 'function' ? target.closest('[data-sign-in-code]') : null;
1898
+ if (!code) return null;
1899
+ var block = typeof code.closest === 'function' ? code.closest('[data-machine-sign-in-block]') : null;
1900
+ var key = block && typeof block.getAttribute === 'function'
1901
+ ? String(block.getAttribute('data-machine-sign-in-block') || '')
1902
+ : '';
1903
+ return key ? { code: code, key: key } : null;
1904
+ }
1905
+ scope.addEventListener('input', function (event) {
1906
+ var hit = draftTargetOf(event);
1907
+ if (!hit) return;
1908
+ state.signInDrafts[hit.key] = draftFromSignInInput(hit.code, true);
1909
+ });
1910
+ scope.addEventListener('focusin', function (event) {
1911
+ var hit = draftTargetOf(event);
1912
+ if (!hit) return;
1913
+ state.signInDrafts[hit.key] = draftFromSignInInput(hit.code, true);
1914
+ });
1915
+ scope.addEventListener('focusout', function (event) {
1916
+ var hit = draftTargetOf(event);
1917
+ if (!hit) return;
1918
+ // Fix round 3b (H): NOT `focused: false` outright. Chrome dispatches focusout on the input
1919
+ // DURING the removal the More page performs, and at dispatch time a removal blur and a genuine
1920
+ // one are indistinguishable — the user's next keystrokes would go to <body>. The value and the
1921
+ // caret are stored now; the flag is decided one microtask later, when "was this input thrown
1922
+ // away?" has an answer. A microtask, not a timer: this module still owns exactly one timer.
1923
+ var entry = draftFromSignInInput(hit.code, true);
1924
+ state.signInDrafts[hit.key] = entry;
1925
+ Promise.resolve().then(function () {
1926
+ // The input is gone, so the blur was the removal itself — keep `focused`, and the card the
1927
+ // rebuild paints takes the focus back. (`=== false` strictly: a node without the property is
1928
+ // treated as connected, i.e. as a real blur.)
1929
+ if (hit.code.isConnected === false) return;
1930
+ // Only the flag of THIS write is settled: a later input/focusin write has already replaced
1931
+ // the entry and owns the truth about where the user is typing.
1932
+ if (state.signInDrafts[hit.key] === entry) entry.focused = false;
1933
+ });
1934
+ });
1935
+ scope.addEventListener('click', function (event) {
1936
+ var target = event.target;
1937
+ var closest = target && typeof target.closest === 'function' ? target.closest.bind(target) : null;
1938
+ if (!closest) return;
1939
+ var toggleNode = closest('[data-machine-toggle]');
1940
+ if (toggleNode) {
1941
+ event.preventDefault();
1942
+ toggle(toggleNode.getAttribute('data-machine-toggle'), toggleNode.getAttribute('data-machine-field'));
1943
+ return;
1944
+ }
1945
+ var newSession = closest('[data-machine-new-session]');
1946
+ if (newSession) { event.preventDefault(); newSessionHere(newSession.getAttribute('data-machine-new-session')); return; }
1947
+ var remove = closest('[data-machine-remove]');
1948
+ if (remove) { event.preventDefault(); removeMachine(remove.getAttribute('data-machine-remove')); return; }
1949
+ var join = closest('[data-machine-join]');
1950
+ if (join) { event.preventDefault(); openAddDialog({ url: join.getAttribute('data-machine-url') || '' }); return; }
1951
+ var signIn = closest('[data-machine-signin]');
1952
+ if (signIn) { event.preventDefault(); signInToo(signIn.getAttribute('data-machine-signin')); return; }
1953
+ // WP-9: the Review sheet's own actions (same verbs, same wording as the desktop dialog).
1954
+ var reviewOpen = closest('[data-machine-review]');
1955
+ if (reviewOpen) { event.preventDefault(); openReviewSheet(reviewOpen.getAttribute('data-machine-review')); return; }
1956
+ var skip = closest('[data-review-skip]');
1957
+ if (skip) { event.preventDefault(); reviewSkip(skip); return; }
1958
+ var diff = closest('[data-review-diff]');
1959
+ if (diff) {
1960
+ event.preventDefault();
1961
+ showDiff(diff.getAttribute('data-review-id'), diff.getAttribute('data-review-agent'), diff.getAttribute('data-review-name'), diff);
1962
+ return;
1963
+ }
1964
+ var savePluginsNode = closest('[data-review-save-plugins]');
1965
+ if (savePluginsNode) {
1966
+ event.preventDefault();
1967
+ savePlugins(savePluginsNode.getAttribute('data-review-id'), savePluginsNode);
1968
+ return;
1969
+ }
1970
+ // WP-4: "Remove from <name>" — the instance id is the sheet's own (review.instanceId), never
1971
+ // read back out of the markup.
1972
+ var removeModelsNode = closest('[data-role="models-remove"]');
1973
+ if (removeModelsNode) {
1974
+ event.preventDefault();
1975
+ removeModels(review.instanceId, removeModelsNode);
1976
+ return;
1977
+ }
1978
+ // WP-5: "Use on <name>" — one route per tap, the sheet's own id, the row's own route/origin.
1979
+ var copyModelNode = closest('[data-role="models-copy"]');
1980
+ if (copyModelNode) {
1981
+ event.preventDefault();
1982
+ copyModel(review.instanceId, copyModelNode);
1983
+ return;
1984
+ }
1985
+ var action = closest('[data-review-action]');
1986
+ if (action) {
1987
+ event.preventDefault();
1988
+ reviewAction(
1989
+ action.getAttribute('data-review-id'),
1990
+ action.getAttribute('data-review-agent'),
1991
+ action.getAttribute('data-review-name'),
1992
+ action.getAttribute('data-review-box-hash'),
1993
+ action.getAttribute('data-review-verb'),
1994
+ action,
1995
+ );
1996
+ return;
1997
+ }
1998
+ // WP-3: sign a coding agent in ON another machine (the passkey row above is the CTM LOGIN).
1999
+ var agentSignIn = closest('[data-machine-sign-in]');
2000
+ if (agentSignIn) {
2001
+ event.preventDefault();
2002
+ machineSignIn(agentSignIn.getAttribute('data-machine-sign-in'), agentSignIn.getAttribute('data-machine-agent'), agentSignIn);
2003
+ return;
2004
+ }
2005
+ var codeSubmit = closest('[data-sign-in-code-submit]');
2006
+ var copyCode = closest('[data-sign-in-copy-code]');
2007
+ if (codeSubmit || copyCode) {
2008
+ event.preventDefault();
2009
+ var block = closest('[data-machine-sign-in-block]');
2010
+ // The instance id is a uuid-like string that itself contains hyphens, so it is read from
2011
+ // the card (never by splitting the block's own key), and the agent from the block.
2012
+ var card = block && typeof block.closest === 'function' ? block.closest('[data-machine-id]') : null;
2013
+ var machineId = card ? card.getAttribute('data-machine-id') : '';
2014
+ if (copyCode) {
2015
+ var codeEl = block && typeof block.querySelector === 'function' ? block.querySelector('[data-sign-in-user-code]') : null;
2016
+ copySignInCode(codeEl ? codeEl.textContent : '', copyCode);
2017
+ return;
2018
+ }
2019
+ submitSignInCode(machineId, block ? block.getAttribute('data-machine-agent') : '', codeSubmit);
2020
+ return;
2021
+ }
2022
+ if (closest('[data-machine-add]')) { event.preventDefault(); openAddDialog(); return; }
2023
+ if (closest('[data-machine-show-code]')) { event.preventDefault(); openShowCodeDialog(); return; }
2024
+ if (closest('[data-pool-leave]')) { event.preventDefault(); leavePool(); return; }
2025
+ if (closest('[data-machines-refresh]')) { event.preventDefault(); load({ silent: true }); }
2026
+ });
2027
+ scope.addEventListener('change', function (event) {
2028
+ var target = event.target;
2029
+ var closest = target && typeof target.closest === 'function' ? target.closest.bind(target) : null;
2030
+ // WP-4: a tick on a route row is the only thing that arms "Remove from <name>".
2031
+ var pick = closest ? closest('[data-role="models-pick"]') : null;
2032
+ if (pick) { modelsPicked(); return; }
2033
+ var input = closest ? closest('[data-machine-rename]') : null;
2034
+ if (!input) return;
2035
+ rename(input.getAttribute('data-machine-rename'), input.value);
2036
+ });
2037
+ // Enter in the code box means the same thing as tapping Submit (desktop parity).
2038
+ scope.addEventListener('keydown', function (event) {
2039
+ if (!event || event.key !== 'Enter') return;
2040
+ var target = event.target;
2041
+ var closest = target && typeof target.closest === 'function' ? target.closest.bind(target) : null;
2042
+ var input = closest ? closest('[data-sign-in-code]') : null;
2043
+ if (!input) return;
2044
+ event.preventDefault();
2045
+ var block = closest('[data-machine-sign-in-block]');
2046
+ var card = block && typeof block.closest === 'function' ? block.closest('[data-machine-id]') : null;
2047
+ submitSignInCode(
2048
+ card ? card.getAttribute('data-machine-id') : '',
2049
+ block ? block.getAttribute('data-machine-agent') : '',
2050
+ input,
2051
+ );
2052
+ });
2053
+ return true;
2054
+ }
2055
+
2056
+ return {
2057
+ state: state,
2058
+ render: render,
2059
+ bind: bind,
2060
+ load: load,
2061
+ loadHistories: loadHistories,
2062
+ headerHtml: headerHtml,
2063
+ listHtml: listHtml,
2064
+ bodyHtml: bodyHtml,
2065
+ machineCardHtml: machineCardHtml,
2066
+ selfCardHtml: selfCardHtml,
2067
+ signInRowHtml: signInRowHtml,
2068
+ // WP-3: the agent sign-in block (desktop parity) — presenters plus the two writes.
2069
+ signInBlockHtml: signInBlockHtml,
2070
+ signedOutAgents: signedOutAgents,
2071
+ renderSignInBlock: renderSignInBlock,
2072
+ machineSignIn: machineSignIn,
2073
+ submitSignInCode: submitSignInCode,
2074
+ // WP-3 fix (C)/(D): the bounded re-read and the card's own refusal copy.
2075
+ signInErrorText: signInErrorText,
2076
+ startSignInPoll: startSignInPoll,
2077
+ stopSignInPoll: stopSignInPoll,
2078
+ stopSignInPolls: stopSignInPolls,
2079
+ credentialBadge: credentialBadge,
2080
+ toggle: toggle,
2081
+ rename: rename,
2082
+ removeMachine: removeMachine,
2083
+ leavePool: leavePool,
2084
+ newSessionHere: newSessionHere,
2085
+ signInToo: signInToo,
2086
+ openAddDialog: openAddDialog,
2087
+ submitAddDialog: submitAddDialog,
2088
+ registerLegacy: registerLegacy,
2089
+ openShowCodeDialog: openShowCodeDialog,
2090
+ closeDialog: closeDialog,
2091
+ // WP-9 (§9 S-9): the row line + Review sheet, exposed like every other presenter here.
2092
+ skillsRowHtml: skillsRowHtml,
2093
+ skillsLineText: skillsLineText,
2094
+ skillsNeedsReview: skillsNeedsReview,
2095
+ // WP-3 (§8 WP-3): the row line + Review sheet's Models half, exposed like the skills twins.
2096
+ modelsRowHtml: modelsRowHtml,
2097
+ modelsLineText: modelsLineText,
2098
+ modelsNeedsReview: modelsNeedsReview,
2099
+ reviewModelsHtml: reviewModelsHtml,
2100
+ reviewSheetHtml: reviewSheetHtml,
2101
+ reviewHeldBackHtml: reviewHeldBackHtml,
2102
+ reviewOnBoxHtml: reviewOnBoxHtml,
2103
+ reviewPluginsHtml: reviewPluginsHtml,
2104
+ openReviewSheet: openReviewSheet,
2105
+ reviewRefresh: reviewRefresh,
2106
+ reviewAction: reviewAction,
2107
+ reviewSkip: reviewSkip,
2108
+ savePlugins: savePlugins,
2109
+ // WP-4 (§8 WP-4): the Models section's one control — the ids, the arming, and the write.
2110
+ modelsChecked: modelsChecked,
2111
+ modelsPicked: modelsPicked,
2112
+ removeModels: removeModels,
2113
+ // WP-5 (§8 WP-5): the "From this hub" subsection — the rows, the one read, and the write.
2114
+ fromHubHtml: fromHubHtml,
2115
+ fromHubItemsHtml: fromHubItemsHtml,
2116
+ fetchFromHub: fetchFromHub,
2117
+ copyModel: copyModel,
2118
+ showDiff: showDiff,
2119
+ diffHtml: diffHtml,
2120
+ preview: function () { return dialog ? previewFields() : null; },
2121
+ dialog: function () { return dialog; },
2122
+ readOrigins: poolOrigins,
2123
+ rememberOrigins: rememberOrigins,
2124
+ };
2125
+ }
2126
+
2127
+ var out = {
2128
+ create: create,
2129
+ STALE_NOTE: STALE_NOTE,
2130
+ HISTORY_MS: HISTORY_MS,
2131
+ STATUS_ID: STATUS_ID,
2132
+ // WP-3 fix (C): the bounded re-read's cadence and cap, so a spec asserts them instead of guessing.
2133
+ SIGN_IN_POLL_MS: SIGN_IN_POLL_MS,
2134
+ SIGN_IN_POLL_MAX_TICKS: SIGN_IN_POLL_MAX_TICKS,
2135
+ // picker helpers (D-1/D-2/D-3), used by the phone new-session picker and provable on their own.
2136
+ pickerMode: pickerMode,
2137
+ sheetGroups: sheetGroups,
2138
+ sheetRowHtml: sheetRowHtml,
2139
+ sheetListHtml: sheetListHtml,
2140
+ credentialBadge: credentialBadge,
2141
+ // WP-3 (box-owned sessions): the one sentence for a /delegate pick the box cannot reach, read by
2142
+ // the shared Delegate pane on the phone (PHONE = DESKTOP PARITY).
2143
+ modelsDelegateWarnings: modelsDelegateWarnings,
2144
+ };
2145
+ if (typeof module === 'object' && module.exports) module.exports = out;
2146
+ else root.CtmPhoneMachines = out;
2147
+ })(typeof window !== 'undefined' ? window : this);