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,848 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Skills Studio 2.0: the Edit tab
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // Three panes (Files | Document | Details) and a status bar; at a narrow window one pane at a time, picked above
6
+ // them. The document is the app's code editor (sx-cm.js over public/shared/editor/), one state per open file; the
7
+ // Details pane is the inspector (sx-inspector.js). Files of a skill open in tabs; a binary file opens in a viewer
8
+ // (an image is shown, anything else is described), never as text. ⌘/Ctrl+S saves the file on screen; what is typed
9
+ // while it saves stays unsaved; a file changed elsewhere since it was opened is never overwritten unasked (Keep
10
+ // editing is where the keyboard starts). The preview is the shared Markdown renderer (escaped first, links limited
11
+ // to http, https and mailto), Mermaid blocks drawn by the vendored Mermaid in strict mode.
12
+
13
+ import { studioErrorText } from './sx-errors.js';
14
+ import { createFrontmatter } from '../ss-frontmatter.js';
15
+ import { toEditing } from '../ss-doc.js';
16
+ import { refreshDirty } from '../ss-tabs.js';
17
+ import { revealHidden } from '../ss-unicode.js';
18
+ import { lintArtifact } from '../ss-lint.js';
19
+ import { renderMarkdown } from '../../editor/editor-markdown.js';
20
+ import { dropdownHTML, wireDropdowns, setDropdownValue } from '../../cc-dropdown.js';
21
+ import { html, raw, setHTML, ICONS, formatBytes, formatNumber, base64Url, fileToBase64, rovingFocus } from './sx-dom.js';
22
+ import { createStudioEditor } from './sx-cm.js';
23
+ import { HEAD, openFile, saveTab, reloadTab, closeTab, discardTab, dirtyTabs, noteDraft, dropDraft, putFiles, deleteFile, renameFile, isTextFile, changedElsewhere } from './sx-docs.js';
24
+ import { documentFacts, fileKind, shortDigest } from './sx-editor-model.js';
25
+ import { renderInspector } from './sx-inspector.js';
26
+ import { applySnippet } from './sx-snippets.js';
27
+ import { refreshWorkspaceChrome } from './sx-workspace-view.js';
28
+
29
+ // The frontmatter is read with the copy of `yaml` vendored in /vendor/yaml/ (the server reads it with the same package).
30
+ const YAML_MODULE = '/vendor/yaml/yaml-2.9.1.min.js';
31
+
32
+ let _ed = null; // the editor of the window (one, with a state per document)
33
+ let _edLoading = null;
34
+ let _edPanel = null;
35
+ let _edFailed = null;
36
+ let _fm = null;
37
+ let _fmLoading = null;
38
+ let _ctx = null; // the views' context of the last drawing
39
+ let _shown = null; // { ws, tab } in the editor now
40
+ let _quiet = false; // a state being put in the view: not an edit
41
+ let _inspTimer = 0;
42
+ let _statusTimer = 0;
43
+ let _life = null;
44
+ let _mermaid = null;
45
+ let _blobUrl = null;
46
+ // The Edit tab on screen → the workspace it shows. A save, a read or a timer of one workspace changes the page only
47
+ // while its own Edit is the one drawn: never the tree, the tabs or the status of another skill opened meanwhile.
48
+ const _editOf = new WeakMap();
49
+
50
+ /** Whether `ws`'s Edit tab is the one on screen now. */
51
+ function editShows(ctx, ws) {
52
+ const el = ctx?.panel?.querySelector('.sx-edit');
53
+ return !!el && _editOf.get(el) === ws && !!ws.shown && ws.tab === 'edit';
54
+ }
55
+
56
+ function loadFrontmatter() {
57
+ if (_fm) return Promise.resolve(_fm);
58
+ if (!_fmLoading) {
59
+ _fmLoading = import(YAML_MODULE).then((YAML) => (_fm = createFrontmatter(YAML))).catch((err) => { _fmLoading = null; throw err; });
60
+ }
61
+ return _fmLoading;
62
+ }
63
+
64
+ /** The window closed: its editor goes (the view, its listeners, the timers, an image's object URL). */
65
+ export function disposeEditor() {
66
+ try { _ed?.destroy(); } catch { /* already gone */ }
67
+ // An editor still loading is destroyed as it arrives (ensureEditor: it is no longer the one being loaded).
68
+ _ed = null; _edLoading = null; _edPanel = null; _shown = null; _edFailed = null; _ctx = null;
69
+ clearTimeout(_inspTimer); clearTimeout(_statusTimer);
70
+ _life?.abort(); _life = null;
71
+ if (_blobUrl) { URL.revokeObjectURL(_blobUrl); _blobUrl = null; }
72
+ }
73
+
74
+ /** A workspace closed (or its skill was deleted): the window's editor no longer shows or follows any of its documents. */
75
+ export function forgetEditorWorkspace(ws) {
76
+ if (_shown?.ws !== ws) return;
77
+ _shown = null;
78
+ try { _ed?.clear(); } catch { /* the view is going anyway */ }
79
+ }
80
+
81
+ export const editTabsDirty = (ws) => dirtyTabs(ws).length > 0;
82
+
83
+ function ensureEditor(ctx) {
84
+ if (_ed && _edPanel === ctx.panel) return Promise.resolve(_ed);
85
+ if (_edPanel !== ctx.panel) { try { _ed?.destroy(); } catch { /* */ } _ed = null; _edLoading = null; }
86
+ if (!_edLoading) {
87
+ const parent = document.createElement('div');
88
+ _edPanel = ctx.panel;
89
+ const loading = createStudioEditor({
90
+ parent, t: ctx.t,
91
+ actions: { save: () => { if (_shown && _ctx) saveDocument(_ctx, _shown.ws, _shown.tab); } },
92
+ onUpdate: (update) => onEditorUpdate(update),
93
+ }).then((ed) => {
94
+ // The window closed (or another opened) while it loaded: destroyed at once, never kept.
95
+ if (_edLoading !== loading) { try { ed.destroy(); } catch { /* */ } throw Object.assign(new Error('The window closed.'), { code: 'closed' }); }
96
+ _ed = ed; _edFailed = null; return ed;
97
+ }).catch((err) => { if (_edLoading === loading) { _edLoading = null; _edFailed = err; } throw err; });
98
+ _edLoading = loading;
99
+ }
100
+ return _edLoading;
101
+ }
102
+
103
+ function onEditorUpdate(update) {
104
+ if (_quiet || !_shown) return;
105
+ const { ws, tab } = _shown;
106
+ tab.cmState = update.state;
107
+ if (!update.docChanged) return;
108
+ tab.content = update.state.doc.toString();
109
+ refreshDirty(tab);
110
+ noteDraft(ws, tab);
111
+ const ctx = _ctx;
112
+ if (!ctx) return;
113
+ markDocTab(ctx, ws, tab);
114
+ refreshWorkspaceChrome(ctx, ws);
115
+ scheduleInspector(ctx, ws);
116
+ scheduleStatus(ctx, ws);
117
+ }
118
+
119
+ // ── Layout ──
120
+
121
+ const fileLabel = (path) => revealHidden(path === null ? '' : String(path).split('/').pop());
122
+
123
+ function treeOf(ws) {
124
+ // Folders and files as a nested list, SKILL.md first.
125
+ const root = { dirs: new Map(), files: [] };
126
+ const dirNode = (path) => {
127
+ let node = root;
128
+ for (const part of path.split('/').filter(Boolean)) {
129
+ if (!node.dirs.has(part)) node.dirs.set(part, { name: part, path: node === root ? part : `${node.path}/${part}`, dirs: new Map(), files: [] });
130
+ node = node.dirs.get(part);
131
+ }
132
+ return node;
133
+ };
134
+ for (const d of ws.dirs || []) dirNode(d);
135
+ for (const f of ws.files || []) {
136
+ if (f.path === null) { root.files.push(f); continue; }
137
+ const parts = f.path.split('/');
138
+ const parent = parts.length > 1 ? dirNode(parts.slice(0, -1).join('/')) : root;
139
+ parent.files.push(f);
140
+ }
141
+ return root;
142
+ }
143
+
144
+ function treeHTML(ctx, ws, node, depth = 0) {
145
+ const { t } = ctx;
146
+ const active = ws.tabs[ws.active];
147
+ const files = node.files.slice().sort((a, b) => (a.path === HEAD ? -1 : b.path === HEAD ? 1 : String(a.path).localeCompare(String(b.path))));
148
+ const dirs = [...node.dirs.values()].sort((a, b) => a.name.localeCompare(b.name));
149
+ const canChange = ws.subject.editable && ws.kind === 'skill';
150
+ const fileRow = (f) => {
151
+ const kind = fileKind(f.path || 'x.md', { binary: !!f.binary });
152
+ const tab = ws.tabs.find((tb) => tb.path === f.path);
153
+ const isHead = f.path === HEAD || f.path === null;
154
+ const problems = isHead && ws.lastProblems ? ws.lastProblems.studio.errors.length + ws.lastProblems.studio.warnings.length : 0;
155
+ return html`<li role="none"><div class="sx-tree-row${active && active.path === f.path ? ' is-current' : ''}" style="--depth:${depth}">
156
+ <button type="button" class="sx-tree-item" role="treeitem" data-file="${f.path === null ? '' : f.path}" data-head="${isHead ? '1' : ''}" aria-selected="${active && active.path === f.path ? 'true' : 'false'}" tabindex="-1">
157
+ ${kind.kind === 'image' ? ICONS.image : ICONS.file}<span class="sx-tree-name">${f.path === null ? `${ws.label}.md` : fileLabel(f.path)}</span>
158
+ ${tab?.dirty ? html`<span class="sx-dot" role="img" aria-label="${t('skillsStudio.ws.unsaved')}"></span>` : ''}
159
+ ${problems ? html`<span class="sx-tree-badge sx-tree-badge--warn" aria-label="${ctx.tp('skillsStudio.edit.problemsCount', problems)}">${problems}</span>` : ''}
160
+ ${f.binary || kind.kind === 'image' ? html`<span class="sx-tree-badge">${t(kind.kind === 'image' ? 'skillsStudio.edit.image' : 'skillsStudio.edit.binary')}</span>` : ''}
161
+ </button>
162
+ ${canChange && !isHead ? html`<span class="sx-tree-actions">
163
+ ${ws.subject.mode === 'library' ? html`<button type="button" class="sx-icon-btn sx-icon-btn--sm" data-rename="${f.path}" aria-label="${t('skillsStudio.edit.renameNamed', { name: f.path })}" title="${t('skillsStudio.edit.rename')}">${ICONS.edit}</button>` : ''}
164
+ <button type="button" class="sx-icon-btn sx-icon-btn--sm" data-delete="${f.path}" aria-label="${t('skillsStudio.edit.deleteNamed', { name: f.path })}" title="${t('skillsStudio.edit.delete')}">${ICONS.trash}</button></span>` : ''}
165
+ </div></li>`;
166
+ };
167
+ const dirRow = (d) => html`<li role="none"><div class="sx-tree-row sx-tree-row--dir" style="--depth:${depth}"><span class="sx-tree-item sx-tree-dir" role="treeitem" aria-expanded="true" tabindex="-1">${ICONS.folder}<span class="sx-tree-name">${revealHidden(d.name)}/</span></span></div>
168
+ <ul role="group" class="sx-tree-group">${treeHTML(ctx, ws, d, depth + 1)}</ul></li>`;
169
+ return html`${files.map(fileRow)}${dirs.map(dirRow)}`;
170
+ }
171
+
172
+ function newFileHTML(ctx, ws) {
173
+ const { t } = ctx;
174
+ if (!ws.subject.editable || ws.kind !== 'skill') return '';
175
+ const entries = [
176
+ { value: '', label: t('skillsStudio.edit.newFile'), selected: true, disabled: true },
177
+ { value: 'reference', label: t('skillsStudio.edit.newKind.reference') },
178
+ { value: 'script', label: t('skillsStudio.edit.newKind.script') },
179
+ { value: 'asset', label: t('skillsStudio.edit.newKind.asset') },
180
+ { value: 'other', label: t('skillsStudio.edit.newKind.other') },
181
+ ];
182
+ return html`<div class="sx-tree-tools">
183
+ ${raw(dropdownHTML('sx-new-file', entries, { label: t('skillsStudio.edit.newFile'), classes: 'sx-dd' }))}
184
+ <button type="button" class="sx-btn" data-edit="upload">${ICONS.upload}<span>${t('skillsStudio.edit.upload')}</span></button>
185
+ <input type="file" id="sx-upload-input" hidden>
186
+ </div>`;
187
+ }
188
+
189
+ function docTabsHTML(ctx, ws) {
190
+ const { t } = ctx;
191
+ return html`${ws.tabs.map((tab, i) => html`<div class="sx-doc-tab${i === ws.active ? ' is-active' : ''}${tab.dirty ? ' is-dirty' : ''}" data-doc-index="${i}">
192
+ <button type="button" class="sx-doc-tab-btn" role="tab" aria-selected="${i === ws.active ? 'true' : 'false'}" tabindex="${i === ws.active ? '0' : '-1'}" data-doc="${i}" title="${tab.label}">
193
+ ${tab.dirty ? html`<span class="sx-dot" role="img" aria-label="${t('skillsStudio.ws.unsaved')}"></span>` : ''}<span>${fileLabel(tab.path) || tab.label}</span></button>
194
+ ${tab.path === HEAD || tab.path === null ? '' : html`<button type="button" class="sx-doc-tab-close" data-doc-close="${i}" aria-label="${t('skillsStudio.edit.closeTab', { name: tab.label })}">${ICONS.close}</button>`}
195
+ </div>`)}`;
196
+ }
197
+
198
+ function layoutHTML(ctx, ws) {
199
+ const { t } = ctx;
200
+ const active = ws.tabs[ws.active] || null;
201
+ const md = active && !active.binary && fileKind(active.path || 'x.md').kind === 'markdown';
202
+ const pane = ws.pane || 'doc';
203
+ return html`<div class="sx-edit" data-pane="${pane}">
204
+ <div class="sx-pane-switch" role="group" aria-label="${t('skillsStudio.edit.showPane')}">
205
+ ${['files', 'doc', 'details'].map((p) => html`<button type="button" data-pane-show="${p}" aria-pressed="${pane === p ? 'true' : 'false'}">${t(`skillsStudio.edit.pane.${p}`)}</button>`)}
206
+ </div>
207
+ <div class="sx-panes">
208
+ <aside class="sx-pane sx-pane--files" aria-labelledby="sx-files-h">
209
+ <p class="sx-pane-label" id="sx-files-h">${t('skillsStudio.edit.pane.files')}</p>
210
+ <ul class="sx-tree" role="tree" aria-labelledby="sx-files-h" id="sx-tree">${treeHTML(ctx, ws, treeOf(ws))}</ul>
211
+ ${ws.filesTruncated ? html`<p class="sx-help">${t('skillsStudio.edit.filesTruncated')}</p>` : ''}
212
+ ${newFileHTML(ctx, ws)}
213
+ </aside>
214
+ <section class="sx-pane sx-pane--doc" aria-label="${t('skillsStudio.edit.pane.doc')}">
215
+ <div class="sx-doc-bar">
216
+ <div class="sx-doc-tabs" role="tablist" aria-label="${t('skillsStudio.edit.openFiles')}" id="sx-doc-tabs">${docTabsHTML(ctx, ws)}</div>
217
+ <span class="sx-spacer"></span>
218
+ ${md ? html`<button type="button" class="sx-btn sx-btn--sm" data-edit="preview" aria-pressed="${ws.preview ? 'true' : 'false'}">${ICONS.eye}<span>${t('skillsStudio.edit.preview')}</span></button>` : ''}
219
+ ${active && !active.binary && !active.readOnly ? html`<button type="button" class="sx-btn sx-btn--sm" data-edit="compare" aria-pressed="${ws.compare ? 'true' : 'false'}" title="${t('skillsStudio.edit.compareHelp')}">${ICONS.code}<span>${t('skillsStudio.edit.compare')}</span></button>` : ''}
220
+ </div>
221
+ <div class="sx-doc-body">
222
+ <div class="sx-ed-host" id="sx-ed-host"${active && !active.binary && !ws.preview ? '' : ' hidden'}><p class="sx-help sx-ed-loading" role="status">${t('skillsStudio.edit.loadingEditor')}</p></div>
223
+ <div class="sx-preview" id="sx-preview" tabindex="0" aria-label="${t('skillsStudio.edit.previewLabel')}"${ws.preview && md ? '' : ' hidden'}></div>
224
+ <div class="sx-binary" id="sx-binary"${active?.binary ? '' : ' hidden'}></div>
225
+ </div>
226
+ </section>
227
+ <aside class="sx-pane sx-pane--details" aria-label="${t('skillsStudio.edit.pane.details')}" id="sx-details"></aside>
228
+ </div>
229
+ <div class="sx-statusbar" id="sx-status" role="status" aria-live="polite"></div>
230
+ </div>`;
231
+ }
232
+
233
+ /** Draw the Edit tab of a workspace. */
234
+ export function renderEditTab(panel, ctx, ws) {
235
+ _ctx = ctx;
236
+ _life?.abort();
237
+ _life = new AbortController();
238
+ const signal = _life.signal;
239
+ if (_shown && _shown.ws === ws && _ed) _shown.tab.cmScroll = _ed.snapshotScroll();
240
+ setHTML(panel, layoutHTML(ctx, ws));
241
+ _editOf.set(panel.querySelector('.sx-edit'), ws);
242
+ wireEdit(panel, ctx, ws, signal);
243
+ const tree = panel.querySelector('#sx-tree');
244
+ if (tree) rovingFocus(tree, '.sx-tree-item[data-file]', { signal });
245
+ renderStatus(ctx, ws);
246
+ if (!_fm) loadFrontmatter().then(() => { if (_ctx === ctx && editShows(ctx, ws)) { renderDetails(ctx, ws); scheduleStatus(ctx, ws); } }).catch(() => renderDetails(ctx, ws));
247
+ renderDetails(ctx, ws);
248
+ showActive(ctx, ws);
249
+ if (ws.pendingReveal) {
250
+ const { file, line } = ws.pendingReveal;
251
+ ws.pendingReveal = null;
252
+ (async () => {
253
+ if (file && file !== (ws.tabs[ws.active]?.path)) await openDocument(ctx, ws, file);
254
+ if (line) setTimeout(() => _ed?.reveal(line), 60);
255
+ })();
256
+ }
257
+ }
258
+
259
+ async function showActive(ctx, ws) {
260
+ const panel = ctx.panel;
261
+ const tab = ws.tabs[ws.active];
262
+ const host = panel?.querySelector('#sx-ed-host');
263
+ const preview = panel?.querySelector('#sx-preview');
264
+ const binary = panel?.querySelector('#sx-binary');
265
+ if (!host) return;
266
+ if (!tab) { host.hidden = true; return; }
267
+ if (tab.binary) {
268
+ host.hidden = true;
269
+ if (preview) preview.hidden = true;
270
+ renderBinary(binary, ctx, ws, tab);
271
+ _shown = null;
272
+ return;
273
+ }
274
+ if (binary) binary.hidden = true;
275
+ if (ws.preview && preview && fileKind(tab.path || 'x.md').kind === 'markdown') { host.hidden = true; preview.hidden = false; renderPreview(preview, tab.content); }
276
+ else { host.hidden = false; if (preview) preview.hidden = true; }
277
+ const gen = ctx.gen();
278
+ let ed;
279
+ try { ed = await ensureEditor(ctx); } catch (err) {
280
+ if (!ctx.alive(gen)) return;
281
+ mountFallback(host, ctx, ws, tab, err);
282
+ return;
283
+ }
284
+ if (!ctx.alive(gen) || !host.isConnected || ws.tabs[ws.active] !== tab) return;
285
+ host.querySelector('.sx-ed-loading')?.remove();
286
+ if (ed.view.dom.parentElement !== host) host.append(ed.view.dom);
287
+ if (!tab.cmState) tab.cmState = ed.createState({ text: tab.content, readOnly: tab.readOnly || !ws.subject.editable });
288
+ _quiet = true;
289
+ try { ed.show(tab.cmState, { scroll: tab.cmScroll || null }); } finally { _quiet = false; }
290
+ _shown = { ws, tab };
291
+ ed.remeasure();
292
+ if (!tab.cmLanguage) {
293
+ tab.cmLanguage = true;
294
+ ed.language(tab.path || `${ws.label}.md`).then((support) => {
295
+ if (!support || !tab.cmState) return;
296
+ tab.cmState = ed.apply(_shown?.tab === tab ? null : tab.cmState, { effects: ed.effects.language(support) });
297
+ }).catch(() => {});
298
+ }
299
+ const head = tab.path === HEAD || tab.path === null;
300
+ if (head && !tab.cmLint) {
301
+ tab.cmLint = true;
302
+ ed.lintExtension((text) => diagnosticsFor(ws, text)).then((ext) => {
303
+ if (!ext || !tab.cmState) return;
304
+ tab.cmState = ed.apply(_shown?.tab === tab ? null : tab.cmState, { effects: ed.effects.lint(ext) });
305
+ }).catch(() => {});
306
+ }
307
+ if (ws.compare) applyCompare(ctx, ws, tab, true);
308
+ }
309
+
310
+ /** The editor could not load: a plain text box edits the same document (nothing is lost, nothing is shown wrong). */
311
+ function mountFallback(host, ctx, ws, tab, err) {
312
+ const { t } = ctx;
313
+ setHTML(host, html`<div class="sx-fallback">
314
+ <p class="sx-note sx-note--warn" role="alert">${t('skillsStudio.edit.editorFailed', { error: studioErrorText(ctx.t, err) })} <button type="button" class="sx-link" data-edit="retry-editor">${t('skillsStudio.shell.retry')}</button></p>
315
+ <label class="sx-sr" for="sx-fallback-text">${tab.label}</label>
316
+ <textarea class="sx-input sx-fallback-text" id="sx-fallback-text" spellcheck="false"${tab.readOnly || !ws.subject.editable ? ' readonly' : ''}>${tab.content}</textarea>
317
+ </div>`);
318
+ const area = host.querySelector('#sx-fallback-text');
319
+ area?.addEventListener('input', () => {
320
+ tab.content = area.value;
321
+ tab.cmState = null;
322
+ refreshDirty(tab);
323
+ noteDraft(ws, tab);
324
+ markDocTab(ctx, ws, tab);
325
+ refreshWorkspaceChrome(ctx, ws);
326
+ scheduleInspector(ctx, ws);
327
+ scheduleStatus(ctx, ws);
328
+ });
329
+ area?.addEventListener('keydown', (e) => { if ((ctx.mac ? e.metaKey : e.ctrlKey) && e.key.toLowerCase() === 's') { e.preventDefault(); saveDocument(ctx, ws, tab); } });
330
+ host.querySelector('[data-edit="retry-editor"]')?.addEventListener('click', () => { _edFailed = null; showActive(ctx, ws); });
331
+ _shown = null;
332
+ }
333
+
334
+ function diagnosticsFor(ws, text) {
335
+ if (!_fm) return [];
336
+ try {
337
+ const folderName = ws.kind === 'skill' ? (ws.row.folderName || ws.label) : '';
338
+ return lintArtifact({ type: ws.kind, parsed: _fm.parse(text), folderName }).all;
339
+ } catch { return []; }
340
+ }
341
+
342
+ function renderBinary(host, ctx, ws, tab) {
343
+ if (!host) return;
344
+ const { t, locale } = ctx;
345
+ host.hidden = false;
346
+ if (_blobUrl) { URL.revokeObjectURL(_blobUrl); _blobUrl = null; }
347
+ const kind = fileKind(tab.path || '', { binary: true });
348
+ let viewer = html`<span class="sx-binary-icon" aria-hidden="true">${ICONS.file}</span>`;
349
+ if (kind.kind === 'image' && tab.base64) {
350
+ try { _blobUrl = base64Url(tab.base64, kind.mime); viewer = html`<img class="sx-binary-img" src="${_blobUrl}" alt="${t('skillsStudio.edit.imageAlt', { name: tab.label })}">`; } catch { /* described below */ }
351
+ }
352
+ setHTML(host, html`<div class="sx-binary-view" role="group" aria-label="${tab.label}">
353
+ ${viewer}
354
+ <p class="sx-binary-name">${revealHidden(tab.label)}</p>
355
+ <p class="sx-help">${tab.size ? t('skillsStudio.edit.binaryInfo', { size: formatBytes(tab.size, locale) }) : t('skillsStudio.edit.binaryInfoNoSize')}</p>
356
+ ${tab.path === HEAD ? html`<p class="sx-note sx-note--warn" role="note">${t('skillsStudio.edit.binaryHead')}</p>` : ''}
357
+ ${_blobUrl ? html`<a class="sx-btn" href="${_blobUrl}" download="${String(tab.path || 'file').split('/').pop()}">${ICONS.download}<span>${t('skillsStudio.edit.download')}</span></a>` : ''}
358
+ </div>`);
359
+ }
360
+
361
+ function renderPreview(host, text) {
362
+ // The shared renderer escapes the text before it writes a tag, and links only to http, https, mailto and relative
363
+ // targets (a relative one is a file of this skill: wireEdit opens it here).
364
+ setHTML(host, raw(`<div class="sx-md">${renderMarkdown(text)}</div>`));
365
+ const blocks = host.querySelectorAll('pre > code.lang-mermaid');
366
+ if (!blocks.length) return;
367
+ (async () => {
368
+ try {
369
+ if (!_mermaid) {
370
+ // The copy vendored in /vendor/mermaid/ (tests/vendored-markdown-libs.test.mjs reads this literal path).
371
+ const mod = await import('/vendor/mermaid/mermaid-11.17.2.esm.min.mjs');
372
+ _mermaid = mod.default;
373
+ _mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'dark' });
374
+ }
375
+ let n = 0;
376
+ for (const code of blocks) {
377
+ const source = code.textContent || '';
378
+ try {
379
+ const { svg } = await _mermaid.render(`sx-mmd-${Date.now().toString(36)}-${n++}`, source);
380
+ const box = document.createElement('div');
381
+ box.className = 'sx-mermaid';
382
+ box.innerHTML = svg; // Mermaid in strict mode sanitises what it draws
383
+ code.parentElement.replaceWith(box);
384
+ } catch { /* the code block stays as it is */ }
385
+ }
386
+ } catch { /* Mermaid could not load: the code blocks stay */ }
387
+ })();
388
+ }
389
+
390
+ function renderDetails(ctx, ws) {
391
+ const host = ctx.panel?.querySelector('#sx-details');
392
+ if (!host || !editShows(ctx, ws)) return;
393
+ const head = ws.tabs.find((tb) => tb.path === HEAD || tb.path === null) || null;
394
+ const active = ws.tabs[ws.active] || null;
395
+ renderInspector(host, {
396
+ ctx, ws, fm: _fm, head,
397
+ headShowing: !!head && active === head,
398
+ knownNames: ctx.rows.filter((r) => r.source === 'synabun').map((r) => r.folderName),
399
+ replaceHead: (next, o = {}) => replaceDocumentText(ctx, ws, head, next, o),
400
+ reveal: (line) => setTimeout(() => _ed?.reveal(line), 30),
401
+ openHead: () => { const at = ws.tabs.indexOf(head); if (at >= 0 && at !== ws.active) switchDocument(ctx, ws, at); },
402
+ caret: () => (_shown?.tab === active && _ed ? _ed.caret() : null),
403
+ insertSnippet: (snippet) => insertSnippet(ctx, ws, snippet),
404
+ });
405
+ }
406
+
407
+ function scheduleInspector(ctx, ws) {
408
+ if (!editShows(ctx, ws)) return; // another skill's Edit keeps its own timer
409
+ clearTimeout(_inspTimer);
410
+ const gen = ctx.gen();
411
+ _inspTimer = setTimeout(() => {
412
+ if (!ctx.alive(gen) || !editShows(ctx, ws)) return;
413
+ const host = ctx.panel?.querySelector('#sx-details');
414
+ // A field being typed in is not redrawn under the cursor: the rest follows when it is left.
415
+ if (host && host.contains(document.activeElement) && document.activeElement.matches('input, textarea')) return;
416
+ renderDetails(ctx, ws);
417
+ const tree = ctx.panel?.querySelector('#sx-tree');
418
+ if (tree) setHTML(tree, treeHTML(ctx, ws, treeOf(ws)));
419
+ }, 300);
420
+ }
421
+
422
+ function scheduleStatus(ctx, ws) {
423
+ if (!editShows(ctx, ws)) return; // another skill's Edit keeps its own timer
424
+ clearTimeout(_statusTimer);
425
+ _statusTimer = setTimeout(() => renderStatus(ctx, ws), 120);
426
+ }
427
+
428
+ function renderStatus(ctx, ws) {
429
+ const host = ctx.panel?.querySelector('#sx-status');
430
+ if (!host || !editShows(ctx, ws)) return;
431
+ const { t, locale } = ctx;
432
+ const tab = ws.tabs[ws.active];
433
+ if (!tab) { setHTML(host, ''); return; }
434
+ const facts = documentFacts({ path: tab.path, text: tab.content, eol: tab.eol, mixed: tab.mixedEol, bom: tab.bom, binary: tab.binary, size: tab.size, head: (tab.path === HEAD) });
435
+ const kindWord = t(`skillsStudio.edit.kind.${facts.kind}`);
436
+ const state = tab.saving ? t('skillsStudio.edit.state.saving') : tab.dirty ? t('skillsStudio.edit.state.unsaved')
437
+ : tab.readOnly || !ws.subject.editable ? t('skillsStudio.edit.state.readOnly')
438
+ : ws.subject.mode === 'library' ? t('skillsStudio.edit.state.draftSaved') : t('skillsStudio.edit.state.saved');
439
+ const rev = ws.subject.mode === 'library' ? (ws.revision ? t('skillsStudio.edit.revision', { id: shortDigest(ws.revision.contentDigest) }) : t('skillsStudio.edit.noVersion')) : (tab.saved.revision ? t('skillsStudio.edit.fileRevision', { id: shortDigest(tab.saved.revision) }) : '');
440
+ const parts = [
441
+ html`<span>${fileLabel(tab.path) || tab.label}</span>`,
442
+ html`<span>${kindWord}</span>`,
443
+ facts.binary ? '' : html`<span>${facts.eol}${facts.bom ? ' · BOM' : ''}</span>`,
444
+ facts.binary ? '' : html`<span>${ctx.tp('skillsStudio.edit.lines', facts.lines, { n: formatNumber(facts.lines, locale) })}</span>`,
445
+ facts.binary ? '' : html`<span title="${t('skillsStudio.edit.tokensHelp')}">${t('skillsStudio.edit.tokens', { n: formatNumber(facts.tokens, locale) })}</span>`,
446
+ facts.guidance ? html`<span class="sx-guidance${facts.guidance.over ? ' is-over' : ''}" title="${t('skillsStudio.edit.guidanceHelp', { lines: facts.guidance.maxLines, tokens: formatNumber(facts.guidance.maxTokens, locale) })}"><span class="sx-meter" aria-hidden="true"><i style="width:${Math.round(facts.guidance.ratio * 100)}%"></i></span>${t('skillsStudio.edit.guidance', { tokens: formatNumber(facts.guidance.tokens, locale), max: formatNumber(facts.guidance.maxTokens, locale) })}</span>` : '',
447
+ html`<span>${formatBytes(facts.bytes, locale)}</span>`,
448
+ html`<span class="sx-state">${state}</span>`,
449
+ rev ? html`<span>${rev}</span>` : '',
450
+ ];
451
+ setHTML(host, html`${parts}`);
452
+ }
453
+
454
+ function markDocTab(ctx, ws, tab) {
455
+ const bar = ctx.panel?.querySelector('#sx-doc-tabs');
456
+ if (!bar || !editShows(ctx, ws)) return;
457
+ const at = ws.tabs.indexOf(tab);
458
+ const el = bar.querySelector(`[data-doc-index="${at}"]`);
459
+ if (!el) return;
460
+ const was = el.classList.contains('is-dirty');
461
+ if (was === tab.dirty) return;
462
+ setHTML(bar, docTabsHTML(ctx, ws));
463
+ }
464
+
465
+ // ── Documents ──
466
+
467
+ function switchDocument(ctx, ws, index) {
468
+ if (index === ws.active || index < 0 || index >= ws.tabs.length) return;
469
+ if (_shown && _shown.ws === ws && _ed) _shown.tab.cmScroll = _ed.snapshotScroll();
470
+ ws.active = index;
471
+ ws.compare = false;
472
+ ctx.render();
473
+ }
474
+
475
+ async function openDocument(ctx, ws, path) {
476
+ const gen = ctx.gen();
477
+ try {
478
+ await openFile(ws, path, ctx.api, { ask: (tab) => askRestore(ctx, tab) });
479
+ } catch (err) {
480
+ if (!ctx.alive(gen)) return;
481
+ const words = err.code === 'needs-restart' ? ctx.t('skillsStudio.edit.needsRestart')
482
+ : err.code === 'needs-library' ? ctx.t('skillsStudio.edit.needsLibrary')
483
+ : ctx.t('skillsStudio.edit.openFailed', { file: path, error: studioErrorText(ctx.t, err) });
484
+ ctx.toast(words, 'error');
485
+ return;
486
+ }
487
+ // The workspace it was opened for is shown again only when its Edit is still the one on screen.
488
+ if (!ctx.alive(gen) || !editShows(ctx, ws)) return;
489
+ ws.compare = false;
490
+ ctx.render();
491
+ }
492
+
493
+ async function askRestore(ctx, tab) {
494
+ const { t } = ctx;
495
+ const answer = await ctx.ask({
496
+ title: t('skillsStudio.dialog.restoreTitle'),
497
+ lines: [t('skillsStudio.dialog.restoreChanged', { file: tab.label })],
498
+ buttons: [{ id: 'drop', label: t('skillsStudio.dialog.restoreDrop'), kind: 'danger' }, { id: 'restore', label: t('skillsStudio.dialog.restorePut'), kind: 'primary' }],
499
+ cancelId: null,
500
+ });
501
+ return answer === 'restore';
502
+ }
503
+
504
+ /** Change a document's whole text (a form field, a snippet): through the editor when it shows, one undo step. */
505
+ function replaceDocumentText(ctx, ws, tab, next, { caret = null } = {}) {
506
+ if (!tab || tab.readOnly) return;
507
+ if (_shown?.tab === tab && _ed) { _ed.replaceText(next, { caret }); return; }
508
+ tab.content = next;
509
+ tab.cmState = null; // made again from the text when it shows
510
+ refreshDirty(tab);
511
+ noteDraft(ws, tab);
512
+ markDocTab(ctx, ws, tab);
513
+ refreshWorkspaceChrome(ctx, ws);
514
+ scheduleInspector(ctx, ws);
515
+ }
516
+
517
+ async function insertSnippet(ctx, ws, snippet) {
518
+ const { t } = ctx;
519
+ const active = ws.tabs[ws.active];
520
+ const head = ws.tabs.find((tb) => tb.path === HEAD || tb.path === null);
521
+ const target = snippet.kind === 'frontmatter' ? head : active;
522
+ if (!target || target.readOnly || target.binary) return;
523
+ if (snippet.kind === 'frontmatter' && active !== head) { ctx.toast(t('skillsStudio.snippets.openHeadFirst'), 'error'); return; }
524
+ const caret = _shown?.tab === target && _ed ? _ed.caret() : null;
525
+ if (snippet.kind === 'frontmatter' && !_fm) { try { await loadFrontmatter(); } catch { ctx.toast(t('skillsStudio.snippets.failed.other'), 'error'); return; } }
526
+ // A frontmatter snippet is keys merged by the one-key patch; a body snippet goes in at the caret.
527
+ const res = applySnippet(target.content, snippet, { caret, fm: _fm });
528
+ if (!res.ok) { ctx.toast(t(`skillsStudio.snippets.failed.${['invalid', 'unsupported', 'alias'].includes(res.reason) ? 'yaml' : 'other'}`), 'error'); return; }
529
+ replaceDocumentText(ctx, ws, target, res.text, { caret: res.caret });
530
+ ctx.announce(t('skillsStudio.snippets.inserted', { name: snippet.custom ? snippet.name : t(`skillsStudio.snippets.${snippet.id}.name`) }));
531
+ _ed?.focus();
532
+ }
533
+
534
+ // ── Saving ──
535
+
536
+ /** Save the document on screen (⌘/Ctrl+S, the Save button). */
537
+ export function saveActive(ctx, ws) {
538
+ const tab = ws.tabs[ws.active];
539
+ if (tab) saveDocument(ctx, ws, tab);
540
+ }
541
+
542
+ export async function saveDocument(ctx, ws, tab, { over = null } = {}) {
543
+ const { t } = ctx;
544
+ if (!tab || tab.binary || tab.readOnly || !ws.subject.editable) return false;
545
+ if (!tab.dirty && !over) { ctx.announce(t('skillsStudio.toast.nothingToSave')); return true; }
546
+ const gen = ctx.gen();
547
+ const wasAdopt = ws.subject.mode === 'adopt';
548
+ const pending = saveTab(ws, tab, ctx.api, { over, key: ctx.key });
549
+ refreshWorkspaceChrome(ctx, ws);
550
+ scheduleStatus(ctx, ws);
551
+ let res;
552
+ try { res = await pending; } catch (err) {
553
+ if (ctx.alive(gen)) { ctx.toast(t('skillsStudio.toast.saveFailed', { file: tab.label, error: studioErrorText(ctx.t, err) }), 'error'); refreshWorkspaceChrome(ctx, ws); scheduleStatus(ctx, ws); }
554
+ return false;
555
+ }
556
+ if (!ctx.alive(gen)) return res.status === 'saved';
557
+ // The saved document's workspace may not be the one on screen any more (another skill was opened while it saved):
558
+ // the toast says what happened; its tree, tabs and status are drawn only into its own Edit.
559
+ refreshWorkspaceChrome(ctx, ws);
560
+ scheduleStatus(ctx, ws);
561
+ if (res.status === 'saved') {
562
+ const errors = (res.diagnostics || []).filter((d) => d.severity === 'error').length;
563
+ const msg = res.stillDirty ? t('skillsStudio.toast.savedTypedSince', { file: tab.label })
564
+ : errors ? ctx.tp('skillsStudio.toast.savedWithProblems', errors, { file: tab.label })
565
+ : ws.subject.mode === 'library' ? t('skillsStudio.toast.savedDraft', { file: tab.label }) : t('skillsStudio.toast.saved', { file: tab.label });
566
+ ctx.toast(msg, errors ? 'error' : 'info');
567
+ ctx.announce(msg);
568
+ if (_ed && _shown?.tab === tab) _ed.apply(null, { effects: _ed.effects.saved() });
569
+ if (wasAdopt && ws.subject.mode === 'library') { if (ws.shown) ctx.render(); ctx.reload(); }
570
+ else if (editShows(ctx, ws)) {
571
+ const tree = ctx.panel?.querySelector('#sx-tree');
572
+ if (tree) setHTML(tree, treeHTML(ctx, ws, treeOf(ws)));
573
+ const bar = ctx.panel?.querySelector('#sx-doc-tabs');
574
+ if (bar) setHTML(bar, docTabsHTML(ctx, ws));
575
+ }
576
+ return true;
577
+ }
578
+ if (res.status === 'unchanged') return true;
579
+ return resolveConflict(ctx, ws, tab, res);
580
+ }
581
+
582
+ /** A save found the file changed (or removed) elsewhere: keep editing, take theirs, or write mine over it. */
583
+ async function resolveConflict(ctx, ws, tab, res) {
584
+ const { t } = ctx;
585
+ const gone = res.status === 'gone';
586
+ const buttons = gone
587
+ ? [{ id: 'cancel', label: t('skillsStudio.dialog.keepEditing') }, ...(ws.subject.mode === 'inplace' ? [] : [{ id: 'overwrite', label: t('skillsStudio.dialog.recreate'), kind: 'primary' }])]
588
+ : [{ id: 'cancel', label: t('skillsStudio.dialog.keepEditing') }, { id: 'reload', label: t('skillsStudio.dialog.loadTheirs'), kind: 'danger' }, { id: 'overwrite', label: t('skillsStudio.dialog.overwriteMine'), kind: 'primary' }];
589
+ const answer = await ctx.ask({
590
+ title: t(gone ? 'skillsStudio.dialog.goneTitle' : 'skillsStudio.dialog.conflictTitle'),
591
+ lines: [t(gone ? 'skillsStudio.dialog.goneBody' : 'skillsStudio.dialog.conflictBody', { file: tab.label }), t('skillsStudio.dialog.stillHere')],
592
+ buttons,
593
+ focus: 'cancel',
594
+ });
595
+ if (answer === 'overwrite') return saveDocument(ctx, ws, tab, { over: res.current || 'absent' });
596
+ if (answer === 'reload') {
597
+ const gen = ctx.gen();
598
+ try { await reloadTab(ws, tab, ctx.api); } catch (err) { if (ctx.alive(gen)) ctx.toast(t('skillsStudio.toast.reloadFailed', { error: studioErrorText(ctx.t, err) }), 'error'); return false; }
599
+ tab.cmState = null; tab.cmLanguage = false; tab.cmLint = false;
600
+ if (ctx.alive(gen) && editShows(ctx, ws)) ctx.render();
601
+ }
602
+ return false;
603
+ }
604
+
605
+ /** Ask what happens to a document's unsaved text before it closes. @returns {Promise<boolean>} it may close */
606
+ async function settleTab(ctx, ws, tab) {
607
+ if (!tab.dirty) return true;
608
+ const { t } = ctx;
609
+ const answer = await ctx.ask({
610
+ title: t('skillsStudio.dialog.unsavedTitle'),
611
+ lines: [t('skillsStudio.dialog.unsavedOne', { file: tab.label })],
612
+ buttons: [{ id: 'cancel', label: t('skillsStudio.dialog.cancel') }, { id: 'discard', label: t('skillsStudio.dialog.discard'), kind: 'danger' }, { id: 'save', label: t('skillsStudio.dialog.save'), kind: 'primary' }],
613
+ focus: 'cancel',
614
+ });
615
+ if (answer === 'save') return saveDocument(ctx, ws, tab);
616
+ if (answer === 'discard') { discardTab(ws, tab); return true; }
617
+ return false;
618
+ }
619
+
620
+ // ── Files ──
621
+
622
+ const STARTERS = {
623
+ reference: (n) => ({ path: `references/${n}.md`, content: `# ${n}\n\n` }),
624
+ script: (n) => ({ path: `scripts/${n}.sh`, content: '#!/usr/bin/env bash\nset -euo pipefail\n\n', executable: true }),
625
+ asset: (n) => ({ path: `assets/${n}`, content: '' }),
626
+ other: (n) => ({ path: n, content: '' }),
627
+ };
628
+
629
+ function pathProblem(path, ws) {
630
+ const p = String(path || '').trim();
631
+ if (!p) return 'required';
632
+ if (p.startsWith('/') || p.includes('\\') || p.split('/').some((s) => s === '..' || s === '.' || s === '')) return 'invalid';
633
+ if (p.split('/').some((s) => s.startsWith('.'))) return 'hidden';
634
+ if ((ws.files || []).some((f) => f.path && f.path.toLowerCase() === p.toLowerCase())) return 'exists';
635
+ return null;
636
+ }
637
+
638
+ async function askName(ctx, { title, label, value, ws }) {
639
+ const { t } = ctx;
640
+ const box = document.createElement('div');
641
+ box.className = 'sx-dlg-form';
642
+ const lab = document.createElement('label');
643
+ lab.className = 'sx-field-label';
644
+ lab.htmlFor = 'sx-name-input';
645
+ lab.textContent = label;
646
+ const input = document.createElement('input');
647
+ input.id = 'sx-name-input';
648
+ input.className = 'sx-input';
649
+ input.value = value;
650
+ input.autocomplete = 'off';
651
+ input.spellcheck = false;
652
+ const err = document.createElement('p');
653
+ err.className = 'ss-dlg-error';
654
+ err.setAttribute('role', 'alert');
655
+ box.append(lab, input, err);
656
+ setTimeout(() => { input.focus(); input.select(); }, 0);
657
+ let chosen = null;
658
+ const answer = await ctx.ask({
659
+ title, extra: box,
660
+ buttons: [{ id: 'cancel', label: t('skillsStudio.dialog.cancel') }, { id: 'ok', label: t('skillsStudio.dialog.ok'), kind: 'primary' }],
661
+ accept: (id) => {
662
+ if (id !== 'ok') return true;
663
+ const why = pathProblem(input.value, ws);
664
+ if (why) { err.textContent = t(`skillsStudio.edit.pathProblem.${why}`); return false; }
665
+ chosen = input.value.trim();
666
+ return true;
667
+ },
668
+ });
669
+ return answer === 'ok' ? chosen : null;
670
+ }
671
+
672
+ async function newFile(ctx, ws, kind) {
673
+ const { t } = ctx;
674
+ const make = STARTERS[kind] || STARTERS.other;
675
+ const suggestion = make(kind === 'other' ? 'notes.md' : kind === 'asset' ? 'template.txt' : kind === 'script' ? 'run' : 'guide');
676
+ const path = await askName(ctx, { title: t('skillsStudio.edit.newFileTitle'), label: t(`skillsStudio.edit.newFileLabel.${kind}`), value: suggestion.path, ws });
677
+ if (!path) return;
678
+ const gen = ctx.gen();
679
+ try {
680
+ await putFiles(ws, [{ path, content: kind === 'script' ? STARTERS.script('x').content : (kind === 'reference' && path.endsWith('.md') ? `# ${path.split('/').pop().replace(/\.md$/, '')}\n\n` : ''), executable: kind === 'script' }], ctx.api, { key: ctx.key });
681
+ } catch (err) { if (ctx.alive(gen)) ctx.toast(t('skillsStudio.edit.createFailed', { error: studioErrorText(ctx.t, err) }), 'error'); return; }
682
+ if (!ctx.alive(gen)) return;
683
+ ctx.toast(t('skillsStudio.edit.created', { file: path }));
684
+ await openDocument(ctx, ws, path);
685
+ }
686
+
687
+ async function uploadFile(ctx, ws, file) {
688
+ const { t } = ctx;
689
+ const path = await askName(ctx, { title: t('skillsStudio.edit.uploadTitle'), label: t('skillsStudio.edit.uploadLabel'), value: file.name, ws });
690
+ if (!path) return;
691
+ const gen = ctx.gen();
692
+ try {
693
+ const { base64, bytes } = await fileToBase64(file);
694
+ const item = isTextFile(bytes) ? { path, content: new TextDecoder().decode(bytes) } : { path, contentBase64: base64 };
695
+ await putFiles(ws, [item], ctx.api, { key: ctx.key });
696
+ } catch (err) { if (ctx.alive(gen)) ctx.toast(t('skillsStudio.edit.uploadFailed', { error: studioErrorText(ctx.t, err) }), 'error'); return; }
697
+ if (!ctx.alive(gen)) return;
698
+ ctx.toast(t('skillsStudio.edit.uploaded', { file: path }));
699
+ await openDocument(ctx, ws, path);
700
+ }
701
+
702
+ async function removeFile(ctx, ws, path) {
703
+ const { t } = ctx;
704
+ const open = ws.tabs.find((tb) => tb.path === path);
705
+ const unsaved = !!open?.dirty;
706
+ const answer = await ctx.ask({
707
+ title: t('skillsStudio.edit.deleteTitle', { file: path }),
708
+ lines: [t(ws.subject.mode === 'library' ? 'skillsStudio.edit.deleteFromDraft' : 'skillsStudio.edit.deleteAdopt'), ...(unsaved ? [t('skillsStudio.edit.deleteUnsaved')] : [])],
709
+ buttons: unsaved
710
+ ? [{ id: 'cancel', label: t('skillsStudio.dialog.cancel') }, { id: 'keep', label: t('skillsStudio.edit.deleteKeep') }, { id: 'delete', label: t('skillsStudio.edit.deleteDiscard'), kind: 'danger' }]
711
+ : [{ id: 'cancel', label: t('skillsStudio.dialog.cancel') }, { id: 'delete', label: t('skillsStudio.edit.delete'), kind: 'danger' }],
712
+ focus: 'cancel',
713
+ });
714
+ if (answer !== 'delete' && answer !== 'keep') return;
715
+ const gen = ctx.gen();
716
+ try { await deleteFile(ws, path, ctx.api, { key: ctx.key }); } catch (err) { if (ctx.alive(gen)) ctx.toast(t('skillsStudio.edit.deleteFailed', { error: studioErrorText(ctx.t, err) }), 'error'); return; }
717
+ if (open) {
718
+ if (answer === 'keep' && open.dirty) {
719
+ // The text stays open, unsaved: the next save puts the file back.
720
+ open.saved = { raw: open.saved.raw, revision: null };
721
+ refreshDirty(open);
722
+ noteDraft(ws, open);
723
+ } else { dropDraft(ws, open); closeTab(ws, open); }
724
+ }
725
+ if (!ctx.alive(gen)) return;
726
+ ctx.toast(answer === 'keep' && open ? t('skillsStudio.edit.deletedKept', { file: path }) : t('skillsStudio.edit.deleted', { file: path }));
727
+ if (editShows(ctx, ws)) ctx.render();
728
+ }
729
+
730
+ async function renameDraftFile(ctx, ws, from) {
731
+ const { t } = ctx;
732
+ const to = await askName(ctx, { title: t('skillsStudio.edit.renameTitle', { file: from }), label: t('skillsStudio.edit.renameLabel'), value: from, ws });
733
+ if (!to || to === from) return;
734
+ const gen = ctx.gen();
735
+ try { await renameFile(ws, from, to, ctx.api, { key: ctx.key }); } catch (err) { if (ctx.alive(gen)) ctx.toast(t('skillsStudio.edit.renameFailed', { error: studioErrorText(ctx.t, err) }), 'error'); return; }
736
+ if (!ctx.alive(gen)) return;
737
+ ctx.toast(t('skillsStudio.edit.renamed', { from, to }));
738
+ if (editShows(ctx, ws)) ctx.render();
739
+ }
740
+
741
+ async function applyCompare(ctx, ws, tab, on) {
742
+ if (!_ed || _shown?.tab !== tab) return;
743
+ if (!on) { tab.cmState = _ed.apply(null, { effects: _ed.effects.compare(null) }); return; }
744
+ const ext = await _ed.compareExtension(toEditing(tab.saved.raw).text, ctx.t('skillsStudio.edit.revertChange'));
745
+ if (!ext || _shown?.tab !== tab || !ws.compare) return;
746
+ tab.cmState = _ed.apply(null, { effects: _ed.effects.compare(ext) });
747
+ }
748
+
749
+ function wireEdit(panel, ctx, ws, signal) {
750
+ wireDropdowns(panel, { signal });
751
+ // Escape in the text: the editor's own layers first (its search bar, a completion list, a problem's tooltip); with
752
+ // none open, out of the text to the file's tab, so the next Escape reaches the window. (The shared editor keeps
753
+ // its keys from the page, so without this Escape would do nothing there, and the keyboard could not leave it.)
754
+ panel.addEventListener('keydown', (e) => {
755
+ if (e.key !== 'Escape' || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
756
+ const ed = e.target.closest?.('.cm-editor');
757
+ if (!ed || !panel.contains(ed) || ed.querySelector('.cm-search, .cm-tooltip-autocomplete, .cm-tooltip-lint')) return;
758
+ e.preventDefault();
759
+ e.stopPropagation();
760
+ (panel.querySelector('.sx-doc-tab-btn[aria-selected="true"]') || ctx.panel)?.focus({ preventScroll: true });
761
+ }, { capture: true, signal });
762
+ // A link of the preview to a file of this skill (`references/guide.md`, relative to the file shown) opens it in its
763
+ // tab; a web or mail link opens outside as it is.
764
+ panel.addEventListener('click', (e) => {
765
+ const a = e.target.closest?.('#sx-preview a[href]');
766
+ if (!a) return;
767
+ const href = a.getAttribute('href') || '';
768
+ if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith('//') || href.startsWith('#') || href.startsWith('/')) return;
769
+ e.preventDefault();
770
+ const parts = String(ws.tabs[ws.active]?.path || HEAD).split('/').slice(0, -1);
771
+ try {
772
+ for (const seg of href.split(/[?#]/)[0].split('/')) {
773
+ if (!seg || seg === '.') continue;
774
+ if (seg === '..') parts.pop(); else parts.push(decodeURIComponent(seg));
775
+ }
776
+ } catch { /* a malformed escape: no such file */ }
777
+ const path = parts.join('/');
778
+ if ((ws.files || []).some((f) => f.path === path)) openDocument(ctx, ws, path);
779
+ else ctx.toast(ctx.t('skillsStudio.edit.linkMissing', { file: path || href }), 'error');
780
+ }, { signal });
781
+ panel.querySelector('#sx-new-file')?.addEventListener('change', (e) => {
782
+ const kind = e.target.value;
783
+ setDropdownValue(e.target, '');
784
+ if (kind) newFile(ctx, ws, kind);
785
+ }, { signal });
786
+ const upload = panel.querySelector('#sx-upload-input');
787
+ upload?.addEventListener('change', () => { const f = upload.files?.[0]; upload.value = ''; if (f) uploadFile(ctx, ws, f); }, { signal });
788
+ panel.addEventListener('click', async (e) => {
789
+ const t = e.target;
790
+ const paneBtn = t.closest?.('[data-pane-show]');
791
+ if (paneBtn) { ws.pane = paneBtn.dataset.paneShow; panel.querySelector('.sx-edit').dataset.pane = ws.pane; panel.querySelectorAll('[data-pane-show]').forEach((b) => b.setAttribute('aria-pressed', b === paneBtn ? 'true' : 'false')); if (ws.pane === 'doc') _ed?.remeasure(); return; }
792
+ const fileBtn = t.closest?.('.sx-tree-item[data-file]');
793
+ if (fileBtn) { const path = fileBtn.dataset.head && ws.kind !== 'skill' ? null : fileBtn.dataset.file; const at = ws.tabs.findIndex((tb) => tb.path === path); if (at >= 0) switchDocument(ctx, ws, at); else await openDocument(ctx, ws, path); if (ws.pane === 'files') { ws.pane = 'doc'; } return; }
794
+ const del = t.closest?.('[data-delete]');
795
+ if (del) { removeFile(ctx, ws, del.dataset.delete); return; }
796
+ const ren = t.closest?.('[data-rename]');
797
+ if (ren) { renameDraftFile(ctx, ws, ren.dataset.rename); return; }
798
+ const docBtn = t.closest?.('[data-doc]');
799
+ if (docBtn) { switchDocument(ctx, ws, Number(docBtn.dataset.doc)); return; }
800
+ const closeBtn = t.closest?.('[data-doc-close]');
801
+ if (closeBtn) {
802
+ const tab = ws.tabs[Number(closeBtn.dataset.docClose)];
803
+ if (tab && await settleTab(ctx, ws, tab) && ws.tabs.includes(tab)) { closeTab(ws, tab); if (editShows(ctx, ws)) ctx.render(); }
804
+ return;
805
+ }
806
+ const act = t.closest?.('[data-edit]')?.dataset.edit;
807
+ if (act === 'upload') upload?.click();
808
+ else if (act === 'preview') { ws.preview = !ws.preview; ctx.render(); }
809
+ else if (act === 'compare') { ws.compare = !ws.compare; t.closest('[data-edit]').setAttribute('aria-pressed', ws.compare ? 'true' : 'false'); applyCompare(ctx, ws, ws.tabs[ws.active], ws.compare); }
810
+ }, { signal });
811
+ const docTabs = panel.querySelector('#sx-doc-tabs');
812
+ docTabs?.addEventListener('keydown', (e) => {
813
+ if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
814
+ const at = ws.active + (e.key === 'ArrowRight' ? 1 : -1);
815
+ if (at < 0 || at >= ws.tabs.length) return;
816
+ e.preventDefault();
817
+ switchDocument(ctx, ws, at);
818
+ requestAnimationFrame(() => ctx.panel?.querySelector(`[data-doc="${at}"]`)?.focus());
819
+ }, { signal });
820
+ }
821
+
822
+ /** Keys of the Edit tab beyond the editor's own: none today (⌘/Ctrl+S is handled by the workspace). */
823
+ export function editShortcut() { return false; }
824
+
825
+ /** A change made elsewhere (another window, the Assistant): an open document whose file changed says so, once. */
826
+ export async function onEditExternalChange(ctx, ws, event) {
827
+ const { t } = ctx;
828
+ if (ws.subject.mode === 'library' && ws.draft && (!event.skillId || event.skillId === ws.skill?.skillId)) {
829
+ const gen = ctx.gen();
830
+ let draft;
831
+ try { draft = (await ctx.api.fetchSkillsDraft(ws.draft.draftId)).draft; } catch { return; }
832
+ if (!ctx.alive(gen)) return;
833
+ for (const path of changedElsewhere(ws, draft)) {
834
+ const key = `${draft.etag}:${path}`;
835
+ if (ws.seenElsewhere.has(key)) continue;
836
+ ws.seenElsewhere.add(key);
837
+ ctx.toast(t('skillsStudio.toast.changedElsewhere', { file: path }));
838
+ }
839
+ return;
840
+ }
841
+ if (ws.subject.mode === 'inplace' && typeof event.artifactId === 'string' && event.artifactId === ws.subject.v1Id && typeof event.revision === 'string') {
842
+ const tab = ws.tabs[0];
843
+ if (tab && !tab.saving && tab.saved.revision && tab.saved.revision !== event.revision && !ws.seenElsewhere.has(event.revision)) {
844
+ ws.seenElsewhere.add(event.revision);
845
+ ctx.toast(t('skillsStudio.toast.changedElsewhere', { file: tab.label }));
846
+ }
847
+ }
848
+ }