synabun 2.0.9 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (679) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +25 -2
  3. package/THIRD-PARTY-LICENSES.md +68 -4
  4. package/bin/synabun.js +71 -24
  5. package/docs/whatsapp.md +2 -2
  6. package/hooks/claude-code/pre-websearch.mjs +12 -1
  7. package/hooks/claude-code/shared.mjs +5 -0
  8. package/hooks/claude-code/stop.mjs +1 -1
  9. package/launcher.mjs +9 -3
  10. package/lib/claude-hooks.js +172 -18
  11. package/lib/client-config-repair.js +17 -8
  12. package/lib/desktop-app.js +7 -1
  13. package/lib/external-tools.js +1 -1
  14. package/lib/first-launch-protection.js +57 -12
  15. package/lib/mcp-dist.js +176 -0
  16. package/lib/release-npm-ci.mjs +138 -0
  17. package/lib/release-publish.mjs +91 -0
  18. package/lib/release-scan.mjs +50 -0
  19. package/lib/server-crash-record.js +123 -0
  20. package/lib/server-exit.js +318 -0
  21. package/lib/server-supervisor.js +508 -0
  22. package/lib/start-launcher.js +43 -5
  23. package/lib/tools-cli.js +45 -0
  24. package/lib/update-snapshot.js +228 -0
  25. package/lib/virustotal.mjs +192 -0
  26. package/lib/windows-crash-event.js +269 -0
  27. package/mcp-server/dist/http.d.ts +20 -1
  28. package/mcp-server/dist/http.d.ts.map +1 -1
  29. package/mcp-server/dist/http.js +59 -12
  30. package/mcp-server/dist/http.js.map +1 -1
  31. package/mcp-server/dist/index.d.ts +33 -27
  32. package/mcp-server/dist/index.d.ts.map +1 -1
  33. package/mcp-server/dist/index.js +128 -143
  34. package/mcp-server/dist/index.js.map +1 -1
  35. package/mcp-server/dist/preload.js +1 -1
  36. package/mcp-server/dist/preload.js.map +1 -1
  37. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  38. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  39. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  40. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  41. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  42. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  43. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  44. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  45. package/mcp-server/dist/services/neural-interface.d.ts +16 -10
  46. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  47. package/mcp-server/dist/services/neural-interface.js +67 -11
  48. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  49. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  50. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  51. package/mcp-server/dist/services/server-instructions.js +20 -62
  52. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  53. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  54. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  55. package/mcp-server/dist/services/tool-catalog.js +339 -0
  56. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  57. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  58. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  59. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  60. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  61. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  62. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  63. package/mcp-server/dist/services/tool-list.js +334 -0
  64. package/mcp-server/dist/services/tool-list.js.map +1 -0
  65. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  66. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  67. package/mcp-server/dist/services/tool-runtime.js +400 -0
  68. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  69. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  70. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  71. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  72. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  73. package/mcp-server/dist/tools/agents.d.ts +10 -10
  74. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  75. package/mcp-server/dist/tools/agents.js +22 -13
  76. package/mcp-server/dist/tools/agents.js.map +1 -1
  77. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  78. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  79. package/mcp-server/dist/tools/image-tools.js +1 -1
  80. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  81. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  82. package/mcp-server/dist/tools/skill-tools.js +639 -0
  83. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  84. package/mcp-server/dist/tools/skill.d.ts +7 -0
  85. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  86. package/mcp-server/dist/tools/skill.js +11 -0
  87. package/mcp-server/dist/tools/skill.js.map +1 -0
  88. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  89. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  90. package/mcp-server/dist/tools/tool-groups.js +147 -0
  91. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  92. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  93. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  94. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  95. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  96. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  97. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  98. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  99. package/mcp-server/package-lock.json +2 -2
  100. package/mcp-server/package.json +1 -1
  101. package/mcp-server/src/http.ts +75 -12
  102. package/mcp-server/src/index.ts +147 -154
  103. package/mcp-server/src/preload.ts +12 -12
  104. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  105. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  106. package/mcp-server/src/services/neural-interface.ts +79 -20
  107. package/mcp-server/src/services/server-instructions.ts +27 -63
  108. package/mcp-server/src/services/tool-catalog.ts +403 -0
  109. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  110. package/mcp-server/src/services/tool-list.ts +410 -0
  111. package/mcp-server/src/services/tool-runtime.ts +457 -0
  112. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  113. package/mcp-server/src/tools/agents.ts +22 -14
  114. package/mcp-server/src/tools/image-tools.ts +1 -1
  115. package/mcp-server/src/tools/skill-tools.ts +589 -0
  116. package/mcp-server/src/tools/skill.ts +13 -0
  117. package/mcp-server/src/tools/tool-groups.ts +164 -0
  118. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  119. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  120. package/neural-interface/i18n/en.json +3680 -206
  121. package/neural-interface/i18n/pt-BR.json +3628 -157
  122. package/neural-interface/lib/account-usage.js +5 -3
  123. package/neural-interface/lib/app-controls-api.js +161 -0
  124. package/neural-interface/lib/assistant-api.js +148 -35
  125. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  126. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  127. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  128. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  129. package/neural-interface/lib/assistant-brains/opencode.js +122 -44
  130. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +59 -0
  131. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +97 -0
  132. package/neural-interface/lib/assistant-catalog.js +23 -9
  133. package/neural-interface/lib/assistant-dispatch.js +874 -156
  134. package/neural-interface/lib/assistant-envelope.js +22 -2
  135. package/neural-interface/lib/assistant-persona.js +80 -11
  136. package/neural-interface/lib/assistant-plan-permissions.js +10 -9
  137. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  138. package/neural-interface/lib/assistant-route-gate.js +6 -4
  139. package/neural-interface/lib/assistant-router.js +13 -8
  140. package/neural-interface/lib/assistant-runtime.js +183 -26
  141. package/neural-interface/lib/assistant-task-prompt.js +78 -14
  142. package/neural-interface/lib/assistant-tools.js +51 -0
  143. package/neural-interface/lib/backup-service.js +620 -3
  144. package/neural-interface/lib/backup-zip-reader.js +10 -5
  145. package/neural-interface/lib/backup-zip-writer.js +257 -0
  146. package/neural-interface/lib/browser-execution.js +10 -2
  147. package/neural-interface/lib/browser-guard-cdp.js +83 -0
  148. package/neural-interface/lib/browser-network-guard.js +284 -0
  149. package/neural-interface/lib/browser-policy-roots.js +64 -0
  150. package/neural-interface/lib/browser-tool-policy.js +186 -31
  151. package/neural-interface/lib/claude-agent-bridge.js +4 -2
  152. package/neural-interface/lib/claude-hooks-status.js +103 -0
  153. package/neural-interface/lib/claude-model-catalog.js +64 -14
  154. package/neural-interface/lib/claude-temporary.js +5 -11
  155. package/neural-interface/lib/cli-install.js +18 -138
  156. package/neural-interface/lib/cli-resolver.js +4 -1
  157. package/neural-interface/lib/cli-update-planner.js +32 -31
  158. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  159. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  160. package/neural-interface/lib/codex-settings-schema.js +109 -47
  161. package/neural-interface/lib/codex-worker-app-server.js +172 -0
  162. package/neural-interface/lib/confined-fs.js +30 -3
  163. package/neural-interface/lib/crash-notice-api.js +98 -0
  164. package/neural-interface/lib/editor-completion-opencode.js +164 -67
  165. package/neural-interface/lib/exec-loop-command.js +4 -1
  166. package/neural-interface/lib/http-guards.js +8 -2
  167. package/neural-interface/lib/install-details.js +4 -1
  168. package/neural-interface/lib/launch-cache.js +293 -0
  169. package/neural-interface/lib/login-start.js +524 -0
  170. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  171. package/neural-interface/lib/mcp-client-config.js +0 -29
  172. package/neural-interface/lib/mcp-external-servers.js +323 -0
  173. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  174. package/neural-interface/lib/morelogin.js +30 -0
  175. package/neural-interface/lib/native-loop-providers.js +565 -50
  176. package/neural-interface/lib/native-loop-runtime.js +287 -41
  177. package/neural-interface/lib/opencode-config.js +557 -0
  178. package/neural-interface/lib/opencode-db-schema.js +58 -0
  179. package/neural-interface/lib/opencode-history.js +49 -204
  180. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  181. package/neural-interface/lib/opencode-native-boundary.js +44 -0
  182. package/neural-interface/lib/opencode-protocol.js +310 -0
  183. package/neural-interface/lib/opencode-provider-view.js +81 -0
  184. package/neural-interface/lib/opencode-runtime-config.js +32 -15
  185. package/neural-interface/lib/opencode-serve-records.js +624 -0
  186. package/neural-interface/lib/opencode-v2-client.js +53 -131
  187. package/neural-interface/lib/opencode-v2-ws-requests.js +35 -195
  188. package/neural-interface/lib/opencode-worker-activity.js +229 -0
  189. package/neural-interface/lib/opencode-worker-data.js +27 -0
  190. package/neural-interface/lib/opencode2-client.js +796 -0
  191. package/neural-interface/lib/opencode2-proxy.js +76 -0
  192. package/neural-interface/lib/opencode2-translate.js +1057 -0
  193. package/neural-interface/lib/owner-authority.js +249 -0
  194. package/neural-interface/lib/page-session.js +275 -0
  195. package/neural-interface/lib/peer-identity.js +763 -0
  196. package/neural-interface/lib/process-identity.js +361 -0
  197. package/neural-interface/lib/request-gate.js +17 -2
  198. package/neural-interface/lib/rulesets/api.js +1 -1
  199. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  200. package/neural-interface/lib/rulesets/installer.js +92 -27
  201. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  202. package/neural-interface/lib/session-cache-builder.js +18 -10
  203. package/neural-interface/lib/session-title-generator.js +58 -18
  204. package/neural-interface/lib/skills/api-v2.js +495 -0
  205. package/neural-interface/lib/skills/bundle.js +418 -0
  206. package/neural-interface/lib/skills/catalog.js +294 -0
  207. package/neural-interface/lib/skills/creation-api.js +191 -0
  208. package/neural-interface/lib/skills/creation-brief.js +247 -0
  209. package/neural-interface/lib/skills/creation-sources.js +267 -0
  210. package/neural-interface/lib/skills/creation.js +843 -0
  211. package/neural-interface/lib/skills/deploy.js +564 -0
  212. package/neural-interface/lib/skills/digest.js +119 -0
  213. package/neural-interface/lib/skills/discover.js +111 -0
  214. package/neural-interface/lib/skills/draft-confinement.js +214 -0
  215. package/neural-interface/lib/skills/eval-confinement.js +397 -0
  216. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  217. package/neural-interface/lib/skills/eval-trace.js +119 -0
  218. package/neural-interface/lib/skills/evals-api.js +212 -0
  219. package/neural-interface/lib/skills/evals.js +1573 -0
  220. package/neural-interface/lib/skills/export.js +200 -0
  221. package/neural-interface/lib/skills/frontmatter.js +64 -0
  222. package/neural-interface/lib/skills/health.js +75 -0
  223. package/neural-interface/lib/skills/json-edit.js +165 -0
  224. package/neural-interface/lib/skills/known-bridge-digests.json +387 -0
  225. package/neural-interface/lib/skills/known-bridges.js +96 -0
  226. package/neural-interface/lib/skills/library.js +823 -0
  227. package/neural-interface/lib/skills/local-store.js +21 -0
  228. package/neural-interface/lib/skills/migration.js +433 -0
  229. package/neural-interface/lib/skills/operations.js +45 -0
  230. package/neural-interface/lib/skills/ownership.js +427 -0
  231. package/neural-interface/lib/skills/packages.js +88 -0
  232. package/neural-interface/lib/skills/reviews.js +212 -0
  233. package/neural-interface/lib/skills/revision.js +80 -0
  234. package/neural-interface/lib/skills/roots.js +262 -0
  235. package/neural-interface/lib/skills/runtime-checks.js +355 -0
  236. package/neural-interface/lib/skills/scan-assets.js +252 -0
  237. package/neural-interface/lib/skills/scan-context.js +26 -0
  238. package/neural-interface/lib/skills/scan-shell.js +324 -0
  239. package/neural-interface/lib/skills/scan.js +364 -0
  240. package/neural-interface/lib/skills/scanners.js +54 -0
  241. package/neural-interface/lib/skills/service.js +522 -0
  242. package/neural-interface/lib/skills/telemetry.js +113 -0
  243. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  244. package/neural-interface/lib/style-guide/api.js +4 -3
  245. package/neural-interface/lib/tool-lists.js +124 -0
  246. package/neural-interface/lib/untrusted-file-headers.js +27 -0
  247. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  248. package/neural-interface/lib/worker-confinement.js +1192 -0
  249. package/neural-interface/lib/worker-guard-setting.js +56 -0
  250. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  251. package/neural-interface/lib/worker-relay-ports.js +227 -0
  252. package/neural-interface/lib/worker-resume.js +229 -0
  253. package/neural-interface/lib/worker-sandbox.js +299 -0
  254. package/neural-interface/package-lock.json +1354 -50
  255. package/neural-interface/package.json +6 -2
  256. package/neural-interface/public/index.html +5 -3
  257. package/neural-interface/public/index2d.html +19 -11
  258. package/neural-interface/public/onboarding.html +2 -2
  259. package/neural-interface/public/shared/account-menu.js +8 -5
  260. package/neural-interface/public/shared/agent-runtime-options.js +76 -16
  261. package/neural-interface/public/shared/api.js +1527 -1336
  262. package/neural-interface/public/shared/app-frame.js +192 -0
  263. package/neural-interface/public/shared/assistant/asst-agents-dock.js +205 -19
  264. package/neural-interface/public/shared/assistant/asst-api.js +11 -3
  265. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  266. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  267. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  268. package/neural-interface/public/shared/assistant/asst-brain-picker.js +241 -112
  269. package/neural-interface/public/shared/assistant/asst-composer.js +1 -1
  270. package/neural-interface/public/shared/assistant/asst-control.js +157 -26
  271. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  272. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  273. package/neural-interface/public/shared/assistant/asst-hosts.js +49 -2
  274. package/neural-interface/public/shared/assistant/asst-limit-dock.js +40 -0
  275. package/neural-interface/public/shared/assistant/asst-limit-wait.js +294 -0
  276. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  277. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  278. package/neural-interface/public/shared/assistant/asst-menu.js +84 -12
  279. package/neural-interface/public/shared/assistant/asst-panel.js +303 -44
  280. package/neural-interface/public/shared/assistant/asst-rack-styles.js +93 -58
  281. package/neural-interface/public/shared/assistant/asst-redact.js +186 -0
  282. package/neural-interface/public/shared/assistant/asst-render.js +770 -100
  283. package/neural-interface/public/shared/assistant/asst-route.js +83 -12
  284. package/neural-interface/public/shared/assistant/asst-run-stage.js +231 -0
  285. package/neural-interface/public/shared/assistant/asst-sanitize.js +193 -3
  286. package/neural-interface/public/shared/assistant/asst-scene-styles.js +273 -0
  287. package/neural-interface/public/shared/assistant/asst-scenes.js +530 -0
  288. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +3 -15
  289. package/neural-interface/public/shared/assistant/asst-sidepanel.js +67 -4
  290. package/neural-interface/public/shared/assistant/asst-slash.js +9 -7
  291. package/neural-interface/public/shared/assistant/asst-state.js +25 -15
  292. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  293. package/neural-interface/public/shared/assistant/asst-styles.js +102 -21
  294. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +165 -13
  295. package/neural-interface/public/shared/automation-context.js +5 -2
  296. package/neural-interface/public/shared/cc-dropdown.js +550 -0
  297. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  298. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  299. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  300. package/neural-interface/public/shared/cdx/cdx-output.js +8 -7
  301. package/neural-interface/public/shared/cdx/cdx-panel.js +58 -150
  302. package/neural-interface/public/shared/cdx/cdx-protocol.js +28 -1
  303. package/neural-interface/public/shared/cdx/cdx-render.js +62 -32
  304. package/neural-interface/public/shared/cdx/cdx-requests.js +3 -3
  305. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  306. package/neural-interface/public/shared/cdx/cdx-styles.js +15 -3
  307. package/neural-interface/public/shared/cdx/cdx-tabs.js +201 -74
  308. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  309. package/neural-interface/public/shared/cli-install-view.js +4 -8
  310. package/neural-interface/public/shared/cli-status.js +2 -2
  311. package/neural-interface/public/shared/constants.js +2 -2
  312. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  313. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  314. package/neural-interface/public/shared/context-menu-model.js +366 -0
  315. package/neural-interface/public/shared/context-menu.js +372 -0
  316. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  317. package/neural-interface/public/shared/cp/cp-markdown.js +4 -57
  318. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  319. package/neural-interface/public/shared/data-cache.js +223 -0
  320. package/neural-interface/public/shared/desktop-bridge.js +11 -0
  321. package/neural-interface/public/shared/desktop-titlebar-view.js +78 -1
  322. package/neural-interface/public/shared/desktop-titlebar.js +214 -28
  323. package/neural-interface/public/shared/desktop-update-view.js +35 -5
  324. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  325. package/neural-interface/public/shared/file-links.js +73 -8
  326. package/neural-interface/public/shared/html-shell.js +78 -86
  327. package/neural-interface/public/shared/i18n.js +2 -0
  328. package/neural-interface/public/shared/launch-prefetch.js +59 -0
  329. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  330. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  331. package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +3 -2
  332. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  333. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +9 -2
  334. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  335. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  336. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  337. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  338. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  339. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  340. package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +54 -17
  341. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  342. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +140 -254
  343. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  344. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +97 -203
  345. package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +11 -9
  346. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +15 -4
  347. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -22
  348. package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +401 -0
  349. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +20 -13
  350. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  351. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +53 -82
  352. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  353. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  354. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  355. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  356. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +48 -40
  357. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  358. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +205 -62
  359. package/neural-interface/public/shared/opencode-providers.js +39 -0
  360. package/neural-interface/public/shared/page-session.js +96 -0
  361. package/neural-interface/public/shared/registry.js +5 -3
  362. package/neural-interface/public/shared/retired-2d.js +3 -0
  363. package/neural-interface/public/shared/schedules-view.js +452 -0
  364. package/neural-interface/public/shared/settings/settings-app-pane.js +335 -0
  365. package/neural-interface/public/shared/settings/settings-app.js +243 -0
  366. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  367. package/neural-interface/public/shared/settings/settings-data.js +90 -0
  368. package/neural-interface/public/shared/settings/settings-ia.js +23 -6
  369. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  370. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  371. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  372. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  373. package/neural-interface/public/shared/sidepanel-session-status.js +49 -0
  374. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  375. package/neural-interface/public/shared/skills/ss-creation.js +219 -0
  376. package/neural-interface/public/shared/skills/ss-dialog.js +130 -0
  377. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  378. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  379. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  380. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  381. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  382. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  383. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  384. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  385. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  386. package/neural-interface/public/shared/skills/ss-lint.js +141 -0
  387. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  388. package/neural-interface/public/shared/skills/ss-profiles.js +475 -0
  389. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  390. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  391. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  392. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  393. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  394. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  395. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  396. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +236 -0
  397. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  398. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  399. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  400. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  401. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  402. package/neural-interface/public/shared/skills/studio/sx-dom.js +229 -0
  403. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  404. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  405. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  406. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  407. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  408. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  409. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +279 -0
  410. package/neural-interface/public/shared/skills/studio/sx-install.js +296 -0
  411. package/neural-interface/public/shared/skills/studio/sx-library-view.js +639 -0
  412. package/neural-interface/public/shared/skills/studio/sx-library.js +475 -0
  413. package/neural-interface/public/shared/skills/studio/sx-main.js +681 -0
  414. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  415. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  416. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  417. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  418. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  419. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  420. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  421. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  422. package/neural-interface/public/shared/skills/studio/sx-subject.js +248 -0
  423. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  424. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  425. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  426. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +425 -0
  427. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  428. package/neural-interface/public/shared/styles.css +2065 -2150
  429. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  430. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  431. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  432. package/neural-interface/public/shared/tool-list-view.js +112 -0
  433. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  434. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  435. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  436. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  437. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  438. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  439. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  440. package/neural-interface/public/shared/tour/tour.css +144 -0
  441. package/neural-interface/public/shared/ui-assistant.js +27 -5
  442. package/neural-interface/public/shared/ui-automation-studio.js +155 -152
  443. package/neural-interface/public/shared/ui-bookmarks.js +2 -1
  444. package/neural-interface/public/shared/ui-claude-panel.js +153 -13
  445. package/neural-interface/public/shared/ui-codex-panel.js +1 -0
  446. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  447. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  448. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  449. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  450. package/neural-interface/public/shared/ui-detail.js +21 -9
  451. package/neural-interface/public/shared/ui-file-links.js +1 -0
  452. package/neural-interface/public/shared/ui-image-gallery.js +56 -52
  453. package/neural-interface/public/shared/ui-invite.js +18 -2
  454. package/neural-interface/public/shared/ui-keybinds.js +26 -1
  455. package/neural-interface/public/shared/ui-link.js +9 -9
  456. package/neural-interface/public/shared/ui-menubar.js +229 -40
  457. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  458. package/neural-interface/public/shared/ui-native-loop-router.js +9 -1
  459. package/neural-interface/public/shared/ui-navbar.js +4 -24
  460. package/neural-interface/public/shared/ui-opencode-panel-v2.js +1 -0
  461. package/neural-interface/public/shared/ui-panels.js +96 -65
  462. package/neural-interface/public/shared/ui-schedules-studio.js +2267 -953
  463. package/neural-interface/public/shared/ui-sessions.js +20 -7
  464. package/neural-interface/public/shared/ui-settings.js +679 -496
  465. package/neural-interface/public/shared/ui-sidebar.js +4 -4
  466. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +13 -0
  467. package/neural-interface/public/shared/ui-sidepanel-sessions.js +56 -5
  468. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  469. package/neural-interface/public/shared/ui-sidepanel-tray.js +84 -8
  470. package/neural-interface/public/shared/ui-sidepanel-windows.js +16 -11
  471. package/neural-interface/public/shared/ui-skills.js +42 -2781
  472. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  473. package/neural-interface/public/shared/ui-sync.js +22 -5
  474. package/neural-interface/public/shared/ui-terminal.js +159 -199
  475. package/neural-interface/public/shared/ui-tool-updates.js +3 -1
  476. package/neural-interface/public/shared/ui-tooltip.js +10 -5
  477. package/neural-interface/public/shared/ui-trash.js +14 -5
  478. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  479. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  480. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -61
  481. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  482. package/neural-interface/public/shared/ui-tutorial.js +285 -499
  483. package/neural-interface/public/shared/ui-update.js +121 -16
  484. package/neural-interface/public/shared/ui-whiteboard.js +313 -59
  485. package/neural-interface/public/shared/ui-worker-end.js +4 -0
  486. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  487. package/neural-interface/public/shared/view-variant.js +66 -0
  488. package/neural-interface/public/shared/whatsapp/wa-view.js +1 -1
  489. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  490. package/neural-interface/public/shared/worker-end.js +10 -3
  491. package/neural-interface/public/shared/worker-restart.js +12 -0
  492. package/neural-interface/public/sidepanel-session.html +1 -1
  493. package/neural-interface/public/variant/2d/background.js +2 -1
  494. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  495. package/neural-interface/public/variant/2d/graph.js +42 -20
  496. package/neural-interface/public/variant/2d/hulls.js +14 -9
  497. package/neural-interface/public/variant/2d/lasso.js +11 -7
  498. package/neural-interface/public/variant/2d/main.js +36 -2
  499. package/neural-interface/public/variant/2d/minimap.js +11 -13
  500. package/neural-interface/public/variant/3d/main.js +72 -6
  501. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  502. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  503. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  504. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  505. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  506. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  507. package/neural-interface/public/vendor/highlight/LICENSE +29 -0
  508. package/neural-interface/public/vendor/highlight/github-dark-dimmed-11.12.0.min.css +9 -0
  509. package/neural-interface/public/vendor/highlight/highlight-11.12.0.min.js +1262 -0
  510. package/neural-interface/public/vendor/highlight/manifest.json +5 -0
  511. package/neural-interface/public/vendor/html2canvas/LICENSE +22 -0
  512. package/neural-interface/public/vendor/html2canvas/html2canvas-1.4.1.min.js +20 -0
  513. package/neural-interface/public/vendor/html2canvas/manifest.json +4 -0
  514. package/neural-interface/public/vendor/marked/LICENSE.md +44 -0
  515. package/neural-interface/public/vendor/marked/manifest.json +4 -0
  516. package/neural-interface/public/vendor/marked/marked-14.1.4.esm.js +2502 -0
  517. package/neural-interface/public/vendor/mermaid/LICENSE +21 -0
  518. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/abnfDiagram-PAR5IXJI.mjs +1 -0
  519. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architecture-7GRP2DOG-M7G3SWCO.mjs +1 -0
  520. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architectureDiagram-2PNWDENW.mjs +36 -0
  521. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/blockDiagram-PUWGUBOA.mjs +129 -0
  522. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/c4Diagram-3NLNHZP5.mjs +38 -0
  523. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-2NMSUPJH.mjs +1 -0
  524. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-32MCUW2P.mjs +206 -0
  525. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3NPNU5GP.mjs +1 -0
  526. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3SMGFRGC.mjs +1 -0
  527. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4CAJECRU.mjs +14 -0
  528. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4OADUZ5Q.mjs +10 -0
  529. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IMINLNL.mjs +1 -0
  530. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IQCK5RY.mjs +3 -0
  531. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5MVFYWFU.mjs +1 -0
  532. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AQ6EADP3.mjs +1 -0
  533. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AZZRMDJM.mjs +15 -0
  534. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-CLS4B6BI.mjs +168 -0
  535. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-DZP67EKU.mjs +70 -0
  536. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-EICP7K5X.mjs +1 -0
  537. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-GDY2UAB4.mjs +1 -0
  538. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-H7VHZCWX.mjs +1 -0
  539. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-IJBDOHL6.mjs +1 -0
  540. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JFFVMJIA.mjs +1 -0
  541. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JQRUD6KW.mjs +1 -0
  542. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JZGDI5JK.mjs +1 -0
  543. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-KD6SPIPN.mjs +2 -0
  544. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NF366YI6.mjs +231 -0
  545. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NUDPKBEF.mjs +32 -0
  546. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-OJINMHTS.mjs +1 -0
  547. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Q6NGPIAH.mjs +2 -0
  548. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-QJSWEUOL.mjs +132 -0
  549. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-R7JDMUKV.mjs +1 -0
  550. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RCXAM2GK.mjs +1 -0
  551. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RT4MAWR3.mjs +1 -0
  552. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SFMINX4W.mjs +1 -0
  553. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SLDQ463B.mjs +88 -0
  554. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-STOV2HOB.mjs +1 -0
  555. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-T5OCTHI4.mjs +1 -0
  556. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-TCEHX7R3.mjs +1 -0
  557. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VQC2EELJ.mjs +1 -0
  558. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VRZKL3EH.mjs +1 -0
  559. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VTHDQ7BV.mjs +1 -0
  560. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VY2OSTJR.mjs +2 -0
  561. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-WUBWJARI.mjs +1 -0
  562. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YBMQ5JGJ.mjs +2 -0
  563. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YDMLL4PJ.mjs +1 -0
  564. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Z6V44ARX.mjs +62 -0
  565. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZJJ7VQJP.mjs +339 -0
  566. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZNLK7HRR.mjs +1 -0
  567. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZQN7UHIL.mjs +1 -0
  568. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZS6VBONO.mjs +174 -0
  569. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZUNWM646.mjs +1 -0
  570. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-FGAMPOII.mjs +1 -0
  571. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-v2-AI5RJEK4.mjs +1 -0
  572. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cose-bilkent-Q72C2OJ3.mjs +1 -0
  573. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefin-OW5HDTMX-6OCTWOGG.mjs +1 -0
  574. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefinDiagram-2OOWUY4M.mjs +62 -0
  575. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/dagre-MPVFI544.mjs +4 -0
  576. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-AE4NWXFW.mjs +24 -0
  577. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-BUJID5UG.mjs +41 -0
  578. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-C44CZ5NI.mjs +24 -0
  579. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-EYOLEXXS.mjs +30 -0
  580. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-P7B6PWOJ.mjs +3 -0
  581. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ebnfDiagram-FTPZ3PV6.mjs +1 -0
  582. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/erDiagram-H3ERR26V.mjs +99 -0
  583. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/eventmodeling-NTZA5JFV-WKSWQHFS.mjs +1 -0
  584. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/flowDiagram-YHGXBVSY.mjs +1 -0
  585. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ganttDiagram-RWFTB7ED.mjs +292 -0
  586. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraph-4MIJSDKK-JH2HKYUO.mjs +1 -0
  587. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraphDiagram-MAR6GKOP.mjs +106 -0
  588. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/info-A6RAGUB7-3TX3OFWW.mjs +1 -0
  589. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/infoDiagram-LV547AFH.mjs +2 -0
  590. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ishikawaDiagram-PUJQGNDS.mjs +70 -0
  591. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/journeyDiagram-EW5QJT4W.mjs +139 -0
  592. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/kanban-definition-JSQERAZB.mjs +89 -0
  593. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/katex-CZ4GXH2S.mjs +257 -0
  594. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/mindmap-definition-VBJCLQLM.mjs +96 -0
  595. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/packet-AYTQ26CC-MKII6P3Y.mjs +1 -0
  596. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pegDiagram-7WILOQTJ.mjs +1 -0
  597. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pie-WAS4IAKB-6B4OPMJ5.mjs +1 -0
  598. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pieDiagram-EFA7CBSB.mjs +39 -0
  599. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/quadrantDiagram-WARVYA7P.mjs +7 -0
  600. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/radar-RG4KPBEZ-UPTR3XUL.mjs +1 -0
  601. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-74A4TZTK-MEFUPFMS.mjs +1 -0
  602. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-abnf-HS5TGJTU-R5YG2S5D.mjs +1 -0
  603. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-ebnf-LZEXJU2U-G5QEX2L6.mjs +1 -0
  604. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-peg-WCYAUIDC-H5I2ZRY2.mjs +1 -0
  605. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroadDiagram-5NIQVNLQ.mjs +1 -0
  606. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/requirementDiagram-SXREGOKZ.mjs +84 -0
  607. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sankeyDiagram-VV74M2VP.mjs +40 -0
  608. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sequenceDiagram-52ZFIFDL.mjs +162 -0
  609. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sizeCapture-FGA5AGYP.mjs +1 -0
  610. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-3QAKPIVI.mjs +1 -0
  611. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-v2-HN2P4RDZ.mjs +1 -0
  612. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanes-QLVP42QD.mjs +1 -0
  613. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanesDiagram-JKPDBFCN.mjs +8 -0
  614. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/timeline-definition-GS45XAXW.mjs +120 -0
  615. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treeView-Q6P3EWNA-ZRJGANNQ.mjs +1 -0
  616. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treemap-WGGIJYW6-K64VN5AS.mjs +1 -0
  617. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/vennDiagram-QIQU2ZRJ.mjs +34 -0
  618. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardley-WFR3VGLG-D67OWCCH.mjs +1 -0
  619. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardleyDiagram-U6YNM66N.mjs +78 -0
  620. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/xychartDiagram-XJAZEDNM.mjs +7 -0
  621. package/neural-interface/public/vendor/mermaid/manifest.json +4 -0
  622. package/neural-interface/public/vendor/mermaid/mermaid-11.17.2.esm.min.mjs +14 -0
  623. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  624. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  625. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  626. package/neural-interface/scripts/build-vendor.mjs +116 -3
  627. package/neural-interface/server.js +4834 -2541
  628. package/neural-interface/templates/rulesets/core.md +1 -1
  629. package/neural-interface/templates/rulesets/legacy-hashes.json +18 -0
  630. package/neural-interface/templates/rulesets/manifest.json +8 -8
  631. package/npm-shrinkwrap.json +18 -12
  632. package/package.json +16 -3
  633. package/postinstall.js +9 -4
  634. package/preuninstall.js +1 -1
  635. package/setup.js +97 -322
  636. package/skills/skill-forge/SKILL.md +177 -0
  637. package/skills/skill-forge/evals/evals.json +54 -0
  638. package/skills/skill-forge/references/package.md +105 -0
  639. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  640. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  641. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  642. package/skills/synabun/SKILL.md +10 -7
  643. package/skills/synabun/modules/audit.md +1 -5
  644. package/skills/synabun/modules/changelog.md +1 -5
  645. package/skills/synabun/modules/idea.md +1 -1
  646. package/skills/synabun/modules/memorize.md +6 -9
  647. package/skills/synabun/modules/schedule.md +5 -5
  648. package/mcp-server/dist/services/profiles.d.ts +0 -120
  649. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  650. package/mcp-server/dist/services/profiles.js +0 -387
  651. package/mcp-server/dist/services/profiles.js.map +0 -1
  652. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  653. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  654. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  655. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  656. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  657. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  658. package/mcp-server/dist/tools/leonardo.js +0 -21
  659. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  660. package/mcp-server/dist/tools/profile.d.ts +0 -17
  661. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  662. package/mcp-server/dist/tools/profile.js +0 -114
  663. package/mcp-server/dist/tools/profile.js.map +0 -1
  664. package/mcp-server/src/services/profiles.ts +0 -428
  665. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  666. package/mcp-server/src/tools/leonardo.ts +0 -30
  667. package/mcp-server/src/tools/profile.ts +0 -122
  668. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  669. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  670. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  671. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  672. package/skills/leonardo/SKILL.md +0 -168
  673. package/skills/leonardo/icon.svg +0 -1
  674. package/skills/leonardo/modules/browser-guide.md +0 -378
  675. package/skills/leonardo/modules/image-prompter.md +0 -283
  676. package/skills/leonardo/modules/model-advisor.md +0 -100
  677. package/skills/leonardo/modules/prompt-library.md +0 -110
  678. package/skills/leonardo/modules/style-guide.md +0 -186
  679. package/skills/leonardo/modules/video-prompter.md +0 -317
