synabun 2.0.10 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (517) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +25 -2
  3. package/THIRD-PARTY-LICENSES.md +22 -0
  4. package/bin/synabun.js +71 -24
  5. package/docs/whatsapp.md +2 -2
  6. package/hooks/claude-code/pre-websearch.mjs +12 -1
  7. package/hooks/claude-code/shared.mjs +5 -0
  8. package/hooks/claude-code/stop.mjs +1 -1
  9. package/lib/claude-hooks.js +172 -18
  10. package/lib/client-config-repair.js +17 -8
  11. package/lib/desktop-app.js +7 -1
  12. package/lib/external-tools.js +1 -1
  13. package/lib/first-launch-protection.js +57 -12
  14. package/lib/mcp-dist.js +176 -0
  15. package/lib/release-npm-ci.mjs +138 -0
  16. package/lib/release-publish.mjs +91 -0
  17. package/lib/release-scan.mjs +50 -0
  18. package/lib/server-crash-record.js +123 -0
  19. package/lib/server-exit.js +318 -0
  20. package/lib/server-supervisor.js +508 -0
  21. package/lib/start-launcher.js +37 -4
  22. package/lib/tools-cli.js +45 -0
  23. package/lib/update-snapshot.js +228 -0
  24. package/lib/virustotal.mjs +192 -0
  25. package/lib/windows-crash-event.js +269 -0
  26. package/mcp-server/dist/http.d.ts +20 -1
  27. package/mcp-server/dist/http.d.ts.map +1 -1
  28. package/mcp-server/dist/http.js +59 -12
  29. package/mcp-server/dist/http.js.map +1 -1
  30. package/mcp-server/dist/index.d.ts +33 -27
  31. package/mcp-server/dist/index.d.ts.map +1 -1
  32. package/mcp-server/dist/index.js +128 -143
  33. package/mcp-server/dist/index.js.map +1 -1
  34. package/mcp-server/dist/preload.js +1 -1
  35. package/mcp-server/dist/preload.js.map +1 -1
  36. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  37. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  38. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  39. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  40. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  41. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  42. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  43. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  44. package/mcp-server/dist/services/neural-interface.d.ts +16 -10
  45. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  46. package/mcp-server/dist/services/neural-interface.js +67 -11
  47. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  48. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  49. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  50. package/mcp-server/dist/services/server-instructions.js +20 -62
  51. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  52. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  53. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  54. package/mcp-server/dist/services/tool-catalog.js +339 -0
  55. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  56. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  57. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  58. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  59. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  60. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  61. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  62. package/mcp-server/dist/services/tool-list.js +334 -0
  63. package/mcp-server/dist/services/tool-list.js.map +1 -0
  64. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  65. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  66. package/mcp-server/dist/services/tool-runtime.js +400 -0
  67. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  68. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  69. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  70. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  71. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  72. package/mcp-server/dist/tools/agents.d.ts +10 -10
  73. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  74. package/mcp-server/dist/tools/agents.js +22 -13
  75. package/mcp-server/dist/tools/agents.js.map +1 -1
  76. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  77. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  78. package/mcp-server/dist/tools/image-tools.js +1 -1
  79. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  80. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  81. package/mcp-server/dist/tools/skill-tools.js +639 -0
  82. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  83. package/mcp-server/dist/tools/skill.d.ts +7 -0
  84. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  85. package/mcp-server/dist/tools/skill.js +11 -0
  86. package/mcp-server/dist/tools/skill.js.map +1 -0
  87. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  88. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  89. package/mcp-server/dist/tools/tool-groups.js +147 -0
  90. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  91. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  92. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  93. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  94. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  95. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  96. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  97. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  98. package/mcp-server/package-lock.json +2 -2
  99. package/mcp-server/package.json +1 -1
  100. package/mcp-server/src/http.ts +75 -12
  101. package/mcp-server/src/index.ts +147 -154
  102. package/mcp-server/src/preload.ts +12 -12
  103. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  104. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  105. package/mcp-server/src/services/neural-interface.ts +79 -20
  106. package/mcp-server/src/services/server-instructions.ts +27 -63
  107. package/mcp-server/src/services/tool-catalog.ts +403 -0
  108. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  109. package/mcp-server/src/services/tool-list.ts +410 -0
  110. package/mcp-server/src/services/tool-runtime.ts +457 -0
  111. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  112. package/mcp-server/src/tools/agents.ts +22 -14
  113. package/mcp-server/src/tools/image-tools.ts +1 -1
  114. package/mcp-server/src/tools/skill-tools.ts +589 -0
  115. package/mcp-server/src/tools/skill.ts +13 -0
  116. package/mcp-server/src/tools/tool-groups.ts +164 -0
  117. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  118. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  119. package/neural-interface/i18n/en.json +3463 -200
  120. package/neural-interface/i18n/pt-BR.json +3411 -151
  121. package/neural-interface/lib/assistant-api.js +127 -28
  122. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  123. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  124. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  125. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  126. package/neural-interface/lib/assistant-brains/opencode.js +120 -54
  127. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
  128. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
  129. package/neural-interface/lib/assistant-catalog.js +6 -7
  130. package/neural-interface/lib/assistant-dispatch.js +871 -156
  131. package/neural-interface/lib/assistant-envelope.js +3 -1
  132. package/neural-interface/lib/assistant-persona.js +80 -11
  133. package/neural-interface/lib/assistant-plan-permissions.js +10 -9
  134. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  135. package/neural-interface/lib/assistant-route-gate.js +6 -4
  136. package/neural-interface/lib/assistant-router.js +13 -8
  137. package/neural-interface/lib/assistant-runtime.js +56 -18
  138. package/neural-interface/lib/assistant-task-prompt.js +78 -14
  139. package/neural-interface/lib/assistant-tools.js +51 -0
  140. package/neural-interface/lib/backup-service.js +620 -3
  141. package/neural-interface/lib/backup-zip-reader.js +10 -5
  142. package/neural-interface/lib/backup-zip-writer.js +257 -0
  143. package/neural-interface/lib/browser-execution.js +10 -2
  144. package/neural-interface/lib/browser-guard-cdp.js +83 -0
  145. package/neural-interface/lib/browser-network-guard.js +284 -0
  146. package/neural-interface/lib/browser-policy-roots.js +64 -0
  147. package/neural-interface/lib/browser-tool-policy.js +186 -31
  148. package/neural-interface/lib/claude-agent-bridge.js +4 -2
  149. package/neural-interface/lib/claude-hooks-status.js +103 -0
  150. package/neural-interface/lib/claude-temporary.js +5 -11
  151. package/neural-interface/lib/cli-install.js +18 -138
  152. package/neural-interface/lib/cli-resolver.js +4 -1
  153. package/neural-interface/lib/cli-update-planner.js +32 -31
  154. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  155. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  156. package/neural-interface/lib/codex-worker-app-server.js +172 -0
  157. package/neural-interface/lib/confined-fs.js +30 -3
  158. package/neural-interface/lib/crash-notice-api.js +98 -0
  159. package/neural-interface/lib/editor-completion-opencode.js +68 -68
  160. package/neural-interface/lib/exec-loop-command.js +4 -8
  161. package/neural-interface/lib/http-guards.js +8 -2
  162. package/neural-interface/lib/install-details.js +4 -1
  163. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  164. package/neural-interface/lib/mcp-client-config.js +0 -29
  165. package/neural-interface/lib/mcp-external-servers.js +323 -0
  166. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  167. package/neural-interface/lib/morelogin.js +30 -0
  168. package/neural-interface/lib/native-loop-providers.js +547 -47
  169. package/neural-interface/lib/native-loop-runtime.js +268 -41
  170. package/neural-interface/lib/opencode-config.js +557 -0
  171. package/neural-interface/lib/opencode-db-schema.js +17 -34
  172. package/neural-interface/lib/opencode-history.js +48 -218
  173. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  174. package/neural-interface/lib/opencode-native-boundary.js +44 -0
  175. package/neural-interface/lib/opencode-protocol.js +249 -112
  176. package/neural-interface/lib/opencode-runtime-config.js +32 -22
  177. package/neural-interface/lib/opencode-serve-records.js +39 -21
  178. package/neural-interface/lib/opencode-v2-client.js +48 -153
  179. package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
  180. package/neural-interface/lib/opencode-worker-data.js +27 -0
  181. package/neural-interface/lib/opencode2-client.js +73 -37
  182. package/neural-interface/lib/opencode2-proxy.js +23 -63
  183. package/neural-interface/lib/opencode2-translate.js +51 -2
  184. package/neural-interface/lib/owner-authority.js +249 -0
  185. package/neural-interface/lib/page-session.js +275 -0
  186. package/neural-interface/lib/peer-identity.js +763 -0
  187. package/neural-interface/lib/request-gate.js +17 -2
  188. package/neural-interface/lib/rulesets/api.js +1 -1
  189. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  190. package/neural-interface/lib/rulesets/installer.js +92 -27
  191. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  192. package/neural-interface/lib/session-cache-builder.js +17 -30
  193. package/neural-interface/lib/session-title-generator.js +58 -36
  194. package/neural-interface/lib/skills/api-v2.js +495 -0
  195. package/neural-interface/lib/skills/bundle.js +418 -0
  196. package/neural-interface/lib/skills/catalog.js +294 -0
  197. package/neural-interface/lib/skills/creation-api.js +191 -0
  198. package/neural-interface/lib/skills/creation-brief.js +247 -0
  199. package/neural-interface/lib/skills/creation-sources.js +267 -0
  200. package/neural-interface/lib/skills/creation.js +843 -0
  201. package/neural-interface/lib/skills/deploy.js +564 -0
  202. package/neural-interface/lib/skills/digest.js +119 -0
  203. package/neural-interface/lib/skills/discover.js +111 -0
  204. package/neural-interface/lib/skills/draft-confinement.js +214 -0
  205. package/neural-interface/lib/skills/eval-confinement.js +397 -0
  206. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  207. package/neural-interface/lib/skills/eval-trace.js +119 -0
  208. package/neural-interface/lib/skills/evals-api.js +212 -0
  209. package/neural-interface/lib/skills/evals.js +1573 -0
  210. package/neural-interface/lib/skills/export.js +200 -0
  211. package/neural-interface/lib/skills/frontmatter.js +64 -0
  212. package/neural-interface/lib/skills/health.js +75 -0
  213. package/neural-interface/lib/skills/json-edit.js +165 -0
  214. package/neural-interface/lib/skills/known-bridge-digests.json +387 -0
  215. package/neural-interface/lib/skills/known-bridges.js +96 -0
  216. package/neural-interface/lib/skills/library.js +823 -0
  217. package/neural-interface/lib/skills/local-store.js +21 -0
  218. package/neural-interface/lib/skills/migration.js +433 -0
  219. package/neural-interface/lib/skills/operations.js +45 -0
  220. package/neural-interface/lib/skills/ownership.js +427 -0
  221. package/neural-interface/lib/skills/packages.js +88 -0
  222. package/neural-interface/lib/skills/reviews.js +212 -0
  223. package/neural-interface/lib/skills/revision.js +80 -0
  224. package/neural-interface/lib/skills/roots.js +262 -0
  225. package/neural-interface/lib/skills/runtime-checks.js +355 -0
  226. package/neural-interface/lib/skills/scan-assets.js +252 -0
  227. package/neural-interface/lib/skills/scan-context.js +26 -0
  228. package/neural-interface/lib/skills/scan-shell.js +324 -0
  229. package/neural-interface/lib/skills/scan.js +364 -0
  230. package/neural-interface/lib/skills/scanners.js +54 -0
  231. package/neural-interface/lib/skills/service.js +522 -0
  232. package/neural-interface/lib/skills/telemetry.js +113 -0
  233. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  234. package/neural-interface/lib/tool-lists.js +124 -0
  235. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  236. package/neural-interface/lib/worker-confinement.js +1192 -0
  237. package/neural-interface/lib/worker-guard-setting.js +56 -0
  238. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  239. package/neural-interface/lib/worker-relay-ports.js +227 -0
  240. package/neural-interface/lib/worker-resume.js +229 -0
  241. package/neural-interface/lib/worker-sandbox.js +299 -0
  242. package/neural-interface/package-lock.json +18 -12
  243. package/neural-interface/package.json +2 -2
  244. package/neural-interface/public/index.html +3 -1
  245. package/neural-interface/public/index2d.html +19 -11
  246. package/neural-interface/public/onboarding.html +2 -2
  247. package/neural-interface/public/shared/account-menu.js +8 -5
  248. package/neural-interface/public/shared/agent-runtime-options.js +35 -19
  249. package/neural-interface/public/shared/api.js +1527 -1336
  250. package/neural-interface/public/shared/app-frame.js +192 -0
  251. package/neural-interface/public/shared/assistant/asst-agents-dock.js +33 -2
  252. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  253. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  254. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  255. package/neural-interface/public/shared/assistant/asst-brain-picker.js +49 -35
  256. package/neural-interface/public/shared/assistant/asst-control.js +157 -26
  257. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  258. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  259. package/neural-interface/public/shared/assistant/asst-limit-dock.js +40 -0
  260. package/neural-interface/public/shared/assistant/asst-limit-wait.js +294 -0
  261. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  262. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  263. package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
  264. package/neural-interface/public/shared/assistant/asst-panel.js +106 -11
  265. package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
  266. package/neural-interface/public/shared/assistant/asst-render.js +401 -77
  267. package/neural-interface/public/shared/assistant/asst-route.js +83 -12
  268. package/neural-interface/public/shared/assistant/asst-scenes.js +2 -0
  269. package/neural-interface/public/shared/assistant/asst-sidepanel.js +67 -4
  270. package/neural-interface/public/shared/assistant/asst-slash.js +9 -7
  271. package/neural-interface/public/shared/assistant/asst-state.js +11 -15
  272. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  273. package/neural-interface/public/shared/assistant/asst-styles.js +59 -21
  274. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +156 -16
  275. package/neural-interface/public/shared/automation-context.js +5 -2
  276. package/neural-interface/public/shared/cc-dropdown.js +89 -19
  277. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  278. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  279. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  280. package/neural-interface/public/shared/cdx/cdx-output.js +8 -7
  281. package/neural-interface/public/shared/cdx/cdx-panel.js +45 -149
  282. package/neural-interface/public/shared/cdx/cdx-protocol.js +28 -1
  283. package/neural-interface/public/shared/cdx/cdx-render.js +47 -29
  284. package/neural-interface/public/shared/cdx/cdx-requests.js +3 -3
  285. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  286. package/neural-interface/public/shared/cdx/cdx-styles.js +11 -3
  287. package/neural-interface/public/shared/cdx/cdx-tabs.js +52 -39
  288. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  289. package/neural-interface/public/shared/cli-install-view.js +4 -8
  290. package/neural-interface/public/shared/cli-status.js +2 -2
  291. package/neural-interface/public/shared/constants.js +2 -2
  292. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  293. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  294. package/neural-interface/public/shared/context-menu-model.js +366 -0
  295. package/neural-interface/public/shared/context-menu.js +372 -0
  296. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  297. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  298. package/neural-interface/public/shared/desktop-update-view.js +20 -0
  299. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  300. package/neural-interface/public/shared/html-shell.js +9 -11
  301. package/neural-interface/public/shared/i18n.js +2 -0
  302. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  303. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  304. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  305. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
  306. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  307. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  308. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  309. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  310. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  311. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  312. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  313. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +47 -252
  314. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  315. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
  316. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +6 -21
  317. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
  318. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  319. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
  320. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  321. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  322. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  323. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  324. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +0 -21
  325. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  326. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +14 -47
  327. package/neural-interface/public/shared/page-session.js +96 -0
  328. package/neural-interface/public/shared/registry.js +5 -3
  329. package/neural-interface/public/shared/retired-2d.js +3 -0
  330. package/neural-interface/public/shared/schedules-view.js +195 -61
  331. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  332. package/neural-interface/public/shared/settings/settings-data.js +3 -1
  333. package/neural-interface/public/shared/settings/settings-ia.js +6 -6
  334. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  335. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  336. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  337. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  338. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  339. package/neural-interface/public/shared/skills/ss-creation.js +219 -0
  340. package/neural-interface/public/shared/skills/ss-dialog.js +130 -0
  341. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  342. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  343. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  344. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  345. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  346. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  347. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  348. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  349. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  350. package/neural-interface/public/shared/skills/ss-lint.js +141 -0
  351. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  352. package/neural-interface/public/shared/skills/ss-profiles.js +475 -0
  353. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  354. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  355. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  356. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  357. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  358. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  359. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  360. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +236 -0
  361. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  362. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  363. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  364. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  365. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  366. package/neural-interface/public/shared/skills/studio/sx-dom.js +229 -0
  367. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  368. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  369. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  370. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  371. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  372. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  373. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +279 -0
  374. package/neural-interface/public/shared/skills/studio/sx-install.js +296 -0
  375. package/neural-interface/public/shared/skills/studio/sx-library-view.js +639 -0
  376. package/neural-interface/public/shared/skills/studio/sx-library.js +475 -0
  377. package/neural-interface/public/shared/skills/studio/sx-main.js +681 -0
  378. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  379. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  380. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  381. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  382. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  383. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  384. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  385. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  386. package/neural-interface/public/shared/skills/studio/sx-subject.js +248 -0
  387. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  388. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  389. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  390. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +425 -0
  391. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  392. package/neural-interface/public/shared/styles.css +1525 -1779
  393. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  394. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  395. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  396. package/neural-interface/public/shared/tool-list-view.js +112 -0
  397. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  398. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  399. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  400. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  401. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  402. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  403. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  404. package/neural-interface/public/shared/tour/tour.css +144 -0
  405. package/neural-interface/public/shared/ui-automation-studio.js +91 -128
  406. package/neural-interface/public/shared/ui-claude-panel.js +77 -5
  407. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  408. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  409. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  410. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  411. package/neural-interface/public/shared/ui-detail.js +15 -3
  412. package/neural-interface/public/shared/ui-image-gallery.js +50 -47
  413. package/neural-interface/public/shared/ui-keybinds.js +26 -1
  414. package/neural-interface/public/shared/ui-menubar.js +8 -18
  415. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  416. package/neural-interface/public/shared/ui-native-loop-router.js +7 -1
  417. package/neural-interface/public/shared/ui-navbar.js +4 -24
  418. package/neural-interface/public/shared/ui-panels.js +96 -65
  419. package/neural-interface/public/shared/ui-schedules-studio.js +507 -423
  420. package/neural-interface/public/shared/ui-sessions.js +5 -3
  421. package/neural-interface/public/shared/ui-settings.js +594 -462
  422. package/neural-interface/public/shared/ui-sidepanel-sessions.js +1 -1
  423. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  424. package/neural-interface/public/shared/ui-sidepanel-windows.js +16 -11
  425. package/neural-interface/public/shared/ui-skills.js +33 -2830
  426. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  427. package/neural-interface/public/shared/ui-sync.js +15 -5
  428. package/neural-interface/public/shared/ui-terminal.js +124 -180
  429. package/neural-interface/public/shared/ui-tool-updates.js +3 -1
  430. package/neural-interface/public/shared/ui-tooltip.js +10 -5
  431. package/neural-interface/public/shared/ui-trash.js +6 -4
  432. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  433. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  434. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
  435. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  436. package/neural-interface/public/shared/ui-tutorial.js +285 -509
  437. package/neural-interface/public/shared/ui-update.js +38 -2
  438. package/neural-interface/public/shared/ui-whiteboard.js +311 -58
  439. package/neural-interface/public/shared/ui-worker-end.js +4 -0
  440. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  441. package/neural-interface/public/shared/view-variant.js +66 -0
  442. package/neural-interface/public/shared/whatsapp/wa-view.js +1 -1
  443. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  444. package/neural-interface/public/shared/worker-end.js +10 -3
  445. package/neural-interface/public/shared/worker-restart.js +12 -0
  446. package/neural-interface/public/sidepanel-session.html +1 -1
  447. package/neural-interface/public/variant/2d/background.js +2 -1
  448. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  449. package/neural-interface/public/variant/2d/graph.js +42 -20
  450. package/neural-interface/public/variant/2d/hulls.js +14 -9
  451. package/neural-interface/public/variant/2d/lasso.js +11 -7
  452. package/neural-interface/public/variant/2d/main.js +32 -1
  453. package/neural-interface/public/variant/2d/minimap.js +11 -13
  454. package/neural-interface/public/variant/3d/main.js +69 -6
  455. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  456. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  457. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  458. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  459. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  460. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  461. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  462. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  463. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  464. package/neural-interface/scripts/build-vendor.mjs +32 -0
  465. package/neural-interface/server.js +3934 -2454
  466. package/neural-interface/templates/rulesets/core.md +1 -1
  467. package/neural-interface/templates/rulesets/legacy-hashes.json +18 -0
  468. package/neural-interface/templates/rulesets/manifest.json +8 -8
  469. package/npm-shrinkwrap.json +18 -12
  470. package/package.json +16 -3
  471. package/postinstall.js +9 -4
  472. package/preuninstall.js +1 -1
  473. package/setup.js +97 -322
  474. package/skills/skill-forge/SKILL.md +177 -0
  475. package/skills/skill-forge/evals/evals.json +54 -0
  476. package/skills/skill-forge/references/package.md +105 -0
  477. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  478. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  479. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  480. package/skills/synabun/SKILL.md +10 -7
  481. package/skills/synabun/modules/audit.md +1 -5
  482. package/skills/synabun/modules/changelog.md +1 -5
  483. package/skills/synabun/modules/idea.md +1 -1
  484. package/skills/synabun/modules/memorize.md +6 -9
  485. package/skills/synabun/modules/schedule.md +4 -4
  486. package/mcp-server/dist/services/profiles.d.ts +0 -120
  487. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  488. package/mcp-server/dist/services/profiles.js +0 -387
  489. package/mcp-server/dist/services/profiles.js.map +0 -1
  490. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  491. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  492. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  493. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  494. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  495. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  496. package/mcp-server/dist/tools/leonardo.js +0 -21
  497. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  498. package/mcp-server/dist/tools/profile.d.ts +0 -17
  499. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  500. package/mcp-server/dist/tools/profile.js +0 -114
  501. package/mcp-server/dist/tools/profile.js.map +0 -1
  502. package/mcp-server/src/services/profiles.ts +0 -428
  503. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  504. package/mcp-server/src/tools/leonardo.ts +0 -30
  505. package/mcp-server/src/tools/profile.ts +0 -122
  506. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  507. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  508. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  509. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  510. package/skills/leonardo/SKILL.md +0 -168
  511. package/skills/leonardo/icon.svg +0 -1
  512. package/skills/leonardo/modules/browser-guide.md +0 -378
  513. package/skills/leonardo/modules/image-prompter.md +0 -283
  514. package/skills/leonardo/modules/model-advisor.md +0 -100
  515. package/skills/leonardo/modules/prompt-library.md +0 -110
  516. package/skills/leonardo/modules/style-guide.md +0 -186
  517. package/skills/leonardo/modules/video-prompter.md +0 -317
