@fraziym/axoniz 0.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 (512) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/agents/definition.d.ts +15 -0
  4. package/dist/agents/definition.d.ts.map +1 -0
  5. package/dist/agents/definition.js +41 -0
  6. package/dist/agents/definition.js.map +1 -0
  7. package/dist/agents/index.d.ts +12 -0
  8. package/dist/agents/index.d.ts.map +1 -0
  9. package/dist/agents/index.js +11 -0
  10. package/dist/agents/index.js.map +1 -0
  11. package/dist/agents/registry.d.ts +17 -0
  12. package/dist/agents/registry.d.ts.map +1 -0
  13. package/dist/agents/registry.js +23 -0
  14. package/dist/agents/registry.js.map +1 -0
  15. package/dist/agents/specialized.d.ts +31 -0
  16. package/dist/agents/specialized.d.ts.map +1 -0
  17. package/dist/agents/specialized.js +75 -0
  18. package/dist/agents/specialized.js.map +1 -0
  19. package/dist/awareness/index.d.ts +14 -0
  20. package/dist/awareness/index.d.ts.map +1 -0
  21. package/dist/awareness/index.js +22 -0
  22. package/dist/awareness/index.js.map +1 -0
  23. package/dist/awareness/service.d.ts +180 -0
  24. package/dist/awareness/service.d.ts.map +1 -0
  25. package/dist/awareness/service.js +789 -0
  26. package/dist/awareness/service.js.map +1 -0
  27. package/dist/cli/entry.d.ts +13 -0
  28. package/dist/cli/entry.d.ts.map +1 -0
  29. package/dist/cli/entry.js +35 -0
  30. package/dist/cli/entry.js.map +1 -0
  31. package/dist/comms/telegram.d.ts +41 -0
  32. package/dist/comms/telegram.d.ts.map +1 -0
  33. package/dist/comms/telegram.js +332 -0
  34. package/dist/comms/telegram.js.map +1 -0
  35. package/dist/core/agent.d.ts +252 -0
  36. package/dist/core/agent.d.ts.map +1 -0
  37. package/dist/core/agent.js +1399 -0
  38. package/dist/core/agent.js.map +1 -0
  39. package/dist/core/auth.d.ts +23 -0
  40. package/dist/core/auth.d.ts.map +1 -0
  41. package/dist/core/auth.js +113 -0
  42. package/dist/core/auth.js.map +1 -0
  43. package/dist/core/authority.d.ts +118 -0
  44. package/dist/core/authority.d.ts.map +1 -0
  45. package/dist/core/authority.js +402 -0
  46. package/dist/core/authority.js.map +1 -0
  47. package/dist/core/backend/async_queue.d.ts +17 -0
  48. package/dist/core/backend/async_queue.d.ts.map +1 -0
  49. package/dist/core/backend/async_queue.js +66 -0
  50. package/dist/core/backend/async_queue.js.map +1 -0
  51. package/dist/core/backend/index.d.ts +24 -0
  52. package/dist/core/backend/index.d.ts.map +1 -0
  53. package/dist/core/backend/index.js +128 -0
  54. package/dist/core/backend/index.js.map +1 -0
  55. package/dist/core/backend/llama_server.d.ts +39 -0
  56. package/dist/core/backend/llama_server.d.ts.map +1 -0
  57. package/dist/core/backend/llama_server.js +182 -0
  58. package/dist/core/backend/llama_server.js.map +1 -0
  59. package/dist/core/backend/llamacpp.d.ts +40 -0
  60. package/dist/core/backend/llamacpp.d.ts.map +1 -0
  61. package/dist/core/backend/llamacpp.js +200 -0
  62. package/dist/core/backend/llamacpp.js.map +1 -0
  63. package/dist/core/backend/lmstudio.d.ts +53 -0
  64. package/dist/core/backend/lmstudio.d.ts.map +1 -0
  65. package/dist/core/backend/lmstudio.js +274 -0
  66. package/dist/core/backend/lmstudio.js.map +1 -0
  67. package/dist/core/backend/ollama.d.ts +25 -0
  68. package/dist/core/backend/ollama.d.ts.map +1 -0
  69. package/dist/core/backend/ollama.js +86 -0
  70. package/dist/core/backend/ollama.js.map +1 -0
  71. package/dist/core/backend/openai.d.ts +27 -0
  72. package/dist/core/backend/openai.d.ts.map +1 -0
  73. package/dist/core/backend/openai.js +84 -0
  74. package/dist/core/backend/openai.js.map +1 -0
  75. package/dist/core/backend/types.d.ts +73 -0
  76. package/dist/core/backend/types.d.ts.map +1 -0
  77. package/dist/core/backend/types.js +137 -0
  78. package/dist/core/backend/types.js.map +1 -0
  79. package/dist/core/chat_store.d.ts +77 -0
  80. package/dist/core/chat_store.d.ts.map +1 -0
  81. package/dist/core/chat_store.js +332 -0
  82. package/dist/core/chat_store.js.map +1 -0
  83. package/dist/core/cli.d.ts +82 -0
  84. package/dist/core/cli.d.ts.map +1 -0
  85. package/dist/core/cli.js +602 -0
  86. package/dist/core/cli.js.map +1 -0
  87. package/dist/core/config.d.ts +127 -0
  88. package/dist/core/config.d.ts.map +1 -0
  89. package/dist/core/config.js +295 -0
  90. package/dist/core/config.js.map +1 -0
  91. package/dist/core/debug.d.ts +8 -0
  92. package/dist/core/debug.d.ts.map +1 -0
  93. package/dist/core/debug.js +8 -0
  94. package/dist/core/debug.js.map +1 -0
  95. package/dist/core/downloader.d.ts +31 -0
  96. package/dist/core/downloader.d.ts.map +1 -0
  97. package/dist/core/downloader.js +180 -0
  98. package/dist/core/downloader.js.map +1 -0
  99. package/dist/core/errors.d.ts +80 -0
  100. package/dist/core/errors.d.ts.map +1 -0
  101. package/dist/core/errors.js +134 -0
  102. package/dist/core/errors.js.map +1 -0
  103. package/dist/core/extras.d.ts +160 -0
  104. package/dist/core/extras.d.ts.map +1 -0
  105. package/dist/core/extras.js +553 -0
  106. package/dist/core/extras.js.map +1 -0
  107. package/dist/core/health.d.ts +27 -0
  108. package/dist/core/health.d.ts.map +1 -0
  109. package/dist/core/health.js +96 -0
  110. package/dist/core/health.js.map +1 -0
  111. package/dist/core/history.d.ts +50 -0
  112. package/dist/core/history.d.ts.map +1 -0
  113. package/dist/core/history.js +276 -0
  114. package/dist/core/history.js.map +1 -0
  115. package/dist/core/index.d.ts +6 -0
  116. package/dist/core/index.d.ts.map +1 -0
  117. package/dist/core/index.js +6 -0
  118. package/dist/core/index.js.map +1 -0
  119. package/dist/core/intelligence/ast_index.d.ts +255 -0
  120. package/dist/core/intelligence/ast_index.d.ts.map +1 -0
  121. package/dist/core/intelligence/ast_index.js +1369 -0
  122. package/dist/core/intelligence/ast_index.js.map +1 -0
  123. package/dist/core/intelligence/confidence.d.ts +113 -0
  124. package/dist/core/intelligence/confidence.d.ts.map +1 -0
  125. package/dist/core/intelligence/confidence.js +298 -0
  126. package/dist/core/intelligence/confidence.js.map +1 -0
  127. package/dist/core/intelligence/context_compressor.d.ts +80 -0
  128. package/dist/core/intelligence/context_compressor.d.ts.map +1 -0
  129. package/dist/core/intelligence/context_compressor.js +245 -0
  130. package/dist/core/intelligence/context_compressor.js.map +1 -0
  131. package/dist/core/intelligence/daemon.d.ts +247 -0
  132. package/dist/core/intelligence/daemon.d.ts.map +1 -0
  133. package/dist/core/intelligence/daemon.js +842 -0
  134. package/dist/core/intelligence/daemon.js.map +1 -0
  135. package/dist/core/intelligence/index.d.ts +60 -0
  136. package/dist/core/intelligence/index.d.ts.map +1 -0
  137. package/dist/core/intelligence/index.js +101 -0
  138. package/dist/core/intelligence/index.js.map +1 -0
  139. package/dist/core/intelligence/predictor.d.ts +246 -0
  140. package/dist/core/intelligence/predictor.d.ts.map +1 -0
  141. package/dist/core/intelligence/predictor.js +590 -0
  142. package/dist/core/intelligence/predictor.js.map +1 -0
  143. package/dist/core/intelligence/reflex.d.ts +84 -0
  144. package/dist/core/intelligence/reflex.d.ts.map +1 -0
  145. package/dist/core/intelligence/reflex.js +344 -0
  146. package/dist/core/intelligence/reflex.js.map +1 -0
  147. package/dist/core/intelligence/self_correction.d.ts +121 -0
  148. package/dist/core/intelligence/self_correction.d.ts.map +1 -0
  149. package/dist/core/intelligence/self_correction.js +327 -0
  150. package/dist/core/intelligence/self_correction.js.map +1 -0
  151. package/dist/core/intelligence/skill_distiller.d.ts +100 -0
  152. package/dist/core/intelligence/skill_distiller.d.ts.map +1 -0
  153. package/dist/core/intelligence/skill_distiller.js +246 -0
  154. package/dist/core/intelligence/skill_distiller.js.map +1 -0
  155. package/dist/core/intelligence/swarm.d.ts +330 -0
  156. package/dist/core/intelligence/swarm.d.ts.map +1 -0
  157. package/dist/core/intelligence/swarm.js +1064 -0
  158. package/dist/core/intelligence/swarm.js.map +1 -0
  159. package/dist/core/intelligence/trajectory.d.ts +107 -0
  160. package/dist/core/intelligence/trajectory.d.ts.map +1 -0
  161. package/dist/core/intelligence/trajectory.js +330 -0
  162. package/dist/core/intelligence/trajectory.js.map +1 -0
  163. package/dist/core/logger.d.ts +111 -0
  164. package/dist/core/logger.d.ts.map +1 -0
  165. package/dist/core/logger.js +640 -0
  166. package/dist/core/logger.js.map +1 -0
  167. package/dist/core/loop.d.ts +123 -0
  168. package/dist/core/loop.d.ts.map +1 -0
  169. package/dist/core/loop.js +423 -0
  170. package/dist/core/loop.js.map +1 -0
  171. package/dist/core/memory.d.ts +81 -0
  172. package/dist/core/memory.d.ts.map +1 -0
  173. package/dist/core/memory.js +328 -0
  174. package/dist/core/memory.js.map +1 -0
  175. package/dist/core/metrics.d.ts +29 -0
  176. package/dist/core/metrics.d.ts.map +1 -0
  177. package/dist/core/metrics.js +109 -0
  178. package/dist/core/metrics.js.map +1 -0
  179. package/dist/core/models.d.ts +51 -0
  180. package/dist/core/models.d.ts.map +1 -0
  181. package/dist/core/models.js +146 -0
  182. package/dist/core/models.js.map +1 -0
  183. package/dist/core/optimizer.d.ts +34 -0
  184. package/dist/core/optimizer.d.ts.map +1 -0
  185. package/dist/core/optimizer.js +131 -0
  186. package/dist/core/optimizer.js.map +1 -0
  187. package/dist/core/persona.d.ts +35 -0
  188. package/dist/core/persona.d.ts.map +1 -0
  189. package/dist/core/persona.js +126 -0
  190. package/dist/core/persona.js.map +1 -0
  191. package/dist/core/rate_limit.d.ts +23 -0
  192. package/dist/core/rate_limit.d.ts.map +1 -0
  193. package/dist/core/rate_limit.js +62 -0
  194. package/dist/core/rate_limit.js.map +1 -0
  195. package/dist/core/runner.d.ts +63 -0
  196. package/dist/core/runner.d.ts.map +1 -0
  197. package/dist/core/runner.js +671 -0
  198. package/dist/core/runner.js.map +1 -0
  199. package/dist/core/stream_parser.d.ts +43 -0
  200. package/dist/core/stream_parser.d.ts.map +1 -0
  201. package/dist/core/stream_parser.js +140 -0
  202. package/dist/core/stream_parser.js.map +1 -0
  203. package/dist/core/tool_schemas.d.ts +26 -0
  204. package/dist/core/tool_schemas.d.ts.map +1 -0
  205. package/dist/core/tool_schemas.js +124 -0
  206. package/dist/core/tool_schemas.js.map +1 -0
  207. package/dist/core/which.d.ts +2 -0
  208. package/dist/core/which.d.ts.map +1 -0
  209. package/dist/core/which.js +35 -0
  210. package/dist/core/which.js.map +1 -0
  211. package/dist/goals/index.d.ts +9 -0
  212. package/dist/goals/index.d.ts.map +1 -0
  213. package/dist/goals/index.js +17 -0
  214. package/dist/goals/index.js.map +1 -0
  215. package/dist/goals/service.d.ts +115 -0
  216. package/dist/goals/service.d.ts.map +1 -0
  217. package/dist/goals/service.js +415 -0
  218. package/dist/goals/service.js.map +1 -0
  219. package/dist/goals/types.d.ts +47 -0
  220. package/dist/goals/types.d.ts.map +1 -0
  221. package/dist/goals/types.js +66 -0
  222. package/dist/goals/types.js.map +1 -0
  223. package/dist/index.d.ts +22 -0
  224. package/dist/index.d.ts.map +1 -0
  225. package/dist/index.js +22 -0
  226. package/dist/index.js.map +1 -0
  227. package/dist/integrations/autonomous_startup.d.ts +51 -0
  228. package/dist/integrations/autonomous_startup.d.ts.map +1 -0
  229. package/dist/integrations/autonomous_startup.js +419 -0
  230. package/dist/integrations/autonomous_startup.js.map +1 -0
  231. package/dist/integrations/goose.d.ts +105 -0
  232. package/dist/integrations/goose.d.ts.map +1 -0
  233. package/dist/integrations/goose.js +408 -0
  234. package/dist/integrations/goose.js.map +1 -0
  235. package/dist/integrations/index.d.ts +21 -0
  236. package/dist/integrations/index.d.ts.map +1 -0
  237. package/dist/integrations/index.js +17 -0
  238. package/dist/integrations/index.js.map +1 -0
  239. package/dist/integrations/mcp.d.ts +73 -0
  240. package/dist/integrations/mcp.d.ts.map +1 -0
  241. package/dist/integrations/mcp.js +307 -0
  242. package/dist/integrations/mcp.js.map +1 -0
  243. package/dist/integrations/mempalace.d.ts +18 -0
  244. package/dist/integrations/mempalace.d.ts.map +1 -0
  245. package/dist/integrations/mempalace.js +25 -0
  246. package/dist/integrations/mempalace.js.map +1 -0
  247. package/dist/integrations/multi_agent.d.ts +82 -0
  248. package/dist/integrations/multi_agent.d.ts.map +1 -0
  249. package/dist/integrations/multi_agent.js +280 -0
  250. package/dist/integrations/multi_agent.js.map +1 -0
  251. package/dist/integrations/unified_memory.d.ts +415 -0
  252. package/dist/integrations/unified_memory.d.ts.map +1 -0
  253. package/dist/integrations/unified_memory.js +1489 -0
  254. package/dist/integrations/unified_memory.js.map +1 -0
  255. package/dist/roles/beru.yaml +48 -0
  256. package/dist/sidecar/client.d.ts +63 -0
  257. package/dist/sidecar/client.d.ts.map +1 -0
  258. package/dist/sidecar/client.js +222 -0
  259. package/dist/sidecar/client.js.map +1 -0
  260. package/dist/sidecar/index.d.ts +7 -0
  261. package/dist/sidecar/index.d.ts.map +1 -0
  262. package/dist/sidecar/index.js +6 -0
  263. package/dist/sidecar/index.js.map +1 -0
  264. package/dist/startup.d.ts +22 -0
  265. package/dist/startup.d.ts.map +1 -0
  266. package/dist/startup.js +194 -0
  267. package/dist/startup.js.map +1 -0
  268. package/dist/tools/_internal.d.ts +90 -0
  269. package/dist/tools/_internal.d.ts.map +1 -0
  270. package/dist/tools/_internal.js +375 -0
  271. package/dist/tools/_internal.js.map +1 -0
  272. package/dist/tools/_smoke.d.ts +2 -0
  273. package/dist/tools/_smoke.d.ts.map +1 -0
  274. package/dist/tools/_smoke.js +203 -0
  275. package/dist/tools/_smoke.js.map +1 -0
  276. package/dist/tools/axodex_tools.d.ts +51 -0
  277. package/dist/tools/axodex_tools.d.ts.map +1 -0
  278. package/dist/tools/axodex_tools.js +181 -0
  279. package/dist/tools/axodex_tools.js.map +1 -0
  280. package/dist/tools/code_tools.d.ts +33 -0
  281. package/dist/tools/code_tools.d.ts.map +1 -0
  282. package/dist/tools/code_tools.js +191 -0
  283. package/dist/tools/code_tools.js.map +1 -0
  284. package/dist/tools/computer_tools.d.ts +59 -0
  285. package/dist/tools/computer_tools.d.ts.map +1 -0
  286. package/dist/tools/computer_tools.js +348 -0
  287. package/dist/tools/computer_tools.js.map +1 -0
  288. package/dist/tools/file_tools.d.ts +51 -0
  289. package/dist/tools/file_tools.d.ts.map +1 -0
  290. package/dist/tools/file_tools.js +424 -0
  291. package/dist/tools/file_tools.js.map +1 -0
  292. package/dist/tools/index.d.ts +13 -0
  293. package/dist/tools/index.d.ts.map +1 -0
  294. package/dist/tools/index.js +13 -0
  295. package/dist/tools/index.js.map +1 -0
  296. package/dist/tools/shell_tools.d.ts +39 -0
  297. package/dist/tools/shell_tools.d.ts.map +1 -0
  298. package/dist/tools/shell_tools.js +163 -0
  299. package/dist/tools/shell_tools.js.map +1 -0
  300. package/dist/tools/web_tools.d.ts +31 -0
  301. package/dist/tools/web_tools.d.ts.map +1 -0
  302. package/dist/tools/web_tools.js +117 -0
  303. package/dist/tools/web_tools.js.map +1 -0
  304. package/dist/voice/index.d.ts +41 -0
  305. package/dist/voice/index.d.ts.map +1 -0
  306. package/dist/voice/index.js +58 -0
  307. package/dist/voice/index.js.map +1 -0
  308. package/dist/voice/light_daemon.d.ts +197 -0
  309. package/dist/voice/light_daemon.d.ts.map +1 -0
  310. package/dist/voice/light_daemon.js +746 -0
  311. package/dist/voice/light_daemon.js.map +1 -0
  312. package/dist/voice/setup.d.ts +63 -0
  313. package/dist/voice/setup.d.ts.map +1 -0
  314. package/dist/voice/setup.js +443 -0
  315. package/dist/voice/setup.js.map +1 -0
  316. package/dist/voice/stt.d.ts +148 -0
  317. package/dist/voice/stt.d.ts.map +1 -0
  318. package/dist/voice/stt.js +361 -0
  319. package/dist/voice/stt.js.map +1 -0
  320. package/dist/voice/tts.d.ts +168 -0
  321. package/dist/voice/tts.d.ts.map +1 -0
  322. package/dist/voice/tts.js +698 -0
  323. package/dist/voice/tts.js.map +1 -0
  324. package/dist/voice/voice_loop.d.ts +101 -0
  325. package/dist/voice/voice_loop.d.ts.map +1 -0
  326. package/dist/voice/voice_loop.js +463 -0
  327. package/dist/voice/voice_loop.js.map +1 -0
  328. package/dist/voice/wake_word.d.ts +153 -0
  329. package/dist/voice/wake_word.d.ts.map +1 -0
  330. package/dist/voice/wake_word.js +458 -0
  331. package/dist/voice/wake_word.js.map +1 -0
  332. package/dist/voice/wav.d.ts +99 -0
  333. package/dist/voice/wav.d.ts.map +1 -0
  334. package/dist/voice/wav.js +306 -0
  335. package/dist/voice/wav.js.map +1 -0
  336. package/dist/web/broker.d.ts +33 -0
  337. package/dist/web/broker.d.ts.map +1 -0
  338. package/dist/web/broker.js +58 -0
  339. package/dist/web/broker.js.map +1 -0
  340. package/dist/web/open_browser.d.ts +2 -0
  341. package/dist/web/open_browser.d.ts.map +1 -0
  342. package/dist/web/open_browser.js +42 -0
  343. package/dist/web/open_browser.js.map +1 -0
  344. package/dist/web/server.d.ts +54 -0
  345. package/dist/web/server.d.ts.map +1 -0
  346. package/dist/web/server.js +831 -0
  347. package/dist/web/server.js.map +1 -0
  348. package/dist/web/static/app.js +4121 -0
  349. package/dist/web/static/index.html +2281 -0
  350. package/dist/web/static/js/MODULE_SUMMARY.md +124 -0
  351. package/dist/web/static/js/a11y.js +165 -0
  352. package/dist/web/static/js/admin.js +2145 -0
  353. package/dist/web/static/js/assistant.js +475 -0
  354. package/dist/web/static/js/calendar/reminders.js +114 -0
  355. package/dist/web/static/js/calendar/utils.js +166 -0
  356. package/dist/web/static/js/calendar.js +3348 -0
  357. package/dist/web/static/js/censor.js +356 -0
  358. package/dist/web/static/js/chat.js +4546 -0
  359. package/dist/web/static/js/chatRenderer.js +2294 -0
  360. package/dist/web/static/js/chatStream.js +276 -0
  361. package/dist/web/static/js/codeRunner.js +398 -0
  362. package/dist/web/static/js/color/hex.js +14 -0
  363. package/dist/web/static/js/colorPicker.js +453 -0
  364. package/dist/web/static/js/compare/icons.js +77 -0
  365. package/dist/web/static/js/compare/index.js +1482 -0
  366. package/dist/web/static/js/compare/models.js +104 -0
  367. package/dist/web/static/js/compare/panes.js +817 -0
  368. package/dist/web/static/js/compare/probe.js +78 -0
  369. package/dist/web/static/js/compare/scoreboard.js +223 -0
  370. package/dist/web/static/js/compare/selector.js +1335 -0
  371. package/dist/web/static/js/compare/state.js +58 -0
  372. package/dist/web/static/js/compare/stream.js +695 -0
  373. package/dist/web/static/js/compare/vote.js +254 -0
  374. package/dist/web/static/js/cookbook-diagnosis.js +694 -0
  375. package/dist/web/static/js/cookbook-hwfit.js +1888 -0
  376. package/dist/web/static/js/cookbook.js +2093 -0
  377. package/dist/web/static/js/cookbookDownload.js +633 -0
  378. package/dist/web/static/js/cookbookProgressSignal.js +29 -0
  379. package/dist/web/static/js/cookbookRunning.js +3506 -0
  380. package/dist/web/static/js/cookbookServe.js +2128 -0
  381. package/dist/web/static/js/document.js +9731 -0
  382. package/dist/web/static/js/documentLibrary.js +3376 -0
  383. package/dist/web/static/js/dragSort.js +265 -0
  384. package/dist/web/static/js/editor/ai-inpaint.js +374 -0
  385. package/dist/web/static/js/editor/ai-models.js +273 -0
  386. package/dist/web/static/js/editor/ai-rembg.js +230 -0
  387. package/dist/web/static/js/editor/ai-tool-runner.js +147 -0
  388. package/dist/web/static/js/editor/ai-tools-misc.js +216 -0
  389. package/dist/web/static/js/editor/build/controls.js +366 -0
  390. package/dist/web/static/js/editor/build/popups.js +112 -0
  391. package/dist/web/static/js/editor/build/right-panel.js +200 -0
  392. package/dist/web/static/js/editor/build/toolbar.js +73 -0
  393. package/dist/web/static/js/editor/build/topbar.js +131 -0
  394. package/dist/web/static/js/editor/build/transform-popup.js +109 -0
  395. package/dist/web/static/js/editor/canvas-coords.js +21 -0
  396. package/dist/web/static/js/editor/canvas-events.js +197 -0
  397. package/dist/web/static/js/editor/canvas-transforms.js +132 -0
  398. package/dist/web/static/js/editor/checkerboard.js +24 -0
  399. package/dist/web/static/js/editor/clipboard-and-drop.js +133 -0
  400. package/dist/web/static/js/editor/composite-helpers.js +83 -0
  401. package/dist/web/static/js/editor/filters/blur.js +118 -0
  402. package/dist/web/static/js/editor/filters/edge-feather.js +70 -0
  403. package/dist/web/static/js/editor/fx/adj-popup.js +677 -0
  404. package/dist/web/static/js/editor/fx/filter-string.js +38 -0
  405. package/dist/web/static/js/editor/fx/histogram.js +67 -0
  406. package/dist/web/static/js/editor/fx/pixel-pass.js +249 -0
  407. package/dist/web/static/js/editor/harmonize-masks.js +116 -0
  408. package/dist/web/static/js/editor/history-panel.js +176 -0
  409. package/dist/web/static/js/editor/keyboard-shortcuts.js +271 -0
  410. package/dist/web/static/js/editor/layer-helpers.js +131 -0
  411. package/dist/web/static/js/editor/layer-panel.js +600 -0
  412. package/dist/web/static/js/editor/mask-utils.js +91 -0
  413. package/dist/web/static/js/editor/shortcuts-popover.js +149 -0
  414. package/dist/web/static/js/editor/slider-ux.js +191 -0
  415. package/dist/web/static/js/editor/snap.js +107 -0
  416. package/dist/web/static/js/editor/state.js +257 -0
  417. package/dist/web/static/js/editor/stroke-pipeline.js +162 -0
  418. package/dist/web/static/js/editor/stroke-tool-sliders.js +64 -0
  419. package/dist/web/static/js/editor/tools/clone.js +81 -0
  420. package/dist/web/static/js/editor/tools/crop.js +137 -0
  421. package/dist/web/static/js/editor/tools/flood-fill.js +72 -0
  422. package/dist/web/static/js/editor/tools/lasso-mask.js +171 -0
  423. package/dist/web/static/js/editor/tools/lasso.js +65 -0
  424. package/dist/web/static/js/editor/tools/move.js +79 -0
  425. package/dist/web/static/js/editor/tools/stroke.js +123 -0
  426. package/dist/web/static/js/editor/tools/transform-drag.js +174 -0
  427. package/dist/web/static/js/editor/tools/transform-handles.js +271 -0
  428. package/dist/web/static/js/editor/tools/transform-session.js +381 -0
  429. package/dist/web/static/js/editor/tools/wand.js +46 -0
  430. package/dist/web/static/js/editor/wire-import.js +148 -0
  431. package/dist/web/static/js/editor/wire-inpaint-controls.js +164 -0
  432. package/dist/web/static/js/editor/wire-merge-buttons.js +103 -0
  433. package/dist/web/static/js/editor/wire-selection-controls.js +170 -0
  434. package/dist/web/static/js/editor/wire-topbar-menus.js +174 -0
  435. package/dist/web/static/js/editor/wire-topbar-overflow.js +49 -0
  436. package/dist/web/static/js/editor/wire-topbar.js +188 -0
  437. package/dist/web/static/js/emailInbox.js +1245 -0
  438. package/dist/web/static/js/emailLibrary/replyRecipients.js +27 -0
  439. package/dist/web/static/js/emailLibrary/signatureFold.js +339 -0
  440. package/dist/web/static/js/emailLibrary/state.js +34 -0
  441. package/dist/web/static/js/emailLibrary/utils.js +202 -0
  442. package/dist/web/static/js/emailLibrary.js +5217 -0
  443. package/dist/web/static/js/emojiPicker.js +311 -0
  444. package/dist/web/static/js/escMenuStack.js +102 -0
  445. package/dist/web/static/js/fileHandler.js +295 -0
  446. package/dist/web/static/js/gallery.js +2835 -0
  447. package/dist/web/static/js/galleryEditor.js +3798 -0
  448. package/dist/web/static/js/group.js +955 -0
  449. package/dist/web/static/js/init.js +405 -0
  450. package/dist/web/static/js/keyboard-shortcuts.js +292 -0
  451. package/dist/web/static/js/langIcons.js +187 -0
  452. package/dist/web/static/js/markdown/tableRow.js +19 -0
  453. package/dist/web/static/js/markdown.js +806 -0
  454. package/dist/web/static/js/memory.js +1418 -0
  455. package/dist/web/static/js/modalManager.js +1552 -0
  456. package/dist/web/static/js/modalSnap.js +782 -0
  457. package/dist/web/static/js/model/matchKey.js +19 -0
  458. package/dist/web/static/js/modelPicker.js +695 -0
  459. package/dist/web/static/js/modelSort.js +33 -0
  460. package/dist/web/static/js/models.js +642 -0
  461. package/dist/web/static/js/notes.js +5054 -0
  462. package/dist/web/static/js/package.json +1 -0
  463. package/dist/web/static/js/platform.js +47 -0
  464. package/dist/web/static/js/presets.js +1110 -0
  465. package/dist/web/static/js/providers.js +93 -0
  466. package/dist/web/static/js/rag.js +177 -0
  467. package/dist/web/static/js/research/jobs.js +335 -0
  468. package/dist/web/static/js/research/panel.js +1233 -0
  469. package/dist/web/static/js/researchSynapse.js +225 -0
  470. package/dist/web/static/js/search-chat.js +201 -0
  471. package/dist/web/static/js/search.js +52 -0
  472. package/dist/web/static/js/section-management.js +260 -0
  473. package/dist/web/static/js/sessions.js +3108 -0
  474. package/dist/web/static/js/settings.js +4991 -0
  475. package/dist/web/static/js/sidebar-layout.js +573 -0
  476. package/dist/web/static/js/signature.js +504 -0
  477. package/dist/web/static/js/skills.js +1908 -0
  478. package/dist/web/static/js/slashAutocomplete.js +267 -0
  479. package/dist/web/static/js/slashCommands.js +6099 -0
  480. package/dist/web/static/js/spinner.js +406 -0
  481. package/dist/web/static/js/storage.js +123 -0
  482. package/dist/web/static/js/tasks.js +2699 -0
  483. package/dist/web/static/js/theme.js +2086 -0
  484. package/dist/web/static/js/tileManager.js +386 -0
  485. package/dist/web/static/js/tourAutoplay.js +133 -0
  486. package/dist/web/static/js/tourHints.js +179 -0
  487. package/dist/web/static/js/tts-ai.js +521 -0
  488. package/dist/web/static/js/ui.js +1288 -0
  489. package/dist/web/static/js/util/ordinal.js +13 -0
  490. package/dist/web/static/js/voiceRecorder.js +283 -0
  491. package/dist/web/static/js/windowDrag.js +326 -0
  492. package/dist/web/static/js/windowResize.js +233 -0
  493. package/dist/web/static/landing.html +824 -0
  494. package/dist/web/static/lib/docx.umd.min.js +20116 -0
  495. package/dist/web/static/lib/highlight.min.js +1213 -0
  496. package/dist/web/static/lib/html2pdf.bundle.min.js +3 -0
  497. package/dist/web/static/lib/mammoth.browser.min.js +21 -0
  498. package/dist/web/static/lib/qrcode.min.js +8 -0
  499. package/dist/web/static/lib/xlsx.full.min.js +24 -0
  500. package/dist/web/static/login.html +582 -0
  501. package/dist/web/static/manifest.json +15 -0
  502. package/dist/web/static/style.css +35733 -0
  503. package/dist/web/static/sw.js +144 -0
  504. package/dist/workflows/engine.d.ts +162 -0
  505. package/dist/workflows/engine.d.ts.map +1 -0
  506. package/dist/workflows/engine.js +625 -0
  507. package/dist/workflows/engine.js.map +1 -0
  508. package/dist/workflows/index.d.ts +8 -0
  509. package/dist/workflows/index.d.ts.map +1 -0
  510. package/dist/workflows/index.js +14 -0
  511. package/dist/workflows/index.js.map +1 -0
  512. package/package.json +112 -0