@@ -1,18 +1,26 @@
1
+ import { fetchToolCatalog } from './agent-runtime-options.js';
2
+ import { configureToolPickerTranslation, toolPickerHTML, readToolPicker, toolWord } from './tool-list-picker.js';
1
3
  import { mountMemoryMaintenance } from './ui-memory-maintenance.js';
2
4
  import { buildJudgmentsTab, wireJudgmentsTab } from './ui-judgments.js';
3
5
  import { buildWhatsAppTab, wireWhatsAppTab } from './ui-whatsapp.js';
4
6
  import { buildCodeEditorTab, wireCodeEditorTab } from './settings/settings-code-editor-pane.js';
5
7
  import { buildAboutTab, wireAboutTab } from './settings/settings-about-pane.js';
8
+ import { buildAppTab, wireAppTab } from './settings/settings-app-pane.js';
6
9
  import { SETTINGS_GROUPS, SETTINGS_PAGES, DEFAULT_SETTINGS_PAGE, pageKeys, getSettingsPage, resolveSettingsTarget, homeOfControl } from './settings/settings-ia.js';
7
10
  import * as kit from './settings/settings-kit.js';
11
+ import { skillFoldersHTML, wireSkillFolders, refreshSkillChips, chipState, installedToast } from './settings/settings-skill-folders-pane.js';
8
12
  import { RULES_HOST_LABELS, describeRulesHost, describeRulesInstall, describeRulesInstallAll } from './settings/settings-rules.js';
9
13
  import { readUserContextResponse, describeUserContext, describeUserContextSaveFailure } from './settings/settings-user-context.js';
14
+ import { readWorkerGuardResponse, describeWorkerGuard, describeWorkerGuardSaveFailure } from './settings/settings-worker-guard.js';
15
+ import { CLAUDE_HOOK_ROWS, readClaudeHooksResponse, describeClaudeHooks, describeClaudeHooksSaveFailure } from './settings/settings-claude-hooks.js';
10
16
  import { readGreetingStatus, describeGreeting, describeGreetingSaveFailure } from './settings/settings-greeting.js';
17
+ import { describeOpencodeInstall } from './settings/settings-opencode.js';
11
18
  import { renderGreeting, clearGreeting } from './greeting/greeting-card.js';
12
19
  import { createTunnelCard, drawInto, mcpKeyNeededText, readTunnel } from './ui-tunnel.js';
13
20
  import { remoteMcpAddress } from './tunnel-view.js';
14
21
  import { recheckCliStatus, getCliStatus, addCliToPath } from './cli-status.js';
15
22
  import { cliInstallText, setCliInstallTranslator } from './cli-install-view.js';
23
+ import { dropdownHTML, wireDropdowns } from './cc-dropdown.js';
16
24
  setCliInstallTranslator((key, params) => kit.tx(`cliInstall.${key}`, params));
17
25
  import { setGitBashTranslator } from './git-bash-view.js';
18
26
  import { gitBashRowHtml, wireGitBashRow } from './ui-git-bash.js';
@@ -20,12 +28,14 @@ setGitBashTranslator((key, params) => kit.tx(`gitBash.${key}`, params));
20
28
  import { initI18n, isReady as i18nReady, SUPPORTED_LOCALES, LOCALE_NAMES, SYSTEM_LOCALE, getLocale, getLocaleChoice, setLocaleChoice, getSystemLocale, englishTranslator, loadEnglishTranslator } from './i18n.js';
21
29
  import { wireSettingsShell, wireSettingsSearch, collectSearchRows, revealSettingsTarget, followSettingsTarget, onSettingsTakeover, applyStoredSections, storeSectionState } from './settings/settings-shell.js';
22
30
  import { buildSettingsSearchIndex } from './settings/settings-search.js';
31
+ import { prefetchSettingsData, readSettingsData, refreshSettingsData } from './settings/settings-data.js';
32
+ import { registerLaunchPrefetch } from './launch-prefetch.js';
23
33
  // ═══════════════════════════════════════════
24
34
  // UI-SETTINGS — Settings modal with shared tabs + variant tab injection
25
35
  // ═══════════════════════════════════════════
26
36
  //
27
37
  // The most complex shared UI module. Builds a floating settings panel with
28
- // thirteen task pages in six groups (./settings/settings-ia.js). A page is a
38
+ // fourteen task pages in six groups (./settings/settings-ia.js). A page is a
29
39
  // title, a one-line purpose and section cards (./settings/settings-kit.js);
30
40
  // each builder below owns one pane of a page. Variant-registered tabs (the 2D
31
41
  // Graphics tab) join the Appearance page. Old tab ids still open the right page.
@@ -34,6 +44,7 @@ import { state, emit, on } from './state.js';
34
44
  import { getSettingsTabs } from './registry.js';
35
45
  import { escapeHtml } from './utils.js';
36
46
  import { storage } from './storage.js';
47
+ import { normalizeViewConfig } from './view-variant.js';
37
48
  import { KEYS } from './constants.js';
38
49
  import { registerAction } from './ui-keybinds.js';
39
50
  import { CLI_PROFILES, ensureModelsForProfile, getModelsForProfile } from './agent-runtime-options.js';
@@ -44,6 +55,7 @@ import { getNotifSettings, playTestSound, sendTestBanner, SOUND_PRESETS } from '
44
55
  import {
45
56
  fetchRulesStatus, fetchRulesTextWithFallback, installRules, removeRules, installAllRules, setRulesAutoUpdate, removeLegacyRules,
46
57
  } from './api.js';
58
+ import { centeredInFrame } from './app-frame.js';
47
59
 
48
60
  // ── SVG icon constants ──
49
61
 
@@ -76,6 +88,8 @@ const TAB_ICONS = {
76
88
  whatsapp: '<svg viewBox="0 0 24 24"><path d="M3.5 20.5l1.3-3.9A8.5 8.5 0 1 1 8 19.6z"/><path d="M9.5 8.5c.4 2.6 2.9 5.2 5.5 5.6l1-1.2 1.6.8c-.3 1-1.1 1.6-2.1 1.5-2.9-.3-5.8-3.2-6.1-6.1-.1-1 .5-1.8 1.5-2.1l.8 1.6z"/></svg>',
77
89
  morelogin: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M12 11.5a1 1 0 0 0-1 1c0 1.8-.3 3.6-1.2 5.2"/><path d="M7.8 14.5a8 8 0 0 0-.6 4.3"/><path d="M12 7.5a4.5 4.5 0 0 1 4.5 4.5c0 .6 0 1.2-.1 1.8"/><path d="M5 9.2a7 7 0 0 1 11-1.9"/><path d="M15 13a3 3 0 0 1-3 3"/></svg>',
78
90
  editor: '<svg viewBox="0 0 24 24"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/><line x1="13.5" y1="5" x2="10.5" y2="19"/></svg>',
91
+ // Settings > App: the software's own controls (a window with its title bar)
92
+ app: '<svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="6.5" y1="6.5" x2="6.51" y2="6.5"/><line x1="9.5" y1="6.5" x2="9.51" y2="6.5"/></svg>',
79
93
  about: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="11"/><line x1="12" y1="7.5" x2="12.01" y2="7.5"/></svg>',
80
94
  };
81
95
 