@@ -1,3 +1,5 @@
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';
@@ -6,14 +8,19 @@ import { buildAboutTab, wireAboutTab } from './settings/settings-about-pane.js';
6
8
  import { buildAppTab, wireAppTab } from './settings/settings-app-pane.js';
7
9
  import { SETTINGS_GROUPS, SETTINGS_PAGES, DEFAULT_SETTINGS_PAGE, pageKeys, getSettingsPage, resolveSettingsTarget, homeOfControl } from './settings/settings-ia.js';
8
10
  import * as kit from './settings/settings-kit.js';
11
+ import { skillFoldersHTML, wireSkillFolders, refreshSkillChips, chipState, installedToast } from './settings/settings-skill-folders-pane.js';
9
12
  import { RULES_HOST_LABELS, describeRulesHost, describeRulesInstall, describeRulesInstallAll } from './settings/settings-rules.js';
10
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';
11
16
  import { readGreetingStatus, describeGreeting, describeGreetingSaveFailure } from './settings/settings-greeting.js';
17
+ import { describeOpencodeInstall } from './settings/settings-opencode.js';
12
18
  import { renderGreeting, clearGreeting } from './greeting/greeting-card.js';
13
19
  import { createTunnelCard, drawInto, mcpKeyNeededText, readTunnel } from './ui-tunnel.js';
