synabun 2.0.10 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (517) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +25 -2
  3. package/THIRD-PARTY-LICENSES.md +22 -0
  4. package/bin/synabun.js +71 -24
  5. package/docs/whatsapp.md +2 -2
  6. package/hooks/claude-code/pre-websearch.mjs +12 -1
  7. package/hooks/claude-code/shared.mjs +5 -0
  8. package/hooks/claude-code/stop.mjs +1 -1
  9. package/lib/claude-hooks.js +172 -18
  10. package/lib/client-config-repair.js +17 -8
  11. package/lib/desktop-app.js +7 -1
  12. package/lib/external-tools.js +1 -1
  13. package/lib/first-launch-protection.js +57 -12
  14. package/lib/mcp-dist.js +176 -0
  15. package/lib/release-npm-ci.mjs +138 -0
  16. package/lib/release-publish.mjs +91 -0
  17. package/lib/release-scan.mjs +50 -0
  18. package/lib/server-crash-record.js +123 -0
  19. package/lib/server-exit.js +318 -0
  20. package/lib/server-supervisor.js +508 -0
  21. package/lib/start-launcher.js +37 -4
  22. package/lib/tools-cli.js +45 -0
  23. package/lib/update-snapshot.js +228 -0
  24. package/lib/virustotal.mjs +192 -0
  25. package/lib/windows-crash-event.js +269 -0
  26. package/mcp-server/dist/http.d.ts +20 -1
  27. package/mcp-server/dist/http.d.ts.map +1 -1
  28. package/mcp-server/dist/http.js +59 -12
  29. package/mcp-server/dist/http.js.map +1 -1
  30. package/mcp-server/dist/index.d.ts +33 -27
  31. package/mcp-server/dist/index.d.ts.map +1 -1
  32. package/mcp-server/dist/index.js +128 -143
  33. package/mcp-server/dist/index.js.map +1 -1
  34. package/mcp-server/dist/preload.js +1 -1
  35. package/mcp-server/dist/preload.js.map +1 -1
  36. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  37. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  38. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  39. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  40. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  41. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  42. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  43. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  44. package/mcp-server/dist/services/neural-interface.d.ts +16 -10
  45. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  46. package/mcp-server/dist/services/neural-interface.js +67 -11
  47. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  48. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  49. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  50. package/mcp-server/dist/services/server-instructions.js +20 -62
  51. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  52. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  53. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  54. package/mcp-server/dist/services/tool-catalog.js +339 -0
  55. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  56. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  57. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  58. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  59. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  60. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  61. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  62. package/mcp-server/dist/services/tool-list.js +334 -0
  63. package/mcp-server/dist/services/tool-list.js.map +1 -0
  64. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  65. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  66. package/mcp-server/dist/services/tool-runtime.js +400 -0
  67. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  68. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  69. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  70. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  71. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  72. package/mcp-server/dist/tools/agents.d.ts +10 -10
  73. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  74. package/mcp-server/dist/tools/agents.js +22 -13
  75. package/mcp-server/dist/tools/agents.js.map +1 -1
  76. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  77. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  78. package/mcp-server/dist/tools/image-tools.js +1 -1
  79. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  80. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  81. package/mcp-server/dist/tools/skill-tools.js +639 -0
  82. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  83. package/mcp-server/dist/tools/skill.d.ts +7 -0
  84. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  85. package/mcp-server/dist/tools/skill.js +11 -0
  86. package/mcp-server/dist/tools/skill.js.map +1 -0
  87. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  88. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  89. package/mcp-server/dist/tools/tool-groups.js +147 -0
  90. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  91. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  92. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  93. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  94. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  95. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  96. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  97. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  98. package/mcp-server/package-lock.json +2 -2
  99. package/mcp-server/package.json +1 -1
  100. package/mcp-server/src/http.ts +75 -12
  101. package/mcp-server/src/index.ts +147 -154
  102. package/mcp-server/src/preload.ts +12 -12
  103. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  104. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  105. package/mcp-server/src/services/neural-interface.ts +79 -20
  106. package/mcp-server/src/services/server-instructions.ts +27 -63
  107. package/mcp-server/src/services/tool-catalog.ts +403 -0
  108. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  109. package/mcp-server/src/services/tool-list.ts +410 -0
  110. package/mcp-server/src/services/tool-runtime.ts +457 -0
  111. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  112. package/mcp-server/src/tools/agents.ts +22 -14
  113. package/mcp-server/src/tools/image-tools.ts +1 -1
  114. package/mcp-server/src/tools/skill-tools.ts +589 -0
  115. package/mcp-server/src/tools/skill.ts +13 -0
  116. package/mcp-server/src/tools/tool-groups.ts +164 -0
  117. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  118. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  119. package/neural-interface/i18n/en.json +3463 -200
  120. package/neural-interface/i18n/pt-BR.json +3411 -151
  121. package/neural-interface/lib/assistant-api.js +127 -28
  122. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  123. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  124. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  125. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  126. package/neural-interface/lib/assistant-brains/opencode.js +120 -54
  127. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
  128. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
  129. package/neural-interface/lib/assistant-catalog.js +6 -7
  130. package/neural-interface/lib/assistant-dispatch.js +871 -156
  131. package/neural-interface/lib/assistant-envelope.js +3 -1
  132. package/neural-interface/lib/assistant-persona.js +80 -11
  133. package/neural-interface/lib/assistant-plan-permissions.js +10 -9
  134. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  135. package/neural-interface/lib/assistant-route-gate.js +6 -4
  136. package/neural-interface/lib/assistant-router.js +13 -8
  137. package/neural-interface/lib/assistant-runtime.js +56 -18
  138. package/neural-interface/lib/assistant-task-prompt.js +78 -14
  139. package/neural-interface/lib/assistant-tools.js +51 -0
  140. package/neural-interface/lib/backup-service.js +620 -3
  141. package/neural-interface/lib/backup-zip-reader.js +10 -5
  142. package/neural-interface/lib/backup-zip-writer.js +257 -0
  143. package/neural-interface/lib/browser-execution.js +10 -2
  144. package/neural-interface/lib/browser-guard-cdp.js +83 -0
  145. package/neural-interface/lib/browser-network-guard.js +284 -0
  146. package/neural-interface/lib/browser-policy-roots.js +64 -0
  147. package/neural-interface/lib/browser-tool-policy.js +186 -31
  148. package/neural-interface/lib/claude-agent-bridge.js +4 -2
  149. package/neural-interface/lib/claude-hooks-status.js +103 -0
  150. package/neural-interface/lib/claude-temporary.js +5 -11
  151. package/neural-interface/lib/cli-install.js +18 -138
  152. package/neural-interface/lib/cli-resolver.js +4 -1
  153. package/neural-interface/lib/cli-update-planner.js +32 -31
  154. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  155. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  156. package/neural-interface/lib/codex-worker-app-server.js +172 -0
  157. package/neural-interface/lib/confined-fs.js +30 -3
  158. package/neural-interface/lib/crash-notice-api.js +98 -0
  159. package/neural-interface/lib/editor-completion-opencode.js +68 -68
  160. package/neural-interface/lib/exec-loop-command.js +4 -8
  161. package/neural-interface/lib/http-guards.js +8 -2
  162. package/neural-interface/lib/install-details.js +4 -1
  163. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  164. package/neural-interface/lib/mcp-client-config.js +0 -29
  165. package/neural-interface/lib/mcp-external-servers.js +323 -0
  166. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  167. package/neural-interface/lib/morelogin.js +30 -0
  168. package/neural-interface/lib/native-loop-providers.js +547 -47
  169. package/neural-interface/lib/native-loop-runtime.js +268 -41
  170. package/neural-interface/lib/opencode-config.js +557 -0
  171. package/neural-interface/lib/opencode-db-schema.js +17 -34
  172. package/neural-interface/lib/opencode-history.js +48 -218
  173. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  174. package/neural-interface/lib/opencode-native-boundary.js +44 -0
  175. package/neural-interface/lib/opencode-protocol.js +249 -112
  176. package/neural-interface/lib/opencode-runtime-config.js +32 -22
  177. package/neural-interface/lib/opencode-serve-records.js +39 -21
  178. package/neural-interface/lib/opencode-v2-client.js +48 -153
  179. package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
  180. package/neural-interface/lib/opencode-worker-data.js +27 -0
  181. package/neural-interface/lib/opencode2-client.js +73 -37
  182. package/neural-interface/lib/opencode2-proxy.js +23 -63
  183. package/neural-interface/lib/opencode2-translate.js +51 -2
  184. package/neural-interface/lib/owner-authority.js +249 -0
  185. package/neural-interface/lib/page-session.js +275 -0
  186. package/neural-interface/lib/peer-identity.js +763 -0
  187. package/neural-interface/lib/request-gate.js +17 -2
  188. package/neural-interface/lib/rulesets/api.js +1 -1
  189. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  190. package/neural-interface/lib/rulesets/installer.js +92 -27
  191. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  192. package/neural-interface/lib/session-cache-builder.js +17 -30
  193. package/neural-interface/lib/session-title-generator.js +58 -36
  194. package/neural-interface/lib/skills/api-v2.js +495 -0
  195. package/neural-interface/lib/skills/bundle.js +418 -0
  196. package/neural-interface/lib/skills/catalog.js +294 -0
  197. package/neural-interface/lib/skills/creation-api.js +191 -0
  198. package/neural-interface/lib/skills/creation-brief.js +247 -0
  199. package/neural-interface/lib/skills/creation-sources.js +267 -0
  200. package/neural-interface/lib/skills/creation.js +843 -0
  201. package/neural-interface/lib/skills/deploy.js +564 -0
  202. package/neural-interface/lib/skills/digest.js +119 -0
  203. package/neural-interface/lib/skills/discover.js +111 -0
  204. package/neural-interface/lib/skills/draft-confinement.js +214 -0
  205. package/neural-interface/lib/skills/eval-confinement.js +397 -0
  206. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  207. package/neural-interface/lib/skills/eval-trace.js +119 -0
  208. package/neural-interface/lib/skills/evals-api.js +212 -0
  209. package/neural-interface/lib/skills/evals.js +1573 -0
  210. package/neural-interface/lib/skills/export.js +200 -0
  211. package/neural-interface/lib/skills/frontmatter.js +64 -0
  212. package/neural-interface/lib/skills/health.js +75 -0
  213. package/neural-interface/lib/skills/json-edit.js +165 -0
  214. package/neural-interface/lib/skills/known-bridge-digests.json +387 -0
  215. package/neural-interface/lib/skills/known-bridges.js +96 -0
  216. package/neural-interface/lib/skills/library.js +823 -0
  217. package/neural-interface/lib/skills/local-store.js +21 -0
  218. package/neural-interface/lib/skills/migration.js +433 -0
  219. package/neural-interface/lib/skills/operations.js +45 -0
  220. package/neural-interface/lib/skills/ownership.js +427 -0
  221. package/neural-interface/lib/skills/packages.js +88 -0
  222. package/neural-interface/lib/skills/reviews.js +212 -0
  223. package/neural-interface/lib/skills/revision.js +80 -0
  224. package/neural-interface/lib/skills/roots.js +262 -0
  225. package/neural-interface/lib/skills/runtime-checks.js +355 -0
  226. package/neural-interface/lib/skills/scan-assets.js +252 -0
  227. package/neural-interface/lib/skills/scan-context.js +26 -0
  228. package/neural-interface/lib/skills/scan-shell.js +324 -0
  229. package/neural-interface/lib/skills/scan.js +364 -0
  230. package/neural-interface/lib/skills/scanners.js +54 -0
  231. package/neural-interface/lib/skills/service.js +522 -0
  232. package/neural-interface/lib/skills/telemetry.js +113 -0
  233. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  234. package/neural-interface/lib/tool-lists.js +124 -0
  235. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  236. package/neural-interface/lib/worker-confinement.js +1192 -0
  237. package/neural-interface/lib/worker-guard-setting.js +56 -0
  238. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  239. package/neural-interface/lib/worker-relay-ports.js +227 -0
  240. package/neural-interface/lib/worker-resume.js +229 -0
  241. package/neural-interface/lib/worker-sandbox.js +299 -0
  242. package/neural-interface/package-lock.json +18 -12
  243. package/neural-interface/package.json +2 -2
  244. package/neural-interface/public/index.html +3 -1
  245. package/neural-interface/public/index2d.html +19 -11
  246. package/neural-interface/public/onboarding.html +2 -2
  247. package/neural-interface/public/shared/account-menu.js +8 -5
  248. package/neural-interface/public/shared/agent-runtime-options.js +35 -19
  249. package/neural-interface/public/shared/api.js +1527 -1336
  250. package/neural-interface/public/shared/app-frame.js +192 -0
  251. package/neural-interface/public/shared/assistant/asst-agents-dock.js +33 -2
  252. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  253. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  254. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  255. package/neural-interface/public/shared/assistant/asst-brain-picker.js +49 -35
  256. package/neural-interface/public/shared/assistant/asst-control.js +157 -26
  257. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  258. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  259. package/neural-interface/public/shared/assistant/asst-limit-dock.js +40 -0
  260. package/neural-interface/public/shared/assistant/asst-limit-wait.js +294 -0
  261. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  262. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  263. package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
  264. package/neural-interface/public/shared/assistant/asst-panel.js +106 -11
  265. package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
  266. package/neural-interface/public/shared/assistant/asst-render.js +401 -77
  267. package/neural-interface/public/shared/assistant/asst-route.js +83 -12
  268. package/neural-interface/public/shared/assistant/asst-scenes.js +2 -0
  269. package/neural-interface/public/shared/assistant/asst-sidepanel.js +67 -4
  270. package/neural-interface/public/shared/assistant/asst-slash.js +9 -7
  271. package/neural-interface/public/shared/assistant/asst-state.js +11 -15
  272. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  273. package/neural-interface/public/shared/assistant/asst-styles.js +59 -21
  274. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +156 -16
  275. package/neural-interface/public/shared/automation-context.js +5 -2
  276. package/neural-interface/public/shared/cc-dropdown.js +89 -19
  277. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  278. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  279. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  280. package/neural-interface/public/shared/cdx/cdx-output.js +8 -7
  281. package/neural-interface/public/shared/cdx/cdx-panel.js +45 -149
  282. package/neural-interface/public/shared/cdx/cdx-protocol.js +28 -1
  283. package/neural-interface/public/shared/cdx/cdx-render.js +47 -29
  284. package/neural-interface/public/shared/cdx/cdx-requests.js +3 -3
  285. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  286. package/neural-interface/public/shared/cdx/cdx-styles.js +11 -3
  287. package/neural-interface/public/shared/cdx/cdx-tabs.js +52 -39
  288. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  289. package/neural-interface/public/shared/cli-install-view.js +4 -8
  290. package/neural-interface/public/shared/cli-status.js +2 -2
  291. package/neural-interface/public/shared/constants.js +2 -2
  292. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  293. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  294. package/neural-interface/public/shared/context-menu-model.js +366 -0
  295. package/neural-interface/public/shared/context-menu.js +372 -0
  296. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  297. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  298. package/neural-interface/public/shared/desktop-update-view.js +20 -0
  299. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  300. package/neural-interface/public/shared/html-shell.js +9 -11
  301. package/neural-interface/public/shared/i18n.js +2 -0
  302. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  303. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  304. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  305. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
  306. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  307. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  308. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  309. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  310. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  311. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  312. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  313. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +47 -252
  314. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  315. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
  316. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +6 -21
  317. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
  318. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  319. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
  320. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  321. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  322. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  323. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  324. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +0 -21
  325. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  326. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +14 -47
  327. package/neural-interface/public/shared/page-session.js +96 -0
  328. package/neural-interface/public/shared/registry.js +5 -3
  329. package/neural-interface/public/shared/retired-2d.js +3 -0
  330. package/neural-interface/public/shared/schedules-view.js +195 -61
  331. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  332. package/neural-interface/public/shared/settings/settings-data.js +3 -1
  333. package/neural-interface/public/shared/settings/settings-ia.js +6 -6
  334. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  335. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  336. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  337. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  338. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  339. package/neural-interface/public/shared/skills/ss-creation.js +219 -0
  340. package/neural-interface/public/shared/skills/ss-dialog.js +130 -0
  341. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  342. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  343. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  344. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  345. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  346. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  347. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  348. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  349. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  350. package/neural-interface/public/shared/skills/ss-lint.js +141 -0
  351. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  352. package/neural-interface/public/shared/skills/ss-profiles.js +475 -0
  353. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  354. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  355. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  356. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  357. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  358. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  359. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  360. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +236 -0
  361. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  362. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  363. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  364. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  365. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  366. package/neural-interface/public/shared/skills/studio/sx-dom.js +229 -0
  367. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  368. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  369. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  370. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  371. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  372. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  373. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +279 -0
  374. package/neural-interface/public/shared/skills/studio/sx-install.js +296 -0
  375. package/neural-interface/public/shared/skills/studio/sx-library-view.js +639 -0
  376. package/neural-interface/public/shared/skills/studio/sx-library.js +475 -0
  377. package/neural-interface/public/shared/skills/studio/sx-main.js +681 -0
  378. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  379. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  380. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  381. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  382. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  383. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  384. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  385. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  386. package/neural-interface/public/shared/skills/studio/sx-subject.js +248 -0
  387. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  388. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  389. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  390. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +425 -0
  391. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  392. package/neural-interface/public/shared/styles.css +1525 -1779
  393. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  394. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  395. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  396. package/neural-interface/public/shared/tool-list-view.js +112 -0
  397. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  398. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  399. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  400. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  401. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  402. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  403. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  404. package/neural-interface/public/shared/tour/tour.css +144 -0
  405. package/neural-interface/public/shared/ui-automation-studio.js +91 -128
  406. package/neural-interface/public/shared/ui-claude-panel.js +77 -5
  407. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  408. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  409. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  410. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  411. package/neural-interface/public/shared/ui-detail.js +15 -3
  412. package/neural-interface/public/shared/ui-image-gallery.js +50 -47
  413. package/neural-interface/public/shared/ui-keybinds.js +26 -1
  414. package/neural-interface/public/shared/ui-menubar.js +8 -18
  415. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  416. package/neural-interface/public/shared/ui-native-loop-router.js +7 -1
  417. package/neural-interface/public/shared/ui-navbar.js +4 -24
  418. package/neural-interface/public/shared/ui-panels.js +96 -65
  419. package/neural-interface/public/shared/ui-schedules-studio.js +507 -423
  420. package/neural-interface/public/shared/ui-sessions.js +5 -3
  421. package/neural-interface/public/shared/ui-settings.js +594 -462
  422. package/neural-interface/public/shared/ui-sidepanel-sessions.js +1 -1
  423. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  424. package/neural-interface/public/shared/ui-sidepanel-windows.js +16 -11
  425. package/neural-interface/public/shared/ui-skills.js +33 -2830
  426. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  427. package/neural-interface/public/shared/ui-sync.js +15 -5
  428. package/neural-interface/public/shared/ui-terminal.js +124 -180
  429. package/neural-interface/public/shared/ui-tool-updates.js +3 -1
  430. package/neural-interface/public/shared/ui-tooltip.js +10 -5
  431. package/neural-interface/public/shared/ui-trash.js +6 -4
  432. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  433. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  434. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
  435. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  436. package/neural-interface/public/shared/ui-tutorial.js +285 -509
  437. package/neural-interface/public/shared/ui-update.js +38 -2
  438. package/neural-interface/public/shared/ui-whiteboard.js +311 -58
  439. package/neural-interface/public/shared/ui-worker-end.js +4 -0
  440. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  441. package/neural-interface/public/shared/view-variant.js +66 -0
  442. package/neural-interface/public/shared/whatsapp/wa-view.js +1 -1
  443. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  444. package/neural-interface/public/shared/worker-end.js +10 -3
  445. package/neural-interface/public/shared/worker-restart.js +12 -0
  446. package/neural-interface/public/sidepanel-session.html +1 -1
  447. package/neural-interface/public/variant/2d/background.js +2 -1
  448. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  449. package/neural-interface/public/variant/2d/graph.js +42 -20
  450. package/neural-interface/public/variant/2d/hulls.js +14 -9
  451. package/neural-interface/public/variant/2d/lasso.js +11 -7
  452. package/neural-interface/public/variant/2d/main.js +32 -1
  453. package/neural-interface/public/variant/2d/minimap.js +11 -13
  454. package/neural-interface/public/variant/3d/main.js +69 -6
  455. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  456. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  457. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  458. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  459. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  460. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  461. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  462. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  463. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  464. package/neural-interface/scripts/build-vendor.mjs +32 -0
  465. package/neural-interface/server.js +3934 -2454
  466. package/neural-interface/templates/rulesets/core.md +1 -1
  467. package/neural-interface/templates/rulesets/legacy-hashes.json +18 -0
  468. package/neural-interface/templates/rulesets/manifest.json +8 -8
  469. package/npm-shrinkwrap.json +18 -12
  470. package/package.json +16 -3
  471. package/postinstall.js +9 -4
  472. package/preuninstall.js +1 -1
  473. package/setup.js +97 -322
  474. package/skills/skill-forge/SKILL.md +177 -0
  475. package/skills/skill-forge/evals/evals.json +54 -0
  476. package/skills/skill-forge/references/package.md +105 -0
  477. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  478. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  479. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  480. package/skills/synabun/SKILL.md +10 -7
  481. package/skills/synabun/modules/audit.md +1 -5
  482. package/skills/synabun/modules/changelog.md +1 -5
  483. package/skills/synabun/modules/idea.md +1 -1
  484. package/skills/synabun/modules/memorize.md +6 -9
  485. package/skills/synabun/modules/schedule.md +4 -4
  486. package/mcp-server/dist/services/profiles.d.ts +0 -120
  487. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  488. package/mcp-server/dist/services/profiles.js +0 -387
  489. package/mcp-server/dist/services/profiles.js.map +0 -1
  490. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  491. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  492. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  493. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  494. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  495. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  496. package/mcp-server/dist/tools/leonardo.js +0 -21
  497. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  498. package/mcp-server/dist/tools/profile.d.ts +0 -17
  499. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  500. package/mcp-server/dist/tools/profile.js +0 -114
  501. package/mcp-server/dist/tools/profile.js.map +0 -1
  502. package/mcp-server/src/services/profiles.ts +0 -428
  503. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  504. package/mcp-server/src/tools/leonardo.ts +0 -30
  505. package/mcp-server/src/tools/profile.ts +0 -122
  506. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  507. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  508. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  509. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  510. package/skills/leonardo/SKILL.md +0 -168
  511. package/skills/leonardo/icon.svg +0 -1
  512. package/skills/leonardo/modules/browser-guide.md +0 -378
  513. package/skills/leonardo/modules/image-prompter.md +0 -283
  514. package/skills/leonardo/modules/model-advisor.md +0 -100
  515. package/skills/leonardo/modules/prompt-library.md +0 -110
  516. package/skills/leonardo/modules/style-guide.md +0 -186
  517. package/skills/leonardo/modules/video-prompter.md +0 -317