@@ -108,6 +122,7 @@ function showCCToast(msg, duration = 3500) {
108
122
  // INTERFACE CUSTOMIZATION — config, presets, apply
109
123
  // ═══════════════════════════════════════════
110
124
 
125
+ // Optional legacy view preferences must never reintroduce a 2D selection.
111
126
  const IFACE_DEFAULTS = {
112
127
  visualizationEnabled: true,
113
128
  scale: 1,
@@ -156,7 +171,7 @@ const IFACE_SLIDERS = {
156
171
  export function loadIfaceConfig() {
157
172
  try {
158
173
  const raw = storage.getItem('neural-interface-config');
159
- if (raw) return { ...IFACE_DEFAULTS, ...JSON.parse(raw) };
174
+ if (raw) return { ...IFACE_DEFAULTS, ...normalizeViewConfig(JSON.parse(raw)) };
160
175
  // Migrate legacy ui-scale if present
161
176
  const legacyScale = storage.getItem('neural-ui-scale');
162
177
  if (legacyScale) {
@@ -169,7 +184,7 @@ export function loadIfaceConfig() {
169
184
  }
170
185
 
171
186
  export function saveIfaceConfig(cfg) {
172
- storage.setItem('neural-interface-config', JSON.stringify(cfg));
187
+ storage.setItem('neural-interface-config', JSON.stringify(normalizeViewConfig(cfg)));
173
188
  // Keep legacy key in sync for backwards compat
174
189
  storage.setItem('neural-ui-scale', cfg.scale);
175
190
  }
@@ -546,7 +561,7 @@ function buildSearchHTML() {
546
561
  }
547
562
 
548
563
  // What search reads from the panel beside its index (settings-search.js): the labelled rows of the panes that draw
549
- // themselves, and the names of what the panes load (projects, providers, tools, tool profiles and servers, skills,
564
+ // themselves, and the names of what the panes load (projects, providers, tools, tool lists and servers, skills,
550
565
  // browser profiles, themes, the linked WhatsApp account). Labels and names only: a source names label elements and
551
566
  // the shell reads their text, never a value; `help` is given only where it is the app's own copy. `named` rows may
552
567
  // sit inside an inventory control (a list it tags).
@@ -952,7 +967,7 @@ function buildSetupTab(setupStatus) {
952
967
  { id: 'claude', name: 'Claude', icon: ANTHROPIC_ICON, file: '~/.claude.json', ids: { toggle: 'SET004', cli: 'SET005', primary: 'SET006', remove: 'SET007', view: 'SET008', copy: 'SET009' } },
953
968
  { id: 'gemini', name: 'Gemini', icon: GEMINI_ICON, file: '~/.gemini/settings.json', ids: { toggle: 'SET010', config: 'SET011', primary: 'SET012', remove: 'SET013', view: 'SET014', copy: 'SET015' } },
954
969
  { id: 'codex', name: 'Codex', icon: OPENAI_ICON, file: '~/.codex/config.toml', ids: { toggle: 'SET016', cli: 'SET017', config: 'SET018', primary: 'SET019', remove: 'SET020', view: 'SET021', copy: 'SET022' } },
955
- { id: 'opencode', name: 'OpenCode', icon: OPENCODE_ICON, file: '~/.config/opencode/config.json', ids: { toggle: 'SET023', config: 'SET024', primary: 'SET025', remove: 'SET026', view: 'SET027', copy: 'SET028' } },
970
+ { id: 'opencode', name: 'OpenCode', icon: OPENCODE_ICON, file: '~/.config/opencode/opencode.json', ids: { toggle: 'SET023', config: 'SET024', primary: 'SET025', remove: 'SET026', view: 'SET027', copy: 'SET028' } },
956
971
  ].map((p) => ({ ...p, on: setupStatus[p.id]?.connected || false }));
957
972
 
958
973
  const copyLabel = (invId) => `${COPY_ICON} ${kit.L(invId)}`;
@@ -1170,23 +1185,23 @@ function buildMcpTab() {
1170
1185
  return `
1171
1186
  <div class="stg-pane" data-stg-pane="mcp">
1172
1187
  <div class="stg-mcp-page-tabs" role="tablist" aria-label="${kit.L('MCP001')}" ${kit.inv('MCP001')}>
1173
- ${tab('stg-mcp-page-tab', 'data-pane', 'profiles', m('tab.profiles'), true)}
1188
+ ${tab('stg-mcp-page-tab', 'data-pane', 'tools', m('tab.tools'), true)}
1174
1189
  ${tab('stg-mcp-page-tab', 'data-pane', 'installed', m('tab.installed'), false)}
1175
1190
  </div>
1176
1191
 
1177
- <div class="stg-mcp-pane" data-pane="profiles" data-stg-reveal='.stg-mcp-page-tab[data-pane="profiles"]'>
1192
+ <div class="stg-mcp-pane" data-pane="tools" data-stg-reveal='.stg-mcp-page-tab[data-pane="tools"]'>
1178
1193
  ${kit.card({
1179
- id: 'stg-sec-tool-profiles', section: 'connected_tools.tool_profiles',
1194
+ id: 'stg-sec-default-tools', section: 'connected_tools.default_tools',
1180
1195
  body: `
1181
1196
  <div class="stg-mcp-intro">
1182
- <div class="stg-help" style="margin-top:0">${m('profilesHelp')}</div>
1197
+ <div class="stg-help" style="margin-top:0">${m('defaultToolsHelp')}</div>
1183
1198
  <div class="stg-mcp-note">
1184
1199
  <strong>${m('noteTitle')}</strong>
1185
1200
  <span>${m('noteBody')}</span>
1186
1201
  </div>
1187
1202
  </div>
1188
- <div id="stg-mcp-matrix-wrap" class="stg-mcp-matrix-wrap stg-mcp-block" ${kit.inv('MCP022')} role="group" aria-label="${kit.L('MCP022')}">
1189
- <div class="stg-mcp-matrix-empty">${m('loading')}</div>
1203
+ <div id="stg-mcp-default-tools" class="stg-mcp-default-tools stg-mcp-block" ${kit.inv('MCP022')} role="group" aria-label="${kit.L('MCP022')}">
1204
+ <div class="stg-mcp-empty-note">${m('loading')}</div>
1190
1205
  </div>
1191
1206
  <div id="stg-mcp-always-on" class="stg-mcp-subsection" ${kit.inv('MCP024')}></div>
1192
1207
  <div class="stg-mcp-subsection stg-mcp-servers-section">
@@ -1298,6 +1313,7 @@ function buildOpencodeTab() {
1298
1313
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/></svg>
1299
1314
  </button>
1300
1315
  </span>`, { forId: 'stg-ocp-port', className: 'ocp-server-bar' })}
1316
+ <div class="stg-help ocp-install-note" id="stg-ocp-install" role="status" aria-live="polite" hidden></div>
1301
1317
  </div>
1302
1318
 
1303
1319
  <div class="ocp-group-head">
@@ -1323,9 +1339,10 @@ function buildOpencodeTab() {
1323
1339
  <div class="ocp-group">
1324
1340
  ${kit.advanced(`
1325
1341
  <div class="ocp-config-footer" ${kit.inv('OCP019')}>
1326
- <span class="ocp-config-path">${kit.th('settings.redesign.opencode.configPath', { path: '<code class="stg-code">~/.config/opencode/config.json</code>' })}</span>
1342
+ <span class="ocp-config-path">${kit.th('settings.redesign.opencode.configPath', { path: '<code class="stg-code" id="stg-ocp-config-path">~/.config/opencode/opencode.json</code>' })}</span>
1327
1343
  <button type="button" class="ocp-config-link" id="stg-ocp-open-config" aria-describedby="stg-ocp-open-config-help">${kit.L('OCP019')}</button>
1328
1344
  </div>
1345
+ <p class="stg-help" id="stg-ocp-config-jsonc" hidden></p>
1329
1346
  ${kit.help('OCP019', { forId: 'stg-ocp-open-config' })}`)}
1330
1347
  </div>`,
1331
1348
  })}
@@ -1336,8 +1353,7 @@ function buildOpencodeTab() {
1336
1353
  body: `
1337
1354
  <div class="ocp-compact-group ocp-rows">
1338
1355
  ${check('OCP013', 'stg-ocp-compact-auto')}
1339
- ${check('OCP014', 'stg-ocp-compact-prune')}
1340
- ${kit.field('OCP015', `<input type="number" class="ocp-reserved-input" id="stg-ocp-compact-reserved" value="10000" min="1000" max="50000" step="1000" aria-describedby="stg-ocp-compact-reserved-help">`, { forId: 'stg-ocp-compact-reserved' })}
1356
+ ${kit.field('OCP015', `<input type="number" class="ocp-reserved-input" id="stg-ocp-compact-reserved" value="" placeholder="${kit.te('settings.redesign.opencode.bufferDefault')}" min="0" max="1000000" step="1000" aria-describedby="stg-ocp-compact-reserved-help">`, { forId: 'stg-ocp-compact-reserved' })}
1341
1357
  ${kit.toggleField('OCP016', `<button type="button" class="cc-toggle on" id="stg-ocp-snapshot-toggle" aria-labelledby="stg-ocp-snapshot-toggle-label" aria-describedby="stg-ocp-snapshot-toggle-help"></button>`, { forId: 'stg-ocp-snapshot-toggle', className: 'ocp-snapshot-row' })}
1342
1358
  </div>
1343
1359
  <p class="ocp-footnote">${kit.te('settings.redesign.opencode.snapshotDetail')}</p>`,
@@ -1663,19 +1679,24 @@ function buildSkillsTab(ccSkills) {
1663
1679
  const row = (skill) => {
1664
1680
  const installed = skill.installedTargets || { claude: !!skill.installed, codex: false, opencode: false };
1665
1681
  const anyOn = targets.some(t => installed[t.id]);
1666
- const chips = targets.map(t => `
1667
- <button type="button" class="cc-skill-target${installed[t.id] ? ' on' : ''}"
1668
- data-cc-skill="${skill.dirName}"
1682
+ // What each runtime has of it: a native skill folder, an old bridge not moved yet, or a folder of that name
1683
+ // SynaBun did not install (settings/settings-skill-folders.js chipState).
1684
+ const chips = targets.map(t => {
1685
+ const chip = chipState(skill.targetStates?.[t.id], installed[t.id]);
1686
+ return `
1687
+ <button type="button" class="cc-skill-target${chip.on ? ' on' : ''}"
1688
+ data-cc-skill="${kit.esc(skill.dirName)}"
1669
1689
  data-cc-target="${t.id}"
1670
- data-state="${installed[t.id] ? 'on' : 'off'}"
1671
- ${kit.inv('SKL001')} data-stg-pressed="on" aria-pressed="${installed[t.id] ? 'true' : 'false'}"
1672
- title="${kit.te('settings.redesign.skills.installFor', { skill: skill.name, assistant: t.label })}">${t.label}</button>
1673
- `).join('');
1690
+ data-state="${chip.on ? 'on' : 'off'}" data-install-state="${chip.state}"
1691
+ ${kit.inv('SKL001')} data-stg-pressed="on" aria-pressed="${chip.on ? 'true' : 'false'}"
1692
+ title="${kit.te(chip.key, { skill: skill.name, assistant: t.label })}">${t.label}</button>
1693
+ `;
1694
+ }).join('');
1674
1695
  return `
1675
- <div class="cc-skill-row${anyOn ? ' installed' : ''}" data-skill-name="${skill.dirName}">
1696
+ <div class="cc-skill-row${anyOn ? ' installed' : ''}" data-skill-name="${kit.esc(skill.dirName)}">
1676
1697
  <div class="cc-skill-info">
1677
- <span class="cc-skill-name">/${skill.name}</span>
1678
- <span class="cc-skill-desc">${skill.description || ''}</span>
1698
+ <span class="cc-skill-name">/${kit.esc(skill.name)}</span>
1699
+ <span class="cc-skill-desc">${kit.esc(skill.description || '')}</span>
1679
1700
  </div>
1680
1701
  <div class="cc-skill-targets" role="group" aria-label="${kit.te('settings.redesign.skills.targets', { skill: skill.name })}">${chips}</div>
1681
1702
  </div>`;
@@ -1685,10 +1706,14 @@ function buildSkillsTab(ccSkills) {
1685
1706
  <div class="stg-pane" data-stg-pane="skills">
1686
1707
  ${kit.card({
1687
1708
  id: 'stg-sec-skills', section: 'tool_access.skills',
1688
- body: skills.length
1709
+ body: (skills.length
1689
1710
  ? `<div ${kit.inv('SKL004')} role="group" aria-label="${kit.L('SKL004')}">${skills.map(row).join('')}</div>
1690
1711
  ${kit.help('SKL001')}`
1691
- : `<div class="stg-help" style="margin-top:0">${kit.te('settings.redesign.skills.none')}</div>`,
1712
+ : `<div class="stg-help" style="margin-top:0">${kit.te('settings.redesign.skills.none')}</div>`)
1713
+ // Skills Studio is where a skill is edited, versioned and published; these chips stay for what SynaBun ships.
1714
+ + `<div class="cc-skill-studio"><button type="button" class="stg-action-btn compact" data-cc-open-studio="1">${kit.te('settings.redesign.skills.openStudio')}</button>
1715
+ <span class="stg-help">${kit.te('settings.redesign.skills.openStudioHelp')}</span></div>`
1716
+ + skillFoldersHTML(),
1692
1717
  })}
1693
1718
  </div>`;
1694
1719
  }
@@ -1699,7 +1724,7 @@ function buildPermissionsTab(toolCategories, toolPermissions) {
1699
1724
  // The inventory ids of the "all tools of this group" switches, by group.
1700
1725
  const ALL_SWITCH = {
1701
1726
  memory: 'PER001', browser: 'PER003', whiteboard: 'PER004', cards: 'PER005', automation: 'PER006',
1702
- leonardo: 'PER007', images: 'PER008', gsc: 'PER009', youtube: 'PER010', thirdparty: 'PER011',
1727
+ images: 'PER008', gsc: 'PER009', youtube: 'PER010', thirdparty: 'PER011',
1703
1728
  };
1704
1729
 
1705
1730
  let totalOn = 0, totalAll = 0;
@@ -1819,6 +1844,52 @@ const userContextDigestHTML = (digest) => (digest.rows.length
1819
1844
  ? digest.rows.map(row => `<button type="button" class="uc-digest-row" role="listitem" data-uc-memory="${kit.esc(row.id)}" title="${kit.te('settings.redesign.hooks.uc.openMemory')}"><span class="uc-digest-statement">${kit.esc(row.statement)}</span><span class="uc-digest-facet">${kit.esc(row.facet)}${row.seed ? ` · ${kit.te('settings.redesign.hooks.uc.seed')}` : ''}</span></button>`).join('')
1820
1845
  : `<div class="stg-help uc-digest-message" role="listitem" style="margin:0">${kit.esc(digest.message)}</div>`);
1821
1846
 
1847
+ /**
1848
+ * Put a view (describeClaudeHooks) on the section: the line above, the switch, its notes and Repair, and the
1849
+ * per-moment rows and their count in the Hooks card, which show nothing on or off that was not read.
1850
+ */
1851
+ function drawClaudeHooks(overlay, view) {
1852
+ const section = overlay.querySelector('#stg-sec-automation-behavior');
1853
+ if (!section) return;
1854
+ const note = section.querySelector('#ch-status-note');
1855
+ if (note) { note.hidden = !view.note; note.textContent = view.note?.text || ''; note.dataset.chState = view.state; }
1856
+ const rowBox = section.querySelector('#ch-row');
1857
+ if (rowBox) { rowBox.dataset.chReady = view.ready ? '1' : '0'; rowBox.dataset.chState = view.state; }
1858
+ const toggle = section.querySelector('.cc-toggle[data-ch-master]');
1859
+ if (toggle) {
1860
+ toggle.classList.toggle('on', view.on);
1861
+ toggle.setAttribute('aria-pressed', view.on ? 'true' : 'false');
1862
+ toggle.disabled = view.disabled;
1863
+ toggle.setAttribute('aria-describedby', `ch-master-help ch-notes${view.note ? ' ch-status-note' : ''}`);
1864
+ const item = toggle.closest('.cc-integration-item');
1865
+ item?.classList.toggle('enabled', view.on);
1866
+ item?.classList.toggle('uc-disabled', view.disabled);
1867
+ }
1868
+ const notes = section.querySelector('#ch-notes');
1869
+ if (notes) notes.innerHTML = userContextNotesHTML(view.notes);
1870
+ const repair = section.querySelector('#ch-repair');
1871
+ if (repair) repair.hidden = !(view.repair && !view.disabled);
1872
+ const applies = section.querySelector('#ch-applies');
1873
+ if (applies) applies.textContent = view.applies;
1874
+ const locked = !view.ready || view.disabled;
1875
+ for (const row of CLAUDE_HOOK_ROWS) {
1876
+ const rowToggle = section.querySelector(`.cc-toggle[data-cc-hook="${row.key}"]`);
1877
+ if (!rowToggle) continue;
1878
+ const on = !!view.rows?.[row.key];
1879
+ rowToggle.classList.toggle('on', on);
1880
+ rowToggle.setAttribute('aria-pressed', on ? 'true' : 'false');
1881
+ rowToggle.disabled = locked;
1882
+ const item = rowToggle.closest('.cc-integration-item');
1883
+ item?.classList.toggle('enabled', on);
1884
+ item?.classList.toggle('uc-disabled', locked);
1885
+ }
1886
+ const badge = section.querySelector('#cc-hooks-badge');
1887
+ if (badge) {
1888
+ badge.textContent = view.ready ? `${view.onRows}/${view.totalRows}` : '—';
1889
+ badge.classList.toggle('all-on', view.ready && view.onRows === view.totalRows);
1890
+ }
1891
+ }
1892
+
1822
1893
  /** Put a view (describeUserContext) on the section: the note, each switch and its notes, the digest. */
1823
1894
  function drawUserContext(overlay, view) {
1824
1895
  const section = overlay.querySelector('#stg-sec-automation-behavior');
@@ -1895,16 +1966,19 @@ function tunnelExtraHtml(view, mcpKeyInfo) {
1895
1966
  ].filter(Boolean).join('');
1896
1967
  }
1897
1968
 
1898
- function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo, openclawBridge, greeting, toolPermissions, toolCategories, userContext = null) {
1899
- const gh = ccIntegrations.global.hooks || {};
1900
- const ssOn = !!gh.SessionStart;
1901
- const psOn = !!gh.UserPromptSubmit;
1902
- const pcOn = !!gh.PreCompact;
1903
- const stOn = !!gh.Stop;
1904
- const prOn = !!gh.PreToolUse && !!gh.PostToolBatch;
1905
- const ptOn = !!gh.PostToolUse;
1906
- const subStartOn = !!gh.SubagentStart;
1907
- const subStopOn = !!gh.SubagentStop;
1969
+ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo, openclawBridge, greeting, toolPermissions, toolCategories, userContext = null, workerGuard = null, claudeHooks = null) {
1970
+ // SynaBun's Claude Code hooks (settings-claude-hooks.js): the switch and the rows below show what is really in the
1971
+ // settings files, and nothing (disabled, with the reason) when that could not be read: never "off" by default.
1972
+ const ch = claudeHooks || describeClaudeHooks(null, { tx: kit.tx });
1973
+ const rowOn = (key) => !!ch.rows?.[key];
1974
+ const ssOn = rowOn('SessionStart');
1975
+ const psOn = rowOn('UserPromptSubmit');
1976
+ const pcOn = rowOn('PreCompact');
1977
+ const stOn = rowOn('Stop');
1978
+ const prOn = rowOn('PreToolUse');
1979
+ const ptOn = rowOn('PostToolUse');
1980
+ const subStartOn = rowOn('SubagentStart');
1981
+ const subStopOn = rowOn('SubagentStop');
1908
1982
  const hf = ccIntegrations.hookFeatures || {};
1909
1983
  const cmOn = hf.conversationMemory !== false;
1910
1984
  const ulOn = hf.userLearning !== false;
@@ -1921,7 +1995,8 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
1921
1995
  { key: 'SubagentStart', on: subStartOn, inv: 'AUT034' }, { key: 'SubagentStop', on: subStopOn, inv: 'AUT035' },
1922
1996
  ];
1923
1997
  const onCount = hookRows.filter(r => r.on).length;
1924
- const allOn = onCount === hookRows.length;
1998
+ const allOn = ch.ready && onCount === hookRows.length;
1999
+ const hookRowsLocked = !ch.ready || ch.disabled;
1925
2000
 
1926
2001
  const copyIcon = COPY_ICON;
1927
2002
 
@@ -2046,6 +2121,44 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
2046
2121
  <button type="button" class="conn-add-btn" id="uc-digest-all" ${kit.inv('AUT057')} title="${kit.H('AUT057')}" style="margin:0;width:auto;padding:4px 10px;border-style:solid">${kit.L('AUT057')}</button>
2047
2122
  </div>`;
2048
2123
 
2124
+ // Protect workers (lib/worker-guard-setting.js): what capability read-only / workspace means for every worker the
2125
+ // Assistant dispatches. `workerGuard` is the view of what the server answered (describeWorkerGuard): a server
2126
+ // started before the setting shows the default, disabled, under a line that says a restart is missing.
2127
+ const wg = workerGuard || describeWorkerGuard(null, { tx: kit.tx });
2128
+ const workerGuardBlock = `
2129
+ <div class="cc-hook-toggles" id="wg-row" data-wg-ready="${wg.ready ? '1' : '0'}">
2130
+ <div class="cc-integration-item enabled${wg.disabled ? ' uc-disabled' : ''}" data-wg-row>
2131
+ <div class="cc-integration-info">
2132
+ <div class="cc-integration-label" id="wg-mode-label">${kit.L('AUT061')}</div>
2133
+ <div class="cc-integration-path" id="wg-mode-help">${kit.H('AUT061')}</div>
2134
+ <div class="uc-layer-notes" id="wg-notes" data-stg-data>${userContextNotesHTML(wg.note ? [wg.note] : [])}</div>
2135
+ <div class="stg-help" id="wg-mode-detail" data-stg-data style="margin:4px 0 0">${kit.esc(wg.options.find(o => o.value === wg.mode)?.help || '')}</div>
2136
+ </div>
2137
+ ${kit.dropdown('wg-mode', wg.options.map(o => ({ value: o.value, label: kit.esc(o.label) })), { value: wg.mode, classes: 'stg-dropdown stg-dropdown-inline', label: kit.L('AUT061'), attrs: `${kit.inv('AUT061')} aria-describedby="wg-mode-help wg-notes"`, disabled: wg.disabled })}
2138
+ </div>
2139
+ </div>
2140
+ <div class="stg-help uc-save-status" id="wg-save-status" role="alert" data-stg-data hidden></div>
2141
+ <div class="stg-help" id="wg-applies">${kit.esc(wg.applies)}</div>`;
2142
+
2143
+ // SynaBun's hooks in Claude Code: one switch for all of them (POST / DELETE /api/claude-code/integrations, target
2144
+ // global), what is really installed under it, and Repair when the files are out of step. Outside the collapsed
2145
+ // card below, so it can be found; the per-moment rows stay in that card.
2146
+ const claudeHooksBlock = `
2147
+ <div class="stg-help uc-status-note" id="ch-status-note" role="status" data-ch-state="${ch.state}" data-stg-data${ch.note ? '' : ' hidden'}>${ch.note ? kit.esc(ch.note.text) : ''}</div>
2148
+ <div class="cc-hook-toggles" id="ch-row" data-ch-ready="${ch.ready ? '1' : '0'}" data-ch-state="${ch.state}">
2149
+ <div class="cc-integration-item${ch.on ? ' enabled' : ''}${ch.disabled ? ' uc-disabled' : ''}" data-ch-master-row>
2150
+ <div class="cc-integration-info">
2151
+ <div class="cc-integration-label" id="ch-master-label">${kit.L('AUT062')}</div>
2152
+ <div class="cc-integration-path" id="ch-master-help">${kit.H('AUT062')}</div>
2153
+ <div class="uc-layer-notes" id="ch-notes" data-stg-data>${userContextNotesHTML(ch.notes)}</div>
2154
+ <button type="button" class="cc-copy-btn stg-text-btn ch-repair" id="ch-repair" title="${kit.te('settings.redesign.hooks.claudeHooks.repair.title')}"${ch.repair && !ch.disabled ? '' : ' hidden'}>${kit.te('settings.redesign.hooks.claudeHooks.repair.action')}</button>
2155
+ </div>
2156
+ <button type="button" class="cc-toggle${ch.on ? ' on' : ''}" data-ch-master ${kit.inv('AUT062')} aria-pressed="${ch.on ? 'true' : 'false'}" aria-labelledby="ch-master-label" aria-describedby="ch-master-help ch-notes${ch.note ? ' ch-status-note' : ''}"${ch.disabled ? ' disabled' : ''}></button>
2157
+ </div>
2158
+ </div>
2159
+ <div class="stg-help uc-save-status" id="ch-save-status" role="alert" data-stg-data hidden></div>
2160
+ <div class="stg-help uc-applies" id="ch-applies">${kit.esc(ch.applies)}</div>`;
2161
+
2049
2162
  return `
2050
2163
  <div class="stg-pane" data-stg-pane="hooks">
2051
2164
 
@@ -2060,11 +2173,16 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
2060
2173
  body: `
2061
2174
  ${userContextBlock}
2062
2175
 
2176
+ ${workerGuardBlock}
2177
+
2178
+ ${claudeHooksBlock}
2179
+
2063
2180
  ${kit.card({
2064
2181
  id: 'stg-sec-hooks', level: 'h4', collapsible: true, collapsed: true, advanced: true, attrs: 'data-cc-target="global"',
2065
2182
  title: h('hooksTitle'), purpose: h('hooksPurpose'),
2066
2183
  // These hooks are Claude Code's own (~/.claude/settings.json, hooks/claude-code): no other assistant runs them.
2067
- status: `<span class="cc-hooks-badge${allOn ? ' all-on' : ''}" id="cc-hooks-badge">${onCount}/${hookRows.length}</span>${providerBadge({ cli: true, vscode: true, web: false, cowork: false, codex: false })}`,
2184
+ // A count only from a state that was read: "—" otherwise, never "0/8".
2185
+ status: `<span class="cc-hooks-badge${allOn ? ' all-on' : ''}" id="cc-hooks-badge">${ch.ready ? `${onCount}/${hookRows.length}` : '—'}</span>${providerBadge({ cli: true, vscode: true, web: false, cowork: false, codex: false })}`,
2068
2186
  body: `
2069
2187
  <div class="stg-help" style="margin:0 0 10px">${h('claudeOnly')}</div>
2070
2188
  <div class="cc-hook-toggles">
@@ -2083,12 +2201,12 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
2083
2201
  </div>
2084
2202
  <div class="cc-hook-toggles" style="margin-top:10px">
2085
2203
  ${hookRows.map(r => `
2086
- <div class="cc-integration-item${r.on ? ' enabled' : ''}" data-hook="${r.key}">
2204
+ <div class="cc-integration-item${r.on ? ' enabled' : ''}${hookRowsLocked ? ' uc-disabled' : ''}" data-hook="${r.key}">
2087
2205
  <div class="cc-integration-info">
2088
2206
  <div class="cc-integration-label" id="cc-hook-${r.key}-label">${kit.L(r.inv)}</div>
2089
2207
  <div class="cc-integration-path" id="cc-hook-${r.key}-help">${kit.H(r.inv)}</div>
2090
2208
  </div>
2091
- <button type="button" class="cc-toggle${r.on ? ' on' : ''}" data-cc-hook="${r.key}" data-cc-scope="global" ${kit.inv(r.inv)} aria-labelledby="cc-hook-${r.key}-label" aria-describedby="cc-hook-${r.key}-help"></button>
2209
+ <button type="button" class="cc-toggle${r.on ? ' on' : ''}" data-cc-hook="${r.key}" data-cc-scope="global" ${kit.inv(r.inv)} aria-pressed="${r.on ? 'true' : 'false'}" aria-labelledby="cc-hook-${r.key}-label" aria-describedby="cc-hook-${r.key}-help"${hookRowsLocked ? ' disabled' : ''}></button>
2092
2210
  </div>`).join('')}
2093
2211
  </div>`,
2094
2212
  })}`,
@@ -3168,10 +3286,45 @@ function landOnSearchEntry(overlay, entry, { focus = false, watch = true } = {})
3168
3286
  }
3169
3287
 
3170
3288
  // ═══════════════════════════════════════════
3289
+ // The search index of a locale (and of the variant tabs the page registered): what the strings are, not what
3290
+ // the panes hold, so it is made once (at launch idle, or by the first open) and every open reuses it.
3291
+ let _searchIndexMemo = null;
3292
+ function memoSearchIndex(en, variantTabs, build) {
3293
+ const key = JSON.stringify([getLocale(), typeof en === 'function', (variantTabs || []).map(tab => [tab.id, tab.label || ''])]);
3294
+ if (_searchIndexMemo?.key !== key) _searchIndexMemo = { key, index: build() };
3295
+ return _searchIndexMemo.index;
3296
+ }
3297
+
3298
+ /** Make the index now (launch idle), with the English names when the locale is another (loaded once, here). */
3299
+ function prebuildSettingsSearchIndex() {
3300
+ const variantTabs = getSettingsTabs();
3301
+ const make = (en) => memoSearchIndex(en, variantTabs, () => buildSettingsSearchIndex({ t: kit.tx, en, variants: variantTabs.map(tab => ({ id: tab.id, label: tab.label })) }));
3302
+ if (getLocale() === 'en') { make(null); return undefined; }
3303
+ const english = englishTranslator();
3304
+ if (english) { make(english); return undefined; }
3305
+ make(null);
3306
+ return loadEnglishTranslator().then((en) => { if (en) make(en); }).catch(() => {});
3307
+ }
3308
+
3171
3309
  // MAIN ENTRY — openSettingsModal
3172
3310
  // ═══════════════════════════════════════════
3173
3311
 
3174
- export async function openSettingsModal(options = {}) {
3312
+ // An open that comes while another is still reading its data joins it: one panel, the later options applied.
3313
+ let _settingsOpening = null;
3314
+ export function openSettingsModal(options = {}) {
3315
+ if (_settingsOpening) {
3316
+ return _settingsOpening.then(() => {
3317
+ const panel = document.getElementById('settings-panel');
3318
+ if (panel) { panel.style.zIndex = '300001'; applySettingsOpenOptions(panel, options); }
3319
+ });
3320
+ }
3321
+ const opening = openSettingsModalNow(options);
3322
+ const guarded = opening.catch(() => {}).finally(() => { if (_settingsOpening === guarded) _settingsOpening = null; });
3323
+ _settingsOpening = guarded;
3324
+ return opening;
3325
+ }
3326
+
3327
+ async function openSettingsModalNow(options = {}) {
3175
3328
  // If already open, just bring it to front
3176
3329
  const existing = document.getElementById('settings-panel');
3177
3330
  if (existing) {
@@ -3205,33 +3358,32 @@ export async function openSettingsModal(options = {}) {
3205
3358
  // The Assistant's user context (lib/assistant-user-context.js): what its endpoint answered, as a reading.
3206
3359
  // No answer is a reading too: the section says what is true instead of showing switches that do nothing.
3207
3360
  let userContextReading = readUserContextResponse({ status: 0 });
3361
+ // Protect workers (lib/worker-guard-setting.js): what its endpoint answered, as a reading.
3362
+ let workerGuardReading = readWorkerGuardResponse({ status: 0 });
3363
+ // SynaBun's Claude Code hooks (lib/claude-hooks-status.js): the `claudeHooks` of the integrations read, as a reading.
3364
+ let claudeHooksReading = readClaudeHooksResponse({ status: 0 });
3208
3365
 
3209
3366
  try {
3210
- const [settingsRes, connRes, ccRes, skillsRes, tunnelRes, keyRes, bridgeRes, greetRes, setupRes, cliRes, toolPermsRes, toolCatsRes, discordRes, skinsRes, iconsRes, youtubeRes, typesafeRes, userContextRes] = await Promise.allSettled([
3211
- fetch('/api/settings').then(r => r.json()),
3212
- fetch('/api/connections').then(r => r.json()),
3213
- fetch('/api/claude-code/integrations').then(r => r.json()),
3214
- fetch('/api/claude-code/skills').then(r => r.json()),
3215
- fetch('/api/tunnel/status').then(r => r.json()),
3216
- fetch('/api/mcp-key').then(r => r.json()),
3217
- fetch('/api/bridges/openclaw').then(r => r.json()),
3218
- loadGreetingReading(),
3219
- fetch('/api/setup/status').then(r => r.json()),
3220
- fetch('/api/cli/config').then(r => r.json()),
3221
- fetch('/api/claude-code/tool-permissions').then(r => r.json()),
3222
- fetch('/api/claude-code/tool-categories').then(r => r.json()),
3223
- fetch('/api/discord/config').then(r => r.json()),
3224
- fetch('/api/skins').then(r => r.json()),
3225
- fetch('/api/file-icons').then(r => r.json()),
3226
- fetch('/api/youtube/config').then(r => r.json()),
3227
- fetch('/api/typesafe/config').then(r => r.json()),
3228
- loadUserContextReading(),
3229
- ]);
3367
+ // Read through the page's data cache (settings/settings-data.js): kept answers at once (each open its own
3368
+ // copy; read again in the background when old), so Settings waits only the first time.
3369
+ const reads = await readSettingsData();
3370
+ // The greeting and the user context are readings of what their endpoints answered (no answer is a reading too).
3371
+ const readingOf = (entry, make) => ({ status: 'fulfilled', value: entry?.status === 'fulfilled' ? make(entry.value) : make({ status: 0, error: entry?.reason?.message }) });
3372
+ const greetRes = readingOf(reads.greeting, readGreetingStatus);
3373
+ const userContextRes = readingOf(reads.userContext, readUserContextResponse);
3374
+ const workerGuardRes = readingOf(reads.workerGuard, readWorkerGuardResponse);
3375
+ const settingsRes = reads.settings, connRes = reads.connections, ccRes = reads.integrations, skillsRes = reads.skills;
3376
+ const tunnelRes = reads.tunnel, keyRes = reads.mcpKey, bridgeRes = reads.openclaw, setupRes = reads.setup, cliRes = reads.cli;
3377
+ const toolPermsRes = reads.toolPermissions, toolCatsRes = reads.toolCategories, discordRes = reads.discord, skinsRes = reads.skins;
3378
+ const iconsRes = reads.icons, youtubeRes = reads.youtube, typesafeRes = reads.typesafe;
3230
3379
  if (typesafeRes.status === 'fulfilled' && typesafeRes.value.ok) typesafeConfig = typesafeRes.value;
3231
3380
  if (userContextRes.status === 'fulfilled') userContextReading = userContextRes.value;
3381
+ if (workerGuardRes.status === 'fulfilled') workerGuardReading = workerGuardRes.value;
3232
3382
  if (settingsRes.status === 'fulfilled') settings = settingsRes.value;
3233
3383
  if (connRes.status === 'fulfilled' && connRes.value.connections) connections = connRes.value.connections;
3234
- if (ccRes.status === 'fulfilled' && ccRes.value.ok) ccIntegrations = ccRes.value;
3384
+ // Read as it came (status and body): a refusal or an old server is said as what it is, never as hooks that are off.
3385
+ if (ccRes.status === 'fulfilled' && ccRes.value?.status >= 200 && ccRes.value.status < 300 && ccRes.value.body?.ok) ccIntegrations = ccRes.value.body;
3386
+ claudeHooksReading = readingOf(ccRes, readClaudeHooksResponse).value;
3235
3387
  if (skillsRes.status === 'fulfilled' && skillsRes.value.ok) ccSkills = skillsRes.value.skills || [];
3236
3388
  if (tunnelRes.status === 'fulfilled' && tunnelRes.value.ok) tunnelStatus = tunnelRes.value;
3237
3389
  if (keyRes.status === 'fulfilled' && keyRes.value.ok) mcpKeyInfo = keyRes.value;
@@ -3256,15 +3408,18 @@ export async function openSettingsModal(options = {}) {
3256
3408
  || getSettingsPage(storage.getItem(KEYS.SETTINGS_LAST_PAGE))?.id
3257
3409
  || DEFAULT_SETTINGS_PAGE;
3258
3410
  const openerEl = document.activeElement;
3411
+ // Listeners on document and window that belong to this panel end with it (close aborts it).
3412
+ const panelLife = new AbortController();
3259
3413
 
3260
3414
  // ── Panel ──
3261
3415
  const overlay = document.createElement('div');
3262
3416
  overlay.className = 'settings-panel glass resizable';
3263
3417
  overlay.id = 'settings-panel';
3264
3418
 
3265
- // Always open centered at default size
3266
- overlay.style.left = Math.max(20, (window.innerWidth - 900) / 2) + 'px';
3267
- overlay.style.top = Math.max(48, (window.innerHeight - 650) / 2) + 'px';
3419
+ // Always open centered at default size, in the app frame
3420
+ const at = centeredInFrame(900, 650);
3421
+ overlay.style.left = at.left + 'px';
3422
+ overlay.style.top = at.top + 'px';
3268
3423
 
3269
3424
  // ── Assemble HTML ──
3270
3425
  overlay.innerHTML = `
@@ -3297,7 +3452,7 @@ export async function openSettingsModal(options = {}) {
3297
3452
  <div class="settings-content">
3298
3453
  ${buildPagesHTML({
3299
3454
  server: buildServerTab(settings, connections),
3300
- hooks: buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo, openclawBridge, describeGreeting(greetingReading, null, { tx: kit.tx, t: kit.tx }), toolPermissions, toolCategories, describeUserContext(userContextReading, { tx: kit.tx })),
3455
+ hooks: buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo, openclawBridge, describeGreeting(greetingReading, null, { tx: kit.tx, t: kit.tx }), toolPermissions, toolCategories, describeUserContext(userContextReading, { tx: kit.tx }), describeWorkerGuard(workerGuardReading, { tx: kit.tx }), describeClaudeHooks(claudeHooksReading, { tx: kit.tx })),
3301
3456
  terminal: buildTerminalTab(cliConfig),
3302
3457
  opencode: buildOpencodeTab(),
3303
3458
  notifications: buildNotificationsTab(),
@@ -3320,6 +3475,7 @@ export async function openSettingsModal(options = {}) {
3320
3475
  skins: buildSkinsTab(skinsData.skins, skinsData.active),
3321
3476
  interface: buildInterfaceTab(),
3322
3477
  icons: buildIconsTab(customIconsData),
3478
+ app: buildAppTab(),
3323
3479
  about: buildAboutTab(),
3324
3480
  }, variantTabs, initialPage)}
3325
3481
  </div>
@@ -3361,10 +3517,14 @@ export async function openSettingsModal(options = {}) {
3361
3517
  // ── Close helper ──
3362
3518
  const close = () => {
3363
3519
  document.removeEventListener('keydown', onSettingsEsc);
3520
+ const first = !panelLife.signal.aborted;
3521
+ panelLife.abort();
3364
3522
  // Tabs that poll (Judgments) clear their timers on this event.
3365
3523
  overlay.dispatchEvent(new CustomEvent('settings-close'));
3366
3524
  backdrop.remove();
3367
3525
  overlay.remove();
3526
+ // What was changed here is what the next open shows: its reads are asked for again now, in the background.
3527
+ if (first) refreshSettingsData();
3368
3528
  // Give the keyboard back to whatever opened Settings.
3369
3529
  if (openerEl && openerEl !== document.body && document.body.contains(openerEl)) { try { openerEl.focus({ preventScroll: true }); } catch {} }
3370
3530
  };
@@ -3525,12 +3685,12 @@ export async function openSettingsModal(options = {}) {
3525
3685
  + '</div>';
3526
3686
  html += '<div style="padding:4px 0">';
3527
3687
  if (data.parent) {
3528
- html += `<div class="cc-browse-item" data-path="${escapeHtml(data.parent)}" style="padding:4px 10px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px;color:var(--t-muted)">`
3688
+ html += `<div class="cc-browse-item" data-path="${kit.esc(data.parent)}" style="padding:4px 10px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px;color:var(--t-muted)">`
3529
3689
  + '<svg viewBox="0 0 24 24" style="width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2"><polyline points="15 18 9 12 15 6"/></svg>'
3530
3690
  + '' + kit.te('settings.redesign.server.parent') + '</div>';
3531
3691
  }
3532
3692
  for (const d of data.directories) {
3533
- html += `<div class="cc-browse-item" data-path="${escapeHtml(data.current + '/' + d)}" style="padding:4px 10px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px">`
3693
+ html += `<div class="cc-browse-item" data-path="${kit.esc(data.current + '/' + d)}" style="padding:4px 10px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px">`
3534
3694
  + '<svg viewBox="0 0 24 24" style="width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>'
3535
3695
  + escapeHtml(d) + '</div>';
3536
3696
  }
@@ -3914,7 +4074,7 @@ export async function openSettingsModal(options = {}) {
3914
4074
  container.querySelectorAll('.stg-dropdown.open').forEach(dd => {
3915
4075
  if (!dd.contains(e.target)) dd.classList.remove('open');
3916
4076
  });
3917
- });
4077
+ }, { signal: panelLife.signal });
3918
4078
  }
3919
4079
 
3920
4080
  function syncStgDropdown(container, hiddenId) {
@@ -4410,6 +4570,9 @@ export async function openSettingsModal(options = {}) {
4410
4570
  // ── Code editor: AI autocomplete (loads and saves itself) ──
4411
4571
  wireCodeEditorTab(overlay);
4412
4572
 
4573
+ // ── App: updates, start at login, the window, maintenance (loads itself when its page is shown) ──
4574
+ wireAppTab(overlay, { toast: showCCToast });
4575
+
4413
4576
  // ── About: version, links, system and dependencies (loads itself the first time its page is shown) ──
4414
4577
  wireAboutTab(overlay, { toast: showCCToast });
4415
4578
 
@@ -4598,9 +4761,14 @@ export async function openSettingsModal(options = {}) {
4598
4761
  });
4599
4762
  const data = await res.json();
4600
4763
  showPathAction(profileId, data);
4764
+ const refusal = profileId === 'opencode' && data.support ? describeOpencodeInstall(data.support, { th: kit.th, tx: kit.tx }) : null;
4601
4765
  if (data.ok && data.found && data.path && data.state === 'broken') {
4602
4766
  // Found, but it does not start: not put in the field, so a broken path is never saved by accident.
4603
4767
  showCCToast(kit.tx('settings.redesign.terminal.detectionFailed', { message: data.error ? `${data.path}: ${data.error}` : data.path }));
4768
+ } else if (data.ok && data.found && data.path && refusal?.status === 'notSupported') {
4769
+ // An OpenCode SynaBun does not drive (OpenCode 1, say): put in the field, with the sentence that says why it will not start.
4770
+ input.value = data.path;
4771
+ showCCToast(refusal.text, 12000);
4604
4772
  } else if (data.ok && data.found && data.path) {
4605
4773
  input.value = data.path;
4606
4774
  showCCToast(kit.tx('settings.redesign.terminal.found', { path: data.version ? `${data.path} (${data.version})` : data.path }));
@@ -4788,7 +4956,7 @@ export async function openSettingsModal(options = {}) {
4788
4956
 
4789
4957
  // Re-poll when tab regains visibility (covers Permissions API gaps on Safari).
4790
4958
  const visHandler = () => { if (!document.hidden) refreshNotifPermRow(); };
4791
- document.addEventListener('visibilitychange', visHandler);
4959
+ document.addEventListener('visibilitychange', visHandler, { signal: panelLife.signal });
4792
4960
 
4793
4961
  // Request permission
4794
4962
  if (requestPermBtn) {
@@ -4858,9 +5026,15 @@ export async function openSettingsModal(options = {}) {
4858
5026
  if (data.ok) {
4859
5027
  const nowOn = !isOn;
4860
5028
  toggleBtn.classList.toggle('on');
5029
+ toggleBtn.setAttribute('aria-pressed', nowOn ? 'true' : 'false');
4861
5030
  const row = toggleBtn.closest('.cc-integration-item');
4862
5031
  if (row) row.classList.toggle('enabled', nowOn);
4863
- if (target === 'global') { updateGlobalHookBadge(); }
5032
+ if (target === 'global') {
5033
+ updateGlobalHookBadge();
5034
+ // The answer's state of every hook (a server from before it sends none): the switch above follows it.
5035
+ if (data.claudeHooks) { claudeHooksReading = readClaudeHooksResponse({ status: res.status, body: { ok: true, claudeHooks: data.claudeHooks } }); redrawClaudeHooks(); }
5036
+ refreshSettingsData(['integrations']);
5037
+ }
4864
5038
  else { toggleBtn.textContent = nowOn ? kit.tx('settings.redesign.hooks.enabled') : kit.tx('settings.redesign.hooks.enable'); if (panel) panel.classList.toggle('enabled'); const badge = panel?.querySelector('.cc-panel-status'); if (badge) { badge.textContent = nowOn ? kit.tx('settings.redesign.hooks.active') : kit.tx('settings.redesign.hooks.off'); badge.className = 'cc-panel-status ' + (nowOn ? 'active' : 'inactive'); } updateProjectCount(); }
4865
5039
  } else { alert(data.error || kit.tx('settings.redesign.hooks.failedToToggleHook')); }
4866
5040
  } catch (err) { alert(kit.tx('settings.redesign.hooks.failed2', { message: err.message })); }
@@ -4872,6 +5046,41 @@ export async function openSettingsModal(options = {}) {
4872
5046
  toggle.addEventListener('click', () => { ccToggleHook('global', null, toggle, overlay.querySelector('.iface-section[data-cc-target="global"]'), toggle.dataset.ccHook); });
4873
5047
  });
4874
5048
 
5049
+ // ── SynaBun hooks in Claude Code: one switch for all of them, and Repair ──
5050
+ // Each change is the server's: the row is drawn again from the state its answer carries (what the files hold now),
5051
+ // and a change that failed leaves the row as it was, with the reason under it. One change at a time.
5052
+ function redrawClaudeHooks() { drawClaudeHooks(overlay, describeClaudeHooks(claudeHooksReading, { tx: kit.tx })); }
5053
+ const setClaudeHooksSaveStatus = (text = '') => {
5054
+ const status = overlay.querySelector('#ch-save-status');
5055
+ if (status) { status.hidden = !text; status.textContent = text; }
5056
+ };
5057
+ let claudeHooksBusy = false;
5058
+ async function changeClaudeHooks(action) {
5059
+ if (claudeHooksBusy || claudeHooksReading.state !== 'ready') return;
5060
+ claudeHooksBusy = true;
5061
+ setClaudeHooksSaveStatus('');
5062
+ const row = overlay.querySelector('[data-ch-master-row]');
5063
+ if (row) row.style.opacity = '0.6';
5064
+ let failure = null;
5065
+ try {
5066
+ // On and Repair ask for the hooks to run (unblock: Claude Code's own disableAllHooks in the same file goes too).
5067
+ const payload = action === 'off' ? { target: 'global' } : { target: 'global', unblock: true };
5068
+ const res = await fetch('/api/claude-code/integrations', { method: action === 'off' ? 'DELETE' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
5069
+ const body = await res.json().catch(() => null);
5070
+ if (res.ok && body?.ok) {
5071
+ claudeHooksReading = readClaudeHooksResponse({ status: res.status, body: { ok: true, claudeHooks: body.claudeHooks } });
5072
+ refreshSettingsData(['integrations']);
5073
+ } else failure = { status: res.status, body };
5074
+ } catch (err) { failure = { status: 0, error: err?.message }; }
5075
+ finally { claudeHooksBusy = false; if (row) row.style.opacity = ''; }
5076
+ redrawClaudeHooks();
5077
+ if (failure) setClaudeHooksSaveStatus(describeClaudeHooksSaveFailure({ action, ...failure }, { tx: kit.tx }));
5078
+ }
5079
+ overlay.querySelector('.cc-toggle[data-ch-master]')?.addEventListener('click', (e) => {
5080
+ changeClaudeHooks(e.currentTarget.classList.contains('on') ? 'off' : 'on');
5081
+ });
5082
+ overlay.querySelector('#ch-repair')?.addEventListener('click', () => changeClaudeHooks('repair'));
5083
+
4875
5084
  // ── The Assistant's user context: layers and the digest preview ──
4876
5085
  const ucRowsEl = overlay.querySelector('#uc-digest-rows');
4877
5086
  const redrawUserContext = (reading = userContextReading) => drawUserContext(overlay, describeUserContext(reading, { tx: kit.tx }));
@@ -4898,6 +5107,47 @@ export async function openSettingsModal(options = {}) {
4898
5107
  if (!link) return;
4899
5108
  try { applySettingsOpenOptions(overlay, JSON.parse(link.dataset.ucGoto)); } catch { /* a malformed target goes nowhere */ }
4900
5109
  });
5110
+ // Protect workers: the choice is saved at once and follows the server's answer; a save that failed puts the
5111
+ // dropdown back where it was, with the reason under it. Saves go one after the other, in the order of the picks.
5112
+ {
5113
+ const input = overlay.querySelector('#wg-mode');
5114
+ const dd = overlay.querySelector('.cc-dropdown[data-for="wg-mode"]');
5115
+ const status = overlay.querySelector('#wg-save-status');
5116
+ const detail = overlay.querySelector('#wg-mode-detail');
5117
+ const setStatus = (text) => { if (status) { status.textContent = text; status.hidden = !text; } };
5118
+ const showMode = (mode) => {
5119
+ if (input) input.value = mode;
5120
+ const view = describeWorkerGuard(workerGuardReading, { tx: kit.tx });
5121
+ const option = view.options.find(o => o.value === mode);
5122
+ dd?.querySelector('.cc-dropdown-value') && (dd.querySelector('.cc-dropdown-value').textContent = option?.label || mode);
5123
+ dd?.querySelectorAll('.cc-dropdown-item').forEach(item => item.classList.toggle('active', item.dataset.value === mode));
5124
+ if (detail) detail.textContent = option?.help || '';
5125
+ };
5126
+ let saves = Promise.resolve();
5127
+ input?.addEventListener('change', () => {
5128
+ if (workerGuardReading.state !== 'ready') return;
5129
+ const mode = String(input.value || '');
5130
+ if (!mode || mode === workerGuardReading.mode) { showMode(workerGuardReading.mode); return; }
5131
+ setStatus('');
5132
+ showMode(mode);
5133
+ if (dd) dd.style.opacity = '0.4';
5134
+ saves = saves.then(async () => {
5135
+ let failure = null;
5136
+ try {
5137
+ const res = await fetch('/api/assistant/worker-guard', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode }) });
5138
+ const body = await res.json().catch(() => null);
5139
+ const reading = readWorkerGuardResponse({ status: res.status, body });
5140
+ if (res.ok && reading.state === 'ready') workerGuardReading = reading;
5141
+ else failure = { status: res.status, body };
5142
+ } catch (err) {
5143
+ failure = { status: 0, error: err?.message };
5144
+ }
5145
+ if (dd) dd.style.opacity = '';
5146
+ showMode(workerGuardReading.mode);
5147
+ if (failure) setStatus(describeWorkerGuardSaveFailure({ ...failure, mode }, { tx: kit.tx }));
5148
+ });
5149
+ });
5150
+ }
4901
5151
  // The switch moves at once and follows the server's answer: the stored settings when the save went through,
4902
5152
  // back where it was (with the reason next to the switches) when it did not. Saves go one after the other, in
4903
5153
  // the order of the clicks, so a second click made before the first was answered is kept, not dropped.
@@ -5151,6 +5401,12 @@ export async function openSettingsModal(options = {}) {
5151
5401
  btn.addEventListener('click', () => { const idx = btn.dataset.ccProjectToggle; const panel = overlay.querySelector(`.cc-panel[data-cc-idx="${idx}"]`); const projectPath = panel?.dataset.ccPath; if (projectPath) ccToggleHook('project', projectPath, btn, panel); });
5152
5402
  });
5153
5403
 
5404
+ // Skills Studio, from Settings → Skills: Settings closes, the Studio opens on its Library.
5405
+ overlay.querySelector('[data-cc-open-studio]')?.addEventListener('click', () => {
5406
+ close();
5407
+ emit('skills:open', { place: 'library' });
5408
+ });
5409
+
5154
5410
  // Skill target chips — one skill can be installed independently into Claude / Codex / OpenCode
5155
5411
  const runtimeLabels = { claude: 'Claude Code', codex: 'Codex CLI', opencode: 'OpenCode CLI' };
5156
5412
  overlay.querySelectorAll('.cc-skill-target[data-cc-skill]').forEach(chip => {
@@ -5160,28 +5416,48 @@ export async function openSettingsModal(options = {}) {
5160
5416
  const isOn = chip.classList.contains('on');
5161
5417
  try {
5162
5418
  chip.style.opacity = '0.4'; chip.style.pointerEvents = 'none';
5163
- const res = await fetch('/api/skills/install', {
5419
+ const send = (extra = {}) => fetch('/api/skills/install', {
5164
5420
  method: isOn ? 'DELETE' : 'POST',
5165
5421
  headers: { 'Content-Type': 'application/json' },
5166
- body: JSON.stringify({ name: skillName, target }),
5167
- });
5168
- const data = await res.json();
5422
+ body: JSON.stringify({ name: skillName, target, ...extra }),
5423
+ }).then((res) => res.json());
5424
+ let data = await send();
5425
+ // A skill of that name that SynaBun did not install, or that was changed since, is neither replaced nor
5426
+ // removed unasked: the server says what it is, and only a yes sends its digest back (a backup is kept).
5427
+ if (!data.ok && data.currentDigest && (data.code === 'skill_unowned' || data.code === 'skill_modified')) {
5428
+ if (!confirm(data.question || data.error)) return;
5429
+ data = await send({ expectedDestinationDigest: data.currentDigest });
5430
+ }
5169
5431
  if (data.ok) {
5170
5432
  chip.classList.toggle('on');
5171
5433
  chip.dataset.state = isOn ? 'off' : 'on';
5434
+ // (a server from before the skill folders answers without `destination`: the chip then only says it is installed)
5435
+ const now = chipState(isOn ? null : (data.destination ? (data.shared ? { native: 'absent', shared: 'native' } : { native: 'native', bridge: !!data.bridge }) : undefined), !isOn);
5436
+ chip.dataset.installState = now.state;
5437
+ chip.setAttribute('aria-pressed', now.on ? 'true' : 'false');
5438
+ chip.title = kit.tx(now.key, { skill: skillName, assistant: chip.textContent.trim() });
5172
5439
  const row = chip.closest('.cc-skill-row');
5173
5440
  if (row) {
5174
5441
  const anyOn = !!row.querySelector('.cc-skill-target.on');
5175
5442
  row.classList.toggle('installed', anyOn);
5176
5443
  }
5177
5444
  const runtime = runtimeLabels[target] || target;
5178
- showCCToast(isOn ? kit.tx('settings.redesign.hooks.skillUninstalledRestartToApply', { runtime: runtime }) : kit.tx('settings.redesign.hooks.skillInstalledRestartToApply', { runtime: runtime }));
5445
+ // The shared ~/.agents/skills is read by Codex and OpenCode both: the other chips of the skill follow.
5446
+ if (data.shared) refreshSkillChips(overlay);
5447
+ // An install is a native skill folder now; the server says when an old bridge of it is still there.
5448
+ if (isOn) showCCToast(kit.tx('settings.redesign.hooks.skillUninstalledRestartToApply', { runtime: runtime }));
5449
+ else if (data.destination) showCCToast(kit.tx(installedToast(data), { skill: skillName, runtime: runtime }), 6000);
5450
+ else showCCToast(kit.tx('settings.redesign.hooks.skillInstalledRestartToApply', { runtime: runtime }));
5179
5451
  } else { alert(data.error || kit.tx('settings.redesign.hooks.failedToToggleSkill')); }
5180
5452
  } catch (err) { alert(kit.tx('settings.redesign.hooks.failed2', { message: err.message })); }
5181
5453
  finally { chip.style.opacity = ''; chip.style.pointerEvents = ''; }
5182
5454
  });
5183
5455
  });
5184
5456
 
5457
+ // Skill folders: the move of the old bridges to native skill folders, and the shared ~/.agents/skills switch.
5458
+ // After a move, an undo, or a change made anywhere else, the chips are read again in place.
5459
+ wireSkillFolders(overlay, { toast: (message, ms) => showCCToast(message, ms), onChanged: () => refreshSkillChips(overlay), signal: panelLife.signal });
5460
+
5185
5461
  // ── Setup tab: per-provider MCP toggles, CLI copy, config copy, SynaBun rules ──
5186
5462
  {
5187
5463
  const checkIcon = '<svg viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="20 6 9 17 4 12"/></svg>';
@@ -5337,7 +5613,7 @@ export async function openSettingsModal(options = {}) {
5337
5613
  // An edited copy is removed only on an explicit yes (force). A file without SynaBun's markers is the
5338
5614
  // user's own and stays whatever is sent, so there is nothing to confirm for it.
5339
5615
  const edited = info?.state === 'modified' && info.replaceable !== false;
5340
- // OpenCode: a line in config.json that SynaBun did not add keeps the file on a plain remove (the answer
5616
+ // OpenCode: a line in opencode.json that SynaBun did not add keeps the file on a plain remove (the answer
5341
5617
  // says so, and the host is then no longer managed). Asked again, the confirm sends force, which takes that line too.
5342
5618
  const listed = info?.entry === 'user';
5343
5619
  const force = edited || (listed && info.managed === false);
@@ -5480,8 +5756,8 @@ export async function openSettingsModal(options = {}) {
5480
5756
  wireRulesControls('codex', 'codex');
5481
5757
 
5482
5758
  // ── OpenCode ──
5483
- wireSetupMcpToggle('opencode', '/api/opencode/mcp', '~/.config/opencode/config.json');
5484
- // Config preview — OpenCode stores MCP servers under the "mcp" key of config.json
5759
+ wireSetupMcpToggle('opencode', '/api/opencode/mcp', '~/.config/opencode/opencode.json');
5760
+ // Config preview — OpenCode 2 keeps MCP servers under "mcp" → "servers" of opencode.json
5485
5761
  {
5486
5762
  const preview = overlay.querySelector('#setup-opencode-config-preview');
5487
5763
  let cachedConfig = '';
@@ -5489,15 +5765,18 @@ export async function openSettingsModal(options = {}) {
5489
5765
  const ep = setupStatus.paths?.envPath || '<path-to>/synabun/.env';
5490
5766
  fetch('/api/opencode/mcp').then(r => r.json()).then(data => {
5491
5767
  if (data.ok && data.data && Object.keys(data.data).length) {
5492
- cachedConfig = JSON.stringify({ mcp: data.data }, null, 2);
5768
+ cachedConfig = JSON.stringify({ mcp: { servers: data.data } }, null, 2);
5493
5769
  preview.textContent = cachedConfig;
5494
5770
  } else if (data.ok) {
5771
+ const launch = mcpLaunch();
5495
5772
  cachedConfig = JSON.stringify({
5496
5773
  mcp: {
5497
- SynaBun: {
5498
- type: 'stdio',
5499
- ...mcpLaunch(),
5500
- env: { DOTENV_PATH: ep },
5774
+ servers: {
5775
+ SynaBun: {
5776
+ type: 'local',
5777
+ command: [launch.command, ...launch.args],
5778
+ environment: { DOTENV_PATH: ep },
5779
+ },
5501
5780
  },
5502
5781
  },
5503
5782
  }, null, 2);
@@ -5591,7 +5870,7 @@ export async function openSettingsModal(options = {}) {
5591
5870
  overlay.querySelectorAll('.bc-dropdown.open').forEach(dd => {
5592
5871
  if (!dd.contains(e.target)) dd.classList.remove('open');
5593
5872
  });
5594
- });
5873
+ }, { signal: panelLife.signal });
5595
5874
 
5596
5875
  // Sync dropdown display from hidden input value (for applyBrowserConfig)
5597
5876
  function syncBcDropdowns() {
@@ -6091,12 +6370,12 @@ export async function openSettingsModal(options = {}) {
6091
6370
  + '</div>';
6092
6371
  html += '<div style="padding:4px 0">';
6093
6372
  if (data.parent) {
6094
- html += `<div class="cc-browse-item" data-path="${escapeHtml(data.parent)}" style="padding:4px 10px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px;color:var(--t-muted)">`
6373
+ html += `<div class="cc-browse-item" data-path="${kit.esc(data.parent)}" style="padding:4px 10px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px;color:var(--t-muted)">`
6095
6374
  + '<svg viewBox="0 0 24 24" style="width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2"><polyline points="15 18 9 12 15 6"/></svg>'
6096
6375
  + '' + kit.te('settings.redesign.browser.parent') + '</div>';
6097
6376
  }
6098
6377
  for (const d of data.directories) {
6099
- html += `<div class="cc-browse-item" data-path="${escapeHtml(data.current + '/' + d)}" style="padding:4px 10px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px">`
6378
+ html += `<div class="cc-browse-item" data-path="${kit.esc(data.current + '/' + d)}" style="padding:4px 10px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px">`
6100
6379
  + '<svg viewBox="0 0 24 24" style="width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>'
6101
6380
  + escapeHtml(d) + '</div>';
6102
6381
  }
@@ -7274,7 +7553,10 @@ export async function openSettingsModal(options = {}) {
7274
7553
  const ocpToolsEl = overlay.querySelector('#stg-ocp-tools');
7275
7554
  const ocpHistoryMetaEl = overlay.querySelector('#stg-ocp-history-meta');
7276
7555
  const ocpHistoryClearBtn = overlay.querySelector('#stg-ocp-clear-history');
7277
- const esc = s => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
7556
+ const ocpInstallEl = overlay.querySelector('#stg-ocp-install');
7557
+ const ocpConfigPathEl = overlay.querySelector('#stg-ocp-config-path');
7558
+ const ocpConfigJsoncEl = overlay.querySelector('#stg-ocp-config-jsonc');
7559
+ const esc = s => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
7278
7560
 
7279
7561
  // ── Helpers ──
7280
7562
  let _ocpConnected = new Set();
@@ -7296,6 +7578,7 @@ export async function openSettingsModal(options = {}) {
7296
7578
  let _ollamaConfiguring = false;
7297
7579
  let _ollamaPollTimer = null;
7298
7580
  let _ollamaLastModelNames = '';
7581
+ let _ollamaConfigureError = '';
7299
7582
  let _ocpHistoryStats = null;
7300
7583
 
7301
7584
  function formatBytes(bytes) {
@@ -7326,7 +7609,7 @@ export async function openSettingsModal(options = {}) {
7326
7609
  ocpHistoryMetaEl.textContent = kit.tx('settings.redesign.opencode.historyWipeInProgress');
7327
7610
  return;
7328
7611
  }
7329
- const totalBytes = (Number(data.databaseBytes) || 0) + (Number(data.artifactBytes) || 0);
7612
+ const totalBytes = Number(data.databaseBytes) || 0;
7330
7613
  ocpHistoryMetaEl.textContent = kit.tx('settings.redesign.opencode.stored', { formatCount: formatCount(data.sessions, 'session'), formatBytes: formatBytes(totalBytes) });
7331
7614
  ocpHistoryClearBtn.disabled = false;
7332
7615
  } catch (error) {
@@ -7338,8 +7621,7 @@ export async function openSettingsModal(options = {}) {
7338
7621
  if (document.querySelector('.ocp-history-clear-overlay')) return;
7339
7622
  const confirmation = _ocpHistoryStats?.confirmationPhrase || 'DELETE OPENCODE HISTORY';
7340
7623
  const sessionCount = Number(_ocpHistoryStats?.sessions) || 0;
7341
- const totalBytes = (Number(_ocpHistoryStats?.databaseBytes) || 0)
7342
- + (Number(_ocpHistoryStats?.artifactBytes) || 0);
7624
+ const totalBytes = Number(_ocpHistoryStats?.databaseBytes) || 0;
7343
7625
  const modal = document.createElement('div');
7344
7626
  modal.className = 'tag-delete-overlay ocp-history-clear-overlay';
7345
7627
  modal.innerHTML = `
@@ -7353,9 +7635,9 @@ export async function openSettingsModal(options = {}) {
7353
7635
  <p class="ocp-history-modal-subtitle">${formatCount(sessionCount, 'session')} ${kit.th('settings.redesign.opencode.currentlyStored', { formatBytes: formatBytes(totalBytes) })}</p>
7354
7636
  </div>
7355
7637
  </div>
7356
- <div class="ocp-history-warning"><strong>${kit.te('settings.redesign.opencode.thisCannotBeUndone')}</strong> ${kit.te('settings.redesign.opencode.activeConversationsOrAutomationsMustBe')}</div>
7638
+ <div class="ocp-history-warning"><strong>${kit.te('settings.redesign.opencode.thisCannotBeUndone')}</strong> ${kit.te('settings.redesign.opencode.historyClearSteps')}</div>
7357
7639
  <ul class="ocp-history-scope">
7358
- <li>${kit.te('settings.redesign.opencode.deletesConversationsMessagesToolOutputTo')}</li>
7640
+ <li>${kit.te('settings.redesign.opencode.historyClearDeletes')}</li>
7359
7641
  <li>${kit.te('settings.redesign.opencode.deletesSynabunSDerivedOpencodeResume')}</li>
7360
7642
  <li>${kit.te('settings.redesign.opencode.keepsProviderCredentialsAccountConfigSet')}</li>
7361
7643
  </ul>
@@ -7403,7 +7685,7 @@ export async function openSettingsModal(options = {}) {
7403
7685
  confirmButton.disabled = true;
7404
7686
  confirmButton.textContent = kit.tx('settings.redesign.opencode.clearing');
7405
7687
  feedback.dataset.state = 'working';
7406
- feedback.textContent = kit.tx('settings.redesign.opencode.stoppingOpencodeAndClearingItsHistory');
7688
+ feedback.textContent = kit.tx('settings.redesign.opencode.historyClearWorking');
7407
7689
  const requestId = crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`;
7408
7690
  sessionStorage.setItem('synabun-opencode-history-clear-request', requestId);
7409
7691
  try {
@@ -7416,9 +7698,7 @@ export async function openSettingsModal(options = {}) {
7416
7698
  if (!response.ok || !data.ok) throw new Error(data.error || kit.tx('settings.redesign.opencode.historyWipeFailedHttp', { status: response.status }));
7417
7699
 
7418
7700
  const result = data.result || {};
7419
- const reclaimed = Math.max(0,
7420
- (Number(result.databaseBytesBefore) || 0) - (Number(result.databaseBytesAfter) || 0)
7421
- + (Number(result.artifactBytesDeleted) || 0));
7701
+ const reclaimed = Math.max(0, (Number(result.databaseBytesBefore) || 0) - (Number(result.databaseBytesAfter) || 0));
7422
7702
  const warnings = Array.isArray(result.warnings) ? result.warnings.filter(Boolean) : [];
7423
7703
  feedback.dataset.state = warnings.length ? 'warning' : 'working';
7424
7704
  feedback.textContent = warnings.length
@@ -7459,7 +7739,7 @@ export async function openSettingsModal(options = {}) {
7459
7739
  dotColor = '#facc15';
7460
7740
  statusText = kit.tx('settings.redesign.opencode.configuring');
7461
7741
  statusCls = 'stg-provider-status-key';
7462
- bodyHtml = '<div class="stg-provider-note">' + kit.te('settings.redesign.opencode.writingProviderConfigAndRestartingOpenco') + '</div>';
7742
+ bodyHtml = '<div class="stg-provider-note">' + kit.te('settings.redesign.opencode.writingProviderConfig') + '</div>';
7463
7743
  } else if (!_ollamaRunning) {
7464
7744
  dotColor = 'rgba(248,113,113,0.7)';
7465
7745
  statusText = kit.tx('settings.redesign.opencode.notDetected');
@@ -7470,6 +7750,11 @@ export async function openSettingsModal(options = {}) {
7470
7750
  statusText = kit.tx('settings.redesign.opencode.runningVersion', { version: _ollamaVersion ? ' v' + _ollamaVersion : '' });
7471
7751
  statusCls = 'stg-provider-status-key';
7472
7752
  bodyHtml = '<div class="stg-provider-note">' + kit.te('settings.redesign.opencode.noModelsRun') + ' <code class="stg-code">ollama pull &lt;model&gt;</code></div>';
7753
+ } else if (_ollamaConfigureError) {
7754
+ dotColor = 'rgba(248,113,113,0.7)';
7755
+ statusText = kit.tx('settings.redesign.opencode.runningVersion', { version: _ollamaVersion ? ' v' + _ollamaVersion : '' });
7756
+ statusCls = 'stg-provider-status-disconnected';
7757
+ bodyHtml = '<div class="stg-provider-note">' + esc(kit.tx('settings.redesign.opencode.error2', { message: _ollamaConfigureError })) + '</div>';
7473
7758
  } else if (_ollamaConfigured) {
7474
7759
  dotColor = '#4ade80';
7475
7760
  statusText = kit.tx('settings.redesign.opencode.runningVersion', { version: _ollamaVersion ? ' v' + _ollamaVersion : '' });
@@ -7505,27 +7790,16 @@ export async function openSettingsModal(options = {}) {
7505
7790
  const btn = existing.querySelector('.stg-ollama-remove');
7506
7791
  if (btn) { btn.disabled = true; btn.textContent = kit.tx('settings.redesign.opencode.removing'); }
7507
7792
  try {
7508
- await fetch('/api/ollama/configure', { method: 'DELETE' });
7793
+ const resp = await fetch('/api/ollama/configure', { method: 'DELETE' });
7794
+ const data = await resp.json().catch(() => ({}));
7795
+ if (!resp.ok || data.ok === false) throw new Error(data.error || `HTTP ${resp.status}`);
7509
7796
  _ollamaConfigured = false;
7510
7797
  _ollamaConfiguredCount = 0;
7511
7798
  _ollamaUserRemoved = true;
7512
7799
  renderOllamaCard();
7513
- showCCToast(kit.tx('settings.redesign.opencode.ollamaProviderRemovedServerRestarting'));
7514
- // Poll for server restart then refresh providers
7515
- let polls = 0;
7516
- const pollId = setInterval(async () => {
7517
- polls++;
7518
- if (polls > 8) { clearInterval(pollId); return; }
7519
- try {
7520
- const sr = await fetch('/api/opencode/status');
7521
- const sd = await sr.json();
7522
- if (sd.running) {
7523
- clearInterval(pollId);
7524
- await refreshOcpProviders();
7525
- document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
7526
- }
7527
- } catch {}
7528
- }, 2000);
7800
+ showCCToast(kit.tx('settings.redesign.opencode.ollamaProviderRemoved'));
7801
+ await refreshOcpProviders();
7802
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
7529
7803
  } catch (e) {
7530
7804
  showCCToast(kit.tx('settings.redesign.opencode.error2', { message: e.message }));
7531
7805
  }
@@ -7559,7 +7833,9 @@ export async function openSettingsModal(options = {}) {
7559
7833
 
7560
7834
  // Auto-configure if: running + has models + not configured + not user-removed
7561
7835
  // Also re-configure if models changed while already configured
7562
- const shouldConfigure = _ollamaRunning && _ollamaModels.length > 0 && !_ollamaUserRemoved
7836
+ // A refusal (an opencode.json SynaBun cannot change now) is asked again only when the models change.
7837
+ if (modelsChanged) _ollamaConfigureError = '';
7838
+ const shouldConfigure = _ollamaRunning && _ollamaModels.length > 0 && !_ollamaUserRemoved && !_ollamaConfigureError
7563
7839
  && (!_ollamaConfigured || modelsChanged);
7564
7840
 
7565
7841
  if (shouldConfigure && !_ollamaConfiguring) {
@@ -7567,31 +7843,16 @@ export async function openSettingsModal(options = {}) {
7567
7843
  renderOllamaCard();
7568
7844
  try {
7569
7845
  const cfgResp = await fetch('/api/ollama/configure', { method: 'POST' });
7570
- const cfgData = await cfgResp.json();
7571
- if (cfgData.ok) {
7846
+ const cfgData = await cfgResp.json().catch(() => ({}));
7847
+ _ollamaConfiguring = false;
7848
+ if (cfgResp.ok && cfgData.ok) {
7849
+ // OpenCode reads the provider at once: its models are listed now.
7572
7850
  _ollamaConfigured = true;
7573
7851
  _ollamaConfiguredCount = cfgData.modelsConfigured;
7574
- // Wait for OpenCode server restart then refresh providers
7575
- setTimeout(async () => {
7576
- let polls = 0;
7577
- const pollId = setInterval(async () => {
7578
- polls++;
7579
- if (polls > 8) { clearInterval(pollId); _ollamaConfiguring = false; renderOllamaCard(); return; }
7580
- try {
7581
- const sr = await fetch('/api/opencode/status');
7582
- const sd = await sr.json();
7583
- if (sd.running) {
7584
- clearInterval(pollId);
7585
- _ollamaConfiguring = false;
7586
- await refreshOcpProviders();
7587
- renderOllamaCard();
7588
- document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
7589
- }
7590
- } catch {}
7591
- }, 2000);
7592
- }, 1000);
7852
+ await refreshOcpProviders();
7853
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
7593
7854
  } else {
7594
- _ollamaConfiguring = false;
7855
+ _ollamaConfigureError = cfgData.error || `HTTP ${cfgResp.status}`;
7595
7856
  }
7596
7857
  } catch {
7597
7858
  _ollamaConfiguring = false;
@@ -7843,7 +8104,7 @@ export async function openSettingsModal(options = {}) {
7843
8104
  }
7844
8105
  for (const om of oauthMethods) {
7845
8106
  const label = om.label || om.name || kit.tx('settings.redesign.opencode.reconnectProvider', { name });
7846
- btns.push(`<button type="button" class="stg-action-btn compact stg-text-btn stg-ocp-oauth-btn" data-provider="${esc(id)}" data-method="${om._idx}">${esc(label)}</button>`);
8107
+ btns.push(`<button type="button" class="stg-action-btn compact stg-text-btn stg-ocp-oauth-btn" data-provider="${esc(id)}" data-method="${om._idx}" data-method-id="${esc(om.id || '')}">${esc(label)}</button>`);
7847
8108
  }
7848
8109
  authHtml = btns.join('');
7849
8110
  removeHtml = `<button type="button" class="stg-action-btn compact stg-text-btn stg-text-btn-danger stg-ocp-remove-btn" data-provider="${esc(id)}" data-env="${esc(primaryEnv)}">${kit.te('settings.redesign.opencode.removeConfirm')}</button>`;
@@ -7857,7 +8118,7 @@ export async function openSettingsModal(options = {}) {
7857
8118
  }
7858
8119
  for (const om of oauthMethods) {
7859
8120
  const label = om.label || om.name || kit.tx('settings.redesign.opencode.loginWith', { name: name });
7860
- btns.push(`<button type="button" class="stg-action-btn compact stg-ocp-oauth-btn" data-provider="${esc(id)}" data-method="${om._idx}">${esc(label)}</button>`);
8121
+ btns.push(`<button type="button" class="stg-action-btn compact stg-ocp-oauth-btn" data-provider="${esc(id)}" data-method="${om._idx}" data-method-id="${esc(om.id || '')}">${esc(label)}</button>`);
7861
8122
  }
7862
8123
  authHtml = btns.join('');
7863
8124
  }
@@ -7873,9 +8134,16 @@ export async function openSettingsModal(options = {}) {
7873
8134
  </div>`;
7874
8135
 
7875
8136
  // OAuth wait indicator (hidden via inline style — JS removes it to show)
8137
+ // OAuth wait (hidden via inline style — JS removes it to show): OpenCode's instructions (a device code),
8138
+ // and, for a sign-in that ends with a code the provider shows, the field it is pasted in.
7876
8139
  const oauthWaitHtml = `<div class="stg-ocp-oauth-wait" data-provider="${esc(id)}" style="display:none">
7877
- ${kit.te('settings.redesign.opencode.waitingForAuthorization')}
8140
+ <span class="stg-ocp-oauth-status">${kit.te('settings.redesign.opencode.waitingForAuthorization')}</span>
7878
8141
  <span class="stg-ocp-oauth-code" style="display:none"></span>
8142
+ <div class="stg-ocp-key-row stg-ocp-oauth-code-row" style="display:none">
8143
+ <input type="text" class="stg-input stg-input-sm stg-ocp-oauth-code-input" placeholder="${kit.te('settings.redesign.opencode.oauthCodePlaceholder')}" aria-label="${kit.te('settings.redesign.opencode.oauthCodeFor', { name })}" autocomplete="off" spellcheck="false" style="flex:1">
8144
+ <button type="button" class="stg-action-btn compact stg-ocp-oauth-code-submit">${kit.te('settings.redesign.opencode.oauthConnect')}</button>
8145
+ </div>
8146
+ <div class="stg-ocp-key-error"></div>
7879
8147
  </div>`;
7880
8148
 
7881
8149
  const hiddenSet = getHiddenModels();
@@ -8071,28 +8339,15 @@ export async function openSettingsModal(options = {}) {
8071
8339
  try {
8072
8340
  const resp = await fetch(`/api/opencode/auth/${encodeURIComponent(provId)}`, {
8073
8341
  method: 'PUT', headers: { 'Content-Type': 'application/json' },
8074
- body: JSON.stringify({ type: 'api', key }),
8342
+ body: JSON.stringify({ key }),
8075
8343
  });
8076
- const data = await resp.json();
8344
+ const data = await resp.json().catch(() => ({}));
8077
8345
  if (resp.ok && data.ok !== false) {
8078
8346
  form.style.display = 'none';
8079
8347
  input.value = '';
8080
- showCCToast(kit.tx('settings.redesign.opencode.apiKeySavedForServerRestarting', { provId: provId }));
8081
- // Poll until server is back (takeover kills + respawns, ~5-6s)
8082
- let polls = 0;
8083
- const pollId = setInterval(async () => {
8084
- polls++;
8085
- if (polls > 8) { clearInterval(pollId); return; }
8086
- try {
8087
- const sr = await fetch('/api/opencode/status');
8088
- const sd = await sr.json();
8089
- if (sd.running) {
8090
- clearInterval(pollId);
8091
- await refreshOcpProviders();
8092
- document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8093
- }
8094
- } catch {}
8095
- }, 2000);
8348
+ showCCToast(kit.tx('settings.redesign.opencode.apiKeySaved', { provId: provId }));
8349
+ await refreshOcpProviders();
8350
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8096
8351
  } else {
8097
8352
  const msg = data.error || kit.tx('settings.redesign.opencode.failedToSaveKey');
8098
8353
  if (errEl) { errEl.textContent = msg; errEl.style.display = ''; }
@@ -8125,24 +8380,11 @@ export async function openSettingsModal(options = {}) {
8125
8380
  method: 'DELETE',
8126
8381
  headers: { 'Content-Type': 'application/json' },
8127
8382
  });
8128
- const data = await resp.json();
8383
+ const data = await resp.json().catch(() => ({}));
8129
8384
  if (resp.ok && data.ok !== false) {
8130
- showCCToast(kit.tx('settings.redesign.opencode.apiKeyRemovedForServerRestarting', { provId: provId }));
8131
- // Poll until server is back (takeover kills + respawns, ~5-6s)
8132
- let polls = 0;
8133
- const pollId = setInterval(async () => {
8134
- polls++;
8135
- if (polls > 8) { clearInterval(pollId); return; }
8136
- try {
8137
- const sr = await fetch('/api/opencode/status');
8138
- const sd = await sr.json();
8139
- if (sd.running) {
8140
- clearInterval(pollId);
8141
- await refreshOcpProviders();
8142
- document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8143
- }
8144
- } catch {}
8145
- }, 2000);
8385
+ showCCToast(kit.tx('settings.redesign.opencode.apiKeyRemoved', { provId: provId }));
8386
+ await refreshOcpProviders();
8387
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8146
8388
  } else {
8147
8389
  showCCToast(data.error || kit.tx('settings.redesign.opencode.failedToRemoveKey'));
8148
8390
  }
@@ -8159,42 +8401,22 @@ export async function openSettingsModal(options = {}) {
8159
8401
  btn.addEventListener('click', async () => {
8160
8402
  const provId = btn.dataset.provider;
8161
8403
  const methodIdx = parseInt(btn.dataset.method, 10);
8404
+ const card = btn.closest('.stg-provider-card');
8405
+ const providerName = card?.querySelector('.stg-provider-name')?.textContent?.trim() || provId;
8162
8406
  btn.disabled = true; btn.textContent = kit.tx('settings.redesign.opencode.connecting');
8163
- const waitEl = ocpProvidersEl.querySelector(`.stg-ocp-oauth-wait[data-provider="${provId}"]`);
8164
- const codeEl = waitEl?.querySelector('.stg-ocp-oauth-code');
8165
8407
  try {
8166
8408
  const resp = await fetch(`/api/opencode/oauth/${encodeURIComponent(provId)}/authorize`, {
8167
8409
  method: 'POST',
8168
8410
  headers: { 'Content-Type': 'application/json' },
8169
- body: JSON.stringify({ method: isNaN(methodIdx) ? 0 : methodIdx }),
8411
+ body: JSON.stringify({ methodID: btn.dataset.methodId || undefined, method: isNaN(methodIdx) ? 0 : methodIdx }),
8170
8412
  });
8171
- const data = await resp.json();
8172
- const authData = data.data || data;
8173
- if (authData.url) {
8174
- window.open(authData.url, '_blank');
8175
- if (waitEl) waitEl.style.display = '';
8176
- if (authData.code && codeEl) { codeEl.textContent = authData.code; codeEl.style.display = ''; }
8177
- if (authData.instructions) showCCToast(authData.instructions);
8178
- // Poll for completion
8179
- let attempts = 0;
8180
- const poll = setInterval(async () => {
8181
- attempts++;
8182
- if (attempts > 60) { clearInterval(poll); if (waitEl) waitEl.style.display = 'none'; return; }
8183
- try {
8184
- const pr = await fetch('/api/opencode/providers/full');
8185
- const pd = await pr.json();
8186
- const connected = pd.data?.connected || [];
8187
- if (connected.includes(provId)) {
8188
- clearInterval(poll);
8189
- if (waitEl) waitEl.style.display = 'none';
8190
- showCCToast(kit.tx('settings.redesign.opencode.connected', { provId: provId }));
8191
- await refreshOcpProviders();
8192
- document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8193
- }
8194
- } catch {}
8195
- }, 2000);
8413
+ const data = await resp.json().catch(() => ({}));
8414
+ const auth = data.data || {};
8415
+ if (resp.ok && data.ok !== false && auth.url) {
8416
+ window.open(auth.url, '_blank', 'noopener');
8417
+ showOcpOAuthWait(provId, providerName, auth);
8196
8418
  } else {
8197
- showCCToast(authData.error || data.error || kit.tx('settings.redesign.opencode.oauthFailedNoAuthorizationUrlReturned'));
8419
+ showCCToast(data.error || kit.tx('settings.redesign.opencode.oauthFailedNoAuthorizationUrlReturned'));
8198
8420
  }
8199
8421
  } catch (e) {
8200
8422
  showCCToast(kit.tx('settings.redesign.opencode.oauthError', { message: e.message }));
@@ -8204,6 +8426,81 @@ export async function openSettingsModal(options = {}) {
8204
8426
  });
8205
8427
  }
8206
8428
 
8429
+ // ── A sign-in under way (OpenCode 2's attempt: `auto` finishes by itself, `code` with the code pasted) ──
8430
+ const _ocpOAuthPolls = new Map(); // provider id → timer
8431
+ function endOcpOAuth(provId) {
8432
+ clearInterval(_ocpOAuthPolls.get(provId));
8433
+ _ocpOAuthPolls.delete(provId);
8434
+ const waitEl = ocpProvidersEl?.querySelector(`.stg-ocp-oauth-wait[data-provider="${CSS.escape(provId)}"]`);
8435
+ if (waitEl) waitEl.style.display = 'none';
8436
+ }
8437
+ async function finishOcpOAuth(provId) {
8438
+ endOcpOAuth(provId);
8439
+ showCCToast(kit.tx('settings.redesign.opencode.connected', { provId: provId }));
8440
+ await refreshOcpProviders();
8441
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8442
+ }
8443
+ function showOcpOAuthWait(provId, providerName, auth) {
8444
+ endOcpOAuth(provId);
8445
+ const waitEl = ocpProvidersEl?.querySelector(`.stg-ocp-oauth-wait[data-provider="${CSS.escape(provId)}"]`);
8446
+ const codeEl = waitEl?.querySelector('.stg-ocp-oauth-code');
8447
+ const codeRow = waitEl?.querySelector('.stg-ocp-oauth-code-row');
8448
+ const errEl = waitEl?.querySelector('.stg-ocp-key-error');
8449
+ if (waitEl) waitEl.style.display = '';
8450
+ if (codeEl) { codeEl.textContent = auth.instructions || ''; codeEl.style.display = auth.instructions ? '' : 'none'; }
8451
+ if (errEl) errEl.textContent = '';
8452
+ if (auth.method === 'code') {
8453
+ if (!codeRow) return;
8454
+ codeRow.style.display = '';
8455
+ const input = codeRow.querySelector('.stg-ocp-oauth-code-input');
8456
+ const submit = codeRow.querySelector('.stg-ocp-oauth-code-submit');
8457
+ input?.focus();
8458
+ const send = async () => {
8459
+ const code = input?.value?.trim();
8460
+ if (!code) return;
8461
+ submit.disabled = true;
8462
+ try {
8463
+ const resp = await fetch(`/api/opencode/oauth/${encodeURIComponent(provId)}/callback`, {
8464
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
8465
+ body: JSON.stringify({ attemptID: auth.attemptID, code }),
8466
+ });
8467
+ const data = await resp.json().catch(() => ({}));
8468
+ if (!resp.ok || data.ok === false) throw new Error(data.error || `HTTP ${resp.status}`);
8469
+ input.value = '';
8470
+ await finishOcpOAuth(provId);
8471
+ } catch (e) {
8472
+ if (errEl) errEl.textContent = kit.tx('settings.redesign.opencode.oauthFailed', { message: e.message });
8473
+ } finally {
8474
+ submit.disabled = false;
8475
+ }
8476
+ };
8477
+ if (submit) submit.onclick = send;
8478
+ if (input) input.onkeydown = (event) => { if (event.key === 'Enter') { event.preventDefault(); send(); } };
8479
+ return;
8480
+ }
8481
+ if (codeRow) codeRow.style.display = 'none';
8482
+ if (!auth.attemptID) return;
8483
+ // Every 2 s for five minutes: then the attempt is let go (OpenCode's own attempt ends by itself).
8484
+ let attempts = 0;
8485
+ _ocpOAuthPolls.set(provId, setInterval(async () => {
8486
+ attempts++;
8487
+ if (attempts > 150) { endOcpOAuth(provId); showCCToast(kit.tx('settings.redesign.opencode.oauthGaveUp')); return; }
8488
+ try {
8489
+ const resp = await fetch(`/api/opencode/oauth/${encodeURIComponent(provId)}/status?attemptID=${encodeURIComponent(auth.attemptID)}`);
8490
+ const data = await resp.json().catch(() => ({}));
8491
+ if (!resp.ok || data.ok === false) throw new Error(data.error || `HTTP ${resp.status}`);
8492
+ const status = data.data?.status;
8493
+ if (status === 'complete') await finishOcpOAuth(provId);
8494
+ else if (status === 'expired') { endOcpOAuth(provId); showCCToast(kit.tx('settings.redesign.opencode.oauthExpired')); }
8495
+ else if (status === 'failed') { endOcpOAuth(provId); showCCToast(kit.tx('settings.redesign.opencode.oauthFailed', { message: data.data?.message || status })); }
8496
+ } catch (e) {
8497
+ endOcpOAuth(provId);
8498
+ showCCToast(kit.tx('settings.redesign.opencode.oauthFailed', { message: e.message }));
8499
+ }
8500
+ }, 2000));
8501
+ }
8502
+ panelLife.signal.addEventListener('abort', () => { for (const provId of [..._ocpOAuthPolls.keys()]) endOcpOAuth(provId); });
8503
+
8207
8504
  // ── Delegated card click-to-expand (survives re-renders) ──
8208
8505
  if (ocpProvidersEl) {
8209
8506
  ocpProvidersEl.addEventListener('click', (e) => {
@@ -8221,64 +8518,60 @@ export async function openSettingsModal(options = {}) {
8221
8518
  }
8222
8519
 
8223
8520
  // ── Tools/Permissions rendering ──
8224
- async function renderToolPermissions() {
8225
- if (!ocpToolsEl) return;
8226
- try {
8227
- const [toolsResp, configResp] = await Promise.all([
8228
- fetch('/api/opencode/tools'),
8229
- fetch('/api/opencode/config'),
8230
- ]);
8231
- const toolsData = await toolsResp.json();
8232
- const configData = await configResp.json();
8233
- _ocpConfig = configData.data || {};
8234
- const toolIds = Array.isArray(toolsData.data) ? toolsData.data : (toolsData.data?.ids || []);
8235
- const permissions = _ocpConfig.permission || {};
8236
- const tools = _ocpConfig.tools || {};
8237
-
8238
- if (!toolIds.length) {
8239
- ocpToolsEl.innerHTML = '<div class="settings-hint" style="opacity:0.4">' + kit.te('settings.redesign.opencode.noToolsAvailable') + '</div>';
8240
- return;
8521
+ // What each of OpenCode's own tools may do: the rules of OpenCode's global opencode.json
8522
+ // (GET /api/opencode/config: `actions`, `tools`), read and written there whether or not OpenCode runs.
8523
+ const OCP_EFFECTS = ['allow', 'ask', 'deny'];
8524
+ if (ocpToolsEl) {
8525
+ wireDropdowns(ocpToolsEl, { signal: panelLife.signal });
8526
+ ocpToolsEl.addEventListener('change', async (event) => {
8527
+ const input = event.target;
8528
+ const action = input?.dataset?.tool;
8529
+ if (!action || !OCP_EFFECTS.includes(input.value)) return;
8530
+ try {
8531
+ const resp = await fetch('/api/opencode/config', {
8532
+ method: 'PATCH', headers: { 'Content-Type': 'application/json' },
8533
+ body: JSON.stringify({ tools: { [action]: input.value } }),
8534
+ });
8535
+ const data = await resp.json().catch(() => ({}));
8536
+ if (!resp.ok || data.ok === false) throw new Error(data.error || `HTTP ${resp.status}`);
8537
+ applyOcpConfig(data.data);
8538
+ } catch (err) {
8539
+ showCCToast(kit.tx('settings.redesign.opencode.saveFailed', { message: err.message }));
8540
+ renderToolPermissions();
8241
8541
  }
8542
+ }, { signal: panelLife.signal });
8543
+ }
8242
8544
 
8243
- ocpToolsEl.innerHTML = `<div class="stg-tool-grid">${toolIds.map(t => {
8244
- const tid = typeof t === 'string' ? t : (t.id || t.name || '');
8245
- if (!tid) return '';
8246
- let currentVal = 'allow';
8247
- if (tools[tid] === false) currentVal = 'deny';
8248
- else if (permissions[tid] === 'ask') currentVal = 'ask';
8249
- return `<div class="stg-tool-row">
8250
- <span class="stg-tool-name">${esc(tid)}</span>
8251
- <select class="stg-tool-select stg-ocp-tool-perm" data-tool="${esc(tid)}" aria-label="${kit.te('settings.redesign.opencode.permissionFor', { tool: tid })}">
8252
- <option value="allow"${currentVal === 'allow' ? ' selected' : ''}>${kit.te('settings.redesign.opencode.allow')}</option>
8253
- <option value="ask"${currentVal === 'ask' ? ' selected' : ''}>${kit.te('settings.redesign.opencode.ask')}</option>
8254
- <option value="deny"${currentVal === 'deny' ? ' selected' : ''}>${kit.te('settings.redesign.opencode.deny')}</option>
8255
- </select>
8545
+ function renderToolPermissionsFrom(view) {
8546
+ if (!ocpToolsEl) return;
8547
+ const actions = Array.isArray(view?.actions) ? view.actions : [];
8548
+ if (!actions.length) {
8549
+ ocpToolsEl.innerHTML = '<div class="settings-hint" style="opacity:0.4">' + kit.te('settings.redesign.opencode.noToolsAvailable') + '</div>';
8550
+ return;
8551
+ }
8552
+ const tools = view.tools || {};
8553
+ const labels = { allow: kit.tx('settings.redesign.opencode.allow'), ask: kit.tx('settings.redesign.opencode.ask'), deny: kit.tx('settings.redesign.opencode.deny') };
8554
+ ocpToolsEl.innerHTML = `<div class="stg-tool-grid">${actions.map((action) => {
8555
+ const current = OCP_EFFECTS.includes(tools[action]) ? tools[action] : 'allow';
8556
+ const id = `stg-ocp-tool-${String(action).replace(/[^a-z0-9_-]/gi, '-')}`;
8557
+ return `<div class="stg-tool-row">
8558
+ <span class="stg-tool-name">${esc(action)}</span>
8559
+ ${dropdownHTML(id, OCP_EFFECTS.map((effect) => ({ value: effect, label: labels[effect], selected: effect === current })), {
8560
+ classes: 'stg-dropdown stg-dropdown-inline stg-ocp-tool-perm',
8561
+ label: kit.tx('settings.redesign.opencode.permissionFor', { tool: action }),
8562
+ inputAttrs: `data-tool="${esc(action)}"`,
8563
+ })}
8256
8564
  </div>`;
8257
- }).join('')}</div>`;
8258
-
8259
- // Wire change events
8260
- ocpToolsEl.querySelectorAll('.stg-ocp-tool-perm').forEach(sel => {
8261
- sel.addEventListener('change', async () => {
8262
- const tid = sel.dataset.tool;
8263
- const val = sel.value;
8264
- const patchBody = {};
8265
- const newPerms = { ...(_ocpConfig.permission || {}) };
8266
- const newTools = { ...(_ocpConfig.tools || {}) };
8267
- if (val === 'deny') { newTools[tid] = false; delete newPerms[tid]; }
8268
- else if (val === 'ask') { delete newTools[tid]; newPerms[tid] = 'ask'; }
8269
- else { delete newTools[tid]; delete newPerms[tid]; }
8270
- patchBody.permission = newPerms;
8271
- patchBody.tools = newTools;
8272
- try {
8273
- await fetch('/api/opencode/config', {
8274
- method: 'PATCH', headers: { 'Content-Type': 'application/json' },
8275
- body: JSON.stringify(patchBody),
8276
- });
8277
- _ocpConfig.permission = newPerms;
8278
- _ocpConfig.tools = newTools;
8279
- } catch {}
8280
- });
8281
- });
8565
+ }).join('')}</div>`;
8566
+ }
8567
+
8568
+ async function renderToolPermissions() {
8569
+ if (!ocpToolsEl) return;
8570
+ try {
8571
+ const resp = await fetch('/api/opencode/config');
8572
+ const data = await resp.json();
8573
+ if (!resp.ok || data.ok === false) throw new Error(data.error || `HTTP ${resp.status}`);
8574
+ applyOcpConfig(data.data);
8282
8575
  } catch {
8283
8576
  ocpToolsEl.innerHTML = '<div class="settings-hint" style="opacity:0.4">' + kit.te('settings.redesign.opencode.failedToLoadTools') + '</div>';
8284
8577
  }
@@ -8296,155 +8589,37 @@ export async function openSettingsModal(options = {}) {
8296
8589
  return null;
8297
8590
  }
8298
8591
 
8299
- function renderMcpMatrix() {
8300
- const wrap = overlay.querySelector('#stg-mcp-matrix-wrap');
8301
- if (!wrap || !_mcpRegistry) return;
8302
- const tg = _mcpRegistry.toolGroups || {};
8303
- const profiles = _mcpRegistry.profiles || {};
8304
- const active = _mcpRegistry.activeProfile || '';
8305
- const profEntries = Object.entries(profiles);
8306
- const switchable = Object.entries(tg).filter(([, info]) => !info.alwaysOn);
8307
-
8308
- if (!profEntries.length) {
8309
- wrap.innerHTML = '<div class="stg-mcp-matrix-empty">' + kit.te('settings.redesign.mcp.noProfiles') + '</div>';
8310
- return;
8311
- }
8312
-
8313
- // Build table header
8314
- let thead = '<tr><th class="stg-mx-label-th"><div class="stg-mx-corner-head">' + kit.te('settings.redesign.mcp.toolGroups') + '</div></th>';
8315
- for (const [name, prof] of profEntries) {
8316
- const isActive = name === active;
8317
- thead += `<th class="stg-mx-profile-th${isActive ? ' active' : ''}" data-profile="${esc(name)}">
8318
- <div class="stg-mx-profile-head" data-profile="${esc(name)}">
8319
- <span class="stg-mx-profile-dot${isActive ? ' on' : ''}"></span>
8320
- <span class="stg-mx-profile-name">${esc(prof.label || name)}</span>
8321
- ${isActive ? '' : `<button class="stg-mx-profile-del" data-profile="${esc(name)}" title="${kit.te('settings.redesign.mcp.deleteProfile')}">\u00d7</button>`}
8322
- </div>
8323
- </th>`;
8324
- }
8325
- thead += '<th class="stg-mx-add-th"><button class="stg-mx-add-btn" title="' + kit.te('settings.redesign.mcp.newProfile') + '">+</button></th></tr>';
8326
-
8327
- // Build table body — switchable groups
8328
- let tbody = '';
8329
- for (const [key, info] of switchable) {
8330
- tbody += `<tr>`;
8331
- tbody += `<td class="stg-mx-label-td"><div class="stg-mx-label-inner">
8332
- <span class="stg-mx-grp-name">${esc(info.label)}</span>
8333
- <span class="stg-mx-grp-count">${info.tools}</span>
8334
- </div></td>`;
8335
- for (const [profName, prof] of profEntries) {
8336
- const isOn = (prof.groups || []).includes(key);
8337
- tbody += `<td class="stg-mx-cell" data-profile="${esc(profName)}" data-group="${esc(key)}" data-kind="group">
8338
- <span class="stg-mx-toggle${isOn ? ' on' : ''}"></span>
8339
- </td>`;
8340
- }
8341
- tbody += '<td class="stg-mx-add-spacer"></td></tr>';
8342
- }
8343
-
8344
- wrap.innerHTML = `<div class="stg-mcp-matrix-scroll"><table id="stg-mcp-matrix"><thead>${thead}</thead><tbody>${tbody}</tbody></table></div>`;
8345
-
8346
- // --- Event handlers ---
8347
-
8348
- // Toggle cells
8349
- wrap.querySelectorAll('.stg-mx-cell').forEach(cell => {
8350
- cell.addEventListener('click', async () => {
8351
- const profName = cell.dataset.profile;
8352
- const prof = _mcpRegistry.profiles[profName];
8353
- if (!prof) return;
8354
- const kind = cell.dataset.kind;
8355
- if (kind === 'group') {
8356
- const g = cell.dataset.group;
8357
- if ((prof.groups || []).includes(g)) {
8358
- prof.groups = prof.groups.filter(x => x !== g);
8359
- } else {
8360
- prof.groups = [...(prof.groups || []), g];
8361
- }
8362
- } else {
8363
- const s = cell.dataset.server;
8364
- if ((prof.servers || []).includes(s)) {
8365
- prof.servers = prof.servers.filter(x => x !== s);
8366
- } else {
8367
- prof.servers = [...(prof.servers || []), s];
8368
- }
8369
- }
8370
- await fetch(`/api/mcp/registry/profiles/${encodeURIComponent(profName)}`, {
8371
- method: 'PUT', headers: { 'Content-Type': 'application/json' },
8372
- body: JSON.stringify(prof),
8373
- });
8374
- await refreshMcpTab();
8375
- });
8376
- });
8377
-
8378
- // Activate profile on name click
8379
- wrap.querySelectorAll('.stg-mx-profile-head').forEach(head => {
8380
- head.addEventListener('click', async (e) => {
8381
- if (e.target.closest('.stg-mx-profile-del')) return;
8382
- const name = head.dataset.profile;
8383
- if (name === _mcpRegistry.activeProfile) return;
8384
- await fetch('/api/mcp/registry/activate', {
8385
- method: 'POST', headers: { 'Content-Type': 'application/json' },
8386
- body: JSON.stringify({ profile: name }),
8387
- });
8388
- showCCToast(kit.tx('settings.redesign.mcp.profileSwitchedTo', { name: name }));
8389
- await refreshMcpTab();
8390
- });
8391
- });
8392
-
8393
- // Delete profile
8394
- wrap.querySelectorAll('.stg-mx-profile-del').forEach(btn => {
8395
- btn.addEventListener('click', async (e) => {
8396
- e.stopPropagation();
8397
- await fetch(`/api/mcp/registry/profiles/${encodeURIComponent(btn.dataset.profile)}`, { method: 'DELETE' });
8398
- await refreshMcpTab();
8399
- });
8592
+ let _settingsToolCatalog = null;
8593
+ const defaultToolsWrap = overlay.querySelector('#stg-mcp-default-tools');
8594
+ if (defaultToolsWrap) wireDropdowns(defaultToolsWrap, { signal: panelLife.signal });
8595
+ function renderDefaultTools() {
8596
+ const wrap = overlay.querySelector('#stg-mcp-default-tools');
8597
+ if (!wrap) return;
8598
+ wrap.innerHTML = _settingsToolCatalog ? toolPickerHTML('stg-default-tools', _settingsToolCatalog.defaultTools, { catalog: _settingsToolCatalog, provider: 'all', label: toolWord('defaultTools') }) : '';
8599
+ wrap.querySelector('#stg-default-tools')?.addEventListener('change', async (event) => {
8600
+ const tools = readToolPicker(event.target, { inherit: false });
8601
+ try {
8602
+ const response = await fetch('/api/mcp/tools/default', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tools }) });
8603
+ const result = await response.json();
8604
+ if (!response.ok || !result.ok) throw new Error(result.error || toolWord('saveFailed'));
8605
+ _settingsToolCatalog.defaultTools = result.tools;
8606
+ showCCToast(toolWord('saved'));
8607
+ } catch (error) { showCCToast(error.message || toolWord('saveFailed')); }
8608
+ renderDefaultTools();
8400
8609
  });
8401
-
8402
- // Inline add profile
8403
- const addBtn = wrap.querySelector('.stg-mx-add-btn');
8404
- if (addBtn) {
8405
- addBtn.addEventListener('click', () => {
8406
- const th = addBtn.closest('th');
8407
- th.innerHTML = `<input class="stg-mx-inline-input" type="text" placeholder="${kit.te('settings.redesign.mcp.name')}" autofocus>`;
8408
- const input = th.querySelector('input');
8409
- input.focus();
8410
- const create = async () => {
8411
- const name = input.value.trim();
8412
- if (!name) { await refreshMcpTab(); return; }
8413
- await fetch(`/api/mcp/registry/profiles/${encodeURIComponent(name)}`, {
8414
- method: 'PUT', headers: { 'Content-Type': 'application/json' },
8415
- body: JSON.stringify({ label: name, groups: [], servers: [] }),
8416
- });
8417
- await refreshMcpTab();
8418
- };
8419
- input.addEventListener('keydown', (e) => {
8420
- if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); create(); }
8421
- if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); refreshMcpTab(); }
8422
- });
8423
- input.addEventListener('blur', create);
8424
- });
8425
- }
8426
8610
  }
8427
8611
 
8428
8612
  function renderAlwaysOn() {
8429
8613
  const el = overlay.querySelector('#stg-mcp-always-on');
8430
- if (!el || !_mcpRegistry) return;
8431
- const tg = _mcpRegistry.toolGroups || {};
8432
- const alwaysOn = Object.entries(tg).filter(([, info]) => info.alwaysOn);
8433
- if (!alwaysOn.length) { el.innerHTML = ''; return; }
8434
- el.innerHTML = `
8435
- <div class="stg-mcp-section-header">${kit.te('settings.redesign.mcp.alwaysOn')}</div>
8436
- <div class="stg-mx-always-on-row">
8437
- ${alwaysOn.map(([, info]) =>
8438
- `<span class="stg-mx-always-chip">${esc(info.label)}<span class="stg-mx-grp-count">${info.tools}</span></span>`
8439
- ).join('')}
8440
- </div>`;
8614
+ if (!el) return;
8615
+ el.innerHTML = _settingsToolCatalog ? `<div class="stg-mcp-section-header">${esc(toolWord('alwaysOn'))}</div><div class="stg-mx-always-on-row">${_settingsToolCatalog.alwaysOnTools.map(name => `<span class="stg-mx-always-chip">${esc(name)}</span>`).join('')}</div>` : '';
8441
8616
  }
8442
8617
 
8443
8618
  function renderExtServerList() {
8444
8619
  const el = overlay.querySelector('#stg-mcp-ext-servers');
8445
8620
  if (!el || !_mcpRegistry) return;
8446
- const servers = _mcpRegistry.servers || {};
8447
- const profiles = Object.entries(_mcpRegistry.profiles || {});
8621
+ const servers = { ...(_mcpRegistry.servers || {}) };
8622
+ for (const row of _settingsToolCatalog?.servers || []) { const name = row.ref.slice(4); if (!servers[name]) servers[name] = { type: toolWord('configuredExternally'), discovered: true }; }
8448
8623
  const entries = Object.entries(servers).filter(([, info]) => !info.builtin);
8449
8624
  if (!entries.length) {
8450
8625
  el.innerHTML = '<div class="stg-mcp-empty-note">' + kit.te('settings.redesign.mcp.noExternalServersConfigured') + '</div>';
@@ -8459,38 +8634,31 @@ export async function openSettingsModal(options = {}) {
8459
8634
  <span class="stg-mx-ext-name">${esc(name)}</span>
8460
8635
  <span class="stg-mx-ext-type">${esc(info.type || '?')}</span>
8461
8636
  </div>
8462
- <div class="stg-mx-ext-sub">${kit.te('settings.redesign.mcp.availableToAnyProfileYouEnable')}</div>
8637
+ <div class="stg-mx-ext-sub">${esc(toolWord('agentAccessHelp'))}</div>
8463
8638
  </div>
8464
- <button class="stg-mx-srv-del stg-mx-ext-remove" data-server="${esc(name)}" title="${kit.te('settings.redesign.mcp.removeServer')}">\u00d7</button>
8639
+ ${info.discovered ? '' : `<button class="stg-mx-srv-del stg-mx-ext-remove" ${kit.inv('MCP004')} data-server="${esc(name)}" title="${kit.te('settings.redesign.mcp.removeServer')}">\u00d7</button>`}
8465
8640
  </div>
8466
- <div class="stg-mx-ext-profiles">
8467
- ${profiles.map(([profName, prof]) => {
8468
- const isOn = (prof.servers || []).includes(name);
8469
- return `<button class="stg-mx-ext-toggle${isOn ? ' on' : ''}" data-profile="${esc(profName)}" data-server="${esc(name)}" title="${isOn ? kit.te('settings.redesign.mcp.disable') : kit.te('settings.redesign.mcp.enable')} ${esc(name)} ${kit.te('settings.redesign.mcp.for')} ${esc(prof.label || profName)}">
8470
- <span class="stg-mx-ext-toggle-name">${esc(prof.label || profName)}</span>
8471
- </button>`;
8472
- }).join('')}
8641
+ <div class="stg-field"><span>${esc(toolWord('agentAccess'))}</span>
8642
+ <button type="button" class="cc-toggle${_mcpRegistry.agentAccess?.[name] === false ? '' : ' on'}" role="switch" ${kit.inv('MCP005')} data-agent-server="${esc(name)}" aria-label="${esc(toolWord('agentAccess'))}: ${esc(name)}" aria-checked="${_mcpRegistry.agentAccess?.[name] !== false}"></button>
8473
8643
  </div>
8474
8644
  </div>`
8475
8645
  ).join('');
8476
8646
 
8477
- el.querySelectorAll('.stg-mx-ext-toggle').forEach(btn => {
8478
- btn.addEventListener('click', async () => {
8479
- const profName = btn.dataset.profile;
8480
- const srvName = btn.dataset.server;
8481
- const prof = _mcpRegistry.profiles[profName];
8482
- if (!prof || !srvName) return;
8483
- if ((prof.servers || []).includes(srvName)) {
8484
- prof.servers = prof.servers.filter(x => x !== srvName);
8485
- } else {
8486
- prof.servers = [...(prof.servers || []), srvName];
8487
- }
8488
- await fetch(`/api/mcp/registry/profiles/${encodeURIComponent(profName)}`, {
8489
- method: 'PUT',
8490
- headers: { 'Content-Type': 'application/json' },
8491
- body: JSON.stringify(prof),
8492
- });
8493
- await refreshMcpTab();
8647
+ el.querySelectorAll('[data-agent-server]').forEach(input => {
8648
+ input.addEventListener('click', async () => {
8649
+ const enabled = input.getAttribute('aria-checked') !== 'true';
8650
+ input.disabled = true;
8651
+ try {
8652
+ const response = await fetch(`/api/mcp/registry/agent-access/${encodeURIComponent(input.dataset.agentServer)}`, {
8653
+ method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled }),
8654
+ });
8655
+ const result = await response.json();
8656
+ if (!response.ok || !result.ok) throw new Error(result.error || toolWord('saveFailed'));
8657
+ _mcpRegistry.agentAccess = result.agentAccess;
8658
+ input.classList.toggle('on', enabled);
8659
+ input.setAttribute('aria-checked', String(enabled));
8660
+ } catch (error) { showCCToast(error.message || toolWord('saveFailed')); }
8661
+ finally { input.disabled = false; }
8494
8662
  });
8495
8663
  });
8496
8664
 
@@ -8645,7 +8813,8 @@ export async function openSettingsModal(options = {}) {
8645
8813
 
8646
8814
  async function refreshMcpTab() {
8647
8815
  await fetchMcpRegistry();
8648
- renderMcpMatrix();
8816
+ _settingsToolCatalog = await fetchToolCatalog(true);
8817
+ renderDefaultTools();
8649
8818
  renderAlwaysOn();
8650
8819
  renderExtServerList();
8651
8820
  renderPluginsList();
@@ -9157,55 +9326,62 @@ export async function openSettingsModal(options = {}) {
9157
9326
  });
9158
9327
 
9159
9328
  // ── Config save helpers ──
9329
+ // PATCH /api/opencode/config (OpenCode 2's global opencode.json): the answer is the configuration as it is now.
9160
9330
  async function saveOcpConfig(patch) {
9161
9331
  try {
9162
- await fetch('/api/opencode/config', {
9332
+ const resp = await fetch('/api/opencode/config', {
9163
9333
  method: 'PATCH', headers: { 'Content-Type': 'application/json' },
9164
9334
  body: JSON.stringify(patch),
9165
9335
  });
9166
- } catch {}
9336
+ const data = await resp.json().catch(() => ({}));
9337
+ if (!resp.ok || data.ok === false) throw new Error(data.error || `HTTP ${resp.status}`);
9338
+ applyOcpConfig(data.data);
9339
+ return true;
9340
+ } catch (err) {
9341
+ showCCToast(kit.tx('settings.redesign.opencode.saveFailed', { message: err.message }));
9342
+ loadOcpConfig();
9343
+ return false;
9344
+ }
9167
9345
  }
9168
9346
 
9169
9347
  // ── Compaction handlers ──
9170
9348
  const compactAutoEl = overlay.querySelector('#stg-ocp-compact-auto');
9171
- const compactPruneEl = overlay.querySelector('#stg-ocp-compact-prune');
9172
9349
  const compactReservedEl = overlay.querySelector('#stg-ocp-compact-reserved');
9173
9350
 
9351
+ // An emptied field is OpenCode's own default again (buffer: null).
9174
9352
  function saveCompaction() {
9175
- saveOcpConfig({
9176
- compaction: {
9177
- auto: compactAutoEl?.checked ?? true,
9178
- prune: compactPruneEl?.checked ?? true,
9179
- reserved: Number(compactReservedEl?.value) || 10000,
9180
- },
9181
- });
9353
+ const raw = String(compactReservedEl?.value ?? '').trim();
9354
+ const buffer = raw === '' ? null : Math.round(Number(raw));
9355
+ if (buffer !== null && !(Number.isFinite(buffer) && buffer >= 0 && buffer <= 1_000_000)) { loadOcpConfig(); return; }
9356
+ saveOcpConfig({ compaction: { auto: compactAutoEl?.checked ?? true, buffer } });
9182
9357
  }
9183
9358
  compactAutoEl?.addEventListener('change', saveCompaction);
9184
- compactPruneEl?.addEventListener('change', saveCompaction);
9185
9359
  compactReservedEl?.addEventListener('change', saveCompaction);
9186
9360
 
9187
- // ── Snapshot toggle (persists to config.json; read by OpenCode at startup) ──
9361
+ // ── Snapshot toggle (OpenCode's `snapshots`, read at once) ──
9188
9362
  const snapshotToggle = overlay.querySelector('#stg-ocp-snapshot-toggle');
9189
- if (snapshotToggle) {
9190
- // Reflect the persisted state on load (defaults on).
9191
- fetch('/api/opencode/snapshot')
9192
- .then(r => r.json())
9193
- .then(d => { if (d && d.ok) snapshotToggle.classList.toggle('on', d.snapshot !== false); })
9194
- .catch(() => {});
9195
- snapshotToggle.addEventListener('click', async () => {
9196
- const next = !snapshotToggle.classList.contains('on');
9197
- snapshotToggle.classList.toggle('on', next); // optimistic
9198
- try {
9199
- const r = await fetch('/api/opencode/snapshot', {
9200
- method: 'POST', headers: { 'Content-Type': 'application/json' },
9201
- body: JSON.stringify({ enabled: next }),
9202
- });
9203
- const d = await r.json();
9204
- if (!d || !d.ok) throw new Error(d?.error || 'failed');
9205
- } catch {
9206
- snapshotToggle.classList.toggle('on', !next); // revert on failure
9207
- }
9208
- });
9363
+ snapshotToggle?.addEventListener('click', () => {
9364
+ const next = !snapshotToggle.classList.contains('on');
9365
+ snapshotToggle.classList.toggle('on', next); // optimistic; the answer (or a failed save's reload) sets it
9366
+ saveOcpConfig({ snapshots: next });
9367
+ });
9368
+
9369
+ // The configuration as GET / PATCH /api/opencode/config give it: the file SynaBun edits, compaction,
9370
+ // snapshots and the tools' permissions.
9371
+ function applyOcpConfig(view) {
9372
+ if (!view || typeof view !== 'object') return;
9373
+ _ocpConfig = view;
9374
+ const comp = view.compaction || {};
9375
+ if (compactAutoEl) compactAutoEl.checked = comp.auto !== false;
9376
+ if (compactReservedEl && document.activeElement !== compactReservedEl) compactReservedEl.value = Number.isFinite(comp.buffer) ? String(comp.buffer) : '';
9377
+ snapshotToggle?.classList.toggle('on', view.snapshots !== false);
9378
+ if (ocpConfigPathEl && view.file) ocpConfigPathEl.textContent = view.file;
9379
+ const jsonc = (Array.isArray(view.files) ? view.files : []).find((file) => file?.exists && /\.jsonc$/i.test(String(file.path || '')));
9380
+ if (ocpConfigJsoncEl) {
9381
+ ocpConfigJsoncEl.hidden = !jsonc;
9382
+ ocpConfigJsoncEl.innerHTML = jsonc ? kit.th('settings.redesign.opencode.jsoncWins', { path: `<code class="stg-code">${esc(jsonc.path)}</code>` }) : '';
9383
+ }
9384
+ renderToolPermissionsFrom(view);
9209
9385
  }
9210
9386
 
9211
9387
  // Capability filter pills — use CSS vars, not inline color
@@ -9243,28 +9419,16 @@ export async function openSettingsModal(options = {}) {
9243
9419
  });
9244
9420
 
9245
9421
  // ── Open config file in Synabun's editor ──
9422
+ // OpenCode 2's global opencode.json (made with its $schema when there is none yet).
9246
9423
  overlay.querySelector('#stg-ocp-open-config')?.addEventListener('click', async () => {
9247
9424
  try {
9248
- let filePath = '';
9249
-
9250
- const revealResp = await fetch('/api/opencode/config/reveal', { method: 'POST' });
9251
- let revealData = null;
9252
- try { revealData = await revealResp.json(); } catch {}
9253
-
9254
- if (revealResp.ok && revealData?.path) {
9255
- const basePath = String(revealData.path).replace(/\\/g, '/').replace(/\/+$/, '');
9256
- filePath = basePath.endsWith('/config.json') ? basePath : `${basePath}/config.json`;
9257
- } else {
9258
- const fileResp = await fetch('/api/opencode/config/file');
9259
- const fileData = await fileResp.json();
9260
- if (!fileResp.ok || !fileData?.path) {
9261
- showCCToast(fileData?.error || revealData?.error || kit.tx('settings.redesign.opencode.failedToOpenOpencodeConfig'));
9262
- return;
9263
- }
9264
- filePath = fileData.path;
9425
+ const fileResp = await fetch('/api/opencode/config/file');
9426
+ const fileData = await fileResp.json().catch(() => ({}));
9427
+ if (!fileResp.ok || !fileData?.path) {
9428
+ showCCToast(fileData?.error || kit.tx('settings.redesign.opencode.failedToOpenOpencodeConfig'));
9429
+ return;
9265
9430
  }
9266
-
9267
- emit('open-file-editor', { filePath });
9431
+ emit('open-file-editor', { filePath: fileData.path });
9268
9432
  } catch (err) {
9269
9433
  console.error('[settings] Failed to open OpenCode config:', err);
9270
9434
  showCCToast(kit.tx('settings.redesign.opencode.failedToOpenOpencodeConfig'));
@@ -9292,38 +9456,46 @@ export async function openSettingsModal(options = {}) {
9292
9456
  } catch {}
9293
9457
  }
9294
9458
 
9295
- // ── Load config values into UI ──
9459
+ // ── Load config values into UI (read from the files: OpenCode need not run) ──
9296
9460
  async function loadOcpConfig() {
9297
- try {
9298
- const resp = await fetch('/api/opencode/config');
9299
- const data = await resp.json();
9300
- _ocpConfig = data.data || {};
9301
- const comp = _ocpConfig.compaction || {};
9302
- if (compactAutoEl) compactAutoEl.checked = comp.auto !== false;
9303
- if (compactPruneEl) compactPruneEl.checked = comp.prune !== false;
9304
- if (compactReservedEl) compactReservedEl.value = comp.reserved || 10000;
9305
- } catch {}
9461
+ await renderToolPermissions();
9462
+ }
9463
+
9464
+ // The OpenCode SynaBun found: its version, or why SynaBun does not drive it and how to get OpenCode 2.
9465
+ function renderOcpInstall(installed) {
9466
+ const note = describeOpencodeInstall(installed, { th: kit.th, tx: kit.tx });
9467
+ if (ocpInstallEl) {
9468
+ ocpInstallEl.hidden = !note.html;
9469
+ ocpInstallEl.dataset.tone = note.tone;
9470
+ ocpInstallEl.innerHTML = note.html;
9471
+ }
9472
+ return note;
9306
9473
  }
9307
9474
 
9308
9475
  // ── Master refresh ──
9309
9476
  async function refreshOcpStatus() {
9310
9477
  setOcpStatus(kit.tx('settings.redesign.opencode.checking'), 'dim');
9478
+ loadOcpConfig();
9311
9479
  try {
9312
9480
  const resp = await fetch('/api/opencode/status');
9313
9481
  const data = await resp.json();
9482
+ const note = renderOcpInstall(data.installed);
9314
9483
  if (data.clearingHistory) {
9315
9484
  setOcpStatus(kit.tx('settings.redesign.opencode.clearingHistory'), 'warn');
9316
9485
  } else if (data.running) {
9317
9486
  setOcpStatus(kit.tx('settings.redesign.opencode.runningVersion', { version: data.version ? ' v' + data.version : '' }), 'ok');
9318
- await Promise.all([
9319
- refreshOcpProviders(),
9320
- loadOcpConfig(),
9321
- renderToolPermissions(),
9322
- ]);
9487
+ await refreshOcpProviders();
9323
9488
  } else {
9324
- setOcpStatus(kit.tx('settings.redesign.opencode.offline'), 'dim');
9489
+ if (note.status === 'notSupported') setOcpStatus(kit.tx('settings.redesign.opencode.notSupportedStatus'), 'err');
9490
+ else if (note.status === 'notInstalled') setOcpStatus(kit.tx('settings.redesign.opencode.notInstalledStatus'), 'dim');
9491
+ else setOcpStatus(kit.tx('settings.redesign.opencode.offline'), 'dim');
9325
9492
  if (ocpProvidersEl) ocpProvidersEl.innerHTML = '<div class="settings-hint" style="opacity:0.4">' + kit.te('settings.redesign.opencode.startTheServerToSeeProviders') + '</div>';
9326
- if (ocpToolsEl) ocpToolsEl.innerHTML = '<div class="settings-hint" style="opacity:0.4">' + kit.te('settings.redesign.opencode.startTheServerToLoadTools') + '</div>';
9493
+ // Something else stands in the way (another serve on the port, a serve that did not answer): the server's sentence.
9494
+ if (!note.html && data.problem && ocpInstallEl) {
9495
+ ocpInstallEl.hidden = false;
9496
+ ocpInstallEl.dataset.tone = 'warn';
9497
+ ocpInstallEl.textContent = data.problem;
9498
+ }
9327
9499
  }
9328
9500
  } catch (err) {
9329
9501
  setOcpStatus(kit.tx('settings.redesign.opencode.error'), 'err');
@@ -9382,7 +9554,7 @@ export async function openSettingsModal(options = {}) {
9382
9554
  // ── Search: the index is built from the strings of the active locale; what the panes load is read at query time ──
9383
9555
  // In another locale the English names are searched too (someone who learned the settings in English still finds
9384
9556
  // them): from the bundle i18n already holds as its fallback, or, when it has none, loaded once as the field is first used.
9385
- const searchIndex = (en) => buildSettingsSearchIndex({ t: kit.tx, en, variants: variantTabs.map(tab => ({ id: tab.id, label: tab.label })) });
9557
+ const searchIndex = (en) => memoSearchIndex(en, variantTabs, () => buildSettingsSearchIndex({ t: kit.tx, en, variants: variantTabs.map(tab => ({ id: tab.id, label: tab.label })) }));
9386
9558
  const english = getLocale() === 'en' ? null : englishTranslator();
9387
9559
  wireSettingsSearch(overlay, {
9388
9560
  index: searchIndex(english),
@@ -9580,7 +9752,18 @@ export function initSettings() {
9580
9752
  const reopenPage = takeSettingsReopen();
9581
9753
  if (reopenPage) setTimeout(() => openSettingsModal({ tab: reopenPage, expand: ['stg-sec-language'] }), 0);
9582
9754
 
9755
+ // Launch: what Settings is built from, and its search index (the English names too), read and made once the
9756
+ // page has painted, so the first open and the first search wait for nothing.
9757
+ registerLaunchPrefetch('settings-data', () => prefetchSettingsData(), { order: 60, owner: true });
9758
+ registerLaunchPrefetch('settings-search-index', () => prebuildSettingsSearchIndex(), { order: 62 });
9759
+ // Changes other windows announce: the next open shows them.
9760
+ on('sync:skin:changed', () => refreshSettingsData(['skins']));
9761
+ on('sync:greeting:changed', () => refreshSettingsData(['greeting']));
9762
+ on('sync:assistant:user-context-changed', () => refreshSettingsData(['userContext']));
9763
+
9583
9764
  // Expose sync functions for inline onclick attributes (if any remain)
9584
9765
  window.checkSyncStatus = checkSyncStatus;
9585
9766
  window.copySyncPrompt = copySyncPrompt;
9586
9767
  }
9768
+
9769
+ configureToolPickerTranslation((key, params) => kit.tx(key, params));