14
20
  import { remoteMcpAddress } from './tunnel-view.js';
15
21
  import { recheckCliStatus, getCliStatus, addCliToPath } from './cli-status.js';
16
22
  import { cliInstallText, setCliInstallTranslator } from './cli-install-view.js';
23
+ import { dropdownHTML, wireDropdowns } from './cc-dropdown.js';
17
24
  setCliInstallTranslator((key, params) => kit.tx(`cliInstall.${key}`, params));
18
25
  import { setGitBashTranslator } from './git-bash-view.js';
19
26
  import { gitBashRowHtml, wireGitBashRow } from './ui-git-bash.js';
@@ -37,6 +44,7 @@ import { state, emit, on } from './state.js';
37
44
  import { getSettingsTabs } from './registry.js';
38
45
  import { escapeHtml } from './utils.js';
39
46
  import { storage } from './storage.js';
47
+ import { normalizeViewConfig } from './view-variant.js';
40
48
  import { KEYS } from './constants.js';
41
49
  import { registerAction } from './ui-keybinds.js';
42
50
  import { CLI_PROFILES, ensureModelsForProfile, getModelsForProfile } from './agent-runtime-options.js';
@@ -47,6 +55,7 @@ import { getNotifSettings, playTestSound, sendTestBanner, SOUND_PRESETS } from '
47
55
  import {
48
56
  fetchRulesStatus, fetchRulesTextWithFallback, installRules, removeRules, installAllRules, setRulesAutoUpdate, removeLegacyRules,
49
57
  } from './api.js';
58
+ import { centeredInFrame } from './app-frame.js';
50
59
 
51
60
  // ── SVG icon constants ──
52
61
 