@@ -0,0 +1,1908 @@
1
+ // skills.js — Skills tab in the Memory modal.
2
+ //
3
+ // Skills are SKILL.md files (frontmatter + body) under data/skills/.
4
+ // This UI supports: list, search, view (read SKILL.md), edit (replace
5
+ // content), publish/draft toggle, delete, and "run as slash" via the
6
+ // /<skill-name> path.
7
+
8
+ import uiModule from './ui.js';
9
+ import * as spinnerModule from './spinner.js';
10
+
11
+ const API = window.location.origin;
12
+ let skills = [];
13
+ let builtinSkills = []; // read-only agent tool capabilities (TOOL_SECTIONS)
14
+ let loaded = false;
15
+ let _loadPromise = null;
16
+
17
+ function esc(s) { return uiModule.esc(String(s ?? '')); }
18
+
19
+ let _pendingFocusSkill = null;
20
+ let _cascadeNext = false; // set true to play the domino-in entrance on the next render
21
+
22
+ function _playSkillsCascade(container = document.getElementById('skills-list')) {
23
+ if (!container || !container.querySelector('.skill-card')) return false;
24
+ container.classList.remove('doclib-just-opened');
25
+ void container.offsetWidth;
26
+ container.classList.add('doclib-just-opened');
27
+ setTimeout(() => container.classList.remove('doclib-just-opened'), 900);
28
+ return true;
29
+ }
30
+
31
+ // Cache of SKILL.md text by skill name, so expanding is instant (no async
32
+ // fetch + content-settle jump). Populated lazily on expand AND eagerly in
33
+ // the background for all visible cards right after render.
34
+ const _mdCache = new Map();
35
+ async function _fetchSkillMarkdown(name) {
36
+ if (_mdCache.has(name)) return _mdCache.get(name);
37
+ const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`);
38
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
39
+ const data = await res.json();
40
+ const md = data.markdown || '';
41
+ _mdCache.set(name, md);
42
+ return md;
43
+ }
44
+ // Background-load the markdown for every currently-rendered skill card so it
45
+ // is ready (in the card's <pre> + _mdLoaded) before the user expands it.
46
+ function _preloadVisibleMarkdown() {
47
+ document.querySelectorAll('#skills-list .skill-card[data-skill-name]').forEach(card => {
48
+ const name = card.dataset.skillName;
49
+ if (!name || card._mdLoaded) return;
50
+ const pre = card.querySelector('.skill-md-pre');
51
+ const apply = (md) => { if (pre) pre.textContent = md || '(empty)'; card._mdLoaded = true; card._md = md || ''; };
52
+ if (_mdCache.has(name)) { apply(_mdCache.get(name)); return; }
53
+ _fetchSkillMarkdown(name).then(apply).catch(() => {});
54
+ });
55
+ }
56
+
57
+ // Collapsed skills sections ("user" / "builtin"), persisted so the
58
+ // choice survives reloads. Built-in defaults to collapsed (it's
59
+ // reference info, not the user's own skills).
60
+ const _collapsedSections = (() => {
61
+ try {
62
+ const raw = localStorage.getItem('skillsSectionsCollapsed');
63
+ if (raw) return new Set(JSON.parse(raw));
64
+ } catch (_) {}
65
+ return new Set(['builtin']);
66
+ })();
67
+ function _saveCollapsedSections() {
68
+ try { localStorage.setItem('skillsSectionsCollapsed', JSON.stringify([..._collapsedSections])); } catch (_) {}
69
+ }
70
+ function _applySectionCollapse(container) {
71
+ if (!container) return;
72
+ container.querySelectorAll('.skills-section-header').forEach(h => {
73
+ h.classList.toggle('collapsed', _collapsedSections.has(h.dataset.section));
74
+ });
75
+ container.querySelectorAll('.doclib-card[data-skill-section]').forEach(c => {
76
+ c.classList.toggle('skill-card-section-hidden', _collapsedSections.has(c.dataset.skillSection));
77
+ });
78
+ }
79
+
80
+ export async function loadSkills(cascade = false) {
81
+ // Play the domino-in entrance on this load (set when the tab is opened,
82
+ // not for the silent re-loads after an edit/delete).
83
+ if (cascade) _cascadeNext = true;
84
+ if (cascade && loaded && !_loadPromise && _playSkillsCascade()) {
85
+ _cascadeNext = false;
86
+ updateCount();
87
+ return;
88
+ }
89
+ if (_loadPromise) return _loadPromise;
90
+ _loadPromise = (async () => {
91
+ try {
92
+ const res = await fetch(`${API}/api/skills`);
93
+ const data = await res.json();
94
+ skills = data.skills || [];
95
+ _loadSkillApprovalThreshold();
96
+ // Built-in capabilities are no longer surfaced in the Skills menu.
97
+ loaded = true;
98
+ renderSkillsList();
99
+ updateCount();
100
+ if (_pendingFocusSkill) {
101
+ _focusSkillRow(_pendingFocusSkill);
102
+ _pendingFocusSkill = null;
103
+ }
104
+ // If a background audit is running, re-show its progress panel.
105
+ if (!_auditPoll) {
106
+ _fetchAuditStatus().then(st => {
107
+ if (st.status === 'running') _auditAllSkills();
108
+ }).catch(() => {});
109
+ }
110
+ } catch (e) {
111
+ console.error('Failed to load skills:', e);
112
+ } finally {
113
+ _loadPromise = null;
114
+ }
115
+ })();
116
+ return _loadPromise;
117
+ }
118
+
119
+ function _focusSkillRow(name) {
120
+ setTimeout(() => {
121
+ const card = document.querySelector(`.skill-card[data-skill-name="${CSS.escape(name)}"]`);
122
+ if (!card) return;
123
+ card.scrollIntoView({ behavior: 'smooth', block: 'center' });
124
+ card.classList.add('skill-row-flash');
125
+ setTimeout(() => card.classList.remove('skill-row-flash'), 2000);
126
+ // Expand it so the linked skill opens to its SKILL.md directly.
127
+ _expandSkillCard(card, name);
128
+ }, 200);
129
+ }
130
+
131
+ // Open the Memory modal → Skills tab → focus a specific skill row.
132
+ // Used by the chat anchor-link delegate ([name](#skill-<name>)).
133
+ export function openSkill(name) {
134
+ _pendingFocusSkill = name || null;
135
+ // Open the memory modal if not already open.
136
+ const memBtn = document.getElementById('tool-memory-btn');
137
+ if (memBtn) memBtn.click();
138
+ // Switch to the skills tab (triggers lazy loadSkills()).
139
+ setTimeout(() => {
140
+ const tab = document.querySelector('.memory-tab[data-memory-tab="skills"]');
141
+ if (tab) tab.click();
142
+ else loadSkills(); // fallback if tab structure differs
143
+ }, 120);
144
+ }
145
+
146
+ let _skillsSort = 'confidence';
147
+ let _showDraftsOnly = false;
148
+ let _showPublishedOnly = false;
149
+ let _confMax = null; // confidence ceiling filter (%, e.g. 90 = show ≤90%); null = off
150
+ let _selectMode = false;
151
+ const _selectedNames = new Set();
152
+ let _skillApprovalThreshold = 0.85;
153
+
154
+ function updateCount() {
155
+ const el = document.getElementById('skills-count');
156
+ if (el) el.textContent = skills.length || '0';
157
+ const elH = document.getElementById('skills-count-h2');
158
+ if (elH) elH.textContent = skills.length + ' skill' + (skills.length === 1 ? '' : 's');
159
+ }
160
+
161
+ function _sortSkills(list) {
162
+ const arr = list.slice();
163
+ if (_skillsSort === 'confidence') {
164
+ arr.sort((a, b) => (b.confidence || 0) - (a.confidence || 0) || (a.name || '').localeCompare(b.name || ''));
165
+ } else if (_skillsSort === 'uses') {
166
+ arr.sort((a, b) => (b.uses || 0) - (a.uses || 0) || (a.name || '').localeCompare(b.name || ''));
167
+ } else if (_skillsSort === 'recent') {
168
+ arr.sort((a, b) => (b.updated_at || b.created_at || 0) - (a.updated_at || a.created_at || 0));
169
+ } else {
170
+ arr.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
171
+ }
172
+ return arr;
173
+ }
174
+
175
+ function _matches(sk, query) {
176
+ const q = query.toLowerCase();
177
+ return (
178
+ (sk.name || '').toLowerCase().includes(q) ||
179
+ (sk.description || '').toLowerCase().includes(q) ||
180
+ (sk.when_to_use || sk.problem || '').toLowerCase().includes(q) ||
181
+ (sk.category || '').toLowerCase().includes(q) ||
182
+ (sk.tags || []).some(t => (t || '').toLowerCase().includes(q))
183
+ );
184
+ }
185
+
186
+ function _statusPill(sk) {
187
+ const s = sk.status || (sk._legacy ? 'legacy' : 'draft');
188
+ if (s === 'published') return '<span class="memory-cat-badge skill-status-pill" data-status="published" style="background:color-mix(in srgb, var(--accent, #4ade80) 30%, transparent)">published</span>';
189
+ if (s === 'draft') return '<span class="memory-cat-badge skill-status-pill" data-status="draft" style="background:color-mix(in srgb, var(--fg) 14%, transparent)">draft</span>';
190
+ return `<span class="memory-cat-badge skill-status-pill" data-status="${esc(s)}" style="opacity:0.6">${esc(s)}</span>`;
191
+ }
192
+
193
+ // Show a "teacher" badge for skills written by the auto-escalation
194
+ // teacher loop. Lets the user tell at-a-glance which procedures were
195
+ // hand-authored vs auto-generated so they can audit (and demote /
196
+ // edit / publish) before trusting them.
197
+ function _sourcePill(sk) {
198
+ if (sk.source !== 'teacher-escalation') return '';
199
+ const teacher = sk.teacher_model || 'teacher';
200
+ return `<span class="memory-cat-badge" title="Created by teacher escalation: ${esc(teacher)}" style="background:color-mix(in srgb, var(--color-warning, #f0ad4e) 22%, transparent);">teacher-created</span>`;
201
+ }
202
+
203
+ function _modelShortName(model) {
204
+ return String(model || '').split('/').filter(Boolean).pop() || String(model || '');
205
+ }
206
+
207
+ function _skillTokens(sk) {
208
+ return new Set(String([
209
+ sk.name || '',
210
+ sk.description || '',
211
+ sk.when_to_use || '',
212
+ ...(sk.tags || []),
213
+ ].join(' ')).toLowerCase()
214
+ .replace(/-\d+\b/g, '')
215
+ .split(/[^a-z0-9]+/)
216
+ .filter(t => t.length > 2 && !['the', 'and', 'with', 'for', 'from', 'using'].includes(t)));
217
+ }
218
+
219
+ function _skillSimilarity(a, b) {
220
+ const A = _skillTokens(a), B = _skillTokens(b);
221
+ if (!A.size || !B.size) return 0;
222
+ let inter = 0;
223
+ for (const t of A) if (B.has(t)) inter++;
224
+ return inter / (A.size + B.size - inter);
225
+ }
226
+
227
+ function _baseSkillName(name) {
228
+ return String(name || '').replace(/-\d+$/, '');
229
+ }
230
+
231
+ function _scoreDuplicateKeeper(sk) {
232
+ return [
233
+ (sk.status === 'published') ? 100000 : 0,
234
+ (sk.uses || 0) * 100,
235
+ Math.round((sk.confidence || 0) * 100),
236
+ sk.audit_by_teacher ? -5 : 0,
237
+ -String(sk.name || '').length / 1000,
238
+ ].reduce((a, b) => a + b, 0);
239
+ }
240
+
241
+ function _duplicateMeta(list) {
242
+ const parent = new Map();
243
+ const names = list.map(s => s.name || s.id).filter(Boolean);
244
+ names.forEach(n => parent.set(n, n));
245
+ const find = (x) => {
246
+ let p = parent.get(x) || x;
247
+ while (p !== parent.get(p)) p = parent.get(p);
248
+ return p;
249
+ };
250
+ const unite = (a, b) => {
251
+ const pa = find(a), pb = find(b);
252
+ if (pa !== pb) parent.set(pb, pa);
253
+ };
254
+ for (let i = 0; i < list.length; i++) {
255
+ for (let j = i + 1; j < list.length; j++) {
256
+ const a = list[i], b = list[j];
257
+ const an = a.name || a.id, bn = b.name || b.id;
258
+ if (!an || !bn) continue;
259
+ if (_baseSkillName(an) === _baseSkillName(bn) || _skillSimilarity(a, b) >= 0.38) {
260
+ unite(an, bn);
261
+ }
262
+ }
263
+ }
264
+ const groups = new Map();
265
+ for (const sk of list) {
266
+ const n = sk.name || sk.id;
267
+ if (!n) continue;
268
+ const root = find(n);
269
+ if (!groups.has(root)) groups.set(root, []);
270
+ groups.get(root).push(sk);
271
+ }
272
+ const meta = new Map();
273
+ let idx = 1;
274
+ for (const group of groups.values()) {
275
+ if (group.length < 2) continue;
276
+ const sorted = group.slice().sort((a, b) => _scoreDuplicateKeeper(b) - _scoreDuplicateKeeper(a));
277
+ const keep = sorted[0].name || sorted[0].id;
278
+ const groupNames = sorted.map(s => s.name || s.id).filter(Boolean);
279
+ for (const sk of sorted) {
280
+ const n = sk.name || sk.id;
281
+ meta.set(n, { group: idx, keep: n === keep, keepName: keep, names: groupNames });
282
+ }
283
+ idx++;
284
+ }
285
+ return meta;
286
+ }
287
+
288
+ function _auditModelPills(sk) {
289
+ const worker = sk.audit_worker_model || '';
290
+ const teacher = sk.audit_teacher_model || '';
291
+ let html = '';
292
+ if (worker) {
293
+ html += `<span class="memory-cat-badge skill-model-pill skill-model-student" title="Last audited by default audit model: ${esc(worker)}">audit</span>`;
294
+ }
295
+ if (sk.audit_by_teacher || teacher) {
296
+ const title = teacher
297
+ ? `Teacher rewrote this skill; audit model passed after the rewrite. Teacher: ${teacher}`
298
+ : 'Teacher rewrote this skill; audit model passed after the rewrite.';
299
+ html += `<span class="memory-cat-badge skill-model-pill skill-model-teacher" title="${esc(title)}">teacher-fixed</span>`;
300
+ }
301
+ return html;
302
+ }
303
+
304
+ function _necessityKind(sk) {
305
+ const nec = sk && sk.necessity;
306
+ if (sk && sk._duplicateGroup) return 'duplicate';
307
+ if (!nec || nec.necessary !== false) return null;
308
+ const reason = String(nec.reason || '').toLowerCase();
309
+ const redundant = (nec.redundant_with || []).filter(Boolean);
310
+ if (redundant.length || /duplicat|redundan|overlap|same skill|same procedure/.test(reason)) return 'duplicate';
311
+ if (/trivial|generic|capable assistant|without a saved|not need|unnecessary/.test(reason)) return 'trivial';
312
+ return 'irrelevant';
313
+ }
314
+
315
+ function _necessityPill(sk) {
316
+ const kind = _necessityKind(sk);
317
+ if (!kind) return '';
318
+ const nec = sk.necessity || {};
319
+ const dup = (nec.redundant_with || []).filter(Boolean);
320
+ const label = kind === 'duplicate' ? (sk._duplicateGroup ? `duplicate #${sk._duplicateGroup}` : 'duplicate')
321
+ : kind === 'trivial' ? 'generic'
322
+ : 'possibly-irrelevant';
323
+ const group = sk._duplicateNames || [];
324
+ const why = sk._duplicateGroup
325
+ ? `Duplicate group #${sk._duplicateGroup}. Recommended keep: ${sk._duplicateKeepName}. Group: ${group.join(', ')}`
326
+ : (nec.reason || 'May not be worth keeping') + (dup.length ? ' | overlaps: ' + dup.join(', ') : '');
327
+ return `<span class="memory-cat-badge skill-necessity-pill skill-necessity-${kind}" title="${esc(why)}">${label}</span>`;
328
+ }
329
+
330
+ function _duplicatePriorityPill(sk) {
331
+ if (!sk._duplicateGroup) return '';
332
+ if (sk._duplicateKeep) {
333
+ return `<span class="memory-cat-badge skill-duplicate-keep" title="Best duplicate candidate by published status, uses, confidence, and specificity">recommended</span>`;
334
+ }
335
+ return `<span class="memory-cat-badge skill-duplicate-lower" title="Lower-priority duplicate. Suggested keeper: ${esc(sk._duplicateKeepName || '')}">lower-priority</span>`;
336
+ }
337
+
338
+ // Verified-by-test indicators shown next to the confidence %. A check when a
339
+ // test/audit run passed; a graduation-cap when the teacher model had to
340
+ // rewrite the skill to make it pass. SVG (no Unicode emoji).
341
+ function _auditMarks(sk) {
342
+ let html = '';
343
+ if (sk.audit_verdict === 'pass') {
344
+ html += `<span class="skill-verified" title="Passed an automated test"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>`;
345
+ }
346
+ if (sk.audit_by_teacher) {
347
+ const teacher = sk.audit_teacher_model ? `: ${sk.audit_teacher_model}` : '';
348
+ html += `<span class="skill-teachermark" title="Teacher rewrote this skill; audit model passed after the rewrite${esc(teacher)}"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 10L12 5 2 10l10 5 10-5z"/><path d="M6 12v5c0 1 3 2 6 2s6-1 6-2v-5"/></svg></span>`;
349
+ }
350
+ return html;
351
+ }
352
+
353
+ // Audit verdict dot — removed at user request. The ✓ check-mark next to the
354
+ // confidence % still indicates a pass. Stub returns empty so the surrounding
355
+ // header HTML still composes without changing other layout.
356
+ function _auditDot(sk) { return ''; }
357
+
358
+ function _isDraftsFilter() { return !!_showDraftsOnly; }
359
+
360
+ // Confidence → colour. 90%+ is solidly green, scaling down through
361
+ // yellow/orange to red at 50% and below (hue 120→0 over 90→50).
362
+ function _confColor(conf) {
363
+ const hue = Math.max(0, Math.min(120, ((conf - 50) / 40) * 120));
364
+ return `hsl(${Math.round(hue)}, 70%, 42%)`;
365
+ }
366
+
367
+ // Shared action icons (collapsed kebab menu + expanded footer use the same).
368
+ const _ICON = {
369
+ del: '<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>',
370
+ edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>',
371
+ approve: '<polyline points="20 6 9 17 4 12"/>',
372
+ unpublish: '<path d="M5 12l5 5L20 7"/>',
373
+ test: '<polygon points="5 3 19 12 5 21 5 3"/>',
374
+ };
375
+ function _svg(paths, { fill = 'none', size = 13 } = {}) {
376
+ const stroke = fill === 'currentColor' ? '' : 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
377
+ return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="${fill}" ${stroke} style="vertical-align:-2px;flex-shrink:0;">${paths}</svg>`;
378
+ }
379
+
380
+ // Kebab dropdown for a collapsed skill card — same actions + icons as the
381
+ // expanded footer (Publish/Unpublish · Edit · Delete).
382
+ function _openSkillMenu(btn, card, sk, name, isPublished) {
383
+ document.querySelectorAll('.skill-kebab-menu').forEach(m => m.remove());
384
+ const menu = document.createElement('div');
385
+ menu.className = 'skill-kebab-menu';
386
+ const mk = (paths, label, opts, onClick) => {
387
+ const item = document.createElement('button');
388
+ item.className = 'skill-kebab-item' + (opts && opts.danger ? ' danger' : '');
389
+ item.innerHTML = _svg(paths, opts) + `<span>${label}</span>`;
390
+ item.addEventListener('click', (e) => { e.stopPropagation(); menu.remove(); onClick(); });
391
+ menu.appendChild(item);
392
+ };
393
+ if (isPublished) mk(_ICON.unpublish, 'Unpublish', {}, () => _setSkillStatus(name, 'draft'));
394
+ else mk(_ICON.approve, 'Publish', {}, () => _setSkillStatus(name, 'published'));
395
+ mk(_ICON.edit, 'Edit', {}, async () => {
396
+ if (!card.classList.contains('doclib-card-expanded')) await _expandSkillCard(card, name);
397
+ _toggleSkillEdit(card, name);
398
+ });
399
+ mk(_ICON.test, 'Test', {}, () => _testSkill(card, name));
400
+ // Audit kicks off the bulk audit-all loop (test → judge → fix → retry → demote).
401
+ // Starts at the top of the list and walks down.
402
+ mk(_ICON.test, 'Audit', {}, () => _auditAllSkills());
403
+ mk(_ICON.del, 'Delete', { danger: true }, () => _deleteSkill(name, card));
404
+
405
+ // Select — enters bulk-select mode and pre-selects this skill. Same pattern
406
+ // as the email/documents/brain Select item, with the email bullet icon.
407
+ const selItem = document.createElement('button');
408
+ selItem.className = 'skill-kebab-item';
409
+ selItem.innerHTML = '<span style="display:inline-flex;width:14px;height:14px;align-items:center;justify-content:center;"><span style="font-size:16px;line-height:1;">●</span></span><span>Select</span>';
410
+ selItem.addEventListener('click', (e) => {
411
+ e.stopPropagation();
412
+ menu.remove();
413
+ if (!_selectMode) _enterSelectMode();
414
+ _selectedNames.add(name);
415
+ renderSkillsList();
416
+ });
417
+ menu.appendChild(selItem);
418
+
419
+ // Mobile-only Cancel — mirrors the email/documents/brain popup pattern.
420
+ // CSS hides `.dropdown-cancel-mobile` on desktop where outside-click
421
+ // already dismisses cleanly.
422
+ const cancelItem = document.createElement('button');
423
+ cancelItem.className = 'skill-kebab-item dropdown-cancel-mobile';
424
+ cancelItem.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg><span>Cancel</span>';
425
+ cancelItem.addEventListener('click', (e) => { e.stopPropagation(); menu.remove(); });
426
+ menu.appendChild(cancelItem);
427
+
428
+ document.body.appendChild(menu);
429
+ const r = btn.getBoundingClientRect();
430
+ menu.style.top = (r.bottom + 4) + 'px';
431
+ menu.style.right = Math.max(6, window.innerWidth - r.right) + 'px';
432
+ // Keep it on-screen (mobile): flip above the button if it would overflow the
433
+ // bottom, clamp the left edge, and cap the height as a last resort.
434
+ const mr = menu.getBoundingClientRect();
435
+ if (mr.bottom > window.innerHeight - 6) {
436
+ menu.style.top = Math.max(6, r.top - mr.height - 4) + 'px';
437
+ }
438
+ if (mr.left < 6) {
439
+ menu.style.right = Math.max(6, window.innerWidth - 6 - mr.width) + 'px';
440
+ }
441
+ const mr2 = menu.getBoundingClientRect();
442
+ if (mr2.bottom > window.innerHeight - 6) {
443
+ menu.style.maxHeight = Math.max(80, window.innerHeight - 12 - mr2.top) + 'px';
444
+ menu.style.overflowY = 'auto';
445
+ }
446
+ const close = (ev) => { if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener('click', close, true); } };
447
+ setTimeout(() => document.addEventListener('click', close, true), 0);
448
+ }
449
+
450
+ // Cards for the agent's built-in tool capabilities (from
451
+ // /api/skills/builtin → TOOL_SECTIONS). Expandable to preview the
452
+ // instruction block; editable with a warning + a revert-to-default
453
+ // button (overrides stored in settings, applied to the prompt).
454
+ function _buildBuiltinCards() {
455
+ return builtinSkills.map(b => {
456
+ const card = document.createElement('div');
457
+ card.className = 'doclib-card skill-card skill-builtin-card';
458
+ card.dataset.builtinName = b.name;
459
+
460
+ const header = document.createElement('div');
461
+ header.className = 'doclib-card-header skill-card-header';
462
+ header.innerHTML = `
463
+ <span class="skill-conf-dot" style="display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--accent, var(--red));flex-shrink:0;margin-right:6px;opacity:0.55;"></span>
464
+ <div style="flex:1;min-width:0;overflow:hidden;">
465
+ <div class="doclib-card-title" style="display:flex;align-items:center;gap:6px;min-width:0;">
466
+ <code style="font-weight:600;font-size:0.9em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:1;min-width:0;">${esc(b.name)}</code>
467
+ <span class="memory-cat-badge" style="background:color-mix(in srgb, var(--fg) 14%, transparent)">built-in</span>
468
+ ${b.is_overridden ? '<span class="memory-cat-badge" title="You have edited this built-in capability" style="background:color-mix(in srgb, var(--color-warning, #f0ad4e) 30%, transparent);">edited</span>' : ''}
469
+ </div>
470
+ ${b.description ? `<div class="doclib-card-session" title="${esc(b.description)}" style="font-size:10px;opacity:0.55;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${esc(b.description)}</div>` : ''}
471
+ </div>
472
+ <span class="doclib-card-chevron"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
473
+ `;
474
+ card.appendChild(header);
475
+
476
+ const preview = document.createElement('div');
477
+ preview.className = 'doclib-card-preview skill-card-preview';
478
+ // Warning banner — editing a built-in changes how the assistant uses a native tool.
479
+ const warn = document.createElement('div');
480
+ warn.className = 'skill-builtin-warn';
481
+ warn.innerHTML = '⚠ This is a built-in capability. Editing changes how the assistant is instructed to use this native tool — it can break or alter core behaviour. Use Revert to restore the shipped default.';
482
+ preview.appendChild(warn);
483
+ const pre = document.createElement('pre');
484
+ pre.className = 'skill-md-pre';
485
+ pre.textContent = ''; // filled on expand
486
+ preview.appendChild(pre);
487
+
488
+ // Footer: Revert (left, only meaningful when overridden) · Edit/Save (right).
489
+ const actions = document.createElement('div');
490
+ actions.className = 'doclib-card-expanded-actions';
491
+
492
+ const revertBtn = document.createElement('button');
493
+ revertBtn.className = 'doclib-card-text-btn doclib-card-action-btn doclib-card-text-btn-danger';
494
+ revertBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>Revert';
495
+ revertBtn.title = 'Restore the original shipped instructions';
496
+ revertBtn.addEventListener('click', (e) => { e.stopPropagation(); _revertBuiltin(b.name); });
497
+
498
+ const editBtn = document.createElement('button');
499
+ editBtn.className = 'doclib-card-text-btn doclib-card-action-btn';
500
+ editBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Edit';
501
+ editBtn.addEventListener('click', (e) => { e.stopPropagation(); _toggleBuiltinEdit(card, b.name); });
502
+
503
+ const rightGroup = document.createElement('div');
504
+ rightGroup.className = 'doclib-action-group';
505
+ const btnRow = document.createElement('div');
506
+ btnRow.className = 'doclib-action-btn-row';
507
+ btnRow.appendChild(editBtn);
508
+ rightGroup.appendChild(btnRow);
509
+
510
+ actions.appendChild(revertBtn);
511
+ actions.appendChild(rightGroup);
512
+ preview.appendChild(actions);
513
+ card.appendChild(preview);
514
+
515
+ card.addEventListener('click', (e) => {
516
+ if (e.target.closest('button, input, textarea')) return;
517
+ _expandBuiltinCard(card, b.name);
518
+ });
519
+ return card;
520
+ });
521
+ }
522
+
523
+ async function _expandBuiltinCard(card, name) {
524
+ const grid = card.closest('.doclib-grid');
525
+ if (card.classList.contains('doclib-card-expanded')) {
526
+ card.classList.remove('doclib-card-expanded');
527
+ return;
528
+ }
529
+ if (grid) grid.querySelectorAll('.doclib-card-expanded').forEach(c => c.classList.remove('doclib-card-expanded'));
530
+ card.classList.add('doclib-card-expanded');
531
+ if (grid) grid.scrollTop = 0;
532
+ const pre = card.querySelector('.skill-md-pre');
533
+ if (pre && !card._loaded) {
534
+ pre.textContent = 'Loading…';
535
+ try {
536
+ const res = await fetch(`${API}/api/skills/builtin/${encodeURIComponent(name)}`);
537
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
538
+ const data = await res.json();
539
+ pre.textContent = data.text || '(empty)';
540
+ card._loaded = true;
541
+ card._text = data.text || '';
542
+ card._default = data.default || '';
543
+ } catch (e) {
544
+ pre.textContent = 'Failed to load.';
545
+ }
546
+ }
547
+ }
548
+
549
+ function _toggleBuiltinEdit(card, name) {
550
+ const preview = card.querySelector('.skill-card-preview');
551
+ if (!preview) return;
552
+ if (preview.querySelector('.skill-md-editor')) { _saveBuiltinEdit(card, name); return; }
553
+ const pre = preview.querySelector('.skill-md-pre');
554
+ const ta = document.createElement('textarea');
555
+ ta.className = 'skill-md-editor';
556
+ ta.spellcheck = false;
557
+ ta.value = (card._text != null ? card._text : (pre ? pre.textContent : '')) || '';
558
+ ta.addEventListener('click', (e) => e.stopPropagation());
559
+ if (pre) pre.style.display = 'none';
560
+ preview.insertBefore(ta, preview.querySelector('.doclib-card-expanded-actions'));
561
+ ta.focus();
562
+ const editBtn = [...preview.querySelectorAll('.doclib-card-action-btn')].find(b => /Edit|Save/.test(b.textContent));
563
+ if (editBtn) editBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>Save';
564
+ }
565
+
566
+ async function _saveBuiltinEdit(card, name) {
567
+ const ta = card.querySelector('.skill-md-editor');
568
+ if (!ta) return;
569
+ try {
570
+ const res = await fetch(`${API}/api/skills/builtin/${encodeURIComponent(name)}`, {
571
+ method: 'PUT',
572
+ headers: { 'Content-Type': 'application/json' },
573
+ body: JSON.stringify({ text: ta.value }),
574
+ });
575
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
576
+ uiModule.showToast('Built-in capability updated');
577
+ builtinSkills = []; // force reload of built-in list (refreshes "edited" badge)
578
+ await loadSkills();
579
+ } catch (e) { uiModule.showError('Save failed: ' + e.message); }
580
+ }
581
+
582
+ async function _revertBuiltin(name) {
583
+ if (!(await uiModule.styledConfirm(`Revert "${name}" to its original built-in instructions?`, { confirmText: 'Revert', danger: true }))) return;
584
+ try {
585
+ const res = await fetch(`${API}/api/skills/builtin/${encodeURIComponent(name)}`, { method: 'DELETE' });
586
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
587
+ uiModule.showToast('Reverted to default');
588
+ builtinSkills = [];
589
+ await loadSkills();
590
+ } catch (e) { uiModule.showError('Revert failed: ' + e.message); }
591
+ }
592
+
593
+ function _getFilteredSkills() {
594
+ const query = (document.getElementById('skills-search')?.value || '').toLowerCase();
595
+ let filtered = query ? skills.filter(sk => _matches(sk, query)) : skills;
596
+ if (_showDraftsOnly) {
597
+ filtered = filtered.filter(sk => (sk.status || 'draft') !== 'published');
598
+ }
599
+ if (_showPublishedOnly) {
600
+ filtered = filtered.filter(sk => (sk.status || 'draft') === 'published');
601
+ }
602
+ if (_confMax != null) {
603
+ // "≤ X%" — surface the lower-confidence skills that may need review.
604
+ filtered = filtered.filter(sk => Math.round((sk.confidence || 0) * 100) <= _confMax);
605
+ }
606
+ return _sortSkills(filtered);
607
+ }
608
+
609
+ function renderSkillsList() {
610
+ const container = document.getElementById('skills-list');
611
+ if (!container) return;
612
+ // Re-render rebuilds the cards (none expanded), so clear the expand flag
613
+ // on the admin-card or it would keep the toolbar hidden with nothing open.
614
+ container.closest('.admin-card')?.classList.remove('skills-has-expanded');
615
+
616
+ const sorted = _getFilteredSkills();
617
+ // Built-in capabilities show as their own read-only section (skipped when
618
+ // the user is filtering to drafts, since built-ins aren't drafts).
619
+ // Skills menu shows the user's own skills only (built-in capabilities
620
+ // are intentionally not surfaced here).
621
+ const showBuiltin = false;
622
+
623
+ if (!sorted.length && !showBuiltin) {
624
+ container.innerHTML = `<div style="text-align:center;opacity:0.4;padding:24px 0;font-size:11px;">${loaded ? 'No skills yet, use agent for it to auto extract them.' : 'Loading…'}</div>`;
625
+ return;
626
+ }
627
+
628
+ // Library-style cards: a compact bar that expands in-place to show the
629
+ // SKILL.md, with a footer (Delete left; Edit / Run / Approve right).
630
+ // Reuses the proven .doclib-card / .doclib-card-preview /
631
+ // .doclib-card-expanded-actions markup so the desktop+mobile expand +
632
+ // footer behaviour matches the document/chat library exactly.
633
+ //
634
+ // #skills-list itself becomes the .doclib-grid (rather than a nested
635
+ // grid) so the global "hide non-grid children when a card is expanded"
636
+ // rule (.admin-card:has(.doclib-card-expanded) > *:not(.doclib-grid))
637
+ // doesn't hide the list container along with everything else.
638
+ container.classList.add('doclib-grid');
639
+ const cards = [];
640
+ const dupeMeta = _duplicateMeta(sorted);
641
+
642
+ for (const sk of sorted) {
643
+ const name = sk.name || sk.id;
644
+ const dm = dupeMeta.get(name);
645
+ if (dm) {
646
+ sk._duplicateGroup = dm.group;
647
+ sk._duplicateKeep = dm.keep;
648
+ sk._duplicateKeepName = dm.keepName;
649
+ sk._duplicateNames = dm.names;
650
+ } else {
651
+ delete sk._duplicateGroup;
652
+ delete sk._duplicateKeep;
653
+ delete sk._duplicateKeepName;
654
+ delete sk._duplicateNames;
655
+ }
656
+ const conf = Math.round((sk.confidence || 0) * 100);
657
+ const uses = sk.uses || 0;
658
+ const isPublished = (sk.status === 'published');
659
+ const confColor = _confColor(conf);
660
+
661
+ const card = document.createElement('div');
662
+ card.className = 'doclib-card skill-card';
663
+ card.dataset.skillName = name;
664
+ card.dataset.skillStatus = sk.status || 'draft';
665
+
666
+ const checked = _selectedNames.has(name) ? 'checked' : '';
667
+ const cbHtml = _selectMode
668
+ ? `<input type="checkbox" class="memory-select-cb skill-select-cb" data-name="${esc(name)}" ${checked} style="margin-right:6px;flex-shrink:0;cursor:pointer;" />`
669
+ : '';
670
+
671
+ // Collapsed header bar: dot · name (wraps) · [pills (right) · stats · menu].
672
+ const header = document.createElement('div');
673
+ header.className = 'doclib-card-header skill-card-header';
674
+ header.innerHTML = `
675
+ ${cbHtml}
676
+ ${_auditDot(sk)}
677
+ <div class="skill-card-textcol">
678
+ <code class="skill-card-name">${esc(name)}</code>
679
+ ${sk.description ? `<div class="skill-card-desc">${esc(sk.description)}</div>` : ''}
680
+ </div>
681
+ <div class="skill-card-right">
682
+ ${_statusPill(sk)}
683
+ ${_sourcePill(sk)}
684
+ ${_auditModelPills(sk)}
685
+ ${_necessityPill(sk)}
686
+ ${_duplicatePriorityPill(sk)}
687
+ <span class="skill-stats">${_auditMarks(sk)}<span class="skill-conf" style="color:${confColor};">${conf}%</span> · ${uses}u</span>
688
+ <span class="skill-chevron-up" title="Collapse"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></span>
689
+ <button class="skill-kebab-btn" title="Actions" aria-label="Actions"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="12" cy="19" r="1.6"/></svg></button>
690
+ </div>
691
+ `;
692
+ card.appendChild(header);
693
+
694
+ // Kebab dropdown (collapsed-bar quick actions: same set + icons as the
695
+ // expanded footer). Clicking the kebab opens it; it doesn't expand.
696
+ header.querySelector('.skill-kebab-btn').addEventListener('click', (e) => {
697
+ e.stopPropagation();
698
+ _openSkillMenu(e.currentTarget, card, sk, name, isPublished);
699
+ });
700
+
701
+ // Preview (hidden until expanded) — SKILL.md goes here + footer.
702
+ const preview = document.createElement('div');
703
+ preview.className = 'doclib-card-preview skill-card-preview';
704
+ const pre = document.createElement('pre');
705
+ pre.className = 'skill-md-pre';
706
+ pre.textContent = ''; // filled on expand
707
+ preview.appendChild(pre);
708
+
709
+ // Footer: Approve/Unpublish on the left, destructive delete on the right.
710
+ const actions = document.createElement('div');
711
+ actions.className = 'doclib-card-expanded-actions';
712
+
713
+ const delBtn = document.createElement('button');
714
+ delBtn.className = 'doclib-card-text-btn doclib-card-action-btn doclib-card-text-btn-danger';
715
+ delBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>Delete';
716
+ delBtn.addEventListener('click', (e) => { e.stopPropagation(); _deleteSkill(name, card); });
717
+
718
+ const editBtn = document.createElement('button');
719
+ editBtn.className = 'doclib-card-text-btn doclib-card-action-btn';
720
+ editBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Edit';
721
+ editBtn.addEventListener('click', (e) => { e.stopPropagation(); _toggleSkillEdit(card, name); });
722
+
723
+ const pubBtn = document.createElement('button');
724
+ pubBtn.className = 'doclib-card-text-btn doclib-card-action-btn';
725
+ if (isPublished) {
726
+ pubBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg>Unpublish';
727
+ pubBtn.title = 'Move back to draft';
728
+ pubBtn.addEventListener('click', (e) => { e.stopPropagation(); _setSkillStatus(name, 'draft'); });
729
+ } else {
730
+ pubBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>Publish';
731
+ pubBtn.title = 'Publish — appears in the skills index';
732
+ pubBtn.style.color = 'var(--color-success, #4caf50)';
733
+ pubBtn.addEventListener('click', (e) => { e.stopPropagation(); _setSkillStatus(name, 'published'); });
734
+ }
735
+
736
+ // Test/audit this one skill — same action that's in the kebab, surfaced in
737
+ // the footer too so it's not buried under the "⋯" menu.
738
+ const testBtn = document.createElement('button');
739
+ testBtn.className = 'doclib-card-text-btn doclib-card-action-btn';
740
+ testBtn.innerHTML = _svg(_ICON.test, { size: 11 }) + 'Test';
741
+ testBtn.title = 'Test this skill — run it + AI judge';
742
+ testBtn.addEventListener('click', (e) => {
743
+ e.stopPropagation();
744
+ // Immediate visual feedback: previously the click looked like nothing
745
+ // happened because _testSkill awaits a status fetch before overwriting
746
+ // the preview — so users would tap a second time. Mark the button as
747
+ // pending right away so the first tap is obviously registered.
748
+ if (testBtn.dataset.busy === '1') return; // also dedupe rapid double-tap
749
+ testBtn.dataset.busy = '1';
750
+ testBtn.disabled = true;
751
+ const _origHTML = testBtn.innerHTML;
752
+ testBtn.innerHTML = _svg(_ICON.test, { size: 11 }) + 'Starting…';
753
+ Promise.resolve(_testSkill(card, name)).finally(() => {
754
+ // The preview gets overwritten by _testSkill, which removes the
755
+ // testBtn from the DOM. The cleanup below only matters if the
756
+ // button still exists (e.g. _testSkill bailed early).
757
+ if (document.body.contains(testBtn)) {
758
+ testBtn.disabled = false;
759
+ testBtn.dataset.busy = '';
760
+ testBtn.innerHTML = _origHTML;
761
+ }
762
+ });
763
+ });
764
+
765
+ const rightGroup = document.createElement('div');
766
+ rightGroup.className = 'doclib-action-group';
767
+ const btnRow = document.createElement('div');
768
+ btnRow.className = 'doclib-action-btn-row';
769
+ btnRow.appendChild(testBtn);
770
+ btnRow.appendChild(editBtn);
771
+ btnRow.appendChild(delBtn);
772
+ rightGroup.appendChild(btnRow);
773
+
774
+ actions.appendChild(pubBtn);
775
+ actions.appendChild(rightGroup);
776
+ preview.appendChild(actions);
777
+ card.appendChild(preview);
778
+
779
+ // Click to expand/collapse (unless in select mode → toggle checkbox).
780
+ card.addEventListener('click', (e) => {
781
+ if (card._suppressNextClick) { card._suppressNextClick = false; return; }
782
+ if (e.target.closest('button, input, textarea')) return;
783
+ if (_selectMode) {
784
+ const cb = card.querySelector('.skill-select-cb');
785
+ if (cb) { cb.checked = !cb.checked; cb.dispatchEvent(new Event('change')); }
786
+ return;
787
+ }
788
+ _expandSkillCard(card, name);
789
+ });
790
+
791
+ // Long-press anywhere on the card opens the kebab dropdown — mirrors the
792
+ // documents library + brain memory pattern. Skip when touch starts on a
793
+ // button/input so per-control handlers keep working.
794
+ {
795
+ const kebab = header.querySelector('.skill-kebab-btn');
796
+ let hold = null;
797
+ let start = null;
798
+ const _lpCancel = () => { if (hold) { clearTimeout(hold); hold = null; } start = null; };
799
+ card.addEventListener('pointerdown', (e) => {
800
+ if (e.target.closest('.skill-kebab-btn, .skill-select-cb, button, input, textarea')) return;
801
+ start = { x: e.clientX, y: e.clientY };
802
+ hold = setTimeout(() => {
803
+ hold = null;
804
+ card._suppressNextClick = true;
805
+ setTimeout(() => { card._suppressNextClick = false; }, 400);
806
+ if (navigator.vibrate) try { navigator.vibrate(15); } catch {}
807
+ if (kebab) kebab.click();
808
+ }, 500);
809
+ });
810
+ card.addEventListener('pointermove', (e) => {
811
+ if (!start) return;
812
+ if (Math.hypot(e.clientX - start.x, e.clientY - start.y) > 10) _lpCancel();
813
+ });
814
+ card.addEventListener('pointerup', _lpCancel);
815
+ card.addEventListener('pointercancel', _lpCancel);
816
+ }
817
+
818
+ cards.push(card);
819
+ }
820
+ container.innerHTML = '';
821
+
822
+ // Two collapsible sections — "Your skills" and "Built-in". Headers and
823
+ // cards are all DIRECT children of the grid (cards tagged with
824
+ // data-skill-section) so the global expand rule — which hides sibling
825
+ // .doclib-card elements by direct-child selector — keeps working.
826
+ // Collapse just toggles display on the tagged cards.
827
+ const _mkSectionHeader = (sectionId, title, count) => {
828
+ const collapsed = _collapsedSections.has(sectionId);
829
+ const hdr = document.createElement('div');
830
+ hdr.className = 'skills-section-label skills-section-header' + (collapsed ? ' collapsed' : '');
831
+ hdr.dataset.section = sectionId;
832
+ hdr.innerHTML =
833
+ `<svg class="skills-section-chevron" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>` +
834
+ `<span>${esc(title)}</span>` +
835
+ `<span class="skills-section-count">${count}</span>`;
836
+ hdr.addEventListener('click', () => {
837
+ if (_collapsedSections.has(sectionId)) _collapsedSections.delete(sectionId);
838
+ else _collapsedSections.add(sectionId);
839
+ _saveCollapsedSections();
840
+ _applySectionCollapse(container);
841
+ });
842
+ return hdr;
843
+ };
844
+
845
+ // "Your skills" section — show the header only when there's also a
846
+ // built-in section to distinguish from (otherwise it's just the list).
847
+ if (cards.length) {
848
+ if (showBuiltin) container.appendChild(_mkSectionHeader('user', 'Your skills', cards.length));
849
+ cards.forEach(c => { c.dataset.skillSection = 'user'; container.appendChild(c); });
850
+ }
851
+
852
+ // Built-in capabilities — read-only cards (the agent's native tools).
853
+ if (showBuiltin) {
854
+ const builtinCards = _buildBuiltinCards();
855
+ container.appendChild(_mkSectionHeader('builtin', 'Built-in capabilities', builtinCards.length));
856
+ builtinCards.forEach(c => { c.dataset.skillSection = 'builtin'; container.appendChild(c); });
857
+ }
858
+
859
+ _applySectionCollapse(container);
860
+
861
+ // Domino-in cascade when the Skills tab is (re)opened — same sleek
862
+ // staggered entrance the document/chat library uses (.doclib-just-opened
863
+ // → section-domino-in on each .doclib-card child). Only consumes the flag
864
+ // set on tab-open, so search/sort/edit re-renders stay instant.
865
+ if (_cascadeNext && cards.length) {
866
+ _cascadeNext = false;
867
+ _playSkillsCascade(container);
868
+ }
869
+
870
+ // Select-mode checkbox wiring (card-body click is handled in the card's
871
+ // own click listener above).
872
+ if (_selectMode) {
873
+ container.querySelectorAll('.skill-select-cb').forEach(cb => {
874
+ cb.addEventListener('change', () => {
875
+ const name = cb.dataset.name;
876
+ if (cb.checked) _selectedNames.add(name); else _selectedNames.delete(name);
877
+ const all = document.getElementById('skills-select-all');
878
+ if (all) {
879
+ const visible = _getFilteredSkills().map(s => s.name || s.id);
880
+ all.checked = visible.length > 0 && visible.every(n => _selectedNames.has(n));
881
+ }
882
+ _updateBulkBar();
883
+ });
884
+ });
885
+ }
886
+
887
+ // Background-load the visible skills' SKILL.md so expanding any of them is
888
+ // instant (no first-time async fetch → no jump). Deferred so it never
889
+ // competes with the render/cascade paint.
890
+ setTimeout(_preloadVisibleMarkdown, 0);
891
+ }
892
+
893
+ // ---- Card expand / edit / actions ----
894
+
895
+ // Collapse an expanded skill card: drop the class AND clear the inline
896
+ // heights skills.js pinned on the card/preview/<pre> (otherwise a collapsed
897
+ // card keeps its full expanded height) and detach its resize listener.
898
+ function _collapseSkillCardEl(c) {
899
+ c.classList.remove('doclib-card-expanded', 'skill-expand-instant');
900
+ c.style.removeProperty('height');
901
+ const pv = c.querySelector('.doclib-card-preview');
902
+ const pr = c.querySelector('.skill-md-pre') || c.querySelector('.skill-md-editor');
903
+ if (pv) { pv.style.removeProperty('height'); pv.style.removeProperty('flex'); pv.style.removeProperty('max-height'); }
904
+ if (pr) { pr.style.removeProperty('height'); pr.style.removeProperty('flex'); }
905
+ if (c._fillH) window.removeEventListener('resize', c._fillH);
906
+ }
907
+
908
+ async function _expandSkillCard(card, name) {
909
+ const grid = card.closest('.doclib-grid');
910
+ const adminCard = card.closest('.admin-card');
911
+ // Toggle collapse if already open.
912
+ if (card.classList.contains('doclib-card-expanded')) {
913
+ _collapseSkillCardEl(card);
914
+ if (adminCard) adminCard.classList.remove('skills-has-expanded');
915
+ return;
916
+ }
917
+ // Were we already showing another expanded card? If so this is a SWITCH,
918
+ // not a fresh open — skip the fade-in. The fade reveals the previous card
919
+ // collapsing behind the new (semi-transparent) one, which read as a jump.
920
+ const switching = !!(grid && grid.querySelector('.doclib-card-expanded'));
921
+ // Collapse any other expanded sibling (full cleanup, not just the class).
922
+ if (grid) grid.querySelectorAll('.doclib-card-expanded').forEach(_collapseSkillCardEl);
923
+ card.classList.add('doclib-card-expanded');
924
+ if (switching) card.classList.add('skill-expand-instant');
925
+ // Explicit class on the admin-card so CSS doesn't depend on :has()
926
+ // (Firefox mobile builds without :has left the expand at ~50%).
927
+ if (adminCard) adminCard.classList.add('skills-has-expanded');
928
+ if (grid) grid.scrollTop = 0;
929
+
930
+ // Firefox doesn't treat the absolutely-positioned card's stretched height
931
+ // (inset:0) or height:100% as DEFINITE, so grid/flex children won't fill.
932
+ // Pin an explicit px height = the card's already-rendered height. A px
933
+ // value is unambiguously definite, so the preview + <pre> finally fill.
934
+ card._fillH = () => {
935
+ // Reset any prior inline heights so we measure the natural box first
936
+ // (and so switching desktop<->mobile never leaves stale px values).
937
+ card.style.removeProperty('height');
938
+ const preview = card.querySelector('.doclib-card-preview');
939
+ const header = card.querySelector('.skill-card-header');
940
+ const pre = card.querySelector('.skill-md-pre') || card.querySelector('.skill-md-editor');
941
+ if (preview) { preview.style.removeProperty('height'); preview.style.removeProperty('flex'); preview.style.removeProperty('max-height'); }
942
+ if (pre) { pre.style.removeProperty('height'); pre.style.removeProperty('flex'); }
943
+
944
+ // The px-pinning is ONLY for the mobile layout (position:absolute fill,
945
+ // where Firefox won't propagate a definite height). On desktop the card
946
+ // expands via normal flex/flow — pinning measured heights there just
947
+ // under-sizes it. So bail on desktop and let the CSS handle it.
948
+ if (!window.matchMedia('(max-width: 768px)').matches) return;
949
+
950
+ const cardH = card.getBoundingClientRect().height;
951
+ if (cardH <= 0) return;
952
+ card.style.setProperty('height', cardH + 'px', 'important');
953
+ if (!preview) return;
954
+
955
+ const px = (el, prop) => parseFloat(getComputedStyle(el)[prop]) || 0;
956
+ const headerH = header ? header.getBoundingClientRect().height : 0;
957
+ const cardPad = px(card, 'paddingTop') + px(card, 'paddingBottom');
958
+ const previewH = Math.max(0, cardH - headerH - cardPad);
959
+ // Force the preview to an explicit height (flex:none so nothing fights it).
960
+ // A max-height (~335px, resolved from a % rule) was capping it — clear it.
961
+ preview.style.setProperty('flex', '0 0 auto', 'important');
962
+ preview.style.setProperty('max-height', 'none', 'important');
963
+ preview.style.setProperty('height', previewH + 'px', 'important');
964
+
965
+ if (pre) {
966
+ // Pre = preview height minus its non-pre siblings (footer, warn banner).
967
+ const prevPad = px(preview, 'paddingTop') + px(preview, 'paddingBottom');
968
+ let siblings = 0;
969
+ for (const child of preview.children) {
970
+ if (child !== pre) siblings += child.getBoundingClientRect().height;
971
+ }
972
+ const preH = Math.max(0, previewH - prevPad - siblings);
973
+ pre.style.setProperty('height', preH + 'px', 'important');
974
+ pre.style.setProperty('flex', '0 0 auto', 'important');
975
+ }
976
+ };
977
+ // Size SYNCHRONOUSLY (not in rAF) so the pinned heights are in place before
978
+ // the browser's first paint of the expanded card. Running it a frame later
979
+ // let the first frame paint at content-height, then snap — the "explosion"
980
+ // that showed on the first expand (when the SKILL.md was still loading).
981
+ card._fillH();
982
+ window.addEventListener('resize', card._fillH);
983
+
984
+ const pre = card.querySelector('.skill-md-pre');
985
+ if (pre && !card._mdLoaded) {
986
+ // Use the cache when available (the bg preload usually has it already),
987
+ // so the content is in place synchronously — no async settle/jump.
988
+ if (_mdCache.has(name)) {
989
+ const md = _mdCache.get(name);
990
+ pre.textContent = md || '(empty)';
991
+ card._mdLoaded = true;
992
+ card._md = md || '';
993
+ } else {
994
+ pre.textContent = 'Loading…';
995
+ try {
996
+ const md = await _fetchSkillMarkdown(name);
997
+ pre.textContent = md || '(empty)';
998
+ card._mdLoaded = true;
999
+ card._md = md;
1000
+ } catch (e) {
1001
+ pre.textContent = 'Failed to load SKILL.md';
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ // Swap the read-only <pre> for an editable <textarea> (and back). The
1008
+ // Edit button toggles; a Save button commits via the markdown endpoint.
1009
+ function _toggleSkillEdit(card, name) {
1010
+ const preview = card.querySelector('.skill-card-preview');
1011
+ if (!preview) return;
1012
+ const existing = preview.querySelector('.skill-md-editor');
1013
+ if (existing) {
1014
+ // Already editing — treat Edit as Save.
1015
+ _saveSkillEdit(card, name);
1016
+ return;
1017
+ }
1018
+ const pre = preview.querySelector('.skill-md-pre');
1019
+ const ta = document.createElement('textarea');
1020
+ ta.className = 'skill-md-editor';
1021
+ ta.spellcheck = false;
1022
+ ta.value = (card._md != null ? card._md : (pre ? pre.textContent : '')) || '';
1023
+ ta.addEventListener('click', (e) => e.stopPropagation());
1024
+ if (pre) pre.style.display = 'none';
1025
+ preview.insertBefore(ta, preview.querySelector('.doclib-card-expanded-actions'));
1026
+ ta.focus();
1027
+ // Flip the Edit button label to "Save".
1028
+ const editBtn = [...preview.querySelectorAll('.doclib-card-action-btn')].find(b => /Edit|Save/.test(b.textContent));
1029
+ if (editBtn) editBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>Save';
1030
+ }
1031
+
1032
+ async function _saveSkillEdit(card, name) {
1033
+ const preview = card.querySelector('.skill-card-preview');
1034
+ const ta = preview?.querySelector('.skill-md-editor');
1035
+ if (!ta) return;
1036
+ try {
1037
+ const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`, {
1038
+ method: 'POST',
1039
+ headers: { 'Content-Type': 'application/json' },
1040
+ body: JSON.stringify({ markdown: ta.value }),
1041
+ });
1042
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1043
+ // Refresh the cached markdown so the preload/expand show the new text.
1044
+ _mdCache.set(name, ta.value);
1045
+ uiModule.showToast('Saved');
1046
+ await loadSkills(); // re-render (frontmatter changes like name/status may have changed)
1047
+ } catch (e) {
1048
+ uiModule.showError('Save failed: ' + e.message);
1049
+ }
1050
+ }
1051
+
1052
+ async function _deleteSkill(name, card = null) {
1053
+ if (!(await uiModule.styledConfirm(`Delete skill "${name}"? This removes the SKILL.md.`, { confirmText: 'Delete', danger: true }))) return;
1054
+ // Locate the card if the caller didn't hand one over, so we can collapse it
1055
+ // away gracefully (same fade+shrink as the document library) instead of
1056
+ // re-rendering the whole list.
1057
+ if (!card) {
1058
+ card = [...document.querySelectorAll('.skill-card')]
1059
+ .find(c => { const n = c.querySelector('.skill-card-name'); return n && n.textContent === name; }) || null;
1060
+ }
1061
+ try {
1062
+ await fetch(`${API}/api/skills/${encodeURIComponent(name)}`, { method: 'DELETE' });
1063
+ _mdCache.delete(name);
1064
+ if (card) {
1065
+ if (card._testPoll) { clearInterval(card._testPoll); card._testPoll = null; }
1066
+ _setCardRunning(card, false);
1067
+ card.classList.add('doclib-card-deleting');
1068
+ card.addEventListener('transitionend', () => card.remove(), { once: true });
1069
+ setTimeout(() => { if (card.parentElement) card.remove(); }, 400);
1070
+ } else {
1071
+ await loadSkills();
1072
+ }
1073
+ uiModule.showToast('Skill deleted');
1074
+ } catch (e) { uiModule.showError('Delete failed: ' + e.message); }
1075
+ }
1076
+
1077
+ async function _setSkillStatus(name, status) {
1078
+ try {
1079
+ await fetch(`${API}/api/skills/${encodeURIComponent(name)}`, {
1080
+ method: 'PUT',
1081
+ headers: { 'Content-Type': 'application/json' },
1082
+ body: JSON.stringify({ status }),
1083
+ });
1084
+ await loadSkills();
1085
+ uiModule.showToast(status === 'published' ? 'Skill approved' : 'Skill moved to draft');
1086
+ } catch (e) { uiModule.showError('Update failed: ' + e.message); }
1087
+ }
1088
+
1089
+ // ---- Test a skill (sandbox agent run + AI eval) ----
1090
+
1091
+ async function _fetchTestStatus(name) {
1092
+ try {
1093
+ const r = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/test-status`);
1094
+ return r.ok ? await r.json() : { status: 'none' };
1095
+ } catch { return { status: 'none' }; }
1096
+ }
1097
+
1098
+ function _renderTestLog(logEl, verdictEl, job, card, name) {
1099
+ if (!logEl) return;
1100
+ logEl.innerHTML = '';
1101
+ const add = (txt, cls) => { const d = document.createElement('div'); if (cls) d.className = cls; d.textContent = txt; logEl.appendChild(d); };
1102
+ for (const ev of (job.log || [])) {
1103
+ if (ev.type === 'skill_test_start') { add('Task: ' + ev.task, 'skill-test-task'); add('Model: ' + ev.model, 'skill-test-meta'); }
1104
+ else if (ev.type === 'agent_step') add('— round ' + ev.round + ' —', 'skill-test-round');
1105
+ else if (ev.type === 'tool_start') add('▸ ' + ev.tool + ' ' + String(ev.command || '').slice(0, 200), 'skill-test-tool');
1106
+ else if (ev.type === 'tool_output') add(String(ev.output || '').slice(0, 500), 'skill-test-out');
1107
+ else if (ev.type === 'say') add(ev.text || '', 'skill-test-say');
1108
+ else if (ev.type === 'evaluating') add('Evaluating run…', 'skill-test-meta');
1109
+ else if (ev.type === 'error') add('Error: ' + (ev.error || 'run failed'), 'skill-test-err');
1110
+ }
1111
+ if (job.status === 'running') add('…running (you can close this — it keeps going)', 'skill-test-meta');
1112
+ logEl.scrollTop = logEl.scrollHeight;
1113
+ if (job.status === 'done' && job.verdict) _renderTestVerdict(verdictEl, job.verdict, card, name);
1114
+ else if (verdictEl) verdictEl.innerHTML = '';
1115
+ }
1116
+
1117
+ // `force` = start a fresh run even if a finished result already exists (Retry).
1118
+ async function _testSkill(card, name, force = false) {
1119
+ if (!card.classList.contains('doclib-card-expanded')) await _expandSkillCard(card, name);
1120
+ const preview = card.querySelector('.skill-card-preview');
1121
+ if (!preview) return;
1122
+ preview.innerHTML =
1123
+ '<div class="skill-test"><div class="skill-test-log"></div>' +
1124
+ '<div class="skill-test-verdict"></div></div>';
1125
+ const logEl = preview.querySelector('.skill-test-log');
1126
+ const verdictEl = preview.querySelector('.skill-test-verdict');
1127
+ if (card._testPoll) { clearInterval(card._testPoll); card._testPoll = null; }
1128
+
1129
+ // Attach to an existing job unless forcing a fresh run.
1130
+ let job = force ? { status: 'none' } : await _fetchTestStatus(name);
1131
+
1132
+ if (job.status === 'none') {
1133
+ logEl.innerHTML = '<div class="skill-test-meta">Starting test…</div>';
1134
+ let model = '', endpoint_url = '';
1135
+ try {
1136
+ const sm = window.sessionModule;
1137
+ model = (sm && sm.getCurrentModel && sm.getCurrentModel()) || '';
1138
+ endpoint_url = (sm && sm.getCurrentEndpointUrl && sm.getCurrentEndpointUrl()) || '';
1139
+ } catch (_) {}
1140
+ try {
1141
+ const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/test`, {
1142
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1143
+ body: JSON.stringify({ model, endpoint_url }),
1144
+ });
1145
+ if (!res.ok) { logEl.innerHTML = '<div class="skill-test-err">Test failed: HTTP ' + res.status + '</div>'; return; }
1146
+ } catch (e) { logEl.innerHTML = '<div class="skill-test-err">Test failed: ' + (e.message || e) + '</div>'; return; }
1147
+ job = await _fetchTestStatus(name);
1148
+ }
1149
+
1150
+ _renderTestLog(logEl, verdictEl, job, card, name);
1151
+ _setCardRunning(card, job.status === 'running');
1152
+
1153
+ if (job.status === 'running') {
1154
+ card._testPoll = setInterval(async () => {
1155
+ // Keep polling even if the card is collapsed (the test runs server-side);
1156
+ // only stop once the card itself is gone from the DOM.
1157
+ if (!document.body.contains(card)) { clearInterval(card._testPoll); card._testPoll = null; _setCardRunning(card, false); return; }
1158
+ const s = await _fetchTestStatus(name);
1159
+ // Update the expanded log only while it's still on screen.
1160
+ if (document.body.contains(logEl)) _renderTestLog(logEl, verdictEl, s, card, name);
1161
+ if (s.status !== 'running') {
1162
+ clearInterval(card._testPoll); card._testPoll = null;
1163
+ _setCardRunning(card, false);
1164
+ // If the log isn't visible (card was collapsed), still update the
1165
+ // header dot/% so the result shows on the folded card.
1166
+ if (!document.body.contains(logEl) && s.verdict && s.verdict.verdict) {
1167
+ _applyVerdictToHeader(card, s.verdict.verdict);
1168
+ }
1169
+ }
1170
+ }, 1300);
1171
+ }
1172
+ }
1173
+
1174
+ // Show/hide the app-wide whirlpool spinner next to the skill name while a test
1175
+ // is in flight. Works on the collapsed header too, since we inject a real DOM
1176
+ // element rather than a CSS pseudo on a class.
1177
+ function _setCardRunning(card, on) {
1178
+ if (!card) return;
1179
+ card.classList.toggle('skill-test-running', !!on);
1180
+ if (on) {
1181
+ if (card._testSpinner) return;
1182
+ const nameEl = card.querySelector('.skill-card-name');
1183
+ if (!nameEl) return;
1184
+ const wp = spinnerModule.createWhirlpool(12);
1185
+ wp.element.style.cssText = 'display:inline-flex;width:12px;height:12px;margin:0 0 0 7px;vertical-align:middle;flex-shrink:0;';
1186
+ // Append INSIDE the <code> name (inline-flow), not after it. The textcol
1187
+ // is a flex column, so a sibling-after lands on its own line — putting
1188
+ // the spinner inside the inline code keeps it on the title row.
1189
+ nameEl.appendChild(wp.element);
1190
+ card._testSpinner = wp;
1191
+ } else if (card._testSpinner) {
1192
+ try { card._testSpinner.destroy(); } catch (_) {}
1193
+ if (card._testSpinner.element && card._testSpinner.element.parentElement) {
1194
+ card._testSpinner.element.remove();
1195
+ }
1196
+ card._testSpinner = null;
1197
+ }
1198
+ }
1199
+
1200
+ // Reflect a test/audit verdict on the (possibly collapsed) card header without
1201
+ // a full reload: the glowing audit dot, the confidence %, and the pass check.
1202
+ // Works whether the card is expanded or folded so a test that finishes after
1203
+ // you collapse still updates the card.
1204
+ function _applyVerdictToHeader(card, verdict) {
1205
+ if (!card || !verdict) return;
1206
+ const dotColor = {
1207
+ pass: 'var(--color-success, #4ade80)',
1208
+ needs_work: 'var(--color-warning, #f0ad4e)',
1209
+ inconclusive: 'var(--color-warning, #f0ad4e)',
1210
+ fail: 'var(--color-danger, #e06c75)',
1211
+ }[verdict];
1212
+ // Audit dot removed at user request — strip any pre-existing one so the
1213
+ // post-audit live update doesn't leave a stale dot from an old render.
1214
+ const header = card.querySelector('.skill-card-header');
1215
+ if (header) {
1216
+ header.querySelectorAll('.skill-audit-dot').forEach(n => n.remove());
1217
+ }
1218
+ const newConf = { pass: 95, needs_work: 60, fail: 40 }[verdict];
1219
+ const statsEl = card.querySelector('.skill-stats');
1220
+ if (statsEl && newConf != null) {
1221
+ const confEl = statsEl.querySelector('.skill-conf');
1222
+ if (confEl) { confEl.textContent = newConf + '%'; confEl.style.color = _confColor(newConf); }
1223
+ }
1224
+ // Fold the verdict into the status (draft / published) pill — colour the
1225
+ // pill itself and append a tiny check/warn/cross glyph so the audit result
1226
+ // lives next to the label instead of dangling in the stats row.
1227
+ const pill = card.querySelector('.skill-status-pill');
1228
+ if (pill) {
1229
+ // Inline glyphs for the per-verdict pill — appear next to the "checked"
1230
+ // label so the verdict reads as a real badge.
1231
+ const ICON = {
1232
+ pass: '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><polyline points="20 6 9 17 4 12"/></svg>',
1233
+ needs_work: '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><line x1="12" y1="8" x2="12" y2="13"/><line x1="12" y1="17" x2="12" y2="17"/></svg>',
1234
+ inconclusive: '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><line x1="12" y1="8" x2="12" y2="13"/><line x1="12" y1="17" x2="12" y2="17"/></svg>',
1235
+ fail: '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
1236
+ }[verdict];
1237
+ // Wash the pill's bg + tint the text so a glance at the badge tells you
1238
+ // pass/needs-work/fail without expanding the card.
1239
+ const tint = {
1240
+ pass: { bg: 'color-mix(in srgb, var(--color-success, #4ade80) 30%, transparent)', fg: 'var(--color-success, #4ade80)' },
1241
+ needs_work: { bg: 'color-mix(in srgb, var(--color-warning, #f0ad4e) 30%, transparent)', fg: 'var(--color-warning, #f0ad4e)' },
1242
+ inconclusive: { bg: 'color-mix(in srgb, var(--color-warning, #f0ad4e) 30%, transparent)', fg: 'var(--color-warning, #f0ad4e)' },
1243
+ fail: { bg: 'color-mix(in srgb, var(--color-danger, #e06c75) 30%, transparent)', fg: 'var(--color-danger, #e06c75)' },
1244
+ }[verdict];
1245
+ // The status pill (draft / published) keeps its own colours now — the
1246
+ // verdict lives in a separate "checked" pill that's inserted next to it.
1247
+ // Remove any prior audit glyph (was previously inserted inside the pill;
1248
+ // now scrub both the in-pill and sibling locations on every refresh).
1249
+ pill.querySelectorAll('.skill-pill-verdict').forEach(n => n.remove());
1250
+ if (pill.parentElement) {
1251
+ pill.parentElement.querySelectorAll(':scope > .skill-pill-verdict').forEach(n => n.remove());
1252
+ }
1253
+ if (ICON) {
1254
+ // Full "checked" pill badge — sits LEFT of the draft/published pill,
1255
+ // styled like the other memory-cat-badges so it reads as a real chip.
1256
+ const span = document.createElement('span');
1257
+ span.className = 'memory-cat-badge skill-pill-verdict';
1258
+ span.title = 'Audited: ' + verdict.replace(/_/g, ' ');
1259
+ span.innerHTML = ICON + '<span>checked</span>';
1260
+ if (tint) {
1261
+ span.style.background = tint.bg;
1262
+ span.style.color = tint.fg;
1263
+ }
1264
+ if (pill.parentElement) {
1265
+ pill.parentElement.insertBefore(span, pill);
1266
+ } else {
1267
+ pill.insertAdjacentElement('beforebegin', span);
1268
+ }
1269
+ }
1270
+ }
1271
+ // Old free-floating .skill-verified check (next to confidence %) is no
1272
+ // longer added — the pill carries the verdict glyph now. Remove a stale
1273
+ // one in case the card was rendered by an earlier build.
1274
+ card.querySelectorAll('.skill-verified').forEach(n => n.remove());
1275
+ }
1276
+
1277
+ function _renderTestVerdict(el, v, card, name) {
1278
+ if (!el) return;
1279
+ const verdict = (v && v.verdict) || 'unknown';
1280
+ const cls = { pass: 'ok', needs_work: 'warn', fail: 'bad', inconclusive: 'unknown' }[verdict] || 'unknown';
1281
+ const label = { pass: 'PASS', needs_work: 'NEEDS WORK', fail: 'FAIL', inconclusive: 'INCONCLUSIVE', unknown: 'UNCLEAR' }[verdict] || 'UNCLEAR';
1282
+ const conf = v && typeof v.confidence === 'number' ? Math.round(v.confidence * 100) + '%' : '';
1283
+ const issues = Array.isArray(v && v.issues) ? v.issues : [];
1284
+ // Reflect the skill's current state: if it's already published, the button
1285
+ // confirms "Approved" (click to unpublish) rather than offering to approve.
1286
+ const isPub = card && card.dataset && card.dataset.skillStatus === 'published';
1287
+ const approveLabel = isPub ? 'Approved' : 'Approve';
1288
+ const approveCls = 'skill-eval-approve' + (isPub ? ' is-approved' : (verdict === 'pass' ? ' suggested' : ''));
1289
+ const approveTitle = isPub ? 'Already approved — click to unpublish' : 'Publish — appears in the skills index';
1290
+ el.innerHTML =
1291
+ '<div class="skill-eval-head"><span class="skill-eval-badge skill-eval-' + cls + '">' + label + (conf ? ' · ' + conf : '') + '</span>' +
1292
+ '<span class="skill-eval-summary">' + esc((v && v.summary) || '') + '</span></div>' +
1293
+ (issues.length ? '<ul class="skill-eval-issues">' + issues.map(i => '<li>' + esc(i) + '</li>').join('') + '</ul>' : '') +
1294
+ '<div class="doclib-card-expanded-actions skill-eval-actions-wrap">' +
1295
+ '<button class="doclib-card-text-btn doclib-card-action-btn ' + approveCls + '" data-act="approve" title="' + approveTitle + '">' + approveLabel + '</button>' +
1296
+ '<div class="doclib-action-group"><div class="doclib-action-btn-row">' +
1297
+ '<button class="doclib-card-text-btn doclib-card-action-btn" data-act="retry" title="Run the test again">Retry</button>' +
1298
+ '<button class="doclib-card-text-btn doclib-card-action-btn" data-act="copy" title="Copy the run output + verdict">Copy</button>' +
1299
+ '<button class="doclib-card-text-btn doclib-card-action-btn" data-act="edit">Edit</button>' +
1300
+ '<button class="doclib-card-text-btn doclib-card-action-btn doclib-card-text-btn-danger" data-act="del"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>Delete</button>' +
1301
+ '</div></div>' +
1302
+ '</div>';
1303
+ _applyVerdictToHeader(card, verdict);
1304
+ el.querySelector('[data-act="approve"]')?.addEventListener('click', async (e) => {
1305
+ e.stopPropagation();
1306
+ const nowPub = card.dataset.skillStatus === 'published';
1307
+ await _setSkillStatus(name, nowPub ? 'draft' : 'published');
1308
+ // _setSkillStatus reloads the list, but if this card survives, relabel it.
1309
+ card.dataset.skillStatus = nowPub ? 'draft' : 'published';
1310
+ const btn = el.querySelector('[data-act="approve"]');
1311
+ if (btn) {
1312
+ const pub = card.dataset.skillStatus === 'published';
1313
+ btn.textContent = pub ? 'Approved' : 'Approve';
1314
+ btn.title = pub ? 'Already approved — click to unpublish' : 'Publish — appears in the skills index';
1315
+ btn.classList.toggle('is-approved', pub);
1316
+ btn.classList.toggle('suggested', !pub && verdict === 'pass');
1317
+ }
1318
+ });
1319
+ el.querySelector('[data-act="del"]')?.addEventListener('click', (e) => { e.stopPropagation(); _deleteSkill(name, card); });
1320
+ el.querySelector('[data-act="edit"]')?.addEventListener('click', (e) => { e.stopPropagation(); _toggleSkillEdit(card, name); });
1321
+ el.querySelector('[data-act="retry"]')?.addEventListener('click', (e) => { e.stopPropagation(); _testSkill(card, name, true); });
1322
+ el.querySelector('[data-act="copy"]')?.addEventListener('click', (e) => {
1323
+ e.stopPropagation();
1324
+ const logEl = card.querySelector('.skill-test-log');
1325
+ const issuesTxt = issues.length ? '\nIssues:\n- ' + issues.join('\n- ') : '';
1326
+ const text = (logEl ? logEl.innerText.trim() + '\n\n' : '') +
1327
+ '=== Eval: ' + label + (conf ? ' (' + conf + ')' : '') + ' ===\n' + ((v && v.summary) || '') + issuesTxt;
1328
+ // Shared helper falls back to execCommand on plain HTTP (navigator.clipboard
1329
+ // is unavailable in non-secure contexts, which is why the raw call failed).
1330
+ uiModule.copyToClipboard(text);
1331
+ });
1332
+ }
1333
+
1334
+ // ---- Audit all skills (autonomous: test → fix → retry → teacher → flag) ----
1335
+
1336
+ let _auditPoll = null;
1337
+ let _auditSeenResults = 0;
1338
+
1339
+ function _confirmAuditSkills(label) {
1340
+ return new Promise(resolve => {
1341
+ let overlay = document.getElementById('skills-audit-confirm-overlay');
1342
+ if (!overlay) {
1343
+ overlay = document.createElement('div');
1344
+ overlay.id = 'skills-audit-confirm-overlay';
1345
+ overlay.className = 'modal';
1346
+ overlay.innerHTML =
1347
+ '<div class="modal-content styled-confirm-box">' +
1348
+ '<div class="modal-header"><h4>Audit Skills</h4></div>' +
1349
+ '<div class="modal-body">' +
1350
+ '<p id="skills-audit-confirm-msg"></p>' +
1351
+ '<label class="memory-bulk-check-all" style="margin-top:10px;display:inline-flex;align-items:center;gap:7px;">' +
1352
+ '<input type="checkbox" id="skills-audit-skip-audited" checked />' +
1353
+ '<span>Skip already audited</span>' +
1354
+ '</label>' +
1355
+ '</div>' +
1356
+ '<div class="modal-footer">' +
1357
+ '<button id="skills-audit-confirm-cancel" class="confirm-btn confirm-btn-secondary">Cancel</button>' +
1358
+ '<button id="skills-audit-confirm-ok" class="confirm-btn confirm-btn-primary">Audit</button>' +
1359
+ '</div>' +
1360
+ '</div>';
1361
+ document.body.appendChild(overlay);
1362
+ }
1363
+
1364
+ const msg = overlay.querySelector('#skills-audit-confirm-msg');
1365
+ const skip = overlay.querySelector('#skills-audit-skip-audited');
1366
+ const okBtn = overlay.querySelector('#skills-audit-confirm-ok');
1367
+ const cancelBtn = overlay.querySelector('#skills-audit-confirm-cancel');
1368
+ msg.textContent = `Audit ${label}? Each is tested from top to bottom, then published or moved to draft using your auto-approve confidence threshold.`;
1369
+ skip.checked = true;
1370
+ overlay.classList.remove('hidden');
1371
+ overlay.style.display = '';
1372
+
1373
+ function cleanup(result) {
1374
+ overlay.classList.add('hidden');
1375
+ overlay.style.display = 'none';
1376
+ okBtn.removeEventListener('click', onOk);
1377
+ cancelBtn.removeEventListener('click', onCancel);
1378
+ overlay.removeEventListener('click', onBackdrop);
1379
+ document.removeEventListener('keydown', onKey);
1380
+ resolve(result);
1381
+ }
1382
+ function onOk() { cleanup({ ok: true, skipAudited: !!skip.checked }); }
1383
+ function onCancel() { cleanup({ ok: false, skipAudited: false }); }
1384
+ function onBackdrop(e) { if (e.target === overlay) onCancel(); }
1385
+ function onKey(e) {
1386
+ if (e.key === 'Escape') {
1387
+ e.preventDefault();
1388
+ e.stopPropagation();
1389
+ cleanup({ ok: false, skipAudited: false });
1390
+ }
1391
+ }
1392
+
1393
+ okBtn.addEventListener('click', onOk);
1394
+ cancelBtn.addEventListener('click', onCancel);
1395
+ overlay.addEventListener('click', onBackdrop);
1396
+ document.addEventListener('keydown', onKey);
1397
+ okBtn.focus();
1398
+ });
1399
+ }
1400
+
1401
+ async function _auditAllSkills(opts = {}) {
1402
+ const panel = document.getElementById('skills-audit-panel');
1403
+ if (!panel) return;
1404
+ // If a run is already going, just (re)attach to it.
1405
+ let st = await _fetchAuditStatus();
1406
+ if (st.status !== 'running') {
1407
+ const explicitNames = Array.isArray(opts.names) ? opts.names.filter(Boolean) : null;
1408
+ const visibleNames = _getFilteredSkills()
1409
+ .map(sk => sk.name || sk.id)
1410
+ .filter(Boolean);
1411
+ const names = explicitNames || visibleNames;
1412
+ const label = explicitNames
1413
+ ? `${names.length} selected ${names.length === 1 ? 'skill' : 'skills'}`
1414
+ : `${names.length} visible ${names.length === 1 ? 'skill' : 'skills'}`;
1415
+ if (!names.length) {
1416
+ uiModule.showToast(explicitNames ? 'No selected skills to audit' : 'No visible skills to audit');
1417
+ return;
1418
+ }
1419
+ const confirmed = await _confirmAuditSkills(label);
1420
+ if (!confirmed.ok) return;
1421
+ try {
1422
+ const r = await fetch(`${API}/api/skills/audit-all`, {
1423
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1424
+ body: JSON.stringify({ scope: explicitNames ? 'selected' : 'all', names, skip_audited: confirmed.skipAudited }),
1425
+ });
1426
+ if (!r.ok) { uiModule.showError('Audit failed to start (HTTP ' + r.status + ')'); return; }
1427
+ st = await _fetchAuditStatus();
1428
+ } catch (e) { uiModule.showError('Audit failed: ' + (e.message || e)); return; }
1429
+ _auditSeenResults = 0;
1430
+ }
1431
+ panel.classList.remove('hidden');
1432
+ _auditSeenResults = Math.min(_auditSeenResults, (st.results || []).length);
1433
+ _renderAuditPanel(panel, st);
1434
+ _applyAuditResults(st);
1435
+ _highlightAuditCard(st.status === 'running' ? st.current : null);
1436
+ if (_auditPoll) clearInterval(_auditPoll);
1437
+ if (st.status === 'running') {
1438
+ _auditPoll = setInterval(async () => {
1439
+ const s = await _fetchAuditStatus();
1440
+ _renderAuditPanel(panel, s);
1441
+ _applyAuditResults(s);
1442
+ _highlightAuditCard(s.status === 'running' ? s.current : null);
1443
+ if (s.status !== 'running') {
1444
+ clearInterval(_auditPoll); _auditPoll = null;
1445
+ _highlightAuditCard(null);
1446
+ loadSkills(); // refresh statuses (some may have been demoted/edited)
1447
+ }
1448
+ }, 1500);
1449
+ }
1450
+ }
1451
+
1452
+ function _findSkillCard(name) {
1453
+ if (!name) return null;
1454
+ return [...document.querySelectorAll('.skill-card[data-skill-name]')]
1455
+ .find(c => c.dataset.skillName === name) || null;
1456
+ }
1457
+
1458
+ function _mergeSkillState(state) {
1459
+ if (!state || !state.name) return;
1460
+ const idx = skills.findIndex(s => (s.name || s.id) === state.name);
1461
+ if (idx >= 0) skills[idx] = { ...skills[idx], ...state };
1462
+ }
1463
+
1464
+ function _applySkillStateToHeader(card, state, fallbackVerdict) {
1465
+ if (!card) return;
1466
+ const verdict = state?.audit_verdict || fallbackVerdict;
1467
+ if (verdict) _applyVerdictToHeader(card, verdict);
1468
+ if (state && typeof state.confidence === 'number') {
1469
+ const conf = Math.round(state.confidence * 100);
1470
+ const confEl = card.querySelector('.skill-conf');
1471
+ if (confEl) { confEl.textContent = conf + '%'; confEl.style.color = _confColor(conf); }
1472
+ }
1473
+ if (state?.status) {
1474
+ card.dataset.skillStatus = state.status;
1475
+ const oldPill = card.querySelector('.skill-status-pill');
1476
+ if (oldPill) {
1477
+ const wrap = document.createElement('span');
1478
+ wrap.innerHTML = _statusPill(state);
1479
+ const next = wrap.firstElementChild;
1480
+ if (next) oldPill.replaceWith(next);
1481
+ }
1482
+ }
1483
+ const right = card.querySelector('.skill-card-right');
1484
+ if (right && state) {
1485
+ right.querySelectorAll('.skill-model-pill, .skill-necessity-pill').forEach(n => n.remove());
1486
+ const stats = right.querySelector('.skill-stats');
1487
+ const wrap = document.createElement('span');
1488
+ wrap.innerHTML = _auditModelPills(state) + _necessityPill(state);
1489
+ [...wrap.children].forEach(p => {
1490
+ if (stats) right.insertBefore(p, stats);
1491
+ else right.appendChild(p);
1492
+ });
1493
+ }
1494
+ }
1495
+
1496
+ function _applyAuditResults(st) {
1497
+ const results = st && Array.isArray(st.results) ? st.results : [];
1498
+ if (!results.length) return;
1499
+ for (const r of results.slice(_auditSeenResults)) {
1500
+ const name = r && r.skill;
1501
+ if (!name) continue;
1502
+ const state = r.skill_state || null;
1503
+ _mergeSkillState(state);
1504
+ const verdict = state?.audit_verdict || r.verdict?.verdict || (r.result === 'flagged' ? 'fail' : null);
1505
+ _applySkillStateToHeader(_findSkillCard(name), state, verdict);
1506
+ }
1507
+ _auditSeenResults = results.length;
1508
+ }
1509
+
1510
+ // Make the card currently being audited glow, so it's obvious which one the
1511
+ // "Audit now" run is processing. Pass null to clear all highlights.
1512
+ function _highlightAuditCard(name) {
1513
+ document.querySelectorAll('.skill-card.skill-audit-active')
1514
+ .forEach(c => { c.classList.remove('skill-audit-active'); _setCardRunning(c, false); });
1515
+ if (!name) return;
1516
+ const card = _findSkillCard(name);
1517
+ if (card) {
1518
+ card.classList.add('skill-audit-active');
1519
+ _setCardRunning(card, true);
1520
+ card.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
1521
+ }
1522
+ }
1523
+
1524
+ async function _fetchAuditStatus() {
1525
+ try {
1526
+ const r = await fetch(`${API}/api/skills/audit-all/status`);
1527
+ return r.ok ? await r.json() : { status: 'none' };
1528
+ } catch { return { status: 'none' }; }
1529
+ }
1530
+
1531
+ function _renderAuditPanel(panel, st) {
1532
+ if (st.status === 'none') { panel.classList.add('hidden'); panel.innerHTML = ''; return; }
1533
+ const done = st.done || 0, total = st.total || 0;
1534
+ const pct = total ? Math.round((done / total) * 100) : 0;
1535
+ const counts = {};
1536
+ for (const r of (st.results || [])) counts[r.result] = (counts[r.result] || 0) + 1;
1537
+ const summary = Object.entries(counts).map(([k, v]) => v + ' ' + k.replace(/_/g, ' ')).join(' · ');
1538
+ const running = st.status === 'running';
1539
+ const cancelled = st.status === 'cancelled';
1540
+ const head = running
1541
+ ? `Auditing ${done}/${total}${st.current ? ' — ' + esc(st.current) : ''}`
1542
+ : cancelled
1543
+ ? `Audit cancelled — ${done}/${total}`
1544
+ : `Audit complete — ${total} skill${total === 1 ? '' : 's'}`;
1545
+ panel.innerHTML =
1546
+ '<div class="skills-audit-head">' +
1547
+ '<span class="skills-audit-title-wrap" style="display:inline-flex;align-items:center;gap:8px;">' +
1548
+ '<span class="skills-audit-title">' + head + '</span>' +
1549
+ '</span>' +
1550
+ (running
1551
+ ? '<button class="memory-toolbar-btn" data-act="audit-cancel">Cancel</button>'
1552
+ : '<button class="memory-toolbar-btn" data-act="audit-close">Close</button>') +
1553
+ '</div>' +
1554
+ '<div class="skills-audit-bar"><div class="skills-audit-fill" style="width:' + pct + '%"></div></div>' +
1555
+ (summary ? '<div class="skills-audit-summary">' + esc(summary) + (st.teacher ? ' · teacher: ' + esc(st.teacher) : '') + '</div>' : '') +
1556
+ '<div class="skills-audit-log">' + (st.log || []).slice(-40).map(l => '<div>' + esc(l) + '</div>').join('') + '</div>';
1557
+ // Whirlpool sits next to the title while the audit is actually running.
1558
+ if (running) {
1559
+ const titleWrap = panel.querySelector('.skills-audit-title-wrap');
1560
+ if (titleWrap) {
1561
+ const wp = spinnerModule.createWhirlpool(12);
1562
+ wp.element.style.cssText = 'display:inline-flex;width:12px;height:12px;margin:0;vertical-align:middle;flex-shrink:0;';
1563
+ titleWrap.appendChild(wp.element);
1564
+ }
1565
+ }
1566
+ const cancel = panel.querySelector('[data-act="audit-cancel"]');
1567
+ if (cancel) cancel.addEventListener('click', async (e) => {
1568
+ e.stopPropagation();
1569
+ cancel.disabled = true;
1570
+ cancel.textContent = 'Cancelling...';
1571
+ try {
1572
+ await fetch(`${API}/api/skills/audit-all/cancel`, { method: 'POST', credentials: 'same-origin' });
1573
+ const s = await _fetchAuditStatus();
1574
+ _renderAuditPanel(panel, { ...s, status: s.status === 'none' ? 'cancelled' : s.status });
1575
+ _highlightAuditCard(null);
1576
+ } catch {
1577
+ cancel.disabled = false;
1578
+ cancel.textContent = 'Cancel';
1579
+ }
1580
+ });
1581
+ const close = panel.querySelector('[data-act="audit-close"]');
1582
+ if (close) close.addEventListener('click', () => { panel.classList.add('hidden'); panel.innerHTML = ''; });
1583
+ const logEl = panel.querySelector('.skills-audit-log');
1584
+ if (logEl) logEl.scrollTop = logEl.scrollHeight;
1585
+ }
1586
+
1587
+ // ---- Select mode / bulk actions ----
1588
+
1589
+ function _enterSelectMode() {
1590
+ _selectMode = true;
1591
+ _selectedNames.clear();
1592
+ const bar = document.getElementById('skills-bulk-bar');
1593
+ const btn = document.getElementById('skills-select-btn');
1594
+ if (bar) bar.classList.remove('hidden');
1595
+ if (btn) { btn.classList.add('active'); btn.textContent = 'Cancel'; }
1596
+ _updateBulkBar();
1597
+ renderSkillsList();
1598
+ }
1599
+
1600
+ function _exitSelectMode() {
1601
+ _selectMode = false;
1602
+ _selectedNames.clear();
1603
+ const bar = document.getElementById('skills-bulk-bar');
1604
+ const btn = document.getElementById('skills-select-btn');
1605
+ const all = document.getElementById('skills-select-all');
1606
+ if (bar) bar.classList.add('hidden');
1607
+ if (btn) { btn.classList.remove('active'); btn.textContent = 'Select'; }
1608
+ if (all) all.checked = false;
1609
+ renderSkillsList();
1610
+ }
1611
+
1612
+ function _updateBulkBar() {
1613
+ const countEl = document.getElementById('skills-selected-count');
1614
+ const delBtn = document.getElementById('skills-bulk-delete');
1615
+ const delNonPassingBtn = document.getElementById('skills-bulk-delete-nonpassing');
1616
+ const pubBtn = document.getElementById('skills-bulk-publish');
1617
+ const auditBtn = document.getElementById('skills-bulk-audit');
1618
+ if (countEl) countEl.textContent = `${_selectedNames.size} Selected`;
1619
+ if (delBtn) delBtn.disabled = _selectedNames.size === 0;
1620
+ if (auditBtn) auditBtn.disabled = _selectedNames.size === 0;
1621
+ if (delNonPassingBtn) {
1622
+ const count = _selectedNonPassingSkills().length;
1623
+ delNonPassingBtn.disabled = count === 0;
1624
+ delNonPassingBtn.title = count
1625
+ ? `Delete ${count} selected non-passing ${count === 1 ? 'skill' : 'skills'}`
1626
+ : 'No selected non-passing skills';
1627
+ }
1628
+ // Approve is only meaningful when at least one selected skill is still a draft.
1629
+ const anyDraft = [..._selectedNames].some(n => {
1630
+ const sk = skills.find(s => (s.name || s.id) === n);
1631
+ return sk && (sk.status || 'draft') !== 'published';
1632
+ });
1633
+ if (pubBtn) pubBtn.disabled = !anyDraft;
1634
+ }
1635
+
1636
+ function _toggleSelectAll() {
1637
+ const all = document.getElementById('skills-select-all');
1638
+ if (!all) return;
1639
+ const visible = _getFilteredSkills().map(s => s.name || s.id);
1640
+ if (all.checked) visible.forEach(n => _selectedNames.add(n));
1641
+ else visible.forEach(n => _selectedNames.delete(n));
1642
+ _updateBulkBar();
1643
+ renderSkillsList();
1644
+ }
1645
+
1646
+ async function _bulkDelete() {
1647
+ if (!_selectedNames.size) return;
1648
+ const n = _selectedNames.size;
1649
+ const ok = await uiModule.styledConfirm(
1650
+ `Delete ${n} ${n === 1 ? 'skill' : 'skills'}? This removes their SKILL.md files.`,
1651
+ { confirmText: 'Delete', danger: true }
1652
+ );
1653
+ if (!ok) return;
1654
+ let deleted = 0;
1655
+ const deletedNames = [];
1656
+ for (const name of _selectedNames) {
1657
+ try {
1658
+ const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}`, { method: 'DELETE' });
1659
+ if (res.ok) {
1660
+ deleted++;
1661
+ deletedNames.push(name);
1662
+ }
1663
+ } catch {}
1664
+ }
1665
+ for (const name of deletedNames) {
1666
+ const card = document.querySelector(`.skill-card[data-skill-name="${CSS.escape(name)}"]`);
1667
+ if (card) card.classList.add('doclib-card-deleting');
1668
+ }
1669
+ if (deletedNames.length) await new Promise(resolve => setTimeout(resolve, 320));
1670
+ _exitSelectMode();
1671
+ await loadSkills();
1672
+ uiModule.showToast(`Deleted ${deleted}`);
1673
+ }
1674
+
1675
+ async function _loadSkillApprovalThreshold() {
1676
+ try {
1677
+ const res = await fetch(`${API}/api/prefs`, { credentials: 'same-origin' });
1678
+ if (!res.ok) return;
1679
+ const prefs = await res.json();
1680
+ const raw = prefs.skill_min_confidence ?? prefs.skill_autosave_min_confidence;
1681
+ const val = Number(raw);
1682
+ if (Number.isFinite(val)) _skillApprovalThreshold = Math.max(0, Math.min(1, val));
1683
+ } catch {}
1684
+ }
1685
+
1686
+ function _selectedNonPassingSkills() {
1687
+ const selected = new Set(_selectedNames);
1688
+ return skills.filter(sk => {
1689
+ const name = sk.name || sk.id;
1690
+ if (!selected.has(name)) return false;
1691
+ const conf = Number(sk.confidence || 0);
1692
+ const necessity = _necessityKind(sk);
1693
+ if (necessity === 'duplicate' || necessity === 'trivial' || necessity === 'irrelevant') return true;
1694
+ if ((sk.audit_verdict || '') !== 'pass') return true;
1695
+ return conf < _skillApprovalThreshold;
1696
+ });
1697
+ }
1698
+
1699
+ async function _bulkDeleteNonPassing() {
1700
+ const targets = _selectedNonPassingSkills();
1701
+ if (!targets.length) {
1702
+ uiModule.showToast('No selected non-passing skills');
1703
+ return;
1704
+ }
1705
+ const thresholdPct = Math.round(_skillApprovalThreshold * 100);
1706
+ const names = targets.map(sk => sk.name || sk.id).filter(Boolean);
1707
+ const ok = await uiModule.styledConfirm(
1708
+ `Delete ${names.length} selected non-passing ${names.length === 1 ? 'skill' : 'skills'}? This removes duplicates, generic/irrelevant skills, failed audits, and anything below ${thresholdPct}%.`,
1709
+ { confirmText: 'Delete non passing', danger: true }
1710
+ );
1711
+ if (!ok) return;
1712
+ let deleted = 0;
1713
+ const deletedNames = [];
1714
+ for (const name of names) {
1715
+ try {
1716
+ const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}`, { method: 'DELETE' });
1717
+ if (res.ok) {
1718
+ deleted++;
1719
+ deletedNames.push(name);
1720
+ _mdCache.delete(name);
1721
+ }
1722
+ } catch {}
1723
+ }
1724
+ for (const name of deletedNames) {
1725
+ const card = document.querySelector(`.skill-card[data-skill-name="${CSS.escape(name)}"]`);
1726
+ if (card) card.classList.add('doclib-card-deleting');
1727
+ }
1728
+ if (deletedNames.length) await new Promise(resolve => setTimeout(resolve, 320));
1729
+ _exitSelectMode();
1730
+ await loadSkills();
1731
+ uiModule.showToast(`Deleted ${deleted} non-passing`);
1732
+ }
1733
+
1734
+ async function _bulkApprove() {
1735
+ if (!_selectedNames.size) return;
1736
+ let published = 0;
1737
+ for (const name of _selectedNames) {
1738
+ const sk = skills.find(s => (s.name || s.id) === name);
1739
+ if (sk && sk.status === 'published') continue;
1740
+ try {
1741
+ const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}`, {
1742
+ method: 'PUT',
1743
+ headers: { 'Content-Type': 'application/json' },
1744
+ body: JSON.stringify({ status: 'published' }),
1745
+ });
1746
+ if (res.ok) published++;
1747
+ } catch {}
1748
+ }
1749
+ _exitSelectMode();
1750
+ await loadSkills();
1751
+ uiModule.showToast(`Published ${published}`);
1752
+ }
1753
+
1754
+ async function _bulkAudit() {
1755
+ if (!_selectedNames.size) return;
1756
+ const selected = new Set(_selectedNames);
1757
+ const ordered = _getFilteredSkills()
1758
+ .map(sk => sk.name || sk.id)
1759
+ .filter(n => selected.has(n));
1760
+ _exitSelectMode();
1761
+ await _auditAllSkills({ names: ordered });
1762
+ }
1763
+
1764
+ async function _showSkillSource(name) {
1765
+ let md = '';
1766
+ try {
1767
+ const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`);
1768
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1769
+ const data = await res.json();
1770
+ md = data.markdown || '';
1771
+ } catch (e) {
1772
+ uiModule.showError('Failed to load SKILL.md');
1773
+ return;
1774
+ }
1775
+
1776
+ // Lightweight modal — reuses the .modal CSS the rest of the app uses.
1777
+ const wrap = document.createElement('div');
1778
+ wrap.className = 'modal';
1779
+ wrap.style.display = 'block';
1780
+ wrap.innerHTML = `
1781
+ <div class="modal-content" style="max-width:760px;max-height:85vh;display:flex;flex-direction:column">
1782
+ <div class="modal-header">
1783
+ <h4>SKILL.md — <code>${esc(name)}</code></h4>
1784
+ <span style="flex:1"></span>
1785
+ <button class="memory-toolbar-btn" id="skill-save-btn">Save</button>
1786
+ <button class="close-btn" id="skill-md-close">✖</button>
1787
+ </div>
1788
+ <div class="modal-body" style="display:flex;flex-direction:column;gap:8px">
1789
+ <textarea id="skill-md-textarea" spellcheck="false" style="flex:1;min-height:50vh;width:100%;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;padding:10px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--fg);box-sizing:border-box"></textarea>
1790
+ <p class="memory-desc" style="margin:0">Edit the frontmatter and body directly. Save replaces the file via PUT /api/skills/{name}.</p>
1791
+ </div>
1792
+ </div>
1793
+ `;
1794
+ document.body.appendChild(wrap);
1795
+ const ta = wrap.querySelector('#skill-md-textarea');
1796
+ ta.value = md;
1797
+ wrap.querySelector('#skill-md-close').addEventListener('click', () => wrap.remove());
1798
+ wrap.addEventListener('click', (e) => { if (e.target === wrap) wrap.remove(); });
1799
+ wrap.querySelector('#skill-save-btn').addEventListener('click', async () => {
1800
+ try {
1801
+ // We use the manage_skills-style edit by going through PUT with a
1802
+ // single 'content' field. The route doesn't accept that yet — use the
1803
+ // tool call instead. We have a /api/skills/{name} PUT for fields, but
1804
+ // a full SKILL.md replace is simpler via the parsed-then-PUT approach
1805
+ // below: parse client-side by uploading via the tool route.
1806
+ const res = await fetch(`${API}/api/skills/${encodeURIComponent(name)}/markdown`, {
1807
+ method: 'POST',
1808
+ headers: { 'Content-Type': 'application/json' },
1809
+ body: JSON.stringify({ markdown: ta.value }),
1810
+ });
1811
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1812
+ uiModule.showToast('Saved');
1813
+ wrap.remove();
1814
+ await loadSkills();
1815
+ } catch (e) {
1816
+ uiModule.showError('Save failed: ' + e.message);
1817
+ }
1818
+ });
1819
+ }
1820
+
1821
+ async function addSkill() {
1822
+ const name = document.getElementById('new-skill-name')?.value.trim()
1823
+ || document.getElementById('new-skill-title')?.value.trim();
1824
+ const description = document.getElementById('new-skill-description')?.value.trim()
1825
+ || document.getElementById('new-skill-title')?.value.trim();
1826
+ const whenToUse = document.getElementById('new-skill-when')?.value.trim()
1827
+ || document.getElementById('new-skill-problem')?.value.trim() || '';
1828
+ const procedureRaw = document.getElementById('new-skill-procedure')?.value.trim()
1829
+ || document.getElementById('new-skill-solution')?.value.trim() || '';
1830
+ const tagsRaw = document.getElementById('new-skill-tags')?.value.trim();
1831
+ const category = document.getElementById('new-skill-category')?.value.trim() || 'general';
1832
+
1833
+ if (!description && !name) {
1834
+ uiModule.showError('Description (or name) is required');
1835
+ return;
1836
+ }
1837
+ const procedure = procedureRaw
1838
+ ? procedureRaw.split('\n').map(s => s.replace(/^\s*(?:[-*]|\d+[.)])\s+/, '').trim()).filter(Boolean)
1839
+ : [];
1840
+ const tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(Boolean) : [];
1841
+
1842
+ try {
1843
+ const res = await fetch(`${API}/api/skills/add`, {
1844
+ method: 'POST',
1845
+ headers: { 'Content-Type': 'application/json' },
1846
+ body: JSON.stringify({
1847
+ name: name || undefined,
1848
+ description,
1849
+ category,
1850
+ when_to_use: whenToUse,
1851
+ procedure,
1852
+ tags,
1853
+ status: 'draft',
1854
+ }),
1855
+ });
1856
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1857
+ ['new-skill-name', 'new-skill-title', 'new-skill-description', 'new-skill-when',
1858
+ 'new-skill-problem', 'new-skill-procedure', 'new-skill-solution', 'new-skill-tags',
1859
+ 'new-skill-category']
1860
+ .forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; });
1861
+ await loadSkills();
1862
+ uiModule.showToast('Skill added (draft)');
1863
+ } catch (err) {
1864
+ uiModule.showError('Failed to add skill: ' + err.message);
1865
+ }
1866
+ }
1867
+
1868
+ document.addEventListener('DOMContentLoaded', () => {
1869
+ document.getElementById('add-skill-btn')?.addEventListener('click', addSkill);
1870
+ document.getElementById('skills-search')?.addEventListener('input', renderSkillsList);
1871
+ document.getElementById('skills-sort')?.addEventListener('change', (e) => {
1872
+ // Dropdown holds two optgroups: Sort (sort:<key>) and Filter (filter:<key>).
1873
+ // Picking a sort option leaves the filter alone, and vice-versa.
1874
+ const v = e.target.value || '';
1875
+ if (v.startsWith('sort:')) {
1876
+ _skillsSort = v.slice(5);
1877
+ } else if (v.startsWith('filter:')) {
1878
+ const f = v.slice(7);
1879
+ if (f === 'all') { _showDraftsOnly = false; _showPublishedOnly = false; _confMax = null; }
1880
+ else if (f === 'drafts') { _showDraftsOnly = true; _showPublishedOnly = false; _confMax = null; }
1881
+ else if (f === 'published') { _showPublishedOnly = true; _showDraftsOnly = false; _confMax = null; }
1882
+ else if (f.startsWith('conf')) { _showDraftsOnly = false; _showPublishedOnly = false; _confMax = parseInt(f.slice(4), 10) || null; }
1883
+ }
1884
+ renderSkillsList();
1885
+ });
1886
+ document.getElementById('skills-select-btn')?.addEventListener('click', () => {
1887
+ if (_selectMode) _exitSelectMode(); else _enterSelectMode();
1888
+ });
1889
+ document.getElementById('skills-audit-btn')?.addEventListener('click', _auditAllSkills);
1890
+ document.getElementById('skills-select-all')?.addEventListener('change', _toggleSelectAll);
1891
+ document.getElementById('skills-bulk-cancel')?.addEventListener('click', _exitSelectMode);
1892
+ document.getElementById('skills-bulk-audit')?.addEventListener('click', _bulkAudit);
1893
+ document.getElementById('skills-bulk-delete')?.addEventListener('click', _bulkDelete);
1894
+ document.getElementById('skills-bulk-delete-nonpassing')?.addEventListener('click', _bulkDeleteNonPassing);
1895
+ document.getElementById('skills-bulk-publish')?.addEventListener('click', _bulkApprove);
1896
+ document.getElementById('new-skill-title')?.addEventListener('keydown', (e) => {
1897
+ if (e.key === 'Enter') addSkill();
1898
+ });
1899
+ document.getElementById('new-skill-name')?.addEventListener('keydown', (e) => {
1900
+ if (e.key === 'Enter') addSkill();
1901
+ });
1902
+ });
1903
+
1904
+ export default { loadSkills, openSkill };
1905
+
1906
+ // Populate the Skills badge on first load so the count is right before the
1907
+ // user clicks into the tab. Cheap fetch — same as the lazy path.
1908
+ document.addEventListener('DOMContentLoaded', () => { loadSkills(); });