@@ -0,0 +1,328 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Skills Studio 2.0: the Test tab, what it decides (P4)
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // Everything the Test tab decides without a page (sx-test-view.js draws it): whether this workspace can be tested
6
+ // and which version is, the form and its defaults (one press of Run evals runs the train and validation cases once on
7
+ // the first runtime that can, against no skill, within $3 and 30 minutes), the request it sends, the runtime and model
8
+ // choices (the server's runtimes, the Assistant's one model catalog), and what a run shows: the headline per runtime,
9
+ // the case rows, the trigger rows, the suite notes, whether a result is stale, the history and the trial navigation.
10
+ // Strings are the view's (skillsStudio.test.*): this module returns codes and numbers.
11
+
12
+ import { EVALS_PATH, readSuite } from '../ss-evals.js';
13
+ import { BUDGET_CHOICES, EVAL_DEFAULTS, MINUTE_CHOICES, REPETITION_CHOICES, isActiveEvalRun, runtimeKey, suiteCounts } from '../ss-eval-plan.js';
14
+ import { staleness } from '../ss-eval-grade.js';
15
+ import { optimiserSplits } from '../ss-eval-optimise.js';
16
+
17
+ export { isActiveEvalRun, BUDGET_CHOICES, MINUTE_CHOICES, REPETITION_CHOICES };
18
+ export const TEST_KIND_CHOICES = Object.freeze(['both', 'task', 'trigger']);
19
+ export const BASELINE_CHOICES = Object.freeze(['no-skill', 'revision', 'off']);
20
+ export const EXPORT_FORMATS = Object.freeze(['native', 'skill-creator', 'agent-skills']);
21
+ export const TEST_FORM_KEY = 'skills-studio-test-form';
22
+ const PROVIDERS = ['claude-code', 'codex', 'opencode'];
23
+
24
+
25
+ /** Label an executed runtime and retain a visibly different requested target. */
26
+ export function executionLabel(t, actual, requested, label = r => `${r.provider}${r.model ? `/${r.model}` : ''}`) {
27
+ const requests = (Array.isArray(requested) ? requested : [requested]).filter(Boolean);
28
+ const shown = label(actual || requests[0] || {});
29
+ const different = requests.filter(r => r.provider !== actual?.provider || (r.model || null) !== (actual?.model || null));
30
+ return different.length && actual ? t('skillsStudio.test.runtimeActual', { actual: shown, requested: different.map(label).join(', ') }) : shown;
31
+ }
32
+
33
+ /** API errors are codes, never server prose. Unknown failures keep a diagnostic code in the page's language. */
34
+ export function testErrorText(t, error) {
35
+ const code = error?.code || error?.body?.code;
36
+ const keys = {
37
+ WORKER_BROWSER: 'workerBrowser',
38
+ PAGE_SECRET_REQUIRED: 'pageSession', UI_HEADER_REQUIRED: 'pageSession',
39
+ GUEST_FORBIDDEN: 'owner', UI_ONLY: 'owner', WORKERS_NOT_PERMITTED: 'owner',
40
+ ASSISTANT_CREDENTIAL_REQUIRED: 'owner', OWNER_REQUIRED: 'owner',
41
+ runtime_unavailable: 'unavailable', grader_unavailable: 'unavailable',
42
+ model_unavailable: 'unavailable', MODEL_UNKNOWN: 'unavailable', MODEL_DISABLED: 'unavailable',
43
+ codex_not_confirmed: 'codex', not_found: 'missing', revision_not_found: 'missing',
44
+ queue_full: 'queueFull', evals_unavailable: 'notReady', suite_invalid: 'suite',
45
+ optimise_unavailable: 'optimise', time_below_estimate: 'time',
46
+ };
47
+ if (keys[code]) return t(`skillsStudio.test.errors.${keys[code]}`);
48
+ return t('skillsStudio.test.errors.request', { code: typeof code === 'string' && /^[a-zA-Z0-9_-]{1,80}$/.test(code) ? code : 'network' });
49
+ }
50
+
51
+ /**
52
+ * A problem of the suite the page words in its own language: the skillsStudio.test.head.* string and its values for a
53
+ * test file a runtime would load by itself (its settings or code, an instruction file at the top of the test folder,
54
+ * a name another file system reads as another file's), or null (the problem's own words are shown).
55
+ */
56
+ export function suiteErrorCopy(e) {
57
+ if (!e || typeof e.file !== 'string' || !e.file) return null;
58
+ if (e.code === 'file-config') return { key: 'skillsStudio.test.head.fixtureConfig', params: { file: e.file, name: e.name || '' } };
59
+ if (e.code === 'file-instructions') return { key: 'skillsStudio.test.head.fixtureInstructions', params: { file: e.file, name: e.name || '' } };
60
+ if (e.code === 'file-alias') return { key: 'skillsStudio.test.head.fixtureAlias', params: { file: e.file } };
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Whether a workspace can be tested, and which version: a library skill's latest revision (the draft's unsaved or
66
+ * unversioned changes are not in it: the tab says so). → { mode, revisionId, draftChanged }
67
+ * mode: 'ok' | 'older-server' (no `evals` capability yet) | 'not-library' (a folder of a runtime: add it to the
68
+ * library first) | 'no-revision' (a new draft with no version yet) | 'guest'
69
+ */
70
+ export function testSubject(ws, { caps = new Set(), capsKnown = true, guest = false, changedFiles = () => [] } = {}) {
71
+ if (guest) return { mode: 'guest', revisionId: null, draftChanged: false };
72
+ if (capsKnown && !caps.has('evals')) return { mode: 'older-server', revisionId: null, draftChanged: false };
73
+ const s = ws?.subject || {};
74
+ if (s.readsFiles !== 'library' || !ws.skill) return { mode: 'not-library', revisionId: null, draftChanged: false, canAdopt: !!s.canAdopt };
75
+ const revisionId = ws.skill.pointers?.latest || ws.revision?.revisionId || null;
76
+ if (!revisionId) return { mode: 'no-revision', revisionId: null, draftChanged: false };
77
+ const draftChanged = !!(ws.draft && ws.revision && changedFiles(ws.revision.manifest?.files || [], ws.draft.files || []).length);
78
+ return { mode: 'ok', revisionId, draftChanged };
79
+ }
80
+
81
+ /** Whether the tested version has test prompts (its manifest names evals/evals.json). */
82
+ export const hasSuite = (revision) => !!(revision?.manifest?.files || []).some((f) => f.path === EVALS_PATH);
83
+
84
+ /** The suite as the page reads it (the same reader as the server): cases, counts, errors, warnings. */
85
+ export function readSuiteText(text, { skillName = '', description = '' } = {}) {
86
+ const read = readSuite(String(text ?? ''), { skillName, description });
87
+ return { ok: read.ok, errors: read.errors, warnings: read.warnings, cases: read.cases, counts: suiteCounts(read.cases) };
88
+ }
89
+
90
+ /** What the suite allows: a final check needs held-out cases; an optimisation a positive and a near-miss to train on and a validation case. */
91
+ export function suiteAllows(cases) {
92
+ const list = cases || [];
93
+ const optimise = optimiserSplits(list);
94
+ return {
95
+ run: list.some((c) => c.split !== 'test'),
96
+ final: list.some((c) => c.split === 'test'),
97
+ optimise: optimise.problems.length === 0,
98
+ optimiseProblems: optimise.problems,
99
+ tasks: list.some((c) => c.kind === 'task' && c.split !== 'test'),
100
+ triggers: list.some((c) => c.kind === 'trigger' && c.split !== 'test'),
101
+ };
102
+ }
103
+
104
+ const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
105
+
106
+ /** A runtime the owner opts into per skill version (Codex: its shell can read files anywhere on the computer). */
107
+ export const isOptInRuntime = (provider, options = null) => provider === 'codex' || !!(options?.runtimes || []).find((r) => r.provider === provider && r.optIn);
108
+
109
+ /**
110
+ * The form, from what was kept and what the server can run: the first runtime that can start (Claude Code first; never
111
+ * an opt-in one such as Codex, which the owner picks each time), its cheap default model, both kinds, no-skill
112
+ * baseline, once, $3, 30 minutes, blind comparisons where the cases ask.
113
+ */
114
+ export function defaultForm(options, stored = null) {
115
+ const runtimes = (options?.runtimes || []).filter((r) => r.available);
116
+ const first = runtimes.find((r) => !isOptInRuntime(r.provider, options))?.provider || runtimes[0]?.provider || 'claude-code';
117
+ const s = isObject(stored) ? stored : {};
118
+ const keepRuntime = (v) => typeof v === 'string' && runtimes.some((r) => r.provider === v.split('|')[0]) && !isOptInRuntime(v.split('|')[0], options);
119
+ const picks = Array.isArray(s.runtimes) ? s.runtimes.filter(keepRuntime).slice(0, 3) : [];
120
+ return {
121
+ kinds: TEST_KIND_CHOICES.includes(s.kinds) ? s.kinds : 'both',
122
+ runtimes: picks.length ? [...new Set(picks)] : [first],
123
+ baseline: BASELINE_CHOICES.includes(s.baseline) ? s.baseline : EVAL_DEFAULTS.baseline,
124
+ baselineRevisionId: typeof s.baselineRevisionId === 'string' ? s.baselineRevisionId : '',
125
+ taskReps: REPETITION_CHOICES.includes(s.taskReps) ? s.taskReps : EVAL_DEFAULTS.repetitions.task,
126
+ triggerReps: REPETITION_CHOICES.includes(s.triggerReps) ? s.triggerReps : EVAL_DEFAULTS.repetitions.trigger,
127
+ grader: typeof s.grader === 'string' && (s.grader === '' || keepRuntime(s.grader)) ? s.grader : '',
128
+ budgetUsd: BUDGET_CHOICES.includes(s.budgetUsd) ? s.budgetUsd : EVAL_DEFAULTS.budgetUsd,
129
+ maxMinutes: MINUTE_CHOICES.includes(s.maxMinutes) ? s.maxMinutes : EVAL_DEFAULTS.maxMinutes,
130
+ judge: s.judge !== false,
131
+ };
132
+ }
133
+
134
+ /** What of a form is kept between openings (no revision ids of one skill leak into another's form; Codex is not kept). */
135
+ export const storedForm = (form) => ({ kinds: form.kinds, runtimes: form.runtimes.filter((v) => !isOptInRuntime(String(v).split('|')[0])), baseline: form.baseline === 'revision' ? 'no-skill' : form.baseline, taskReps: form.taskReps, triggerReps: form.triggerReps, grader: isOptInRuntime(String(form.grader || '').split('|')[0]) ? '' : form.grader, budgetUsd: form.budgetUsd, maxMinutes: form.maxMinutes, judge: form.judge });
136
+
137
+ /** Whether a form puts Codex to work (a runtime row, or the grader). */
138
+ export const formUsesCodex = (form) => (form?.runtimes || []).some((v) => String(v).split('|')[0] === 'codex') || String(form?.grader || '').split('|')[0] === 'codex';
139
+
140
+ /**
141
+ * What the Test tab says about Codex for a form and its estimate: null when the form does not use Codex, else
142
+ * { confirmed, at (when the newest confirmation was made), revisionIds (the versions to confirm: the tested one, and
143
+ * the earlier one it is compared with on Codex) }. Unknown until the estimate says: not confirmed.
144
+ */
145
+ export function codexStatus(form, estimate, { revisionId = null } = {}) {
146
+ if (!formUsesCodex(form)) return null;
147
+ const c = estimate?.codex;
148
+ const revisions = Array.isArray(c?.revisions) && c.revisions.length ? c.revisions : [{ revisionId, trusted: false, at: null }];
149
+ const at = revisions.map((r) => r.at).filter(Boolean).sort().pop() || null;
150
+ return { confirmed: c ? c.confirmed === true : false, at, revisionIds: revisions.map((r) => r.revisionId).filter(Boolean), pending: revisions.filter((r) => !r.trusted).map((r) => r.revisionId).filter(Boolean) };
151
+ }
152
+
153
+ /**
154
+ * The safety lines of the Test tab, per runtime the form uses (codes; the view words them): 'held' for Claude Code
155
+ * and OpenCode (only the skill under test, the folder, a shell with no network and no home folder), 'codex' for Codex.
156
+ */
157
+ export function safetyLines(form, options = null) {
158
+ const used = [...new Set((form?.runtimes || []).map((v) => String(v).split('|')[0]).filter(Boolean))];
159
+ const known = (options?.runtimes || []).map((r) => r.provider);
160
+ return used.filter((p) => !known.length || known.includes(p)).map((provider) => ({ provider, code: isOptInRuntime(provider, options) ? 'codex' : 'held', shell: (options?.runtimes || []).find((r) => r.provider === provider)?.shell === true }));
161
+ }
162
+
163
+ /** A runtime value of the form ("provider" or "provider|model") as the request names it. */
164
+ export function runtimeOf(value) {
165
+ const [provider, ...rest] = String(value || '').split('|');
166
+ const model = rest.join('|');
167
+ return PROVIDERS.includes(provider) ? { provider, ...(model ? { model } : {}) } : null;
168
+ }
169
+
170
+ /**
171
+ * The request a press sends. `purpose`: 'evals' (Run evals), 'final' (the held-out check), 'optimise' (the
172
+ * description: triggers only, three repetitions, one runtime). `caseIds`: a rerun of some cases.
173
+ */
174
+ export function requestOf(form, { revisionId, purpose = 'evals', caseIds = null } = {}) {
175
+ const runtimes = form.runtimes.map(runtimeOf).filter(Boolean);
176
+ const body = { revisionId, purpose, runtimes: purpose === 'optimise' ? runtimes.slice(0, 1) : runtimes, budgetUsd: form.budgetUsd, maxMinutes: form.maxMinutes, judge: form.judge };
177
+ if (purpose !== 'optimise') {
178
+ body.kinds = form.kinds === 'both' ? ['task', 'trigger'] : [form.kinds];
179
+ body.baseline = form.baseline;
180
+ if (form.baseline === 'revision' && form.baselineRevisionId) body.baselineRevisionId = form.baselineRevisionId;
181
+ body.repetitions = { task: form.taskReps, trigger: form.triggerReps };
182
+ } else body.repetitions = { trigger: Math.max(3, form.triggerReps) };
183
+ if (form.grader) body.grader = runtimeOf(form.grader);
184
+ if (Array.isArray(caseIds) && caseIds.length) body.caseIds = caseIds;
185
+ return body;
186
+ }
187
+
188
+ /**
189
+ * A runtime dropdown's entries: per provider its default (the server's cheap model) and the catalog's models; a
190
+ * provider that cannot run here is one greyed line with its reason. `words`: { provider(p), providerDefault(p, model),
191
+ * providerOff(p, reason) }.
192
+ */
193
+ export function runtimeEntries({ options, catalog = null, selected = '', words, allowNone = false, noneLabel = '' }) {
194
+ const entries = allowNone ? [{ value: '', label: noneLabel, selected: !selected }] : [];
195
+ for (const row of options?.runtimes || []) {
196
+ const p = row.provider;
197
+ if (!row.available) { entries.push({ group: words.provider(p), options: [{ value: p, label: words.providerOff(p, row.reason || ''), disabled: true, selected: false }] }); continue; }
198
+ const options_ = [{ value: p, label: words.providerDefault(p, row.defaultModel || ''), selected: selected === p }];
199
+ for (const m of (Array.isArray(catalog?.[p]) ? catalog[p] : []).slice(0, 60)) {
200
+ if (!m?.id || m.status === 'unavailable') continue;
201
+ const value = `${p}|${m.id}`;
202
+ options_.push({ value, label: String(m.label || m.id), selected: selected === value });
203
+ }
204
+ entries.push({ group: words.provider(p), options: options_ });
205
+ }
206
+ if (selected && !entries.some((e) => (e.options || [e]).some((o) => o.value === selected))) {
207
+ const r = runtimeOf(selected);
208
+ if (r) entries.push({ value: selected, label: `${words.provider(r.provider)}${r.model ? ` · ${r.model}` : ''}`, selected: true });
209
+ }
210
+ return entries;
211
+ }
212
+
213
+ /** The trials of a run grouped for its live list: per case, its trials in order. */
214
+ export function trialGroups(run) {
215
+ const groups = new Map();
216
+ for (const t of run?.trials || []) {
217
+ const key = `${t.kind}:${t.caseId}`;
218
+ if (!groups.has(key)) groups.set(key, { kind: t.kind, caseId: t.caseId, split: t.split, trials: [] });
219
+ groups.get(key).trials.push(t);
220
+ }
221
+ return [...groups.values()];
222
+ }
223
+
224
+ /** The state word of a trial for its dot and its label: running, passed, failed, picked, skipped… */
225
+ export function trialTone(t) {
226
+ if (t.status === 'running') return 'running';
227
+ if (t.status === 'pending') return 'pending';
228
+ if (['skipped', 'cancelled'].includes(t.status)) return 'skipped';
229
+ if (t.status !== 'completed') return 'error';
230
+ if (t.kind === 'trigger') {
231
+ const o = t.trigger?.observed ?? t.observed;
232
+ if (o === null || o === undefined) return 'error';
233
+ return o === t.expected || (t.expected === undefined && o) ? 'pass' : 'fail';
234
+ }
235
+ const rate = t.grade?.passRate ?? t.passRate;
236
+ if (rate === null || rate === undefined) return 'neutral';
237
+ return rate >= 0.999 ? 'pass' : rate > 0 ? 'partial' : 'fail';
238
+ }
239
+
240
+ /** How long a run has worked so far, in seconds. */
241
+ export function elapsedSeconds(run, now = Date.now()) {
242
+ const from = Date.parse(run?.startedAt || run?.createdAt || '');
243
+ if (!Number.isFinite(from)) return 0;
244
+ const to = run?.finishedAt ? Date.parse(run.finishedAt) : now;
245
+ return Math.max(0, Math.round((to - from) / 1000));
246
+ }
247
+ export const clockText = (s) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
248
+
249
+ /**
250
+ * What a finished (or partial) run shows: one headline per runtime, the task case rows, the trigger rows, the notes
251
+ * about the suite, and why it is stale. Numbers are left as numbers (or null: not measured).
252
+ */
253
+ export function reportModel(run, { latestRevisionId = null, latestSuiteDigest = null } = {}) {
254
+ const summary = run?.summary || { runtimes: [], notes: [] };
255
+ const stale = staleness(run, { latestRevisionId, latestSuiteDigest });
256
+ return {
257
+ stale,
258
+ runtimes: (summary.runtimes || []).map((r) => ({
259
+ key: r.key, provider: r.provider, model: r.model, requestedRuntimes: r.requestedRuntimes || [],
260
+ task: r.task ? {
261
+ candidate: r.task.candidate?.mean ?? null, baseline: r.task.baseline?.mean ?? null, delta: r.task.delta, pairs: r.task.pairs,
262
+ n: r.task.candidate?.n || 0, sd: r.task.candidate?.sd ?? null,
263
+ cases: r.task.cases.map((c) => ({ ...c, candidateRate: c.candidate?.mean ?? null, baselineRate: c.baseline?.mean ?? null })),
264
+ } : null,
265
+ trigger: r.trigger,
266
+ preference: r.preference, costUsd: r.costUsd, durationMs: r.durationMs?.mean ?? null, failedRuns: r.failedRuns, skipped: r.skipped,
267
+ })),
268
+ notes: summary.notes || [],
269
+ baseline: run?.request?.baseline || 'no-skill',
270
+ };
271
+ }
272
+
273
+ /** A history row: the run, whether it tested the version as it is now, its headline numbers. */
274
+ export function historyRows(list, { latestRevisionId = null, latestSuiteDigest = null } = {}) {
275
+ return (list || []).map((r) => {
276
+ const first = r.summary?.runtimes?.[0] || null;
277
+ return {
278
+ evalRunId: r.evalRunId, state: r.state, createdAt: r.createdAt, purpose: r.request?.purpose || 'evals',
279
+ runtimeDetails: r.summary?.runtimes?.length ? r.summary.runtimes : r.request?.runtimes || [],
280
+ runtimes: (r.summary?.runtimes?.length ? r.summary.runtimes : r.request?.runtimes || []).map(runtimeKey), costUsd: r.spend?.usd ?? null,
281
+ stale: staleness(r, { latestRevisionId, latestSuiteDigest }), revisionId: r.skill?.revisionId || null,
282
+ task: first?.task ? { candidate: first.task.candidate?.mean ?? null, baseline: first.task.baseline?.mean ?? null, delta: first.task.delta } : null,
283
+ trigger: first?.trigger ? { passed: first.trigger.passed, total: first.trigger.queries } : null,
284
+ optimise: r.optimise || null, queuePosition: r.queuePosition || null, progress: r.progress || null,
285
+ };
286
+ });
287
+ }
288
+
289
+ /** The trial after (dir 1) or before (dir -1) one, among a run's trials that ran, wrapping around. */
290
+ export function trialStep(run, trialId, dir = 1) {
291
+ const list = (run?.trials || []).filter((t) => !['pending', 'skipped', 'cancelled'].includes(t.status));
292
+ if (!list.length) return null;
293
+ const at = list.findIndex((t) => t.trialId === trialId);
294
+ if (at < 0) return list[0].trialId;
295
+ return list[(at + dir + list.length) % list.length].trialId;
296
+ }
297
+
298
+ /**
299
+ * The same case in the previous finished run of the skill (its answer beside this one's): the newest earlier run with a
300
+ * trial of the same case, configuration and runtime. → { evalRunId, trialId } or null. `runs` are full views.
301
+ */
302
+ export function previousTrial(runs, current, trial) {
303
+ const earlier = (runs || []).filter((r) => r.evalRunId !== current.evalRunId && Date.parse(r.createdAt) < Date.parse(current.createdAt) && Array.isArray(r.trials))
304
+ .sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
305
+ for (const r of earlier) {
306
+ const t = r.trials.find((x) => x.caseId === trial.caseId && x.configuration === trial.configuration && runtimeKey(x.runtime) === runtimeKey(trial.runtime) && x.status === 'completed');
307
+ if (t) return { evalRunId: r.evalRunId, trialId: t.trialId };
308
+ }
309
+ return null;
310
+ }
311
+
312
+ /** The cases of a run worth running again: task cases that failed an assertion or did not finish, triggers read wrong or not at all. */
313
+ export function rerunCases(run) {
314
+ const ids = new Set();
315
+ for (const t of run?.trials || []) {
316
+ if (t.configuration !== 'candidate' || t.split === 'test') continue;
317
+ const tone = trialTone(t);
318
+ if (['fail', 'partial', 'error'].includes(tone)) ids.add(t.caseId);
319
+ }
320
+ return [...ids];
321
+ }
322
+
323
+ /** A pass rate (0…1) as whole percent text; null as an em dash. */
324
+ export const pct = (x) => (x === null || x === undefined || !Number.isFinite(x) ? '—' : `${Math.round(x * 100)}%`);
325
+ /** A difference of pass rates as signed percentage points. */
326
+ export const pts = (x) => (x === null || x === undefined || !Number.isFinite(x) ? '—' : `${x >= 0 ? '+' : '−'}${Math.round(Math.abs(x) * 100)}`);
327
+ /** Dollars with two decimals (four under a cent). */
328
+ export const money = (x) => (x === null || x === undefined || !Number.isFinite(Number(x)) ? '—' : `$${Number(x) < 0.01 && Number(x) > 0 ? Number(x).toFixed(4) : Number(x).toFixed(2)}`);
@@ -0,0 +1,241 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Skills Studio 2.0: the Versions tab
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // A library skill's history: its draft (the working copy, with the files changed since its version) and every
6
+ // revision, newest first, each with its labels (latest, candidate, best, approved, from SynaBun, installed where),
7
+ // who made it, from what, and when. Two revisions are compared file by file (the service's diff); the draft is
8
+ // compared with its version here. A revision comes back as a new draft (the draft that is there is saved as a
9
+ // version first, or replaced: the owner says); candidate and best are marked by the owner. Approved moves only
10
+ // through Review, and SynaBun's own versions only through an update of the app.
11
+
12
+ import { scanText, scanFindingText, scanReasonText } from './sx-scan.js';
13
+ import { studioErrorText } from './sx-errors.js';
14
+ import { dropdownHTML, wireDropdowns } from '../../cc-dropdown.js';
15
+ import { html, raw, setHTML, ICONS, relativeTime, fullTime } from './sx-dom.js';
16
+ import { serviceDiff, changedFiles, lineDiff } from './sx-diff.js';
17
+ import { diffHTML, textDiffModel } from './sx-diff-view.js';
18
+ import { dirtyTabs, resetDocuments, saveVersion } from './sx-docs.js';
19
+ import { shortDigest } from './sx-editor-model.js';
20
+ import { choosePublishRevision } from './sx-plan.js';
21
+ import { adoptNow } from './sx-actions.js';
22
+
23
+ let _life = null;
24
+ const LABELS = ['latest', 'candidate', 'best', 'approved', 'upstream'];
25
+
26
+ function authorWords(t, a) {
27
+ if (!a || a.kind === 'owner') return t('skillsStudio.versions.author.owner');
28
+ if (a.kind === 'system') return t('skillsStudio.versions.author.system');
29
+ const who = a.kind === 'assistant' ? t('skillsStudio.versions.author.assistant') : t('skillsStudio.versions.author.agent');
30
+ return a.model ? `${who} · ${a.model}` : who;
31
+ }
32
+
33
+ function installedOn(ws, revisionId, t) {
34
+ return [...new Set((ws.skill?.installations || []).filter((i) => i.sourceRevisionId === revisionId).map((i) => t(`skillsStudio.runtime.${i.runtime}`)))];
35
+ }
36
+
37
+ function revisionRow(ctx, ws, rev, i, list) {
38
+ const { t, locale } = ctx;
39
+ const labels = (rev.labels || []).filter((l) => LABELS.includes(l));
40
+ const installed = installedOn(ws, rev.revisionId, t);
41
+ const prev = list.slice(i + 1).find((r) => r.integrity !== 'damaged') || null;
42
+ const damaged = rev.integrity === 'damaged';
43
+ const owner = !damaged && !ctx.guest && ws.subject.mode === 'library';
44
+ return html`<li class="sx-version" data-rev="${rev.revisionId}">
45
+ <div class="sx-version-main">
46
+ <span class="sx-version-id" title="${rev.revisionId}">${t('skillsStudio.versions.rev', { id: shortDigest(rev.contentDigest) })}</span>
47
+ ${labels.map((l) => html`<span class="sx-label sx-label--${l}">${t(`skillsStudio.versions.label.${l}`)}</span>`)}
48
+ ${installed.length ? html`<span class="sx-label sx-label--installed" title="${installed.join(', ')}">${t('skillsStudio.versions.label.installed', { where: installed.join(', ') })}</span>` : ''}
49
+ <span class="sx-spacer"></span>
50
+ <time class="sx-dim" datetime="${rev.createdAt}" title="${fullTime(rev.createdAt, locale)}">${relativeTime(rev.createdAt, locale)}</time>
51
+ </div>
52
+ <p class="sx-version-meta">${authorWords(t, rev.author)} · ${t(`skillsStudio.versions.source.${['bundled', 'adopted', 'draft', 'assistant', 'import', 'restore', 'customize', 'local', 'github'].includes(rev.source?.kind) ? rev.source.kind : 'local'}`)}${rev.source?.appVersion ? ` ${rev.source.appVersion}` : ''} · ${ctx.tp('skillsStudio.versions.files', rev.files || 0)}${rev.note && rev.noteCode !== 'bundled' ? html` · <span class="sx-version-note">${rev.note}</span>` : ''}</p>
53
+ ${damaged ? html`<p class="sx-note sx-note--warn" role="status">${t('skillsStudio.errors.revisionDamaged')}</p>` : ''}
54
+ <button type="button" class="sx-btn sx-btn--sm" ${damaged ? raw('disabled') : ''} data-ver="scan" aria-controls="sx-ver-scan" data-rev="${rev.revisionId}">${scanText(t, rev.scan, rev.contentDigest)}</button>
55
+ <div class="sx-version-actions">
56
+ ${prev && !damaged && prev.integrity !== 'damaged' ? html`<button type="button" class="sx-btn sx-btn--sm" data-ver="diff-prev" data-rev="${rev.revisionId}" data-prev="${prev.revisionId}">${t('skillsStudio.versions.diffPrev')}</button>` : ''}
57
+ ${owner ? html`<button type="button" class="sx-btn sx-btn--sm" data-ver="restore" data-rev="${rev.revisionId}">${t('skillsStudio.versions.restore')}</button>
58
+ ${labels.includes('candidate') ? '' : html`<button type="button" class="sx-btn sx-btn--sm sx-btn--ghost" data-ver="candidate" data-rev="${rev.revisionId}">${t('skillsStudio.versions.markCandidate')}</button>`}
59
+ ${labels.includes('best') ? '' : html`<button type="button" class="sx-btn sx-btn--sm sx-btn--ghost" data-ver="best" data-rev="${rev.revisionId}">${t('skillsStudio.versions.markBest')}</button>`}` : ''}
60
+ ${ws.subject.canPublish && !damaged ? html`<button type="button" class="sx-btn sx-btn--sm" data-ver="publish" data-rev="${rev.revisionId}">${t('skillsStudio.versions.publishThis')}</button>` : ''}
61
+ </div>
62
+ </li>`;
63
+ }
64
+
65
+ /** Draw the Versions tab. */
66
+ export function renderVersionsTab(panel, ctx, ws) {
67
+ const { t } = ctx;
68
+ _life?.abort();
69
+ _life = new AbortController();
70
+ const signal = _life.signal;
71
+ if (ws.subject.mode !== 'library' && !(ws.subject.mode === 'readonly' && ws.skill)) {
72
+ const why = ws.subject.mode === 'adopt' ? 'adopt' : ws.subject.mode === 'inplace' ? 'inplace' : 'readonly';
73
+ setHTML(panel, html`<div class="sx-empty"><span class="sx-placeholder-icon" aria-hidden="true">${ICONS.history}</span>
74
+ <h4 class="sx-empty-title">${t('skillsStudio.versions.noneTitle')}</h4><p>${t(`skillsStudio.versions.none.${why}`)}</p>
75
+ ${why === 'adopt' && !ctx.guest ? html`<button type="button" class="sx-btn sx-btn--primary" data-ver="adopt">${t('skillsStudio.ws.adoptNow')}</button>` : ''}</div>`);
76
+ panel.querySelector('[data-ver="adopt"]')?.addEventListener('click', () => adoptNow(ctx, ws), { signal });
77
+ return;
78
+ }
79
+ setHTML(panel, html`<div class="sx-versions"><p class="sx-help" role="status">${t('skillsStudio.versions.loading')}</p></div>`);
80
+ const gen = ctx.gen();
81
+ ctx.api.fetchSkillRevisions(ws.skill.skillId).then((res) => {
82
+ if (!ctx.alive(gen) || !ws.shown || ws.tab !== 'versions') return;
83
+ ws.revisions = (res.revisions || []).slice().sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
84
+ draw(panel, ctx, ws, signal);
85
+ }).catch((err) => {
86
+ if (!ctx.alive(gen) || ws.tab !== 'versions') return;
87
+ setHTML(panel, html`<div class="sx-empty" role="alert"><p>${t('skillsStudio.versions.loadFailed', { error: studioErrorText(ctx.t, err) })}</p></div>`);
88
+ });
89
+ }
90
+
91
+ function draw(panel, ctx, ws, signal) {
92
+ const { t } = ctx;
93
+ const list = ws.revisions || [];
94
+ const comparable = list.filter((r) => r.integrity !== 'damaged');
95
+ const draftChanges = ws.draft && ws.revision ? changedFiles(ws.revision.manifest?.files || [], ws.draft.files || []) : [];
96
+ const pick = (id, label, selected) => raw(dropdownHTML(id, comparable.map((r, i) => ({ value: r.revisionId, label: `${t('skillsStudio.versions.rev', { id: shortDigest(r.contentDigest) })}${r.labels?.length ? ` · ${r.labels.filter((l) => LABELS.includes(l)).map((l) => t(`skillsStudio.versions.label.${l}`)).join(', ')}` : ''}`, selected: selected ? r.revisionId === selected : i === 0 })), { label }));
97
+ setHTML(panel, html`<div class="sx-versions">
98
+ <div class="sx-versions-head">
99
+ <h4 class="sx-section-title">${ctx.tp('skillsStudio.versions.title', list.length)}</h4>
100
+ <span class="sx-spacer"></span>
101
+ ${comparable.length > 1 ? html`<div class="sx-compare-pick"><span class="sx-filter-key">${t('skillsStudio.versions.from')}</span>${pick('sx-ver-from', t('skillsStudio.versions.from'), comparable[1]?.revisionId)}
102
+ <span class="sx-filter-key">${t('skillsStudio.versions.to')}</span>${pick('sx-ver-to', t('skillsStudio.versions.to'), comparable[0]?.revisionId)}
103
+ <button type="button" class="sx-btn sx-btn--sm" data-ver="diff-pick">${t('skillsStudio.versions.compare')}</button></div>` : ''}
104
+ </div>
105
+ <ol class="sx-timeline">
106
+ ${ws.draft ? html`<li class="sx-version sx-version--draft">
107
+ <div class="sx-version-main"><span class="sx-version-id">${t('skillsStudio.versions.draft')}</span><span class="sx-label sx-label--draft">${t('skillsStudio.versions.label.draft')}</span><span class="sx-spacer"></span>
108
+ ${ws.draft.updatedAt ? html`<time class="sx-dim" datetime="${ws.draft.updatedAt}" title="${fullTime(ws.draft.updatedAt, ctx.locale)}">${relativeTime(ws.draft.updatedAt, ctx.locale)}</time>` : ''}</div>
109
+ <p class="sx-version-meta">${draftChanges.length ? ctx.tp('skillsStudio.versions.draftChanged', draftChanges.length) : t('skillsStudio.versions.draftSame')}</p>
110
+ <div class="sx-version-actions">
111
+ ${draftChanges.length ? html`<button type="button" class="sx-btn sx-btn--sm" data-ver="diff-draft">${t('skillsStudio.versions.diffDraft')}</button>` : ''}
112
+ ${draftChanges.length && !ctx.guest ? html`<button type="button" class="sx-btn sx-btn--sm sx-btn--primary" data-ver="snapshot">${t('skillsStudio.ws.saveVersion')}</button>` : ''}
113
+ </div></li>` : ''}
114
+ ${list.map((rev, i) => revisionRow(ctx, ws, rev, i, list))}
115
+ </ol>
116
+ ${list.length ? '' : html`<p class="sx-help">${t('skillsStudio.versions.noRevisions')}</p>`}
117
+ <section class="sx-diff-box" id="sx-ver-scan" aria-live="polite"></section>
118
+ <section class="sx-diff-box" id="sx-ver-diff" aria-live="polite"></section>
119
+ </div>`);
120
+ wireDropdowns(panel, { signal });
121
+ panel.addEventListener('click', (e) => {
122
+ const btn = e.target.closest?.('[data-ver]');
123
+ if (!btn || btn.disabled) return;
124
+ onAction(btn.dataset.ver, btn, panel, ctx, ws, signal);
125
+ }, { signal });
126
+ }
127
+
128
+ async function showDiff(panel, ctx, from, to) {
129
+ const { t } = ctx;
130
+ const box = panel.querySelector('#sx-ver-diff');
131
+ if (!box) return;
132
+ setHTML(box, html`<p class="sx-help">${t('skillsStudio.versions.comparing')}</p>`);
133
+ const gen = ctx.gen();
134
+ try {
135
+ const res = await ctx.api.fetchSkillsDiff(from, to);
136
+ if (!ctx.alive(gen) || !box.isConnected) return;
137
+ setHTML(box, html`<h4 class="sx-section-title">${t('skillsStudio.versions.diffTitle')}</h4>${diffHTML(t, serviceDiff(res.diff))}`);
138
+ } catch (err) { if (box.isConnected) setHTML(box, html`<p class="sx-note sx-note--warn" role="alert">${t('skillsStudio.versions.diffFailed', { error: studioErrorText(ctx.t, err) })}</p>`); }
139
+ }
140
+
141
+ async function showDraftDiff(panel, ctx, ws) {
142
+ const { t } = ctx;
143
+ const box = panel.querySelector('#sx-ver-diff');
144
+ if (!box || !ws.draft || !ws.revision) return;
145
+ setHTML(box, html`<p class="sx-help">${t('skillsStudio.versions.comparing')}</p>`);
146
+ const gen = ctx.gen();
147
+ const changes = changedFiles(ws.revision.manifest?.files || [], ws.draft.files || []);
148
+ const files = [];
149
+ for (const c of changes.slice(0, 30)) {
150
+ const binary = (ws.revision.manifest?.files || []).find((f) => f.path === c.path)?.encoding === 'binary';
151
+ if (binary) { files.push({ path: c.path, status: c.status, hunks: [], added: 0, removed: 0, note: 'binary' }); continue; }
152
+ try {
153
+ const before = c.status === 'added' ? '' : ((await ctx.api.fetchSkillsRevisionFile(ws.revision.revisionId, c.path)).content ?? '');
154
+ const afterRes = c.status === 'removed' ? { content: '' } : await ctx.api.fetchSkillsDraftFile(ws.draft.draftId, c.path);
155
+ if (typeof afterRes.contentBase64 === 'string') { files.push({ path: c.path, status: c.status, hunks: [], added: 0, removed: 0, note: 'binary' }); continue; }
156
+ const d = lineDiff(before, afterRes.content ?? '');
157
+ files.push({ ...textDiffModel(c.path, d).files[0] || { path: c.path, hunks: [], added: 0, removed: 0 }, status: c.status });
158
+ } catch { files.push({ path: c.path, status: c.status, hunks: [], added: 0, removed: 0, note: 'too large to compare line by line' }); }
159
+ }
160
+ if (!ctx.alive(gen) || !box.isConnected) return;
161
+ setHTML(box, html`<h4 class="sx-section-title">${t('skillsStudio.versions.diffDraftTitle')}</h4>${diffHTML(t, { files })}`);
162
+ }
163
+
164
+ async function onAction(act, btn, panel, ctx, ws, signal) {
165
+ const { t, tp } = ctx;
166
+ if (act === 'scan') {
167
+ const box = panel.querySelector('#sx-ver-scan');
168
+ const gen = ctx.gen();
169
+ setHTML(box, html`<p role="status">${t('skillsStudio.scan.loading')}</p>`);
170
+ ws.scanRequest = btn.dataset.rev;
171
+ try {
172
+ const { revision } = await ctx.api.fetchSkillsRevision(btn.dataset.rev);
173
+ if (!ctx.alive(gen) || signal.aborted || !box.isConnected || ws.scanRequest !== btn.dataset.rev) return;
174
+ setHTML(box, html`<h4 class="sx-section-title">${t('skillsStudio.scan.title', { id: shortDigest(revision.contentDigest) })}</h4>
175
+ <p>${scanText(t, revision.scan, revision.contentDigest)}</p>
176
+ <ul class="sx-plain-list">${(revision.scan?.findings || []).map((f) => html`<li>${t(`skillsStudio.scan.severity.${f.severity}`)} · ${f.ruleId} · ${f.file || ''}${f.line ? `:${f.line}` : ''} — ${scanFindingText(t, f)}</li>`)}</ul>
177
+ <ul class="sx-plain-list">${(revision.scan?.coverage?.skipped || []).map((f) => html`<li>${f.path} · ${scanReasonText(t, f.reason)}</li>`)}</ul>`);
178
+ box.scrollIntoView({ block: 'nearest' });
179
+ } catch (err) { if (ctx.alive(gen) && !signal.aborted && box.isConnected && ws.scanRequest === btn.dataset.rev) setHTML(box, html`<p role="alert">${studioErrorText(ctx.t, err)}</p>`); }
180
+ return;
181
+ }
182
+ if (act === 'diff-prev') { showDiff(panel, ctx, btn.dataset.prev, btn.dataset.rev); return; }
183
+ if (act === 'diff-pick') {
184
+ const from = panel.querySelector('#sx-ver-from')?.value; const to = panel.querySelector('#sx-ver-to')?.value;
185
+ if (from && to && from !== to) showDiff(panel, ctx, from, to); else ctx.toast(t('skillsStudio.versions.pickTwo'), 'error');
186
+ return;
187
+ }
188
+ if (act === 'diff-draft') { showDraftDiff(panel, ctx, ws); return; }
189
+ // Publish that version: what was planned for another one is dropped there (a plan installs only what it was asked).
190
+ if (act === 'publish') { choosePublishRevision(ws, btn.dataset.rev, { reviews: ctx.data?.reviews || [] }); ws.tab = 'publish'; ctx.render(); return; }
191
+ if (act === 'snapshot') {
192
+ if (dirtyTabs(ws).length) { ctx.toast(t('skillsStudio.toast.saveFirst'), 'error'); return; }
193
+ const gen = ctx.gen();
194
+ try { const out = await saveVersion(ws, ctx.api, { key: ctx.key }); if (!ctx.alive(gen)) return; ctx.toast(out.unchanged ? t('skillsStudio.toast.versionUnchanged') : t('skillsStudio.toast.versionSaved')); await ctx.reload(); }
195
+ catch (err) { if (ctx.alive(gen)) ctx.toast(t('skillsStudio.toast.versionFailed', { error: studioErrorText(ctx.t, err) }), 'error'); }
196
+ return;
197
+ }
198
+ if (act === 'candidate' || act === 'best') {
199
+ const gen = ctx.gen();
200
+ try {
201
+ const skill = (await ctx.api.fetchSkill(ws.skill.skillId)).skill;
202
+ await ctx.api.moveSkillPointer(ws.skill.skillId, { pointer: act, revisionId: btn.dataset.rev, expectedEtag: skill.etag, reason: 'marked in Skills Studio' });
203
+ if (!ctx.alive(gen)) return;
204
+ ctx.toast(t(`skillsStudio.versions.marked.${act}`));
205
+ ws.skill = (await ctx.api.fetchSkill(ws.skill.skillId)).skill;
206
+ if (ws.shown && ws.tab === 'versions') renderVersionsTab(panel, ctx, ws);
207
+ } catch (err) { if (ctx.alive(gen)) ctx.toast(t('skillsStudio.versions.markFailed', { error: studioErrorText(ctx.t, err) }), 'error'); }
208
+ return;
209
+ }
210
+ if (act === 'restore') {
211
+ // Unsaved text in an open document belongs to the draft being replaced: saved or given up first.
212
+ const dirty = dirtyTabs(ws);
213
+ if (dirty.length) {
214
+ await ctx.ask({ title: t('skillsStudio.versions.restoreBlockedTitle'), lines: [tp('skillsStudio.versions.restoreBlocked', dirty.length)], buttons: [{ id: 'ok', label: t('skillsStudio.dialog.ok'), kind: 'primary' }], cancelId: 'ok' });
215
+ return;
216
+ }
217
+ const changes = ws.draft && ws.revision ? changedFiles(ws.revision.manifest?.files || [], ws.draft.files || []) : [];
218
+ let first = 'replace';
219
+ if (changes.length) {
220
+ first = await ctx.ask({
221
+ title: t('skillsStudio.versions.restoreTitle'),
222
+ lines: [tp('skillsStudio.versions.restoreDraftChanged', changes.length), t('skillsStudio.versions.restoreExplain')],
223
+ buttons: [{ id: 'cancel', label: t('skillsStudio.dialog.cancel') }, { id: 'replace', label: t('skillsStudio.versions.restoreReplace'), kind: 'danger' }, { id: 'save', label: t('skillsStudio.versions.restoreSaveFirst'), kind: 'primary' }],
224
+ focus: 'cancel',
225
+ });
226
+ if (first !== 'replace' && first !== 'save') return;
227
+ }
228
+ const gen = ctx.gen();
229
+ try {
230
+ if (first === 'save') await saveVersion(ws, ctx.api, { key: ctx.key, note: 'kept before a restore' });
231
+ await ctx.api.createSkillsDraft({ skillId: ws.skill.skillId, baseRevisionId: btn.dataset.rev }, { idempotencyKey: ctx.key('restore') });
232
+ if (!ctx.alive(gen)) return;
233
+ resetDocuments(ws);
234
+ ws.tab = 'edit';
235
+ ctx.toast(t('skillsStudio.versions.restored', { id: shortDigest(ws.revisions.find((r) => r.revisionId === btn.dataset.rev)?.contentDigest) }));
236
+ await ctx.reload();
237
+ if (ws.shown) ctx.render();
238
+ } catch (err) { if (ctx.alive(gen)) ctx.toast(t('skillsStudio.versions.restoreFailed', { error: studioErrorText(ctx.t, err) }), 'error'); }
239
+ }
240
+ void signal;
241
+ }