@@ -113,6 +122,7 @@ function showCCToast(msg, duration = 3500) {
113
122
  // INTERFACE CUSTOMIZATION — config, presets, apply
114
123
  // ═══════════════════════════════════════════
115
124
 
125
+ // Optional legacy view preferences must never reintroduce a 2D selection.
116
126
  const IFACE_DEFAULTS = {
117
127
  visualizationEnabled: true,
118
128
  scale: 1,
@@ -161,7 +171,7 @@ const IFACE_SLIDERS = {
161
171
  export function loadIfaceConfig() {
162
172
  try {
163
173
  const raw = storage.getItem('neural-interface-config');
164
- if (raw) return { ...IFACE_DEFAULTS, ...JSON.parse(raw) };
174
+ if (raw) return { ...IFACE_DEFAULTS, ...normalizeViewConfig(JSON.parse(raw)) };
165
175
  // Migrate legacy ui-scale if present
166
176
  const legacyScale = storage.getItem('neural-ui-scale');
167
177
  if (legacyScale) {
@@ -174,7 +184,7 @@ export function loadIfaceConfig() {
174
184
  }
175
185
 
176
186
  export function saveIfaceConfig(cfg) {
177
- storage.setItem('neural-interface-config', JSON.stringify(cfg));
187
+ storage.setItem('neural-interface-config', JSON.stringify(normalizeViewConfig(cfg)));
178
188
  // Keep legacy key in sync for backwards compat
179
189
  storage.setItem('neural-ui-scale', cfg.scale);
180
190
  }
@@ -551,7 +561,7 @@ function buildSearchHTML() {
551
561
  }
552
562
 
553
563
  // What search reads from the panel beside its index (settings-search.js): the labelled rows of the panes that draw
554
- // 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,
555
565
  // browser profiles, themes, the linked WhatsApp account). Labels and names only: a source names label elements and
556
566
  // the shell reads their text, never a value; `help` is given only where it is the app's own copy. `named` rows may
557
567
  // sit inside an inventory control (a list it tags).
@@ -957,7 +967,7 @@ function buildSetupTab(setupStatus) {
957
967
  { id: 'claude', name: 'Claude', icon: ANTHROPIC_ICON, file: '~/.claude.json', ids: { toggle: 'SET004', cli: 'SET005', primary: 'SET006', remove: 'SET007', view: 'SET008', copy: 'SET009' } },
958
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' } },
959
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' } },
960
- { 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' } },
961
971
  ].map((p) => ({ ...p, on: setupStatus[p.id]?.connected || false }));
962
972
 
963
973
  const copyLabel = (invId) => `${COPY_ICON} ${kit.L(invId)}`;
@@ -1175,23 +1185,23 @@ function buildMcpTab() {
1175
1185
  return `
1176
1186
  <div class="stg-pane" data-stg-pane="mcp">
1177
1187
  <div class="stg-mcp-page-tabs" role="tablist" aria-label="${kit.L('MCP001')}" ${kit.inv('MCP001')}>
1178
- ${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)}
1179
1189
  ${tab('stg-mcp-page-tab', 'data-pane', 'installed', m('tab.installed'), false)}
1180
1190
  </div>
1181
1191
 
1182
- <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"]'>
1183
1193
  ${kit.card({
1184
- id: 'stg-sec-tool-profiles', section: 'connected_tools.tool_profiles',
1194
+ id: 'stg-sec-default-tools', section: 'connected_tools.default_tools',
1185
1195
  body: `
1186
1196
  <div class="stg-mcp-intro">
1187
- <div class="stg-help" style="margin-top:0">${m('profilesHelp')}</div>
1197
+ <div class="stg-help" style="margin-top:0">${m('defaultToolsHelp')}</div>
1188
1198
  <div class="stg-mcp-note">
1189
1199
  <strong>${m('noteTitle')}</strong>
1190
1200
  <span>${m('noteBody')}</span>
1191
1201
  </div>
1192
1202
  </div>
1193
- <div id="stg-mcp-matrix-wrap" class="stg-mcp-matrix-wrap stg-mcp-block" ${kit.inv('MCP022')} role="group" aria-label="${kit.L('MCP022')}">
1194
- <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>
1195
1205
  </div>
1196
1206
  <div id="stg-mcp-always-on" class="stg-mcp-subsection" ${kit.inv('MCP024')}></div>
1197
1207
  <div class="stg-mcp-subsection stg-mcp-servers-section">
@@ -1303,6 +1313,7 @@ function buildOpencodeTab() {
1303
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>
1304
1314
  </button>
1305
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>
1306
1317
  </div>
1307
1318
 
1308
1319
  <div class="ocp-group-head">
@@ -1328,9 +1339,10 @@ function buildOpencodeTab() {
1328
1339
  <div class="ocp-group">
1329
1340
  ${kit.advanced(`
1330
1341
  <div class="ocp-config-footer" ${kit.inv('OCP019')}>
1331
- <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>
1332
1343
  <button type="button" class="ocp-config-link" id="stg-ocp-open-config" aria-describedby="stg-ocp-open-config-help">${kit.L('OCP019')}</button>
1333
1344
  </div>
1345
+ <p class="stg-help" id="stg-ocp-config-jsonc" hidden></p>
1334
1346
  ${kit.help('OCP019', { forId: 'stg-ocp-open-config' })}`)}
1335
1347
  </div>`,
1336
1348
  })}
@@ -1341,8 +1353,7 @@ function buildOpencodeTab() {
1341
1353
  body: `
1342
1354
  <div class="ocp-compact-group ocp-rows">
1343
1355
  ${check('OCP013', 'stg-ocp-compact-auto')}
1344
- ${check('OCP014', 'stg-ocp-compact-prune')}
1345
- ${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' })}
1346
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' })}
1347
1358
  </div>
1348
1359
  <p class="ocp-footnote">${kit.te('settings.redesign.opencode.snapshotDetail')}</p>`,
@@ -1668,14 +1679,19 @@ function buildSkillsTab(ccSkills) {
1668
1679
  const row = (skill) => {
1669
1680
  const installed = skill.installedTargets || { claude: !!skill.installed, codex: false, opencode: false };
1670
1681
  const anyOn = targets.some(t => installed[t.id]);
1671
- const chips = targets.map(t => `
1672
- <button type="button" class="cc-skill-target${installed[t.id] ? ' on' : ''}"
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' : ''}"
1673
1688
  data-cc-skill="${kit.esc(skill.dirName)}"
1674
1689
  data-cc-target="${t.id}"
1675
- data-state="${installed[t.id] ? 'on' : 'off'}"
1676
- ${kit.inv('SKL001')} data-stg-pressed="on" aria-pressed="${installed[t.id] ? 'true' : 'false'}"
1677
- title="${kit.te('settings.redesign.skills.installFor', { skill: skill.name, assistant: t.label })}">${t.label}</button>
1678
- `).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('');
1679
1695
  return `
1680
1696
  <div class="cc-skill-row${anyOn ? ' installed' : ''}" data-skill-name="${kit.esc(skill.dirName)}">
1681
1697
  <div class="cc-skill-info">
@@ -1690,10 +1706,14 @@ function buildSkillsTab(ccSkills) {
1690
1706
  <div class="stg-pane" data-stg-pane="skills">
1691
1707
  ${kit.card({
1692
1708
  id: 'stg-sec-skills', section: 'tool_access.skills',
1693
- body: skills.length
1709
+ body: (skills.length
1694
1710
  ? `<div ${kit.inv('SKL004')} role="group" aria-label="${kit.L('SKL004')}">${skills.map(row).join('')}</div>
1695
1711
  ${kit.help('SKL001')}`
1696
- : `<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(),
1697
1717
  })}
1698
1718
  </div>`;
1699
1719
  }
@@ -1704,7 +1724,7 @@ function buildPermissionsTab(toolCategories, toolPermissions) {
1704
1724
  // The inventory ids of the "all tools of this group" switches, by group.
1705
1725
  const ALL_SWITCH = {
1706
1726
  memory: 'PER001', browser: 'PER003', whiteboard: 'PER004', cards: 'PER005', automation: 'PER006',
1707
- leonardo: 'PER007', images: 'PER008', gsc: 'PER009', youtube: 'PER010', thirdparty: 'PER011',
1727
+ images: 'PER008', gsc: 'PER009', youtube: 'PER010', thirdparty: 'PER011',
1708
1728
  };
1709
1729
 
1710
1730
  let totalOn = 0, totalAll = 0;
@@ -1824,6 +1844,52 @@ const userContextDigestHTML = (digest) => (digest.rows.length
1824
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('')
1825
1845
  : `<div class="stg-help uc-digest-message" role="listitem" style="margin:0">${kit.esc(digest.message)}</div>`);
1826
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
+
1827
1893
  /** Put a view (describeUserContext) on the section: the note, each switch and its notes, the digest. */
1828
1894
  function drawUserContext(overlay, view) {
1829
1895
  const section = overlay.querySelector('#stg-sec-automation-behavior');
@@ -1900,16 +1966,19 @@ function tunnelExtraHtml(view, mcpKeyInfo) {
1900
1966
  ].filter(Boolean).join('');
1901
1967
  }
1902
1968
 
1903
- function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo, openclawBridge, greeting, toolPermissions, toolCategories, userContext = null) {
1904
- const gh = ccIntegrations.global.hooks || {};
1905
- const ssOn = !!gh.SessionStart;
1906
- const psOn = !!gh.UserPromptSubmit;
1907
- const pcOn = !!gh.PreCompact;
1908
- const stOn = !!gh.Stop;
1909
- const prOn = !!gh.PreToolUse && !!gh.PostToolBatch;
1910
- const ptOn = !!gh.PostToolUse;
1911
- const subStartOn = !!gh.SubagentStart;
1912
- 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');
1913
1982
  const hf = ccIntegrations.hookFeatures || {};
1914
1983
  const cmOn = hf.conversationMemory !== false;
1915
1984
  const ulOn = hf.userLearning !== false;
@@ -1926,7 +1995,8 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
1926
1995
  { key: 'SubagentStart', on: subStartOn, inv: 'AUT034' }, { key: 'SubagentStop', on: subStopOn, inv: 'AUT035' },
1927
1996
  ];
1928
1997
  const onCount = hookRows.filter(r => r.on).length;
1929
- const allOn = onCount === hookRows.length;
1998
+ const allOn = ch.ready && onCount === hookRows.length;
1999
+ const hookRowsLocked = !ch.ready || ch.disabled;
1930
2000
 
1931
2001
  const copyIcon = COPY_ICON;
1932
2002
 
@@ -2051,6 +2121,44 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
2051
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>
2052
2122
  </div>`;
2053
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
+
2054
2162
  return `
2055
2163
  <div class="stg-pane" data-stg-pane="hooks">
2056
2164
 
@@ -2065,11 +2173,16 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
2065
2173
  body: `
2066
2174
  ${userContextBlock}
2067
2175
 
2176
+ ${workerGuardBlock}
2177
+
2178
+ ${claudeHooksBlock}
2179
+
2068
2180
  ${kit.card({
2069
2181
  id: 'stg-sec-hooks', level: 'h4', collapsible: true, collapsed: true, advanced: true, attrs: 'data-cc-target="global"',
2070
2182
  title: h('hooksTitle'), purpose: h('hooksPurpose'),
2071
2183
  // These hooks are Claude Code's own (~/.claude/settings.json, hooks/claude-code): no other assistant runs them.
2072
- 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 })}`,
2073
2186
  body: `
2074
2187
  <div class="stg-help" style="margin:0 0 10px">${h('claudeOnly')}</div>
2075
2188
  <div class="cc-hook-toggles">
@@ -2088,12 +2201,12 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
2088
2201
  </div>
2089
2202
  <div class="cc-hook-toggles" style="margin-top:10px">
2090
2203
  ${hookRows.map(r => `
2091
- <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}">
2092
2205
  <div class="cc-integration-info">
2093
2206
  <div class="cc-integration-label" id="cc-hook-${r.key}-label">${kit.L(r.inv)}</div>
2094
2207
  <div class="cc-integration-path" id="cc-hook-${r.key}-help">${kit.H(r.inv)}</div>
2095
2208
  </div>
2096
- <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>
2097
2210
  </div>`).join('')}
2098
2211
  </div>`,
2099
2212
  })}`,
@@ -3245,6 +3358,10 @@ async function openSettingsModalNow(options = {}) {
3245
3358
  // The Assistant's user context (lib/assistant-user-context.js): what its endpoint answered, as a reading.
3246
3359
  // No answer is a reading too: the section says what is true instead of showing switches that do nothing.
3247
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 });
3248
3365
 
3249
3366
  try {
3250
3367
  // Read through the page's data cache (settings/settings-data.js): kept answers at once (each open its own
@@ -3254,15 +3371,19 @@ async function openSettingsModalNow(options = {}) {
3254
3371
  const readingOf = (entry, make) => ({ status: 'fulfilled', value: entry?.status === 'fulfilled' ? make(entry.value) : make({ status: 0, error: entry?.reason?.message }) });
3255
3372
  const greetRes = readingOf(reads.greeting, readGreetingStatus);
3256
3373
  const userContextRes = readingOf(reads.userContext, readUserContextResponse);
3374
+ const workerGuardRes = readingOf(reads.workerGuard, readWorkerGuardResponse);
3257
3375
  const settingsRes = reads.settings, connRes = reads.connections, ccRes = reads.integrations, skillsRes = reads.skills;
3258
3376
  const tunnelRes = reads.tunnel, keyRes = reads.mcpKey, bridgeRes = reads.openclaw, setupRes = reads.setup, cliRes = reads.cli;
3259
3377
  const toolPermsRes = reads.toolPermissions, toolCatsRes = reads.toolCategories, discordRes = reads.discord, skinsRes = reads.skins;
3260
3378
  const iconsRes = reads.icons, youtubeRes = reads.youtube, typesafeRes = reads.typesafe;
3261
3379
  if (typesafeRes.status === 'fulfilled' && typesafeRes.value.ok) typesafeConfig = typesafeRes.value;
3262
3380
  if (userContextRes.status === 'fulfilled') userContextReading = userContextRes.value;
3381
+ if (workerGuardRes.status === 'fulfilled') workerGuardReading = workerGuardRes.value;
3263
3382
  if (settingsRes.status === 'fulfilled') settings = settingsRes.value;
3264
3383
  if (connRes.status === 'fulfilled' && connRes.value.connections) connections = connRes.value.connections;
3265
- 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;
3266
3387
  if (skillsRes.status === 'fulfilled' && skillsRes.value.ok) ccSkills = skillsRes.value.skills || [];
3267
3388
  if (tunnelRes.status === 'fulfilled' && tunnelRes.value.ok) tunnelStatus = tunnelRes.value;
3268
3389
  if (keyRes.status === 'fulfilled' && keyRes.value.ok) mcpKeyInfo = keyRes.value;
@@ -3295,9 +3416,10 @@ async function openSettingsModalNow(options = {}) {
3295
3416
  overlay.className = 'settings-panel glass resizable';
3296
3417
  overlay.id = 'settings-panel';
3297
3418
 
3298
- // Always open centered at default size
3299
- overlay.style.left = Math.max(20, (window.innerWidth - 900) / 2) + 'px';
3300
- 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';
3301
3423
 
3302
3424
  // ── Assemble HTML ──
3303
3425
  overlay.innerHTML = `
@@ -3330,7 +3452,7 @@ async function openSettingsModalNow(options = {}) {
3330
3452
  <div class="settings-content">
3331
3453
  ${buildPagesHTML({
3332
3454
  server: buildServerTab(settings, connections),
3333
- 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 })),
3334
3456
  terminal: buildTerminalTab(cliConfig),
3335
3457
  opencode: buildOpencodeTab(),
3336
3458
  notifications: buildNotificationsTab(),
@@ -4639,9 +4761,14 @@ async function openSettingsModalNow(options = {}) {
4639
4761
  });
4640
4762
  const data = await res.json();
4641
4763
  showPathAction(profileId, data);
4764
+ const refusal = profileId === 'opencode' && data.support ? describeOpencodeInstall(data.support, { th: kit.th, tx: kit.tx }) : null;
4642
4765
  if (data.ok && data.found && data.path && data.state === 'broken') {
4643
4766
  // Found, but it does not start: not put in the field, so a broken path is never saved by accident.
4644
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);
4645
4772
  } else if (data.ok && data.found && data.path) {
4646
4773
  input.value = data.path;
4647
4774
  showCCToast(kit.tx('settings.redesign.terminal.found', { path: data.version ? `${data.path} (${data.version})` : data.path }));
@@ -4899,9 +5026,15 @@ async function openSettingsModalNow(options = {}) {
4899
5026
  if (data.ok) {
4900
5027
  const nowOn = !isOn;
4901
5028
  toggleBtn.classList.toggle('on');
5029
+ toggleBtn.setAttribute('aria-pressed', nowOn ? 'true' : 'false');
4902
5030
  const row = toggleBtn.closest('.cc-integration-item');
4903
5031
  if (row) row.classList.toggle('enabled', nowOn);
4904
- 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
+ }
4905
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(); }
4906
5039
  } else { alert(data.error || kit.tx('settings.redesign.hooks.failedToToggleHook')); }
4907
5040
  } catch (err) { alert(kit.tx('settings.redesign.hooks.failed2', { message: err.message })); }
@@ -4913,6 +5046,41 @@ async function openSettingsModalNow(options = {}) {
4913
5046
  toggle.addEventListener('click', () => { ccToggleHook('global', null, toggle, overlay.querySelector('.iface-section[data-cc-target="global"]'), toggle.dataset.ccHook); });
4914
5047
  });
4915
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
+
4916
5084
  // ── The Assistant's user context: layers and the digest preview ──
4917
5085
  const ucRowsEl = overlay.querySelector('#uc-digest-rows');
4918
5086
  const redrawUserContext = (reading = userContextReading) => drawUserContext(overlay, describeUserContext(reading, { tx: kit.tx }));
@@ -4939,6 +5107,47 @@ async function openSettingsModalNow(options = {}) {
4939
5107
  if (!link) return;
4940
5108
  try { applySettingsOpenOptions(overlay, JSON.parse(link.dataset.ucGoto)); } catch { /* a malformed target goes nowhere */ }
4941
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
+ }
4942
5151
  // The switch moves at once and follows the server's answer: the stored settings when the save went through,
4943
5152
  // back where it was (with the reason next to the switches) when it did not. Saves go one after the other, in
4944
5153
  // the order of the clicks, so a second click made before the first was answered is kept, not dropped.
@@ -5192,6 +5401,12 @@ async function openSettingsModalNow(options = {}) {
5192
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); });
5193
5402
  });
5194
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
+
5195
5410
  // Skill target chips — one skill can be installed independently into Claude / Codex / OpenCode
5196
5411
  const runtimeLabels = { claude: 'Claude Code', codex: 'Codex CLI', opencode: 'OpenCode CLI' };
5197
5412
  overlay.querySelectorAll('.cc-skill-target[data-cc-skill]').forEach(chip => {
@@ -5201,28 +5416,48 @@ async function openSettingsModalNow(options = {}) {
5201
5416
  const isOn = chip.classList.contains('on');
5202
5417
  try {
5203
5418
  chip.style.opacity = '0.4'; chip.style.pointerEvents = 'none';
5204
- const res = await fetch('/api/skills/install', {
5419
+ const send = (extra = {}) => fetch('/api/skills/install', {
5205
5420
  method: isOn ? 'DELETE' : 'POST',
5206
5421
  headers: { 'Content-Type': 'application/json' },
5207
- body: JSON.stringify({ name: skillName, target }),
5208
- });
5209
- 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
+ }
5210
5431
  if (data.ok) {
5211
5432
  chip.classList.toggle('on');
5212
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() });
5213
5439
  const row = chip.closest('.cc-skill-row');
5214
5440
  if (row) {
5215
5441
  const anyOn = !!row.querySelector('.cc-skill-target.on');
5216
5442
  row.classList.toggle('installed', anyOn);
5217
5443
  }
5218
5444
  const runtime = runtimeLabels[target] || target;
5219
- 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 }));
5220
5451
  } else { alert(data.error || kit.tx('settings.redesign.hooks.failedToToggleSkill')); }
5221
5452
  } catch (err) { alert(kit.tx('settings.redesign.hooks.failed2', { message: err.message })); }
5222
5453
  finally { chip.style.opacity = ''; chip.style.pointerEvents = ''; }
5223
5454
  });
5224
5455
  });
5225
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
+
5226
5461
  // ── Setup tab: per-provider MCP toggles, CLI copy, config copy, SynaBun rules ──
5227
5462
  {
5228
5463
  const checkIcon = '<svg viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="20 6 9 17 4 12"/></svg>';
@@ -5378,7 +5613,7 @@ async function openSettingsModalNow(options = {}) {
5378
5613
  // An edited copy is removed only on an explicit yes (force). A file without SynaBun's markers is the
5379
5614
  // user's own and stays whatever is sent, so there is nothing to confirm for it.
5380
5615
  const edited = info?.state === 'modified' && info.replaceable !== false;
5381
- // 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
5382
5617
  // says so, and the host is then no longer managed). Asked again, the confirm sends force, which takes that line too.
5383
5618
  const listed = info?.entry === 'user';
5384
5619
  const force = edited || (listed && info.managed === false);
@@ -5521,8 +5756,8 @@ async function openSettingsModalNow(options = {}) {
5521
5756
  wireRulesControls('codex', 'codex');
5522
5757
 
5523
5758
  // ── OpenCode ──
5524
- wireSetupMcpToggle('opencode', '/api/opencode/mcp', '~/.config/opencode/config.json');
5525
- // 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
5526
5761
  {
5527
5762
  const preview = overlay.querySelector('#setup-opencode-config-preview');
5528
5763
  let cachedConfig = '';
@@ -5530,15 +5765,18 @@ async function openSettingsModalNow(options = {}) {
5530
5765
  const ep = setupStatus.paths?.envPath || '<path-to>/synabun/.env';
5531
5766
  fetch('/api/opencode/mcp').then(r => r.json()).then(data => {
5532
5767
  if (data.ok && data.data && Object.keys(data.data).length) {
5533
- cachedConfig = JSON.stringify({ mcp: data.data }, null, 2);
5768
+ cachedConfig = JSON.stringify({ mcp: { servers: data.data } }, null, 2);
5534
5769
  preview.textContent = cachedConfig;
5535
5770
  } else if (data.ok) {
5771
+ const launch = mcpLaunch();
5536
5772
  cachedConfig = JSON.stringify({
5537
5773
  mcp: {
5538
- SynaBun: {
5539
- type: 'stdio',
5540
- ...mcpLaunch(),
5541
- env: { DOTENV_PATH: ep },
5774
+ servers: {
5775
+ SynaBun: {
5776
+ type: 'local',
5777
+ command: [launch.command, ...launch.args],
5778
+ environment: { DOTENV_PATH: ep },
5779
+ },
5542
5780
  },
5543
5781
  },
5544
5782
  }, null, 2);
@@ -7315,6 +7553,9 @@ async function openSettingsModalNow(options = {}) {
7315
7553
  const ocpToolsEl = overlay.querySelector('#stg-ocp-tools');
7316
7554
  const ocpHistoryMetaEl = overlay.querySelector('#stg-ocp-history-meta');
7317
7555
  const ocpHistoryClearBtn = overlay.querySelector('#stg-ocp-clear-history');
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');
7318
7559
  const esc = s => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
7319
7560
 
7320
7561
  // ── Helpers ──
@@ -7337,6 +7578,7 @@ async function openSettingsModalNow(options = {}) {
7337
7578
  let _ollamaConfiguring = false;
7338
7579
  let _ollamaPollTimer = null;
7339
7580
  let _ollamaLastModelNames = '';
7581
+ let _ollamaConfigureError = '';
7340
7582
  let _ocpHistoryStats = null;
7341
7583
 
7342
7584
  function formatBytes(bytes) {
@@ -7367,7 +7609,7 @@ async function openSettingsModalNow(options = {}) {
7367
7609
  ocpHistoryMetaEl.textContent = kit.tx('settings.redesign.opencode.historyWipeInProgress');
7368
7610
  return;
7369
7611
  }
7370
- const totalBytes = (Number(data.databaseBytes) || 0) + (Number(data.artifactBytes) || 0);
7612
+ const totalBytes = Number(data.databaseBytes) || 0;
7371
7613
  ocpHistoryMetaEl.textContent = kit.tx('settings.redesign.opencode.stored', { formatCount: formatCount(data.sessions, 'session'), formatBytes: formatBytes(totalBytes) });
7372
7614
  ocpHistoryClearBtn.disabled = false;
7373
7615
  } catch (error) {
@@ -7379,8 +7621,7 @@ async function openSettingsModalNow(options = {}) {
7379
7621
  if (document.querySelector('.ocp-history-clear-overlay')) return;
7380
7622
  const confirmation = _ocpHistoryStats?.confirmationPhrase || 'DELETE OPENCODE HISTORY';
7381
7623
  const sessionCount = Number(_ocpHistoryStats?.sessions) || 0;
7382
- const totalBytes = (Number(_ocpHistoryStats?.databaseBytes) || 0)
7383
- + (Number(_ocpHistoryStats?.artifactBytes) || 0);
7624
+ const totalBytes = Number(_ocpHistoryStats?.databaseBytes) || 0;
7384
7625
  const modal = document.createElement('div');
7385
7626
  modal.className = 'tag-delete-overlay ocp-history-clear-overlay';
7386
7627
  modal.innerHTML = `
@@ -7394,9 +7635,9 @@ async function openSettingsModalNow(options = {}) {
7394
7635
  <p class="ocp-history-modal-subtitle">${formatCount(sessionCount, 'session')} ${kit.th('settings.redesign.opencode.currentlyStored', { formatBytes: formatBytes(totalBytes) })}</p>
7395
7636
  </div>
7396
7637
  </div>
7397
- <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>
7398
7639
  <ul class="ocp-history-scope">
7399
- <li>${kit.te('settings.redesign.opencode.deletesConversationsMessagesToolOutputTo')}</li>
7640
+ <li>${kit.te('settings.redesign.opencode.historyClearDeletes')}</li>
7400
7641
  <li>${kit.te('settings.redesign.opencode.deletesSynabunSDerivedOpencodeResume')}</li>
7401
7642
  <li>${kit.te('settings.redesign.opencode.keepsProviderCredentialsAccountConfigSet')}</li>
7402
7643
  </ul>
@@ -7444,7 +7685,7 @@ async function openSettingsModalNow(options = {}) {
7444
7685
  confirmButton.disabled = true;
7445
7686
  confirmButton.textContent = kit.tx('settings.redesign.opencode.clearing');
7446
7687
  feedback.dataset.state = 'working';
7447
- feedback.textContent = kit.tx('settings.redesign.opencode.stoppingOpencodeAndClearingItsHistory');
7688
+ feedback.textContent = kit.tx('settings.redesign.opencode.historyClearWorking');
7448
7689
  const requestId = crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`;
7449
7690
  sessionStorage.setItem('synabun-opencode-history-clear-request', requestId);
7450
7691
  try {
@@ -7457,9 +7698,7 @@ async function openSettingsModalNow(options = {}) {
7457
7698
  if (!response.ok || !data.ok) throw new Error(data.error || kit.tx('settings.redesign.opencode.historyWipeFailedHttp', { status: response.status }));
7458
7699
 
7459
7700
  const result = data.result || {};
7460
- const reclaimed = Math.max(0,
7461
- (Number(result.databaseBytesBefore) || 0) - (Number(result.databaseBytesAfter) || 0)
7462
- + (Number(result.artifactBytesDeleted) || 0));
7701
+ const reclaimed = Math.max(0, (Number(result.databaseBytesBefore) || 0) - (Number(result.databaseBytesAfter) || 0));
7463
7702
  const warnings = Array.isArray(result.warnings) ? result.warnings.filter(Boolean) : [];
7464
7703
  feedback.dataset.state = warnings.length ? 'warning' : 'working';
7465
7704
  feedback.textContent = warnings.length
@@ -7500,7 +7739,7 @@ async function openSettingsModalNow(options = {}) {
7500
7739
  dotColor = '#facc15';
7501
7740
  statusText = kit.tx('settings.redesign.opencode.configuring');
7502
7741
  statusCls = 'stg-provider-status-key';
7503
- 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>';
7504
7743
  } else if (!_ollamaRunning) {
7505
7744
  dotColor = 'rgba(248,113,113,0.7)';
7506
7745
  statusText = kit.tx('settings.redesign.opencode.notDetected');
@@ -7511,6 +7750,11 @@ async function openSettingsModalNow(options = {}) {
7511
7750
  statusText = kit.tx('settings.redesign.opencode.runningVersion', { version: _ollamaVersion ? ' v' + _ollamaVersion : '' });
7512
7751
  statusCls = 'stg-provider-status-key';
7513
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>';
7514
7758
  } else if (_ollamaConfigured) {
7515
7759
  dotColor = '#4ade80';
7516
7760
  statusText = kit.tx('settings.redesign.opencode.runningVersion', { version: _ollamaVersion ? ' v' + _ollamaVersion : '' });
@@ -7546,27 +7790,16 @@ async function openSettingsModalNow(options = {}) {
7546
7790
  const btn = existing.querySelector('.stg-ollama-remove');
7547
7791
  if (btn) { btn.disabled = true; btn.textContent = kit.tx('settings.redesign.opencode.removing'); }
7548
7792
  try {
7549
- 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}`);
7550
7796
  _ollamaConfigured = false;
7551
7797
  _ollamaConfiguredCount = 0;
7552
7798
  _ollamaUserRemoved = true;
7553
7799
  renderOllamaCard();
7554
- showCCToast(kit.tx('settings.redesign.opencode.ollamaProviderRemovedServerRestarting'));
7555
- // Poll for server restart then refresh providers
7556
- let polls = 0;
7557
- const pollId = setInterval(async () => {
7558
- polls++;
7559
- if (polls > 8) { clearInterval(pollId); return; }
7560
- try {
7561
- const sr = await fetch('/api/opencode/status');
7562
- const sd = await sr.json();
7563
- if (sd.running) {
7564
- clearInterval(pollId);
7565
- await refreshOcpProviders();
7566
- document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
7567
- }
7568
- } catch {}
7569
- }, 2000);
7800
+ showCCToast(kit.tx('settings.redesign.opencode.ollamaProviderRemoved'));
7801
+ await refreshOcpProviders();
7802
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
7570
7803
  } catch (e) {
7571
7804
  showCCToast(kit.tx('settings.redesign.opencode.error2', { message: e.message }));
7572
7805
  }
@@ -7600,7 +7833,9 @@ async function openSettingsModalNow(options = {}) {
7600
7833
 
7601
7834
  // Auto-configure if: running + has models + not configured + not user-removed
7602
7835
  // Also re-configure if models changed while already configured
7603
- 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
7604
7839
  && (!_ollamaConfigured || modelsChanged);
7605
7840
 
7606
7841
  if (shouldConfigure && !_ollamaConfiguring) {
@@ -7608,31 +7843,16 @@ async function openSettingsModalNow(options = {}) {
7608
7843
  renderOllamaCard();
7609
7844
  try {
7610
7845
  const cfgResp = await fetch('/api/ollama/configure', { method: 'POST' });
7611
- const cfgData = await cfgResp.json();
7612
- 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.
7613
7850
  _ollamaConfigured = true;
7614
7851
  _ollamaConfiguredCount = cfgData.modelsConfigured;
7615
- // Wait for OpenCode server restart then refresh providers
7616
- setTimeout(async () => {
7617
- let polls = 0;
7618
- const pollId = setInterval(async () => {
7619
- polls++;
7620
- if (polls > 8) { clearInterval(pollId); _ollamaConfiguring = false; renderOllamaCard(); return; }
7621
- try {
7622
- const sr = await fetch('/api/opencode/status');
7623
- const sd = await sr.json();
7624
- if (sd.running) {
7625
- clearInterval(pollId);
7626
- _ollamaConfiguring = false;
7627
- await refreshOcpProviders();
7628
- renderOllamaCard();
7629
- document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
7630
- }
7631
- } catch {}
7632
- }, 2000);
7633
- }, 1000);
7852
+ await refreshOcpProviders();
7853
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
7634
7854
  } else {
7635
- _ollamaConfiguring = false;
7855
+ _ollamaConfigureError = cfgData.error || `HTTP ${cfgResp.status}`;
7636
7856
  }
7637
7857
  } catch {
7638
7858
  _ollamaConfiguring = false;
@@ -7884,7 +8104,7 @@ async function openSettingsModalNow(options = {}) {
7884
8104
  }
7885
8105
  for (const om of oauthMethods) {
7886
8106
  const label = om.label || om.name || kit.tx('settings.redesign.opencode.reconnectProvider', { name });
7887
- 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>`);
7888
8108
  }
7889
8109
  authHtml = btns.join('');
7890
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>`;
@@ -7898,7 +8118,7 @@ async function openSettingsModalNow(options = {}) {
7898
8118
  }
7899
8119
  for (const om of oauthMethods) {
7900
8120
  const label = om.label || om.name || kit.tx('settings.redesign.opencode.loginWith', { name: name });
7901
- 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>`);
7902
8122
  }
7903
8123
  authHtml = btns.join('');
7904
8124
  }
@@ -7914,9 +8134,16 @@ async function openSettingsModalNow(options = {}) {
7914
8134
  </div>`;
7915
8135
 
7916
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.
7917
8139
  const oauthWaitHtml = `<div class="stg-ocp-oauth-wait" data-provider="${esc(id)}" style="display:none">
7918
- ${kit.te('settings.redesign.opencode.waitingForAuthorization')}
8140
+ <span class="stg-ocp-oauth-status">${kit.te('settings.redesign.opencode.waitingForAuthorization')}</span>
7919
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>
7920
8147
  </div>`;
7921
8148
 
7922
8149
  const hiddenSet = getHiddenModels();
@@ -8112,28 +8339,15 @@ async function openSettingsModalNow(options = {}) {
8112
8339
  try {
8113
8340
  const resp = await fetch(`/api/opencode/auth/${encodeURIComponent(provId)}`, {
8114
8341
  method: 'PUT', headers: { 'Content-Type': 'application/json' },
8115
- body: JSON.stringify({ type: 'api', key }),
8342
+ body: JSON.stringify({ key }),
8116
8343
  });
8117
- const data = await resp.json();
8344
+ const data = await resp.json().catch(() => ({}));
8118
8345
  if (resp.ok && data.ok !== false) {
8119
8346
  form.style.display = 'none';
8120
8347
  input.value = '';
8121
- showCCToast(kit.tx('settings.redesign.opencode.apiKeySavedForServerRestarting', { provId: provId }));
8122
- // Poll until server is back (takeover kills + respawns, ~5-6s)
8123
- let polls = 0;
8124
- const pollId = setInterval(async () => {
8125
- polls++;
8126
- if (polls > 8) { clearInterval(pollId); return; }
8127
- try {
8128
- const sr = await fetch('/api/opencode/status');
8129
- const sd = await sr.json();
8130
- if (sd.running) {
8131
- clearInterval(pollId);
8132
- await refreshOcpProviders();
8133
- document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8134
- }
8135
- } catch {}
8136
- }, 2000);
8348
+ showCCToast(kit.tx('settings.redesign.opencode.apiKeySaved', { provId: provId }));
8349
+ await refreshOcpProviders();
8350
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8137
8351
  } else {
8138
8352
  const msg = data.error || kit.tx('settings.redesign.opencode.failedToSaveKey');
8139
8353
  if (errEl) { errEl.textContent = msg; errEl.style.display = ''; }
@@ -8166,24 +8380,11 @@ async function openSettingsModalNow(options = {}) {
8166
8380
  method: 'DELETE',
8167
8381
  headers: { 'Content-Type': 'application/json' },
8168
8382
  });
8169
- const data = await resp.json();
8383
+ const data = await resp.json().catch(() => ({}));
8170
8384
  if (resp.ok && data.ok !== false) {
8171
- showCCToast(kit.tx('settings.redesign.opencode.apiKeyRemovedForServerRestarting', { provId: provId }));
8172
- // Poll until server is back (takeover kills + respawns, ~5-6s)
8173
- let polls = 0;
8174
- const pollId = setInterval(async () => {
8175
- polls++;
8176
- if (polls > 8) { clearInterval(pollId); return; }
8177
- try {
8178
- const sr = await fetch('/api/opencode/status');
8179
- const sd = await sr.json();
8180
- if (sd.running) {
8181
- clearInterval(pollId);
8182
- await refreshOcpProviders();
8183
- document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8184
- }
8185
- } catch {}
8186
- }, 2000);
8385
+ showCCToast(kit.tx('settings.redesign.opencode.apiKeyRemoved', { provId: provId }));
8386
+ await refreshOcpProviders();
8387
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8187
8388
  } else {
8188
8389
  showCCToast(data.error || kit.tx('settings.redesign.opencode.failedToRemoveKey'));
8189
8390
  }
@@ -8200,43 +8401,22 @@ async function openSettingsModalNow(options = {}) {
8200
8401
  btn.addEventListener('click', async () => {
8201
8402
  const provId = btn.dataset.provider;
8202
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;
8203
8406
  btn.disabled = true; btn.textContent = kit.tx('settings.redesign.opencode.connecting');
8204
- const waitEl = ocpProvidersEl.querySelector(`.stg-ocp-oauth-wait[data-provider="${provId}"]`);
8205
- const codeEl = waitEl?.querySelector('.stg-ocp-oauth-code');
8206
8407
  try {
8207
8408
  const resp = await fetch(`/api/opencode/oauth/${encodeURIComponent(provId)}/authorize`, {
8208
8409
  method: 'POST',
8209
8410
  headers: { 'Content-Type': 'application/json' },
8210
- body: JSON.stringify({ method: isNaN(methodIdx) ? 0 : methodIdx }),
8411
+ body: JSON.stringify({ methodID: btn.dataset.methodId || undefined, method: isNaN(methodIdx) ? 0 : methodIdx }),
8211
8412
  });
8212
- const data = await resp.json();
8213
- const authData = data.data || data;
8214
- if (authData.url) {
8215
- window.open(authData.url, '_blank');
8216
- if (waitEl) waitEl.style.display = '';
8217
- if (authData.code && codeEl) { codeEl.textContent = authData.code; codeEl.style.display = ''; }
8218
- if (authData.instructions) showCCToast(authData.instructions);
8219
- // Poll for completion
8220
- let attempts = 0;
8221
- const poll = setInterval(async () => {
8222
- attempts++;
8223
- if (attempts > 60) { clearInterval(poll); if (waitEl) waitEl.style.display = 'none'; return; }
8224
- try {
8225
- // Only `connected` is read: the compact view (connected providers), not the whole catalog every 2 s.
8226
- const pr = await fetch('/api/opencode/providers/full?view=compact');
8227
- const pd = await pr.json();
8228
- const connected = pd.data?.connected || [];
8229
- if (connected.includes(provId)) {
8230
- clearInterval(poll);
8231
- if (waitEl) waitEl.style.display = 'none';
8232
- showCCToast(kit.tx('settings.redesign.opencode.connected', { provId: provId }));
8233
- await refreshOcpProviders();
8234
- document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8235
- }
8236
- } catch {}
8237
- }, 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);
8238
8418
  } else {
8239
- showCCToast(authData.error || data.error || kit.tx('settings.redesign.opencode.oauthFailedNoAuthorizationUrlReturned'));
8419
+ showCCToast(data.error || kit.tx('settings.redesign.opencode.oauthFailedNoAuthorizationUrlReturned'));
8240
8420
  }
8241
8421
  } catch (e) {
8242
8422
  showCCToast(kit.tx('settings.redesign.opencode.oauthError', { message: e.message }));
@@ -8246,6 +8426,81 @@ async function openSettingsModalNow(options = {}) {
8246
8426
  });
8247
8427
  }
8248
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
+
8249
8504
  // ── Delegated card click-to-expand (survives re-renders) ──
8250
8505
  if (ocpProvidersEl) {
8251
8506
  ocpProvidersEl.addEventListener('click', (e) => {
@@ -8263,64 +8518,60 @@ async function openSettingsModalNow(options = {}) {
8263
8518
  }
8264
8519
 
8265
8520
  // ── Tools/Permissions rendering ──
8266
- async function renderToolPermissions() {
8267
- if (!ocpToolsEl) return;
8268
- try {
8269
- const [toolsResp, configResp] = await Promise.all([
8270
- fetch('/api/opencode/tools'),
8271
- fetch('/api/opencode/config'),
8272
- ]);
8273
- const toolsData = await toolsResp.json();
8274
- const configData = await configResp.json();
8275
- _ocpConfig = configData.data || {};
8276
- const toolIds = Array.isArray(toolsData.data) ? toolsData.data : (toolsData.data?.ids || []);
8277
- const permissions = _ocpConfig.permission || {};
8278
- const tools = _ocpConfig.tools || {};
8279
-
8280
- if (!toolIds.length) {
8281
- ocpToolsEl.innerHTML = '<div class="settings-hint" style="opacity:0.4">' + kit.te('settings.redesign.opencode.noToolsAvailable') + '</div>';
8282
- 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();
8283
8541
  }
8542
+ }, { signal: panelLife.signal });
8543
+ }
8284
8544
 
8285
- ocpToolsEl.innerHTML = `<div class="stg-tool-grid">${toolIds.map(t => {
8286
- const tid = typeof t === 'string' ? t : (t.id || t.name || '');
8287
- if (!tid) return '';
8288
- let currentVal = 'allow';
8289
- if (tools[tid] === false) currentVal = 'deny';
8290
- else if (permissions[tid] === 'ask') currentVal = 'ask';
8291
- return `<div class="stg-tool-row">
8292
- <span class="stg-tool-name">${esc(tid)}</span>
8293
- <select class="stg-tool-select stg-ocp-tool-perm" data-tool="${esc(tid)}" aria-label="${kit.te('settings.redesign.opencode.permissionFor', { tool: tid })}">
8294
- <option value="allow"${currentVal === 'allow' ? ' selected' : ''}>${kit.te('settings.redesign.opencode.allow')}</option>
8295
- <option value="ask"${currentVal === 'ask' ? ' selected' : ''}>${kit.te('settings.redesign.opencode.ask')}</option>
8296
- <option value="deny"${currentVal === 'deny' ? ' selected' : ''}>${kit.te('settings.redesign.opencode.deny')}</option>
8297
- </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
+ })}
8298
8564
  </div>`;
8299
- }).join('')}</div>`;
8300
-
8301
- // Wire change events
8302
- ocpToolsEl.querySelectorAll('.stg-ocp-tool-perm').forEach(sel => {
8303
- sel.addEventListener('change', async () => {
8304
- const tid = sel.dataset.tool;
8305
- const val = sel.value;
8306
- const patchBody = {};
8307
- const newPerms = { ...(_ocpConfig.permission || {}) };
8308
- const newTools = { ...(_ocpConfig.tools || {}) };
8309
- if (val === 'deny') { newTools[tid] = false; delete newPerms[tid]; }
8310
- else if (val === 'ask') { delete newTools[tid]; newPerms[tid] = 'ask'; }
8311
- else { delete newTools[tid]; delete newPerms[tid]; }
8312
- patchBody.permission = newPerms;
8313
- patchBody.tools = newTools;
8314
- try {
8315
- await fetch('/api/opencode/config', {
8316
- method: 'PATCH', headers: { 'Content-Type': 'application/json' },
8317
- body: JSON.stringify(patchBody),
8318
- });
8319
- _ocpConfig.permission = newPerms;
8320
- _ocpConfig.tools = newTools;
8321
- } catch {}
8322
- });
8323
- });
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);
8324
8575
  } catch {
8325
8576
  ocpToolsEl.innerHTML = '<div class="settings-hint" style="opacity:0.4">' + kit.te('settings.redesign.opencode.failedToLoadTools') + '</div>';
8326
8577
  }
@@ -8338,155 +8589,37 @@ async function openSettingsModalNow(options = {}) {
8338
8589
  return null;
8339
8590
  }
8340
8591
 
8341
- function renderMcpMatrix() {
8342
- const wrap = overlay.querySelector('#stg-mcp-matrix-wrap');
8343
- if (!wrap || !_mcpRegistry) return;
8344
- const tg = _mcpRegistry.toolGroups || {};
8345
- const profiles = _mcpRegistry.profiles || {};
8346
- const active = _mcpRegistry.activeProfile || '';
8347
- const profEntries = Object.entries(profiles);
8348
- const switchable = Object.entries(tg).filter(([, info]) => !info.alwaysOn);
8349
-
8350
- if (!profEntries.length) {
8351
- wrap.innerHTML = '<div class="stg-mcp-matrix-empty">' + kit.te('settings.redesign.mcp.noProfiles') + '</div>';
8352
- return;
8353
- }
8354
-
8355
- // Build table header
8356
- let thead = '<tr><th class="stg-mx-label-th"><div class="stg-mx-corner-head">' + kit.te('settings.redesign.mcp.toolGroups') + '</div></th>';
8357
- for (const [name, prof] of profEntries) {
8358
- const isActive = name === active;
8359
- thead += `<th class="stg-mx-profile-th${isActive ? ' active' : ''}" data-profile="${esc(name)}">
8360
- <div class="stg-mx-profile-head" data-profile="${esc(name)}">
8361
- <span class="stg-mx-profile-dot${isActive ? ' on' : ''}"></span>
8362
- <span class="stg-mx-profile-name">${esc(prof.label || name)}</span>
8363
- ${isActive ? '' : `<button class="stg-mx-profile-del" data-profile="${esc(name)}" title="${kit.te('settings.redesign.mcp.deleteProfile')}">\u00d7</button>`}
8364
- </div>
8365
- </th>`;
8366
- }
8367
- thead += '<th class="stg-mx-add-th"><button class="stg-mx-add-btn" title="' + kit.te('settings.redesign.mcp.newProfile') + '">+</button></th></tr>';
8368
-
8369
- // Build table body — switchable groups
8370
- let tbody = '';
8371
- for (const [key, info] of switchable) {
8372
- tbody += `<tr>`;
8373
- tbody += `<td class="stg-mx-label-td"><div class="stg-mx-label-inner">
8374
- <span class="stg-mx-grp-name">${esc(info.label)}</span>
8375
- <span class="stg-mx-grp-count">${info.tools}</span>
8376
- </div></td>`;
8377
- for (const [profName, prof] of profEntries) {
8378
- const isOn = (prof.groups || []).includes(key);
8379
- tbody += `<td class="stg-mx-cell" data-profile="${esc(profName)}" data-group="${esc(key)}" data-kind="group">
8380
- <span class="stg-mx-toggle${isOn ? ' on' : ''}"></span>
8381
- </td>`;
8382
- }
8383
- tbody += '<td class="stg-mx-add-spacer"></td></tr>';
8384
- }
8385
-
8386
- wrap.innerHTML = `<div class="stg-mcp-matrix-scroll"><table id="stg-mcp-matrix"><thead>${thead}</thead><tbody>${tbody}</tbody></table></div>`;
8387
-
8388
- // --- Event handlers ---
8389
-
8390
- // Toggle cells
8391
- wrap.querySelectorAll('.stg-mx-cell').forEach(cell => {
8392
- cell.addEventListener('click', async () => {
8393
- const profName = cell.dataset.profile;
8394
- const prof = _mcpRegistry.profiles[profName];
8395
- if (!prof) return;
8396
- const kind = cell.dataset.kind;
8397
- if (kind === 'group') {
8398
- const g = cell.dataset.group;
8399
- if ((prof.groups || []).includes(g)) {
8400
- prof.groups = prof.groups.filter(x => x !== g);
8401
- } else {
8402
- prof.groups = [...(prof.groups || []), g];
8403
- }
8404
- } else {
8405
- const s = cell.dataset.server;
8406
- if ((prof.servers || []).includes(s)) {
8407
- prof.servers = prof.servers.filter(x => x !== s);
8408
- } else {
8409
- prof.servers = [...(prof.servers || []), s];
8410
- }
8411
- }
8412
- await fetch(`/api/mcp/registry/profiles/${encodeURIComponent(profName)}`, {
8413
- method: 'PUT', headers: { 'Content-Type': 'application/json' },
8414
- body: JSON.stringify(prof),
8415
- });
8416
- await refreshMcpTab();
8417
- });
8418
- });
8419
-
8420
- // Activate profile on name click
8421
- wrap.querySelectorAll('.stg-mx-profile-head').forEach(head => {
8422
- head.addEventListener('click', async (e) => {
8423
- if (e.target.closest('.stg-mx-profile-del')) return;
8424
- const name = head.dataset.profile;
8425
- if (name === _mcpRegistry.activeProfile) return;
8426
- await fetch('/api/mcp/registry/activate', {
8427
- method: 'POST', headers: { 'Content-Type': 'application/json' },
8428
- body: JSON.stringify({ profile: name }),
8429
- });
8430
- showCCToast(kit.tx('settings.redesign.mcp.profileSwitchedTo', { name: name }));
8431
- await refreshMcpTab();
8432
- });
8433
- });
8434
-
8435
- // Delete profile
8436
- wrap.querySelectorAll('.stg-mx-profile-del').forEach(btn => {
8437
- btn.addEventListener('click', async (e) => {
8438
- e.stopPropagation();
8439
- await fetch(`/api/mcp/registry/profiles/${encodeURIComponent(btn.dataset.profile)}`, { method: 'DELETE' });
8440
- await refreshMcpTab();
8441
- });
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();
8442
8609
  });
8443
-
8444
- // Inline add profile
8445
- const addBtn = wrap.querySelector('.stg-mx-add-btn');
8446
- if (addBtn) {
8447
- addBtn.addEventListener('click', () => {
8448
- const th = addBtn.closest('th');
8449
- th.innerHTML = `<input class="stg-mx-inline-input" type="text" placeholder="${kit.te('settings.redesign.mcp.name')}" autofocus>`;
8450
- const input = th.querySelector('input');
8451
- input.focus();
8452
- const create = async () => {
8453
- const name = input.value.trim();
8454
- if (!name) { await refreshMcpTab(); return; }
8455
- await fetch(`/api/mcp/registry/profiles/${encodeURIComponent(name)}`, {
8456
- method: 'PUT', headers: { 'Content-Type': 'application/json' },
8457
- body: JSON.stringify({ label: name, groups: [], servers: [] }),
8458
- });
8459
- await refreshMcpTab();
8460
- };
8461
- input.addEventListener('keydown', (e) => {
8462
- if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); create(); }
8463
- if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); refreshMcpTab(); }
8464
- });
8465
- input.addEventListener('blur', create);
8466
- });
8467
- }
8468
8610
  }
8469
8611
 
8470
8612
  function renderAlwaysOn() {
8471
8613
  const el = overlay.querySelector('#stg-mcp-always-on');
8472
- if (!el || !_mcpRegistry) return;
8473
- const tg = _mcpRegistry.toolGroups || {};
8474
- const alwaysOn = Object.entries(tg).filter(([, info]) => info.alwaysOn);
8475
- if (!alwaysOn.length) { el.innerHTML = ''; return; }
8476
- el.innerHTML = `
8477
- <div class="stg-mcp-section-header">${kit.te('settings.redesign.mcp.alwaysOn')}</div>
8478
- <div class="stg-mx-always-on-row">
8479
- ${alwaysOn.map(([, info]) =>
8480
- `<span class="stg-mx-always-chip">${esc(info.label)}<span class="stg-mx-grp-count">${info.tools}</span></span>`
8481
- ).join('')}
8482
- </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>` : '';
8483
8616
  }
8484
8617
 
8485
8618
  function renderExtServerList() {
8486
8619
  const el = overlay.querySelector('#stg-mcp-ext-servers');
8487
8620
  if (!el || !_mcpRegistry) return;
8488
- const servers = _mcpRegistry.servers || {};
8489
- 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 }; }
8490
8623
  const entries = Object.entries(servers).filter(([, info]) => !info.builtin);
8491
8624
  if (!entries.length) {
8492
8625
  el.innerHTML = '<div class="stg-mcp-empty-note">' + kit.te('settings.redesign.mcp.noExternalServersConfigured') + '</div>';
@@ -8501,38 +8634,31 @@ async function openSettingsModalNow(options = {}) {
8501
8634
  <span class="stg-mx-ext-name">${esc(name)}</span>
8502
8635
  <span class="stg-mx-ext-type">${esc(info.type || '?')}</span>
8503
8636
  </div>
8504
- <div class="stg-mx-ext-sub">${kit.te('settings.redesign.mcp.availableToAnyProfileYouEnable')}</div>
8637
+ <div class="stg-mx-ext-sub">${esc(toolWord('agentAccessHelp'))}</div>
8505
8638
  </div>
8506
- <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>`}
8507
8640
  </div>
8508
- <div class="stg-mx-ext-profiles">
8509
- ${profiles.map(([profName, prof]) => {
8510
- const isOn = (prof.servers || []).includes(name);
8511
- 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)}">
8512
- <span class="stg-mx-ext-toggle-name">${esc(prof.label || profName)}</span>
8513
- </button>`;
8514
- }).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>
8515
8643
  </div>
8516
8644
  </div>`
8517
8645
  ).join('');
8518
8646
 
8519
- el.querySelectorAll('.stg-mx-ext-toggle').forEach(btn => {
8520
- btn.addEventListener('click', async () => {
8521
- const profName = btn.dataset.profile;
8522
- const srvName = btn.dataset.server;
8523
- const prof = _mcpRegistry.profiles[profName];
8524
- if (!prof || !srvName) return;
8525
- if ((prof.servers || []).includes(srvName)) {
8526
- prof.servers = prof.servers.filter(x => x !== srvName);
8527
- } else {
8528
- prof.servers = [...(prof.servers || []), srvName];
8529
- }
8530
- await fetch(`/api/mcp/registry/profiles/${encodeURIComponent(profName)}`, {
8531
- method: 'PUT',
8532
- headers: { 'Content-Type': 'application/json' },
8533
- body: JSON.stringify(prof),
8534
- });
8535
- 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; }
8536
8662
  });
8537
8663
  });
8538
8664
 
@@ -8687,7 +8813,8 @@ async function openSettingsModalNow(options = {}) {
8687
8813
 
8688
8814
  async function refreshMcpTab() {
8689
8815
  await fetchMcpRegistry();
8690
- renderMcpMatrix();
8816
+ _settingsToolCatalog = await fetchToolCatalog(true);
8817
+ renderDefaultTools();
8691
8818
  renderAlwaysOn();
8692
8819
  renderExtServerList();
8693
8820
  renderPluginsList();
@@ -9199,55 +9326,62 @@ async function openSettingsModalNow(options = {}) {
9199
9326
  });
9200
9327
 
9201
9328
  // ── Config save helpers ──
9329
+ // PATCH /api/opencode/config (OpenCode 2's global opencode.json): the answer is the configuration as it is now.
9202
9330
  async function saveOcpConfig(patch) {
9203
9331
  try {
9204
- await fetch('/api/opencode/config', {
9332
+ const resp = await fetch('/api/opencode/config', {
9205
9333
  method: 'PATCH', headers: { 'Content-Type': 'application/json' },
9206
9334
  body: JSON.stringify(patch),
9207
9335
  });
9208
- } 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
+ }
9209
9345
  }
9210
9346
 
9211
9347
  // ── Compaction handlers ──
9212
9348
  const compactAutoEl = overlay.querySelector('#stg-ocp-compact-auto');
9213
- const compactPruneEl = overlay.querySelector('#stg-ocp-compact-prune');
9214
9349
  const compactReservedEl = overlay.querySelector('#stg-ocp-compact-reserved');
9215
9350
 
9351
+ // An emptied field is OpenCode's own default again (buffer: null).
9216
9352
  function saveCompaction() {
9217
- saveOcpConfig({
9218
- compaction: {
9219
- auto: compactAutoEl?.checked ?? true,
9220
- prune: compactPruneEl?.checked ?? true,
9221
- reserved: Number(compactReservedEl?.value) || 10000,
9222
- },
9223
- });
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 } });
9224
9357
  }
9225
9358
  compactAutoEl?.addEventListener('change', saveCompaction);
9226
- compactPruneEl?.addEventListener('change', saveCompaction);
9227
9359
  compactReservedEl?.addEventListener('change', saveCompaction);
9228
9360
 
9229
- // ── Snapshot toggle (persists to config.json; read by OpenCode at startup) ──
9361
+ // ── Snapshot toggle (OpenCode's `snapshots`, read at once) ──
9230
9362
  const snapshotToggle = overlay.querySelector('#stg-ocp-snapshot-toggle');
9231
- if (snapshotToggle) {
9232
- // Reflect the persisted state on load (defaults on).
9233
- fetch('/api/opencode/snapshot')
9234
- .then(r => r.json())
9235
- .then(d => { if (d && d.ok) snapshotToggle.classList.toggle('on', d.snapshot !== false); })
9236
- .catch(() => {});
9237
- snapshotToggle.addEventListener('click', async () => {
9238
- const next = !snapshotToggle.classList.contains('on');
9239
- snapshotToggle.classList.toggle('on', next); // optimistic
9240
- try {
9241
- const r = await fetch('/api/opencode/snapshot', {
9242
- method: 'POST', headers: { 'Content-Type': 'application/json' },
9243
- body: JSON.stringify({ enabled: next }),
9244
- });
9245
- const d = await r.json();
9246
- if (!d || !d.ok) throw new Error(d?.error || 'failed');
9247
- } catch {
9248
- snapshotToggle.classList.toggle('on', !next); // revert on failure
9249
- }
9250
- });
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);
9251
9385
  }
9252
9386
 
9253
9387
  // Capability filter pills — use CSS vars, not inline color
@@ -9285,28 +9419,16 @@ async function openSettingsModalNow(options = {}) {
9285
9419
  });
9286
9420
 
9287
9421
  // ── Open config file in Synabun's editor ──
9422
+ // OpenCode 2's global opencode.json (made with its $schema when there is none yet).
9288
9423
  overlay.querySelector('#stg-ocp-open-config')?.addEventListener('click', async () => {
9289
9424
  try {
9290
- let filePath = '';
9291
-
9292
- const revealResp = await fetch('/api/opencode/config/reveal', { method: 'POST' });
9293
- let revealData = null;
9294
- try { revealData = await revealResp.json(); } catch {}
9295
-
9296
- if (revealResp.ok && revealData?.path) {
9297
- const basePath = String(revealData.path).replace(/\\/g, '/').replace(/\/+$/, '');
9298
- filePath = basePath.endsWith('/config.json') ? basePath : `${basePath}/config.json`;
9299
- } else {
9300
- const fileResp = await fetch('/api/opencode/config/file');
9301
- const fileData = await fileResp.json();
9302
- if (!fileResp.ok || !fileData?.path) {
9303
- showCCToast(fileData?.error || revealData?.error || kit.tx('settings.redesign.opencode.failedToOpenOpencodeConfig'));
9304
- return;
9305
- }
9306
- 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;
9307
9430
  }
9308
-
9309
- emit('open-file-editor', { filePath });
9431
+ emit('open-file-editor', { filePath: fileData.path });
9310
9432
  } catch (err) {
9311
9433
  console.error('[settings] Failed to open OpenCode config:', err);
9312
9434
  showCCToast(kit.tx('settings.redesign.opencode.failedToOpenOpencodeConfig'));
@@ -9334,38 +9456,46 @@ async function openSettingsModalNow(options = {}) {
9334
9456
  } catch {}
9335
9457
  }
9336
9458
 
9337
- // ── Load config values into UI ──
9459
+ // ── Load config values into UI (read from the files: OpenCode need not run) ──
9338
9460
  async function loadOcpConfig() {
9339
- try {
9340
- const resp = await fetch('/api/opencode/config');
9341
- const data = await resp.json();
9342
- _ocpConfig = data.data || {};
9343
- const comp = _ocpConfig.compaction || {};
9344
- if (compactAutoEl) compactAutoEl.checked = comp.auto !== false;
9345
- if (compactPruneEl) compactPruneEl.checked = comp.prune !== false;
9346
- if (compactReservedEl) compactReservedEl.value = comp.reserved || 10000;
9347
- } 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;
9348
9473
  }
9349
9474
 
9350
9475
  // ── Master refresh ──
9351
9476
  async function refreshOcpStatus() {
9352
9477
  setOcpStatus(kit.tx('settings.redesign.opencode.checking'), 'dim');
9478
+ loadOcpConfig();
9353
9479
  try {
9354
9480
  const resp = await fetch('/api/opencode/status');
9355
9481
  const data = await resp.json();
9482
+ const note = renderOcpInstall(data.installed);
9356
9483
  if (data.clearingHistory) {
9357
9484
  setOcpStatus(kit.tx('settings.redesign.opencode.clearingHistory'), 'warn');
9358
9485
  } else if (data.running) {
9359
9486
  setOcpStatus(kit.tx('settings.redesign.opencode.runningVersion', { version: data.version ? ' v' + data.version : '' }), 'ok');
9360
- await Promise.all([
9361
- refreshOcpProviders(),
9362
- loadOcpConfig(),
9363
- renderToolPermissions(),
9364
- ]);
9487
+ await refreshOcpProviders();
9365
9488
  } else {
9366
- 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');
9367
9492
  if (ocpProvidersEl) ocpProvidersEl.innerHTML = '<div class="settings-hint" style="opacity:0.4">' + kit.te('settings.redesign.opencode.startTheServerToSeeProviders') + '</div>';
9368
- 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
+ }
9369
9499
  }
9370
9500
  } catch (err) {
9371
9501
  setOcpStatus(kit.tx('settings.redesign.opencode.error'), 'err');
@@ -9635,3 +9765,5 @@ export function initSettings() {
9635
9765
  window.checkSyncStatus = checkSyncStatus;
9636
9766
  window.copySyncPrompt = copySyncPrompt;
9637
9767
  }
9768
+
9769
+ configureToolPickerTranslation((key, params) => kit.tx(key, params));