synabun 2.0.10 → 2.2.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 (656) hide show
  1. package/CHANGELOG.md +207 -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 +13 -13
  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 +30 -17
  11. package/lib/desktop-app.js +7 -1
  12. package/lib/external-tools.js +7 -6
  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 +302 -0
  24. package/lib/virustotal.mjs +203 -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/budget-judgments.d.ts +1 -1
  37. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
  38. package/mcp-server/dist/services/budget-judgments.js +5 -3
  39. package/mcp-server/dist/services/budget-judgments.js.map +1 -1
  40. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  41. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  42. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  43. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  44. package/mcp-server/dist/services/identity.d.ts +2 -2
  45. package/mcp-server/dist/services/identity.js +2 -2
  46. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  47. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  48. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  49. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  50. package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
  51. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
  52. package/mcp-server/dist/services/memory-sessions.js +28 -2
  53. package/mcp-server/dist/services/memory-sessions.js.map +1 -1
  54. package/mcp-server/dist/services/neural-interface.d.ts +18 -11
  55. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  56. package/mcp-server/dist/services/neural-interface.js +67 -11
  57. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  58. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  59. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  60. package/mcp-server/dist/services/server-instructions.js +22 -63
  61. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  62. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  63. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  64. package/mcp-server/dist/services/tool-catalog.js +339 -0
  65. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  66. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  67. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  68. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  69. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  70. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  71. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  72. package/mcp-server/dist/services/tool-list.js +334 -0
  73. package/mcp-server/dist/services/tool-list.js.map +1 -0
  74. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  75. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  76. package/mcp-server/dist/services/tool-runtime.js +400 -0
  77. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  78. package/mcp-server/dist/services/typesafe-config.js +1 -1
  79. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  80. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  81. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  82. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  83. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  84. package/mcp-server/dist/tools/agents.d.ts +14 -14
  85. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  86. package/mcp-server/dist/tools/agents.js +29 -19
  87. package/mcp-server/dist/tools/agents.js.map +1 -1
  88. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  89. package/mcp-server/dist/tools/choice.d.ts +6 -0
  90. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  91. package/mcp-server/dist/tools/choice.js +18 -13
  92. package/mcp-server/dist/tools/choice.js.map +1 -1
  93. package/mcp-server/dist/tools/git-tools.js +2 -2
  94. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  95. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  96. package/mcp-server/dist/tools/image-tools.js +1 -1
  97. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  98. package/mcp-server/dist/tools/morelogin.js +9 -6
  99. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  100. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  101. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  102. package/mcp-server/dist/tools/skill-tools.js +642 -0
  103. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  104. package/mcp-server/dist/tools/skill.d.ts +7 -0
  105. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  106. package/mcp-server/dist/tools/skill.js +11 -0
  107. package/mcp-server/dist/tools/skill.js.map +1 -0
  108. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  109. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  110. package/mcp-server/dist/tools/tool-groups.js +147 -0
  111. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  112. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  113. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  114. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  115. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  116. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  117. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  118. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  119. package/mcp-server/package-lock.json +2 -2
  120. package/mcp-server/package.json +1 -1
  121. package/mcp-server/src/http.ts +75 -12
  122. package/mcp-server/src/index.ts +147 -154
  123. package/mcp-server/src/preload.ts +12 -12
  124. package/mcp-server/src/services/budget-judgments.ts +5 -3
  125. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  126. package/mcp-server/src/services/identity.ts +2 -2
  127. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  128. package/mcp-server/src/services/memory-sessions.ts +24 -3
  129. package/mcp-server/src/services/neural-interface.ts +81 -21
  130. package/mcp-server/src/services/server-instructions.ts +29 -64
  131. package/mcp-server/src/services/tool-catalog.ts +403 -0
  132. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  133. package/mcp-server/src/services/tool-list.ts +410 -0
  134. package/mcp-server/src/services/tool-runtime.ts +457 -0
  135. package/mcp-server/src/services/typesafe-config.ts +1 -1
  136. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  137. package/mcp-server/src/tools/agents.ts +29 -20
  138. package/mcp-server/src/tools/choice.ts +17 -12
  139. package/mcp-server/src/tools/git-tools.ts +2 -2
  140. package/mcp-server/src/tools/image-tools.ts +1 -1
  141. package/mcp-server/src/tools/morelogin.ts +6 -4
  142. package/mcp-server/src/tools/skill-tools.ts +592 -0
  143. package/mcp-server/src/tools/skill.ts +13 -0
  144. package/mcp-server/src/tools/tool-groups.ts +164 -0
  145. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  146. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  147. package/neural-interface/i18n/en.json +3953 -228
  148. package/neural-interface/i18n/pt-BR.json +3901 -179
  149. package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
  150. package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
  151. package/neural-interface/lib/antigravity/acp-connection.js +299 -0
  152. package/neural-interface/lib/antigravity/bypass.js +100 -0
  153. package/neural-interface/lib/antigravity/catalog.js +273 -0
  154. package/neural-interface/lib/antigravity/home.js +260 -0
  155. package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
  156. package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
  157. package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
  158. package/neural-interface/lib/antigravity/policy.js +74 -0
  159. package/neural-interface/lib/antigravity/tool-call.js +178 -0
  160. package/neural-interface/lib/antigravity/transcripts.js +252 -0
  161. package/neural-interface/lib/antigravity/usage.js +43 -0
  162. package/neural-interface/lib/assistant-api.js +128 -28
  163. package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
  164. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  165. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  166. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  167. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  168. package/neural-interface/lib/assistant-brains/opencode.js +120 -54
  169. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
  170. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
  171. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  172. package/neural-interface/lib/assistant-budget-split.js +42 -15
  173. package/neural-interface/lib/assistant-catalog.js +81 -15
  174. package/neural-interface/lib/assistant-config.js +6 -3
  175. package/neural-interface/lib/assistant-dispatch.js +1073 -200
  176. package/neural-interface/lib/assistant-envelope.js +156 -2
  177. package/neural-interface/lib/assistant-evidence.js +80 -0
  178. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  179. package/neural-interface/lib/assistant-persona.js +93 -19
  180. package/neural-interface/lib/assistant-plan-permissions.js +25 -9
  181. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  182. package/neural-interface/lib/assistant-route-gate.js +6 -4
  183. package/neural-interface/lib/assistant-router.js +245 -38
  184. package/neural-interface/lib/assistant-runtime.js +214 -30
  185. package/neural-interface/lib/assistant-task-prompt.js +87 -21
  186. package/neural-interface/lib/assistant-tools.js +51 -0
  187. package/neural-interface/lib/assistant-usage.js +29 -0
  188. package/neural-interface/lib/backup-service.js +626 -4
  189. package/neural-interface/lib/backup-zip-reader.js +10 -5
  190. package/neural-interface/lib/backup-zip-writer.js +257 -0
  191. package/neural-interface/lib/browser-execution.js +10 -2
  192. package/neural-interface/lib/browser-guard-cdp.js +84 -0
  193. package/neural-interface/lib/browser-network-guard.js +476 -0
  194. package/neural-interface/lib/browser-policy-roots.js +64 -0
  195. package/neural-interface/lib/browser-tool-policy.js +198 -38
  196. package/neural-interface/lib/claude-agent-bridge.js +11 -2
  197. package/neural-interface/lib/claude-history-cache.js +210 -0
  198. package/neural-interface/lib/claude-hooks-status.js +103 -0
  199. package/neural-interface/lib/claude-model-catalog.js +5 -1
  200. package/neural-interface/lib/claude-temporary.js +5 -11
  201. package/neural-interface/lib/cli-install-api.js +1 -1
  202. package/neural-interface/lib/cli-install.js +163 -154
  203. package/neural-interface/lib/cli-resolver.js +13 -3
  204. package/neural-interface/lib/cli-update-planner.js +52 -41
  205. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  206. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  207. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  208. package/neural-interface/lib/codex-worker-app-server.js +191 -0
  209. package/neural-interface/lib/confined-fs.js +30 -3
  210. package/neural-interface/lib/crash-notice-api.js +98 -0
  211. package/neural-interface/lib/editor-completion-opencode.js +68 -68
  212. package/neural-interface/lib/editor-git-api.js +3 -20
  213. package/neural-interface/lib/exec-loop-command.js +13 -10
  214. package/neural-interface/lib/file-explorer-api.js +74 -40
  215. package/neural-interface/lib/file-explorer-watch.js +9 -3
  216. package/neural-interface/lib/git-bash/install.js +52 -6
  217. package/neural-interface/lib/git-bash/manager.js +59 -17
  218. package/neural-interface/lib/git-bash/pins.js +12 -0
  219. package/neural-interface/lib/git-read.js +2 -2
  220. package/neural-interface/lib/git-status-runner.js +390 -0
  221. package/neural-interface/lib/git-status-watchdog.js +40 -0
  222. package/neural-interface/lib/greeting/budgets.js +1 -0
  223. package/neural-interface/lib/greeting/engine.js +2 -2
  224. package/neural-interface/lib/greeting/sources.js +10 -12
  225. package/neural-interface/lib/greeting/writer.js +1 -1
  226. package/neural-interface/lib/http-guards.js +8 -2
  227. package/neural-interface/lib/install-details.js +4 -1
  228. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  229. package/neural-interface/lib/mcp-client-config.js +35 -33
  230. package/neural-interface/lib/mcp-external-servers.js +332 -0
  231. package/neural-interface/lib/mcp-installer.js +2 -1
  232. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  233. package/neural-interface/lib/memory-session-capture.js +17 -3
  234. package/neural-interface/lib/morelogin.js +177 -25
  235. package/neural-interface/lib/native-loop-providers.js +965 -66
  236. package/neural-interface/lib/native-loop-runtime.js +290 -42
  237. package/neural-interface/lib/opencode-config.js +557 -0
  238. package/neural-interface/lib/opencode-db-schema.js +17 -34
  239. package/neural-interface/lib/opencode-history.js +48 -218
  240. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  241. package/neural-interface/lib/opencode-native-boundary.js +53 -0
  242. package/neural-interface/lib/opencode-protocol.js +249 -112
  243. package/neural-interface/lib/opencode-runtime-config.js +32 -22
  244. package/neural-interface/lib/opencode-serve-records.js +39 -21
  245. package/neural-interface/lib/opencode-v2-client.js +48 -153
  246. package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
  247. package/neural-interface/lib/opencode-worker-data.js +27 -0
  248. package/neural-interface/lib/opencode2-client.js +73 -37
  249. package/neural-interface/lib/opencode2-proxy.js +23 -63
  250. package/neural-interface/lib/opencode2-translate.js +51 -2
  251. package/neural-interface/lib/owner-authority.js +249 -0
  252. package/neural-interface/lib/page-session.js +275 -0
  253. package/neural-interface/lib/path-confine.js +12 -9
  254. package/neural-interface/lib/peer-identity.js +793 -0
  255. package/neural-interface/lib/pty-host/core.js +28 -1
  256. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  257. package/neural-interface/lib/pty-host/manager.js +12 -1
  258. package/neural-interface/lib/remote-policy.js +1 -1
  259. package/neural-interface/lib/request-gate.js +20 -2
  260. package/neural-interface/lib/rulesets/api.js +7 -6
  261. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  262. package/neural-interface/lib/rulesets/installer.js +180 -36
  263. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  264. package/neural-interface/lib/rulesets/render.js +1 -1
  265. package/neural-interface/lib/session-cache-builder.js +81 -31
  266. package/neural-interface/lib/session-title-generator.js +58 -36
  267. package/neural-interface/lib/skills/api-v2.js +495 -0
  268. package/neural-interface/lib/skills/bundle.js +418 -0
  269. package/neural-interface/lib/skills/catalog.js +298 -0
  270. package/neural-interface/lib/skills/creation-api.js +191 -0
  271. package/neural-interface/lib/skills/creation-brief.js +247 -0
  272. package/neural-interface/lib/skills/creation-sources.js +285 -0
  273. package/neural-interface/lib/skills/creation.js +843 -0
  274. package/neural-interface/lib/skills/deploy.js +569 -0
  275. package/neural-interface/lib/skills/digest.js +119 -0
  276. package/neural-interface/lib/skills/discover.js +111 -0
  277. package/neural-interface/lib/skills/draft-confinement.js +216 -0
  278. package/neural-interface/lib/skills/eval-confinement.js +399 -0
  279. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  280. package/neural-interface/lib/skills/eval-trace.js +135 -0
  281. package/neural-interface/lib/skills/evals-api.js +212 -0
  282. package/neural-interface/lib/skills/evals.js +1573 -0
  283. package/neural-interface/lib/skills/export.js +203 -0
  284. package/neural-interface/lib/skills/frontmatter.js +64 -0
  285. package/neural-interface/lib/skills/health.js +75 -0
  286. package/neural-interface/lib/skills/json-edit.js +165 -0
  287. package/neural-interface/lib/skills/known-bridge-digests.json +441 -0
  288. package/neural-interface/lib/skills/known-bridges.js +96 -0
  289. package/neural-interface/lib/skills/library.js +823 -0
  290. package/neural-interface/lib/skills/local-store.js +21 -0
  291. package/neural-interface/lib/skills/migration.js +433 -0
  292. package/neural-interface/lib/skills/operations.js +45 -0
  293. package/neural-interface/lib/skills/ownership.js +427 -0
  294. package/neural-interface/lib/skills/packages.js +88 -0
  295. package/neural-interface/lib/skills/reviews.js +212 -0
  296. package/neural-interface/lib/skills/revision.js +80 -0
  297. package/neural-interface/lib/skills/roots.js +269 -0
  298. package/neural-interface/lib/skills/runtime-checks.js +358 -0
  299. package/neural-interface/lib/skills/scan-assets.js +252 -0
  300. package/neural-interface/lib/skills/scan-context.js +26 -0
  301. package/neural-interface/lib/skills/scan-shell.js +324 -0
  302. package/neural-interface/lib/skills/scan.js +364 -0
  303. package/neural-interface/lib/skills/scanners.js +54 -0
  304. package/neural-interface/lib/skills/service.js +530 -0
  305. package/neural-interface/lib/skills/telemetry.js +119 -0
  306. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  307. package/neural-interface/lib/synabun-version.js +1 -1
  308. package/neural-interface/lib/system-activity.js +1 -1
  309. package/neural-interface/lib/tool-lists.js +124 -0
  310. package/neural-interface/lib/whatsapp/brain.js +7 -3
  311. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  312. package/neural-interface/lib/whatsapp/cards.js +22 -9
  313. package/neural-interface/lib/whatsapp/config.js +1 -1
  314. package/neural-interface/lib/worker-confinement.js +1268 -0
  315. package/neural-interface/lib/worker-guard-setting.js +56 -0
  316. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  317. package/neural-interface/lib/worker-question.js +74 -0
  318. package/neural-interface/lib/worker-relay-ports.js +227 -0
  319. package/neural-interface/lib/worker-resume.js +238 -0
  320. package/neural-interface/lib/worker-sandbox.js +329 -0
  321. package/neural-interface/package-lock.json +34 -12
  322. package/neural-interface/package.json +3 -2
  323. package/neural-interface/public/index.html +3 -1
  324. package/neural-interface/public/index2d.html +19 -11
  325. package/neural-interface/public/onboarding.html +3 -3
  326. package/neural-interface/public/shared/account-menu.js +8 -5
  327. package/neural-interface/public/shared/agent-runtime-options.js +125 -31
  328. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  329. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  330. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  331. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  332. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  333. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  334. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  335. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  336. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  337. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  338. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  339. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  340. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  341. package/neural-interface/public/shared/api.js +1527 -1336
  342. package/neural-interface/public/shared/app-frame.js +192 -0
  343. package/neural-interface/public/shared/assistant/asst-agents-dock.js +36 -6
  344. package/neural-interface/public/shared/assistant/asst-api.js +1 -1
  345. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  346. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  347. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  348. package/neural-interface/public/shared/assistant/asst-brain-picker.js +105 -44
  349. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  350. package/neural-interface/public/shared/assistant/asst-control.js +205 -26
  351. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  352. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  353. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  354. package/neural-interface/public/shared/assistant/asst-limit-dock.js +45 -0
  355. package/neural-interface/public/shared/assistant/asst-limit-wait.js +327 -0
  356. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  357. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  358. package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
  359. package/neural-interface/public/shared/assistant/asst-panel.js +151 -19
  360. package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
  361. package/neural-interface/public/shared/assistant/asst-render.js +445 -82
  362. package/neural-interface/public/shared/assistant/asst-route.js +304 -53
  363. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  364. package/neural-interface/public/shared/assistant/asst-scenes.js +56 -1
  365. package/neural-interface/public/shared/assistant/asst-sidepanel.js +117 -10
  366. package/neural-interface/public/shared/assistant/asst-slash.js +17 -9
  367. package/neural-interface/public/shared/assistant/asst-state.js +87 -24
  368. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  369. package/neural-interface/public/shared/assistant/asst-styles.js +93 -21
  370. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +190 -23
  371. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  372. package/neural-interface/public/shared/automation-context.js +5 -2
  373. package/neural-interface/public/shared/cc-dropdown.js +89 -19
  374. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  375. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  376. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  377. package/neural-interface/public/shared/cdx/cdx-output.js +34 -10
  378. package/neural-interface/public/shared/cdx/cdx-panel.js +59 -151
  379. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  380. package/neural-interface/public/shared/cdx/cdx-protocol.js +39 -11
  381. package/neural-interface/public/shared/cdx/cdx-render.js +57 -38
  382. package/neural-interface/public/shared/cdx/cdx-requests.js +14 -4
  383. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  384. package/neural-interface/public/shared/cdx/cdx-styles.js +15 -3
  385. package/neural-interface/public/shared/cdx/cdx-tabs.js +127 -44
  386. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  387. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  388. package/neural-interface/public/shared/cli-install-view.js +11 -12
  389. package/neural-interface/public/shared/cli-status.js +11 -10
  390. package/neural-interface/public/shared/constants.js +5 -4
  391. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  392. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  393. package/neural-interface/public/shared/context-menu-model.js +367 -0
  394. package/neural-interface/public/shared/context-menu.js +372 -0
  395. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  396. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  397. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  398. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  399. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  400. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  401. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  402. package/neural-interface/public/shared/desktop-update-view.js +21 -0
  403. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  404. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  405. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  406. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  407. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  408. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  409. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  410. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  411. package/neural-interface/public/shared/html-shell.js +30 -15
  412. package/neural-interface/public/shared/i18n.js +2 -0
  413. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  414. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  415. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  416. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
  417. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  418. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  419. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  420. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  421. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  422. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  423. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  424. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +74 -259
  425. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  426. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
  427. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  428. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +13 -22
  429. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
  430. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  431. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
  432. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  433. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  434. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  435. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  436. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -21
  437. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  438. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +60 -50
  439. package/neural-interface/public/shared/page-session.js +96 -0
  440. package/neural-interface/public/shared/provider-icons.js +4 -2
  441. package/neural-interface/public/shared/registry.js +5 -3
  442. package/neural-interface/public/shared/retired-2d.js +3 -0
  443. package/neural-interface/public/shared/schedules-view.js +195 -61
  444. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  445. package/neural-interface/public/shared/settings/settings-data.js +3 -1
  446. package/neural-interface/public/shared/settings/settings-ia.js +6 -6
  447. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  448. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  449. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  450. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  451. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  452. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  453. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  454. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  455. package/neural-interface/public/shared/skills/ss-creation.js +222 -0
  456. package/neural-interface/public/shared/skills/ss-dialog.js +133 -0
  457. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  458. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  459. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  460. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  461. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  462. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  463. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  464. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  465. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  466. package/neural-interface/public/shared/skills/ss-lint.js +149 -0
  467. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  468. package/neural-interface/public/shared/skills/ss-profiles.js +513 -0
  469. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  470. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  471. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  472. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  473. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  474. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  475. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  476. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +237 -0
  477. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  478. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  479. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  480. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  481. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  482. package/neural-interface/public/shared/skills/studio/sx-dom.js +230 -0
  483. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  484. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  485. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  486. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  487. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  488. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  489. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  490. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  491. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +282 -0
  492. package/neural-interface/public/shared/skills/studio/sx-install.js +313 -0
  493. package/neural-interface/public/shared/skills/studio/sx-library-view.js +748 -0
  494. package/neural-interface/public/shared/skills/studio/sx-library.js +479 -0
  495. package/neural-interface/public/shared/skills/studio/sx-main.js +690 -0
  496. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  497. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  498. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  499. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  500. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  501. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  502. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  503. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  504. package/neural-interface/public/shared/skills/studio/sx-subject.js +249 -0
  505. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  506. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  507. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  508. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +426 -0
  509. package/neural-interface/public/shared/state.js +1 -1
  510. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  511. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  512. package/neural-interface/public/shared/styles.css +1822 -1756
  513. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  514. package/neural-interface/public/shared/text-cache.js +29 -0
  515. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  516. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  517. package/neural-interface/public/shared/tool-list-view.js +112 -0
  518. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  519. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  520. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  521. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  522. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  523. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  524. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  525. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  526. package/neural-interface/public/shared/tour/tour.css +144 -0
  527. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  528. package/neural-interface/public/shared/ui-assistant.js +2 -0
  529. package/neural-interface/public/shared/ui-automation-studio.js +111 -140
  530. package/neural-interface/public/shared/ui-claude-panel.js +376 -82
  531. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  532. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  533. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  534. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  535. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  536. package/neural-interface/public/shared/ui-detail.js +15 -3
  537. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  538. package/neural-interface/public/shared/ui-file-links.js +2 -2
  539. package/neural-interface/public/shared/ui-games.js +2 -2
  540. package/neural-interface/public/shared/ui-image-gallery.js +50 -47
  541. package/neural-interface/public/shared/ui-judgments.js +1 -1
  542. package/neural-interface/public/shared/ui-keybinds.js +33 -2
  543. package/neural-interface/public/shared/ui-link.js +3 -3
  544. package/neural-interface/public/shared/ui-menubar.js +10 -20
  545. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  546. package/neural-interface/public/shared/ui-native-loop-router.js +11 -1
  547. package/neural-interface/public/shared/ui-navbar.js +49 -24
  548. package/neural-interface/public/shared/ui-notifications.js +1 -0
  549. package/neural-interface/public/shared/ui-panels.js +96 -65
  550. package/neural-interface/public/shared/ui-resume.js +9 -7
  551. package/neural-interface/public/shared/ui-schedules-studio.js +546 -431
  552. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  553. package/neural-interface/public/shared/ui-sessions.js +27 -15
  554. package/neural-interface/public/shared/ui-settings.js +683 -526
  555. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  556. package/neural-interface/public/shared/ui-sidepanel-sessions.js +4 -4
  557. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  558. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  559. package/neural-interface/public/shared/ui-sidepanel-windows.js +18 -13
  560. package/neural-interface/public/shared/ui-skills.js +33 -2830
  561. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  562. package/neural-interface/public/shared/ui-sync.js +15 -5
  563. package/neural-interface/public/shared/ui-terminal.js +270 -190
  564. package/neural-interface/public/shared/ui-tool-updates.js +6 -4
  565. package/neural-interface/public/shared/ui-tooltip.js +81 -51
  566. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  567. package/neural-interface/public/shared/ui-trash.js +6 -4
  568. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  569. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  570. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
  571. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  572. package/neural-interface/public/shared/ui-tutorial.js +285 -509
  573. package/neural-interface/public/shared/ui-update.js +38 -2
  574. package/neural-interface/public/shared/ui-whiteboard.js +312 -59
  575. package/neural-interface/public/shared/ui-worker-end.js +5 -1
  576. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  577. package/neural-interface/public/shared/view-variant.js +66 -0
  578. package/neural-interface/public/shared/whatsapp/wa-view.js +3 -3
  579. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  580. package/neural-interface/public/shared/worker-end.js +10 -3
  581. package/neural-interface/public/shared/worker-questions.js +34 -0
  582. package/neural-interface/public/shared/worker-restart.js +12 -0
  583. package/neural-interface/public/sidepanel-session.html +2 -2
  584. package/neural-interface/public/variant/2d/background.js +2 -1
  585. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  586. package/neural-interface/public/variant/2d/graph.js +42 -20
  587. package/neural-interface/public/variant/2d/hulls.js +14 -9
  588. package/neural-interface/public/variant/2d/lasso.js +11 -7
  589. package/neural-interface/public/variant/2d/main.js +32 -1
  590. package/neural-interface/public/variant/2d/minimap.js +11 -13
  591. package/neural-interface/public/variant/3d/main.js +69 -6
  592. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  593. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  594. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  595. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  596. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  597. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  598. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  599. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  600. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  601. package/neural-interface/scripts/build-vendor.mjs +32 -0
  602. package/neural-interface/server.js +4553 -2855
  603. package/neural-interface/templates/rulesets/README.md +1 -1
  604. package/neural-interface/templates/rulesets/core.md +1 -1
  605. package/neural-interface/templates/rulesets/legacy-hashes.json +23 -5
  606. package/neural-interface/templates/rulesets/manifest.json +8 -8
  607. package/npm-shrinkwrap.json +34 -12
  608. package/package.json +17 -3
  609. package/postinstall.js +9 -4
  610. package/preuninstall.js +1 -1
  611. package/setup.js +97 -322
  612. package/skills/skill-forge/SKILL.md +177 -0
  613. package/skills/skill-forge/evals/evals.json +54 -0
  614. package/skills/skill-forge/references/package.md +105 -0
  615. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  616. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  617. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  618. package/skills/synabun/SKILL.md +10 -7
  619. package/skills/synabun/modules/audit.md +1 -5
  620. package/skills/synabun/modules/changelog.md +1 -5
  621. package/skills/synabun/modules/idea.md +1 -1
  622. package/skills/synabun/modules/memorize.md +6 -9
  623. package/skills/synabun/modules/schedule.md +6 -6
  624. package/mcp-server/dist/services/profiles.d.ts +0 -120
  625. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  626. package/mcp-server/dist/services/profiles.js +0 -387
  627. package/mcp-server/dist/services/profiles.js.map +0 -1
  628. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  629. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  630. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  631. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  632. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  633. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  634. package/mcp-server/dist/tools/leonardo.js +0 -21
  635. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  636. package/mcp-server/dist/tools/profile.d.ts +0 -17
  637. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  638. package/mcp-server/dist/tools/profile.js +0 -114
  639. package/mcp-server/dist/tools/profile.js.map +0 -1
  640. package/mcp-server/src/services/profiles.ts +0 -428
  641. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  642. package/mcp-server/src/tools/leonardo.ts +0 -30
  643. package/mcp-server/src/tools/profile.ts +0 -122
  644. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  645. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  646. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  647. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  648. package/skills/leonardo/SKILL.md +0 -168
  649. package/skills/leonardo/icon.svg +0 -1
  650. package/skills/leonardo/modules/browser-guide.md +0 -378
  651. package/skills/leonardo/modules/image-prompter.md +0 -283
  652. package/skills/leonardo/modules/model-advisor.md +0 -100
  653. package/skills/leonardo/modules/prompt-library.md +0 -110
  654. package/skills/leonardo/modules/style-guide.md +0 -186
  655. package/skills/leonardo/modules/video-prompter.md +0 -317
  656. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -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, setDropdownItems, setDropdownValue } 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
 
@@ -89,7 +98,8 @@ const TAB_ICONS = {
89
98
  const COPY_ICON = '<svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
90
99
  const CHEVRON_ICON = '<svg class="cc-section-chevron" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>';
91
100
  const ANTHROPIC_ICON = '<svg viewBox="0 0 24 24" class="cc-provider-icon"><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fill="currentColor"/></svg>';
92
- const GEMINI_ICON = '<svg viewBox="0 0 24 24" class="cc-provider-icon"><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="currentColor"/></svg>';
101
+ // A line drawing: its paths carry fill="none" themselves, because `.cli-path-icon svg` fills every icon it holds.
102
+ const ANTIGRAVITY_ICON = '<svg viewBox="0 0 24 24" class="cc-provider-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 20 12 4l7.5 16" fill="none"/><path d="M8.2 20a3.8 3.8 0 0 1 7.6 0" fill="none"/></svg>';
93
103
  const OPENAI_ICON = '<svg viewBox="0 0 24 24" class="cc-provider-icon"><path d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z" fill="currentColor"/></svg>';
94
104
  const OPENCODE_ICON = '<svg viewBox="0 0 24 30" class="cc-provider-icon" fill="currentColor"><path d="M18 6H6V24H18V6ZM24 30H0V0H24V30Z"/></svg>';
95
105
  const BROWSER_ICON = '<svg viewBox="0 0 24 24" class="cc-provider-icon"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/><ellipse cx="12" cy="12" rx="4" ry="10" fill="none" stroke="currentColor" stroke-width="1.5"/><line x1="2" y1="12" x2="22" y2="12" stroke="currentColor" stroke-width="1.5"/><path d="M4.5 7h15M4.5 17h15" fill="none" stroke="currentColor" stroke-width="1"/></svg>';
@@ -113,6 +123,7 @@ function showCCToast(msg, duration = 3500) {
113
123
  // INTERFACE CUSTOMIZATION — config, presets, apply
114
124
  // ═══════════════════════════════════════════
115
125
 
126
+ // Optional legacy view preferences must never reintroduce a 2D selection.
116
127
  const IFACE_DEFAULTS = {
117
128
  visualizationEnabled: true,
118
129
  scale: 1,
@@ -161,7 +172,7 @@ const IFACE_SLIDERS = {
161
172
  export function loadIfaceConfig() {
162
173
  try {
163
174
  const raw = storage.getItem('neural-interface-config');
164
- if (raw) return { ...IFACE_DEFAULTS, ...JSON.parse(raw) };
175
+ if (raw) return { ...IFACE_DEFAULTS, ...normalizeViewConfig(JSON.parse(raw)) };
165
176
  // Migrate legacy ui-scale if present
166
177
  const legacyScale = storage.getItem('neural-ui-scale');
167
178
  if (legacyScale) {
@@ -174,7 +185,7 @@ export function loadIfaceConfig() {
174
185
  }
175
186
 
176
187
  export function saveIfaceConfig(cfg) {
177
- storage.setItem('neural-interface-config', JSON.stringify(cfg));
188
+ storage.setItem('neural-interface-config', JSON.stringify(normalizeViewConfig(cfg)));
178
189
  // Keep legacy key in sync for backwards compat
179
190
  storage.setItem('neural-ui-scale', cfg.scale);
180
191
  }
@@ -521,21 +532,29 @@ function buildNavHTML(activeId) {
521
532
  return html;
522
533
  }
523
534
 
524
- /** The nav rail's stand-in on a narrow panel: one labelled select that lists every page. */
535
+ /** The nav rail's stand-in on a narrow panel: one labelled dropdown that lists every page. */
525
536
  function buildPagePickerHTML(activeId) {
526
537
  const groups = SETTINGS_GROUPS.map(group => {
527
538
  const options = SETTINGS_PAGES.filter(p => p.group === group.id).map(page => {
528
539
  const keys = pageKeys(page);
529
- return `<option value="${page.id}"${page.id === activeId ? ' selected' : ''}>${kit.te(keys.navLabel)} — ${kit.te(keys.navHelp)}</option>`;
530
- }).join('');
531
- return options ? `<optgroup label="${kit.te(group.labelKey)}">${options}</optgroup>` : '';
532
- }).join('');
540
+ return { value: page.id, label: `${kit.tx(keys.navLabel)} — ${kit.tx(keys.navHelp)}`, selected: page.id === activeId };
541
+ });
542
+ return options.length ? { group: kit.tx(group.labelKey), options } : null;
543
+ }).filter(Boolean);
544
+ // dropdownHTML escapes the labels itself.
533
545
  return `<div class="stg-page-picker">
534
- <label for="stg-page-select">${kit.te('settings.redesign.common.allPages')}</label>
535
- <select id="stg-page-select">${groups}</select>
546
+ <label id="stg-page-select-label" for="stg-page-select-trigger">${kit.te('settings.redesign.common.allPages')}</label>
547
+ ${dropdownHTML('stg-page-select', groups, { classes: 'stg-page-dropdown', labelledBy: 'stg-page-select-label' })}
536
548
  </div>`;
537
549
  }
538
550
 
551
+ /** A settings field whose control is a styled dropdown: the id stays on its hidden input, the label and help point at its button. */
552
+ function dropdownField(invId, id, entries, { classes = '' } = {}) {
553
+ const control = dropdownHTML(id, entries, { classes, labelledBy: `${id}-label` })
554
+ .replace('role="combobox"', `role="combobox" aria-describedby="${id}-help"`);
555
+ return kit.field(invId, control, { forId: id }).replace(`<label for="${id}">`, `<label id="${id}-label" for="${id}-trigger">`);
556
+ }
557
+
539
558
  /** The search field above the page list, and the results list that takes the list's place while it holds a query. */
540
559
  function buildSearchHTML() {
541
560
  return `<div class="stg-search" role="search">
@@ -551,7 +570,7 @@ function buildSearchHTML() {
551
570
  }
552
571
 
553
572
  // 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,
573
+ // themselves, and the names of what the panes load (projects, providers, tools, tool lists and servers, skills,
555
574
  // browser profiles, themes, the linked WhatsApp account). Labels and names only: a source names label elements and
556
575
  // the shell reads their text, never a value; `help` is given only where it is the app's own copy. `named` rows may
557
576
  // sit inside an inventory control (a list it tags).
@@ -805,9 +824,9 @@ function buildBrowserTab() {
805
824
  ${kit.field('BRW011', dd('BRW011', 'bc-connectMode', 'connect', ['mirror', 'attach', 'morelogin']))}
806
825
  ${kit.field('BRW012', dd('BRW012', 'bc-attachSeed', 'seed', ['real', 'fresh']), { attrs: 'id="bc-attachSeed-row" style="display:none"' })}
807
826
  <div class="stg-field bc-card-row" id="bc-morelogin-row" ${kit.inv('BRW013')} style="display:none">
808
- <label for="bc-morelogin-env-id">${kit.L('BRW013')}</label>
827
+ <label id="bc-morelogin-env-id-label" for="bc-morelogin-env-id-trigger">${kit.L('BRW013')}</label>
809
828
  <div class="browser-cfg-input-row">
810
- <select class="browser-cfg-input" id="bc-morelogin-env-id" style="flex:1" aria-describedby="bc-morelogin-env-id-help"></select>
829
+ ${dropdownHTML('bc-morelogin-env-id', [{ value: '', label: kit.tx('settings.redesign.browser.loadingProfiles') }], { classes: 'bc-ml-dropdown', labelledBy: 'bc-morelogin-env-id-label', attrs: 'style="flex:1;min-width:0"' }).replace('role="combobox"', 'role="combobox" aria-describedby="bc-morelogin-env-id-help"')}
811
830
  <button type="button" class="cli-detect-btn" id="bc-morelogin-refresh" ${kit.inv('BRW014')} title="${kit.H('BRW014')}">${REFRESH}${kit.L('BRW014')}</button>
812
831
  </div>
813
832
  <div class="stg-help" id="bc-morelogin-env-id-help">${kit.H('BRW013')}</div>
@@ -955,9 +974,9 @@ function buildSetupTab(setupStatus) {
955
974
  // primary,remove,view,copy,text}, and the header badge (#setup-<id> .gfx-group-title .setup-status-badge).
956
975
  const providers = [
957
976
  { id: 'claude', name: 'Claude', icon: ANTHROPIC_ICON, file: '~/.claude.json', ids: { toggle: 'SET004', cli: 'SET005', primary: 'SET006', remove: 'SET007', view: 'SET008', copy: 'SET009' } },
958
- { id: 'gemini', name: 'Gemini', icon: GEMINI_ICON, file: '~/.gemini/settings.json', ids: { toggle: 'SET010', config: 'SET011', primary: 'SET012', remove: 'SET013', view: 'SET014', copy: 'SET015' } },
977
+ { id: 'antigravity', name: 'Antigravity', icon: ANTIGRAVITY_ICON, file: '~/.gemini/config/mcp_config.json', ids: { toggle: 'SET010', config: 'SET011', primary: 'SET012', remove: 'SET013', view: 'SET014', copy: 'SET015' } },
959
978
  { 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' } },
979
+ { 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
980
  ].map((p) => ({ ...p, on: setupStatus[p.id]?.connected || false }));
962
981
 
963
982
  const copyLabel = (invId) => `${COPY_ICON} ${kit.L(invId)}`;
@@ -1095,7 +1114,7 @@ function buildTerminalTab(cliConfig) {
1095
1114
  const cliProfiles = [
1096
1115
  { id: 'claude-code', name: 'Claude Code', icon: ANTHROPIC_ICON, color: '#D4A27F', default: 'claude', inv: 'CLI001' },
1097
1116
  { id: 'codex', name: 'Codex', icon: OPENAI_ICON, color: '#74c7a5', default: 'codex', inv: 'CLI003' },
1098
- { id: 'gemini', name: 'Gemini', icon: GEMINI_ICON, color: '#669DF6', default: 'gemini', inv: 'CLI004' },
1117
+ { id: 'antigravity', name: 'Antigravity', icon: ANTIGRAVITY_ICON, color: '#669DF6', default: 'agy', inv: 'CLI004' },
1099
1118
  { id: 'opencode', name: 'OpenCode', icon: OPENCODE_ICON, color: '#E8E0DC', default: 'opencode', inv: 'CLI005' },
1100
1119
  ];
1101
1120
 
@@ -1118,7 +1137,7 @@ function buildTerminalTab(cliConfig) {
1118
1137
  <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
1119
1138
  ${kit.L('CLI002')}
1120
1139
  </button>
1121
- ${p.id === 'gemini' ? '' : `<button type="button" class="cli-detect-btn" data-cli-add-path="${p.id}" hidden>${escapeHtml(cliInstallText('addPath'))}</button>`}
1140
+ <button type="button" class="cli-detect-btn" data-cli-add-path="${p.id}" hidden>${escapeHtml(cliInstallText('addPath'))}</button>
1122
1141
  <span class="cli-path-status${isDefault ? '' : ' custom'}" id="cli-status-${p.id}" ${kit.inv('CLI007')}>${kit.te(isDefault ? 'settings.redesign.terminal.cliStatus.default' : 'settings.redesign.terminal.cliStatus.custom')}</span>
1123
1142
  </div>
1124
1143
  <div class="stg-help" id="cli-path-${p.id}-help">${kit.H(p.inv)}</div>
@@ -1131,7 +1150,7 @@ function buildTerminalTab(cliConfig) {
1131
1150
  { code: 'C:\\Users\\me\\.npm\\claude', key: 'exWindows' },
1132
1151
  { code: '/opt/homebrew/bin/claude', key: 'exUnix' },
1133
1152
  { code: 'wsl claude', key: 'exWsl' },
1134
- { code: 'wsl -d Ubuntu gemini', key: 'exWslDistro' },
1153
+ { code: 'wsl -d Ubuntu claude', key: 'exWslDistro' },
1135
1154
  ];
1136
1155
 
1137
1156
  return `
@@ -1175,23 +1194,23 @@ function buildMcpTab() {
1175
1194
  return `
1176
1195
  <div class="stg-pane" data-stg-pane="mcp">
1177
1196
  <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)}
1197
+ ${tab('stg-mcp-page-tab', 'data-pane', 'tools', m('tab.tools'), true)}
1179
1198
  ${tab('stg-mcp-page-tab', 'data-pane', 'installed', m('tab.installed'), false)}
1180
1199
  </div>
1181
1200
 
1182
- <div class="stg-mcp-pane" data-pane="profiles" data-stg-reveal='.stg-mcp-page-tab[data-pane="profiles"]'>
1201
+ <div class="stg-mcp-pane" data-pane="tools" data-stg-reveal='.stg-mcp-page-tab[data-pane="tools"]'>
1183
1202
  ${kit.card({
1184
- id: 'stg-sec-tool-profiles', section: 'connected_tools.tool_profiles',
1203
+ id: 'stg-sec-default-tools', section: 'connected_tools.default_tools',
1185
1204
  body: `
1186
1205
  <div class="stg-mcp-intro">
1187
- <div class="stg-help" style="margin-top:0">${m('profilesHelp')}</div>
1206
+ <div class="stg-help" style="margin-top:0">${m('defaultToolsHelp')}</div>
1188
1207
  <div class="stg-mcp-note">
1189
1208
  <strong>${m('noteTitle')}</strong>
1190
1209
  <span>${m('noteBody')}</span>
1191
1210
  </div>
1192
1211
  </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>
1212
+ <div id="stg-mcp-default-tools" class="stg-mcp-default-tools stg-mcp-block" ${kit.inv('MCP022')} role="group" aria-label="${kit.L('MCP022')}">
1213
+ <div class="stg-mcp-empty-note">${m('loading')}</div>
1195
1214
  </div>
1196
1215
  <div id="stg-mcp-always-on" class="stg-mcp-subsection" ${kit.inv('MCP024')}></div>
1197
1216
  <div class="stg-mcp-subsection stg-mcp-servers-section">
@@ -1241,12 +1260,7 @@ function buildMcpTab() {
1241
1260
  </div>
1242
1261
  <div class="stg-mcp-form-grid" style="margin-top:10px">
1243
1262
  ${kit.field('MCP012', input('stg-mcp-srv-name', { ph: m('ph.name') }), { forId: 'stg-mcp-srv-name' })}
1244
- ${kit.field('MCP013', `
1245
- <select class="stg-input stg-input-sm" id="stg-mcp-srv-type" aria-describedby="stg-mcp-srv-type-help">
1246
- <option value="stdio">${m('type.stdio')}</option>
1247
- <option value="sse">${m('type.sse')}</option>
1248
- <option value="http">${m('type.http')}</option>
1249
- </select>`, { forId: 'stg-mcp-srv-type' })}
1263
+ ${dropdownField('MCP013', 'stg-mcp-srv-type', ['stdio', 'sse', 'http'].map(t => ({ value: t, label: kit.tx(`settings.redesign.mcp.type.${t}`), selected: t === 'stdio' })))}
1250
1264
  ${kit.field('MCP014', input('stg-mcp-srv-cmd', { ph: 'node' }), { forId: 'stg-mcp-srv-cmd', className: 'stg-mcp-field-wide stg-mcp-field-stdio', attrs: 'data-stg-shown-by="#stg-mcp-srv-type"' })}
1251
1265
  ${kit.field('MCP015', `
1252
1266
  <div class="stg-mcp-arg-chips" id="stg-mcp-arg-chips" ${kit.inv('MCP027')}>
@@ -1272,7 +1286,7 @@ function buildMcpTab() {
1272
1286
  ${platform('claudeCode', true)}
1273
1287
  ${platform('opencode', true)}
1274
1288
  ${platform('codex', true)}
1275
- ${platform('gemini', false)}
1289
+ ${platform('antigravity', false)}
1276
1290
  </div>
1277
1291
  <div class="stg-action-row stg-mcp-form-actions" style="margin-top:10px">
1278
1292
  <button type="button" class="stg-action-btn compact" id="stg-mcp-srv-save" ${kit.inv('MCP020')} title="${kit.H('MCP020')}">${kit.L('MCP020')}</button>
@@ -1303,6 +1317,7 @@ function buildOpencodeTab() {
1303
1317
  <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
1318
  </button>
1305
1319
  </span>`, { forId: 'stg-ocp-port', className: 'ocp-server-bar' })}
1320
+ <div class="stg-help ocp-install-note" id="stg-ocp-install" role="status" aria-live="polite" hidden></div>
1306
1321
  </div>
1307
1322
 
1308
1323
  <div class="ocp-group-head">
@@ -1328,9 +1343,10 @@ function buildOpencodeTab() {
1328
1343
  <div class="ocp-group">
1329
1344
  ${kit.advanced(`
1330
1345
  <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>
1346
+ <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
1347
  <button type="button" class="ocp-config-link" id="stg-ocp-open-config" aria-describedby="stg-ocp-open-config-help">${kit.L('OCP019')}</button>
1333
1348
  </div>
1349
+ <p class="stg-help" id="stg-ocp-config-jsonc" hidden></p>
1334
1350
  ${kit.help('OCP019', { forId: 'stg-ocp-open-config' })}`)}
1335
1351
  </div>`,
1336
1352
  })}
@@ -1341,8 +1357,7 @@ function buildOpencodeTab() {
1341
1357
  body: `
1342
1358
  <div class="ocp-compact-group ocp-rows">
1343
1359
  ${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' })}
1360
+ ${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
1361
  ${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
1362
  </div>
1348
1363
  <p class="ocp-footnote">${kit.te('settings.redesign.opencode.snapshotDetail')}</p>`,
@@ -1602,11 +1617,10 @@ function buildYoutubeTab(yt) {
1602
1617
  ${key('YT008', 'STEAM_API_KEY', 'xxxx', 'password')}
1603
1618
 
1604
1619
  ${kit.subhead(y('pipelineTitle'))}
1605
- ${kit.field('YT009', `
1606
- <select id="yt-upload-transport" class="stg-input" aria-describedby="yt-upload-transport-help">
1607
- <option value="browser"${p.uploadTransport !== 'api' ? ' selected' : ''}>${y('transport.browser')}</option>
1608
- <option value="api"${p.uploadTransport === 'api' ? ' selected' : ''}>${y('transport.api')}</option>
1609
- </select>`, { forId: 'yt-upload-transport' })}
1620
+ ${dropdownField('YT009', 'yt-upload-transport', [
1621
+ { value: 'browser', label: kit.tx('settings.redesign.youtube.transport.browser'), selected: p.uploadTransport !== 'api' },
1622
+ { value: 'api', label: kit.tx('settings.redesign.youtube.transport.api'), selected: p.uploadTransport === 'api' },
1623
+ ])}
1610
1624
  ${kit.field('YT010', `
1611
1625
  <div class="settings-key-row">
1612
1626
  <input id="yt-quality" type="text" value="${esc(dl.quality || 'bestvideo[height<=1080]+bestaudio/best[height<=1080]')}" autocomplete="off" spellcheck="false" aria-describedby="yt-quality-help">
@@ -1660,22 +1674,31 @@ function buildSocialTab(toolCategories, toolPermissions) {
1660
1674
 
1661
1675
  function buildSkillsTab(ccSkills) {
1662
1676
  const skills = ccSkills || [];
1677
+ // Antigravity reads ~/.gemini/config/skills: its chip shows once the server lists that target for the skills (an
1678
+ // older server installs to Claude Code, Codex and OpenCode only, and would refuse the chip's request).
1679
+ const antigravity = skills.some(s => s?.targetStates?.antigravity || (s?.installedTargets && Object.hasOwn(s.installedTargets, 'antigravity')));
1663
1680
  const targets = [
1664
1681
  { id: 'claude', label: kit.te('settings.redesign.product.claude') },
1665
1682
  { id: 'codex', label: kit.te('settings.redesign.product.codex') },
1666
1683
  { id: 'opencode', label: kit.te('settings.redesign.product.opencode') },
1684
+ ...(antigravity ? [{ id: 'antigravity', label: kit.te('settings.redesign.product.antigravity') }] : []),
1667
1685
  ];
1668
1686
  const row = (skill) => {
1669
- const installed = skill.installedTargets || { claude: !!skill.installed, codex: false, opencode: false };
1687
+ const installed = skill.installedTargets || { claude: !!skill.installed, codex: false, opencode: false, antigravity: false };
1670
1688
  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' : ''}"
1689
+ // What each runtime has of it: a native skill folder, an old bridge not moved yet, or a folder of that name
1690
+ // SynaBun did not install (settings/settings-skill-folders.js chipState).
1691
+ const chips = targets.map(t => {
1692
+ const chip = chipState(skill.targetStates?.[t.id], installed[t.id]);
1693
+ return `
1694
+ <button type="button" class="cc-skill-target${chip.on ? ' on' : ''}"
1673
1695
  data-cc-skill="${kit.esc(skill.dirName)}"
1674
1696
  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('');
1697
+ data-state="${chip.on ? 'on' : 'off'}" data-install-state="${chip.state}"
1698
+ ${kit.inv('SKL001')} data-stg-pressed="on" aria-pressed="${chip.on ? 'true' : 'false'}"
1699
+ title="${kit.te(chip.key, { skill: skill.name, assistant: t.label })}">${t.label}</button>
1700
+ `;
1701
+ }).join('');
1679
1702
  return `
1680
1703
  <div class="cc-skill-row${anyOn ? ' installed' : ''}" data-skill-name="${kit.esc(skill.dirName)}">
1681
1704
  <div class="cc-skill-info">
@@ -1690,10 +1713,14 @@ function buildSkillsTab(ccSkills) {
1690
1713
  <div class="stg-pane" data-stg-pane="skills">
1691
1714
  ${kit.card({
1692
1715
  id: 'stg-sec-skills', section: 'tool_access.skills',
1693
- body: skills.length
1716
+ body: (skills.length
1694
1717
  ? `<div ${kit.inv('SKL004')} role="group" aria-label="${kit.L('SKL004')}">${skills.map(row).join('')}</div>
1695
1718
  ${kit.help('SKL001')}`
1696
- : `<div class="stg-help" style="margin-top:0">${kit.te('settings.redesign.skills.none')}</div>`,
1719
+ : `<div class="stg-help" style="margin-top:0">${kit.te('settings.redesign.skills.none')}</div>`)
1720
+ // Skills Studio is where a skill is edited, versioned and published; these chips stay for what SynaBun ships.
1721
+ + `<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>
1722
+ <span class="stg-help">${kit.te('settings.redesign.skills.openStudioHelp')}</span></div>`
1723
+ + skillFoldersHTML(),
1697
1724
  })}
1698
1725
  </div>`;
1699
1726
  }
@@ -1704,7 +1731,7 @@ function buildPermissionsTab(toolCategories, toolPermissions) {
1704
1731
  // The inventory ids of the "all tools of this group" switches, by group.
1705
1732
  const ALL_SWITCH = {
1706
1733
  memory: 'PER001', browser: 'PER003', whiteboard: 'PER004', cards: 'PER005', automation: 'PER006',
1707
- leonardo: 'PER007', images: 'PER008', gsc: 'PER009', youtube: 'PER010', thirdparty: 'PER011',
1734
+ images: 'PER008', gsc: 'PER009', youtube: 'PER010', thirdparty: 'PER011',
1708
1735
  };
1709
1736
 
1710
1737
  let totalOn = 0, totalAll = 0;
@@ -1824,6 +1851,52 @@ const userContextDigestHTML = (digest) => (digest.rows.length
1824
1851
  ? 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
1852
  : `<div class="stg-help uc-digest-message" role="listitem" style="margin:0">${kit.esc(digest.message)}</div>`);
1826
1853
 
1854
+ /**
1855
+ * Put a view (describeClaudeHooks) on the section: the line above, the switch, its notes and Repair, and the
1856
+ * per-moment rows and their count in the Hooks card, which show nothing on or off that was not read.
1857
+ */
1858
+ function drawClaudeHooks(overlay, view) {
1859
+ const section = overlay.querySelector('#stg-sec-automation-behavior');
1860
+ if (!section) return;
1861
+ const note = section.querySelector('#ch-status-note');
1862
+ if (note) { note.hidden = !view.note; note.textContent = view.note?.text || ''; note.dataset.chState = view.state; }
1863
+ const rowBox = section.querySelector('#ch-row');
1864
+ if (rowBox) { rowBox.dataset.chReady = view.ready ? '1' : '0'; rowBox.dataset.chState = view.state; }
1865
+ const toggle = section.querySelector('.cc-toggle[data-ch-master]');
1866
+ if (toggle) {
1867
+ toggle.classList.toggle('on', view.on);
1868
+ toggle.setAttribute('aria-pressed', view.on ? 'true' : 'false');
1869
+ toggle.disabled = view.disabled;
1870
+ toggle.setAttribute('aria-describedby', `ch-master-help ch-notes${view.note ? ' ch-status-note' : ''}`);
1871
+ const item = toggle.closest('.cc-integration-item');
1872
+ item?.classList.toggle('enabled', view.on);
1873
+ item?.classList.toggle('uc-disabled', view.disabled);
1874
+ }
1875
+ const notes = section.querySelector('#ch-notes');
1876
+ if (notes) notes.innerHTML = userContextNotesHTML(view.notes);
1877
+ const repair = section.querySelector('#ch-repair');
1878
+ if (repair) repair.hidden = !(view.repair && !view.disabled);
1879
+ const applies = section.querySelector('#ch-applies');
1880
+ if (applies) applies.textContent = view.applies;
1881
+ const locked = !view.ready || view.disabled;
1882
+ for (const row of CLAUDE_HOOK_ROWS) {
1883
+ const rowToggle = section.querySelector(`.cc-toggle[data-cc-hook="${row.key}"]`);
1884
+ if (!rowToggle) continue;
1885
+ const on = !!view.rows?.[row.key];
1886
+ rowToggle.classList.toggle('on', on);
1887
+ rowToggle.setAttribute('aria-pressed', on ? 'true' : 'false');
1888
+ rowToggle.disabled = locked;
1889
+ const item = rowToggle.closest('.cc-integration-item');
1890
+ item?.classList.toggle('enabled', on);
1891
+ item?.classList.toggle('uc-disabled', locked);
1892
+ }
1893
+ const badge = section.querySelector('#cc-hooks-badge');
1894
+ if (badge) {
1895
+ badge.textContent = view.ready ? `${view.onRows}/${view.totalRows}` : '—';
1896
+ badge.classList.toggle('all-on', view.ready && view.onRows === view.totalRows);
1897
+ }
1898
+ }
1899
+
1827
1900
  /** Put a view (describeUserContext) on the section: the note, each switch and its notes, the digest. */
1828
1901
  function drawUserContext(overlay, view) {
1829
1902
  const section = overlay.querySelector('#stg-sec-automation-behavior');
@@ -1900,16 +1973,19 @@ function tunnelExtraHtml(view, mcpKeyInfo) {
1900
1973
  ].filter(Boolean).join('');
1901
1974
  }
1902
1975
 
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;
1976
+ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo, openclawBridge, greeting, toolPermissions, toolCategories, userContext = null, workerGuard = null, claudeHooks = null) {
1977
+ // SynaBun's Claude Code hooks (settings-claude-hooks.js): the switch and the rows below show what is really in the
1978
+ // settings files, and nothing (disabled, with the reason) when that could not be read: never "off" by default.
1979
+ const ch = claudeHooks || describeClaudeHooks(null, { tx: kit.tx });
1980
+ const rowOn = (key) => !!ch.rows?.[key];
1981
+ const ssOn = rowOn('SessionStart');
1982
+ const psOn = rowOn('UserPromptSubmit');
1983
+ const pcOn = rowOn('PreCompact');
1984
+ const stOn = rowOn('Stop');
1985
+ const prOn = rowOn('PreToolUse');
1986
+ const ptOn = rowOn('PostToolUse');
1987
+ const subStartOn = rowOn('SubagentStart');
1988
+ const subStopOn = rowOn('SubagentStop');
1913
1989
  const hf = ccIntegrations.hookFeatures || {};
1914
1990
  const cmOn = hf.conversationMemory !== false;
1915
1991
  const ulOn = hf.userLearning !== false;
@@ -1926,7 +2002,8 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
1926
2002
  { key: 'SubagentStart', on: subStartOn, inv: 'AUT034' }, { key: 'SubagentStop', on: subStopOn, inv: 'AUT035' },
1927
2003
  ];
1928
2004
  const onCount = hookRows.filter(r => r.on).length;
1929
- const allOn = onCount === hookRows.length;
2005
+ const allOn = ch.ready && onCount === hookRows.length;
2006
+ const hookRowsLocked = !ch.ready || ch.disabled;
1930
2007
 
1931
2008
  const copyIcon = COPY_ICON;
1932
2009
 
@@ -1938,10 +2015,8 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
1938
2015
  { label: 'Claude Web', on: !!compat.web, note: compat.webNote },
1939
2016
  { label: 'Claude Cowork', on: !!compat.cowork },
1940
2017
  ];
1941
- const geminiItems = [
1942
- { label: 'Gemini CLI', on: false },
1943
- { label: 'Gemini Code Assist', on: false },
1944
- { label: 'Gemini Web', on: false },
2018
+ const antigravityItems = [
2019
+ { label: 'Antigravity CLI', on: false },
1945
2020
  ];
1946
2021
  const openaiItems = [
1947
2022
  { label: 'Codex CLI', on: compat.codex !== false },
@@ -1970,7 +2045,7 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
1970
2045
  };
1971
2046
  return `<div class="cc-provider-badges" data-stg-nostatus onclick="event.stopPropagation()" ${kit.inv('AUT046')}>
1972
2047
  ${badge('anthropic', ANTHROPIC_ICON, 'compat.claude', anthropicItems, false)}
1973
- ${badge('google', GEMINI_ICON, 'compat.gemini', geminiItems, true)}
2048
+ ${badge('google', ANTIGRAVITY_ICON, 'compat.antigravity', antigravityItems, true)}
1974
2049
  ${badge('openai', OPENAI_ICON, 'compat.openai', openaiItems, true)}
1975
2050
  </div>`;
1976
2051
  };
@@ -2051,6 +2126,44 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
2051
2126
  <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
2127
  </div>`;
2053
2128
 
2129
+ // Protect workers (lib/worker-guard-setting.js): what capability read-only / workspace means for every worker the
2130
+ // Assistant dispatches. `workerGuard` is the view of what the server answered (describeWorkerGuard): a server
2131
+ // started before the setting shows the default, disabled, under a line that says a restart is missing.
2132
+ const wg = workerGuard || describeWorkerGuard(null, { tx: kit.tx });
2133
+ const workerGuardBlock = `
2134
+ <div class="cc-hook-toggles" id="wg-row" data-wg-ready="${wg.ready ? '1' : '0'}">
2135
+ <div class="cc-integration-item enabled${wg.disabled ? ' uc-disabled' : ''}" data-wg-row>
2136
+ <div class="cc-integration-info">
2137
+ <div class="cc-integration-label" id="wg-mode-label">${kit.L('AUT061')}</div>
2138
+ <div class="cc-integration-path" id="wg-mode-help">${kit.H('AUT061')}</div>
2139
+ <div class="uc-layer-notes" id="wg-notes" data-stg-data>${userContextNotesHTML(wg.note ? [wg.note] : [])}</div>
2140
+ <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>
2141
+ </div>
2142
+ ${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 })}
2143
+ </div>
2144
+ </div>
2145
+ <div class="stg-help uc-save-status" id="wg-save-status" role="alert" data-stg-data hidden></div>
2146
+ <div class="stg-help" id="wg-applies">${kit.esc(wg.applies)}</div>`;
2147
+
2148
+ // SynaBun's hooks in Claude Code: one switch for all of them (POST / DELETE /api/claude-code/integrations, target
2149
+ // global), what is really installed under it, and Repair when the files are out of step. Outside the collapsed
2150
+ // card below, so it can be found; the per-moment rows stay in that card.
2151
+ const claudeHooksBlock = `
2152
+ <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>
2153
+ <div class="cc-hook-toggles" id="ch-row" data-ch-ready="${ch.ready ? '1' : '0'}" data-ch-state="${ch.state}">
2154
+ <div class="cc-integration-item${ch.on ? ' enabled' : ''}${ch.disabled ? ' uc-disabled' : ''}" data-ch-master-row>
2155
+ <div class="cc-integration-info">
2156
+ <div class="cc-integration-label" id="ch-master-label">${kit.L('AUT062')}</div>
2157
+ <div class="cc-integration-path" id="ch-master-help">${kit.H('AUT062')}</div>
2158
+ <div class="uc-layer-notes" id="ch-notes" data-stg-data>${userContextNotesHTML(ch.notes)}</div>
2159
+ <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>
2160
+ </div>
2161
+ <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>
2162
+ </div>
2163
+ </div>
2164
+ <div class="stg-help uc-save-status" id="ch-save-status" role="alert" data-stg-data hidden></div>
2165
+ <div class="stg-help uc-applies" id="ch-applies">${kit.esc(ch.applies)}</div>`;
2166
+
2054
2167
  return `
2055
2168
  <div class="stg-pane" data-stg-pane="hooks">
2056
2169
 
@@ -2065,11 +2178,16 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
2065
2178
  body: `
2066
2179
  ${userContextBlock}
2067
2180
 
2181
+ ${workerGuardBlock}
2182
+
2183
+ ${claudeHooksBlock}
2184
+
2068
2185
  ${kit.card({
2069
2186
  id: 'stg-sec-hooks', level: 'h4', collapsible: true, collapsed: true, advanced: true, attrs: 'data-cc-target="global"',
2070
2187
  title: h('hooksTitle'), purpose: h('hooksPurpose'),
2071
2188
  // 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 })}`,
2189
+ // A count only from a state that was read: "—" otherwise, never "0/8".
2190
+ 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
2191
  body: `
2074
2192
  <div class="stg-help" style="margin:0 0 10px">${h('claudeOnly')}</div>
2075
2193
  <div class="cc-hook-toggles">
@@ -2088,12 +2206,12 @@ function buildConnectionsTab(ccIntegrations, ccSkills, tunnelStatus, mcpKeyInfo,
2088
2206
  </div>
2089
2207
  <div class="cc-hook-toggles" style="margin-top:10px">
2090
2208
  ${hookRows.map(r => `
2091
- <div class="cc-integration-item${r.on ? ' enabled' : ''}" data-hook="${r.key}">
2209
+ <div class="cc-integration-item${r.on ? ' enabled' : ''}${hookRowsLocked ? ' uc-disabled' : ''}" data-hook="${r.key}">
2092
2210
  <div class="cc-integration-info">
2093
2211
  <div class="cc-integration-label" id="cc-hook-${r.key}-label">${kit.L(r.inv)}</div>
2094
2212
  <div class="cc-integration-path" id="cc-hook-${r.key}-help">${kit.H(r.inv)}</div>
2095
2213
  </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>
2214
+ <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
2215
  </div>`).join('')}
2098
2216
  </div>`,
2099
2217
  })}`,
@@ -2381,6 +2499,7 @@ function buildMoreLoginTab() {
2381
2499
  <span class="settings-status-dot disconnected" id="ml-status-dot"></span>
2382
2500
  <span id="ml-status-line">${m('checking')}</span>
2383
2501
  </div>
2502
+ <div class="stg-help" id="ml-path-hint" hidden style="margin:8px 0 0">${m('customPathHint')}</div>
2384
2503
  <div class="stg-help" style="margin:8px 0 12px">${kit.th('settings.redesign.morelogin.intro', {
2385
2504
  link: '<a href="https://www.morelogin.com" target="_blank" rel="noopener">morelogin.com</a>',
2386
2505
  path: `<strong>${m('apiPath')}</strong>`,
@@ -2457,7 +2576,10 @@ function wireMoreLoginTab(overlay) {
2457
2576
  const dot = $('ml-status-dot'), line = $('ml-status-line'), conn = $('ml-conn-status');
2458
2577
  if (!s.installed) { dot.className = 'settings-status-dot disconnected'; line.textContent = kit.tx('settings.redesign.morelogin.moreloginNotInstalled'); if (conn) conn.textContent = kit.tx('settings.redesign.morelogin.notInstalled'); }
2459
2578
  else if (!s.running) { dot.className = 'settings-status-dot disconnected'; line.textContent = kit.tx('settings.redesign.morelogin.installedApiNotRunningPort', { port: s.port }); if (conn) conn.textContent = kit.tx('settings.redesign.morelogin.appClosed'); }
2579
+ else if (s.apiError) { dot.className = 'settings-status-dot disconnected'; line.textContent = kit.tx('settings.redesign.morelogin.runningApiRejected', { port: s.port, reason: String(s.apiError) }); if (conn) conn.textContent = kit.tx('settings.redesign.morelogin.apiError'); }
2460
2580
  else { dot.className = 'settings-status-dot connected'; line.textContent = kit.tx('settings.redesign.morelogin.connectedProfileSPort', { profileCount: s.profileCount, port: s.port }); if (conn) conn.textContent = kit.tx('settings.redesign.morelogin.connected'); }
2581
+ const hint = $('ml-path-hint');
2582
+ if (hint) hint.hidden = !!(s.installed && s.running);
2461
2583
  _defaultEnvId = s.defaultEnvId || null;
2462
2584
  const ds = $('ml-default-status');
2463
2585
  if (ds) ds.textContent = (s.isDefault && s.defaultEnvId) ? kit.tx('settings.redesign.morelogin.defaultAiBrowserSet') : kit.tx('settings.redesign.morelogin.noDefaultSet');
@@ -2503,6 +2625,11 @@ function wireMoreLoginTab(overlay) {
2503
2625
  if ($('ml-api-id') && !$('ml-api-id').value) $('ml-api-id').value = cfg.config.apiId || '';
2504
2626
  }
2505
2627
  const p = await jget('/api/morelogin/profiles');
2628
+ if (p.ok === false && p.error) {
2629
+ const list = $('ml-profile-list');
2630
+ if (list) list.innerHTML = '<div class="settings-hint" role="status">' + escapeHtml(String(p.error)) + '</div>';
2631
+ return;
2632
+ }
2506
2633
  renderProfiles(p.envs || []);
2507
2634
  }
2508
2635
 
@@ -2950,7 +3077,7 @@ function activateSettingsTab(overlay, tabId = DEFAULT_SETTINGS_PAGE, { remember
2950
3077
  nav.setAttribute('aria-current', 'page');
2951
3078
  body.classList.add('active');
2952
3079
  const picker = overlay.querySelector('#stg-page-select');
2953
- if (picker) picker.value = pageId;
3080
+ if (picker) setDropdownValue(picker, pageId);
2954
3081
  if (changed) {
2955
3082
  const content = overlay.querySelector('.settings-content');
2956
3083
  if (content) content.scrollTop = 0;
@@ -3233,7 +3360,7 @@ async function openSettingsModalNow(options = {}) {
3233
3360
  let _bridgeResult = null;
3234
3361
  // The greeting (lib/greeting/): what its endpoint answered, as a reading (no answer is a reading too).
3235
3362
  let greetingReading = readGreetingStatus({ status: 0 });
3236
- let setupStatus = { claude: {}, gemini: {}, codex: {}, opencode: {}, paths: {} };
3363
+ let setupStatus = { claude: {}, antigravity: {}, codex: {}, opencode: {}, paths: {} };
3237
3364
  let cliConfig = {};
3238
3365
  let toolPermissions = {};
3239
3366
  let toolCategories = [];
@@ -3245,6 +3372,10 @@ async function openSettingsModalNow(options = {}) {
3245
3372
  // The Assistant's user context (lib/assistant-user-context.js): what its endpoint answered, as a reading.
3246
3373
  // No answer is a reading too: the section says what is true instead of showing switches that do nothing.
3247
3374
  let userContextReading = readUserContextResponse({ status: 0 });
3375
+ // Protect workers (lib/worker-guard-setting.js): what its endpoint answered, as a reading.
3376
+ let workerGuardReading = readWorkerGuardResponse({ status: 0 });
3377
+ // SynaBun's Claude Code hooks (lib/claude-hooks-status.js): the `claudeHooks` of the integrations read, as a reading.
3378
+ let claudeHooksReading = readClaudeHooksResponse({ status: 0 });
3248
3379
 
3249
3380
  try {
3250
3381
  // Read through the page's data cache (settings/settings-data.js): kept answers at once (each open its own
@@ -3254,15 +3385,19 @@ async function openSettingsModalNow(options = {}) {
3254
3385
  const readingOf = (entry, make) => ({ status: 'fulfilled', value: entry?.status === 'fulfilled' ? make(entry.value) : make({ status: 0, error: entry?.reason?.message }) });
3255
3386
  const greetRes = readingOf(reads.greeting, readGreetingStatus);
3256
3387
  const userContextRes = readingOf(reads.userContext, readUserContextResponse);
3388
+ const workerGuardRes = readingOf(reads.workerGuard, readWorkerGuardResponse);
3257
3389
  const settingsRes = reads.settings, connRes = reads.connections, ccRes = reads.integrations, skillsRes = reads.skills;
3258
3390
  const tunnelRes = reads.tunnel, keyRes = reads.mcpKey, bridgeRes = reads.openclaw, setupRes = reads.setup, cliRes = reads.cli;
3259
3391
  const toolPermsRes = reads.toolPermissions, toolCatsRes = reads.toolCategories, discordRes = reads.discord, skinsRes = reads.skins;
3260
3392
  const iconsRes = reads.icons, youtubeRes = reads.youtube, typesafeRes = reads.typesafe;
3261
3393
  if (typesafeRes.status === 'fulfilled' && typesafeRes.value.ok) typesafeConfig = typesafeRes.value;
3262
3394
  if (userContextRes.status === 'fulfilled') userContextReading = userContextRes.value;
3395
+ if (workerGuardRes.status === 'fulfilled') workerGuardReading = workerGuardRes.value;
3263
3396
  if (settingsRes.status === 'fulfilled') settings = settingsRes.value;
3264
3397
  if (connRes.status === 'fulfilled' && connRes.value.connections) connections = connRes.value.connections;
3265
- if (ccRes.status === 'fulfilled' && ccRes.value.ok) ccIntegrations = ccRes.value;
3398
+ // 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.
3399
+ if (ccRes.status === 'fulfilled' && ccRes.value?.status >= 200 && ccRes.value.status < 300 && ccRes.value.body?.ok) ccIntegrations = ccRes.value.body;
3400
+ claudeHooksReading = readingOf(ccRes, readClaudeHooksResponse).value;
3266
3401
  if (skillsRes.status === 'fulfilled' && skillsRes.value.ok) ccSkills = skillsRes.value.skills || [];
3267
3402
  if (tunnelRes.status === 'fulfilled' && tunnelRes.value.ok) tunnelStatus = tunnelRes.value;
3268
3403
  if (keyRes.status === 'fulfilled' && keyRes.value.ok) mcpKeyInfo = keyRes.value;
@@ -3295,9 +3430,10 @@ async function openSettingsModalNow(options = {}) {
3295
3430
  overlay.className = 'settings-panel glass resizable';
3296
3431
  overlay.id = 'settings-panel';
3297
3432
 
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';
3433
+ // Always open centered at default size, in the app frame
3434
+ const at = centeredInFrame(900, 650);
3435
+ overlay.style.left = at.left + 'px';
3436
+ overlay.style.top = at.top + 'px';
3301
3437
 
3302
3438
  // ── Assemble HTML ──
3303
3439
  overlay.innerHTML = `
@@ -3330,7 +3466,7 @@ async function openSettingsModalNow(options = {}) {
3330
3466
  <div class="settings-content">
3331
3467
  ${buildPagesHTML({
3332
3468
  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 })),
3469
+ 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
3470
  terminal: buildTerminalTab(cliConfig),
3335
3471
  opencode: buildOpencodeTab(),
3336
3472
  notifications: buildNotificationsTab(),
@@ -3375,7 +3511,7 @@ async function openSettingsModalNow(options = {}) {
3375
3511
 
3376
3512
  // ── Nav status dots (Judgments and WhatsApp update theirs as they wire) ──
3377
3513
  kit.setNavStatus(overlay, 'server', settings.storage === 'sqlite' ? 'connected' : 'disconnected');
3378
- kit.setNavStatus(overlay, 'setup', (setupStatus.claude?.connected || setupStatus.gemini?.connected || setupStatus.codex?.connected || setupStatus.opencode?.connected) ? 'connected' : 'disconnected');
3514
+ kit.setNavStatus(overlay, 'setup', (setupStatus.claude?.connected || setupStatus.antigravity?.connected || setupStatus.codex?.connected || setupStatus.opencode?.connected) ? 'connected' : 'disconnected');
3379
3515
  kit.setNavStatus(overlay, 'discord', discordConfig.botToken ? 'connected' : 'disconnected');
3380
3516
  kit.setNavStatus(overlay, 'judgments', (typesafeConfig && typesafeConfig.enabled) ? 'connected' : 'disconnected');
3381
3517
 
@@ -4592,7 +4728,7 @@ async function openSettingsModalNow(options = {}) {
4592
4728
  // Must stay in sync with CLI_DEFAULTS in server.js and the cliProfiles
4593
4729
  // list that renders the inputs — opencode was previously missing here, so
4594
4730
  // its input rendered but its value was never sent to the server.
4595
- const cliDefaults = { 'claude-code': 'claude', 'codex': 'codex', 'gemini': 'gemini', 'opencode': 'opencode' };
4731
+ const cliDefaults = { 'claude-code': 'claude', 'codex': 'codex', 'antigravity': 'agy', 'opencode': 'opencode' };
4596
4732
  const cliProfileIds = Object.keys(cliDefaults);
4597
4733
 
4598
4734
  const pathButtons = overlay.querySelectorAll('[data-cli-add-path]');
@@ -4639,9 +4775,14 @@ async function openSettingsModalNow(options = {}) {
4639
4775
  });
4640
4776
  const data = await res.json();
4641
4777
  showPathAction(profileId, data);
4778
+ const refusal = profileId === 'opencode' && data.support ? describeOpencodeInstall(data.support, { th: kit.th, tx: kit.tx }) : null;
4642
4779
  if (data.ok && data.found && data.path && data.state === 'broken') {
4643
4780
  // Found, but it does not start: not put in the field, so a broken path is never saved by accident.
4644
4781
  showCCToast(kit.tx('settings.redesign.terminal.detectionFailed', { message: data.error ? `${data.path}: ${data.error}` : data.path }));
4782
+ } else if (data.ok && data.found && data.path && refusal?.status === 'notSupported') {
4783
+ // An OpenCode SynaBun does not drive (OpenCode 1, say): put in the field, with the sentence that says why it will not start.
4784
+ input.value = data.path;
4785
+ showCCToast(refusal.text, 12000);
4645
4786
  } else if (data.ok && data.found && data.path) {
4646
4787
  input.value = data.path;
4647
4788
  showCCToast(kit.tx('settings.redesign.terminal.found', { path: data.version ? `${data.path} (${data.version})` : data.path }));
@@ -4899,9 +5040,15 @@ async function openSettingsModalNow(options = {}) {
4899
5040
  if (data.ok) {
4900
5041
  const nowOn = !isOn;
4901
5042
  toggleBtn.classList.toggle('on');
5043
+ toggleBtn.setAttribute('aria-pressed', nowOn ? 'true' : 'false');
4902
5044
  const row = toggleBtn.closest('.cc-integration-item');
4903
5045
  if (row) row.classList.toggle('enabled', nowOn);
4904
- if (target === 'global') { updateGlobalHookBadge(); }
5046
+ if (target === 'global') {
5047
+ updateGlobalHookBadge();
5048
+ // The answer's state of every hook (a server from before it sends none): the switch above follows it.
5049
+ if (data.claudeHooks) { claudeHooksReading = readClaudeHooksResponse({ status: res.status, body: { ok: true, claudeHooks: data.claudeHooks } }); redrawClaudeHooks(); }
5050
+ refreshSettingsData(['integrations']);
5051
+ }
4905
5052
  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
5053
  } else { alert(data.error || kit.tx('settings.redesign.hooks.failedToToggleHook')); }
4907
5054
  } catch (err) { alert(kit.tx('settings.redesign.hooks.failed2', { message: err.message })); }
@@ -4913,6 +5060,41 @@ async function openSettingsModalNow(options = {}) {
4913
5060
  toggle.addEventListener('click', () => { ccToggleHook('global', null, toggle, overlay.querySelector('.iface-section[data-cc-target="global"]'), toggle.dataset.ccHook); });
4914
5061
  });
4915
5062
 
5063
+ // ── SynaBun hooks in Claude Code: one switch for all of them, and Repair ──
5064
+ // Each change is the server's: the row is drawn again from the state its answer carries (what the files hold now),
5065
+ // and a change that failed leaves the row as it was, with the reason under it. One change at a time.
5066
+ function redrawClaudeHooks() { drawClaudeHooks(overlay, describeClaudeHooks(claudeHooksReading, { tx: kit.tx })); }
5067
+ const setClaudeHooksSaveStatus = (text = '') => {
5068
+ const status = overlay.querySelector('#ch-save-status');
5069
+ if (status) { status.hidden = !text; status.textContent = text; }
5070
+ };
5071
+ let claudeHooksBusy = false;
5072
+ async function changeClaudeHooks(action) {
5073
+ if (claudeHooksBusy || claudeHooksReading.state !== 'ready') return;
5074
+ claudeHooksBusy = true;
5075
+ setClaudeHooksSaveStatus('');
5076
+ const row = overlay.querySelector('[data-ch-master-row]');
5077
+ if (row) row.style.opacity = '0.6';
5078
+ let failure = null;
5079
+ try {
5080
+ // On and Repair ask for the hooks to run (unblock: Claude Code's own disableAllHooks in the same file goes too).
5081
+ const payload = action === 'off' ? { target: 'global' } : { target: 'global', unblock: true };
5082
+ const res = await fetch('/api/claude-code/integrations', { method: action === 'off' ? 'DELETE' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
5083
+ const body = await res.json().catch(() => null);
5084
+ if (res.ok && body?.ok) {
5085
+ claudeHooksReading = readClaudeHooksResponse({ status: res.status, body: { ok: true, claudeHooks: body.claudeHooks } });
5086
+ refreshSettingsData(['integrations']);
5087
+ } else failure = { status: res.status, body };
5088
+ } catch (err) { failure = { status: 0, error: err?.message }; }
5089
+ finally { claudeHooksBusy = false; if (row) row.style.opacity = ''; }
5090
+ redrawClaudeHooks();
5091
+ if (failure) setClaudeHooksSaveStatus(describeClaudeHooksSaveFailure({ action, ...failure }, { tx: kit.tx }));
5092
+ }
5093
+ overlay.querySelector('.cc-toggle[data-ch-master]')?.addEventListener('click', (e) => {
5094
+ changeClaudeHooks(e.currentTarget.classList.contains('on') ? 'off' : 'on');
5095
+ });
5096
+ overlay.querySelector('#ch-repair')?.addEventListener('click', () => changeClaudeHooks('repair'));
5097
+
4916
5098
  // ── The Assistant's user context: layers and the digest preview ──
4917
5099
  const ucRowsEl = overlay.querySelector('#uc-digest-rows');
4918
5100
  const redrawUserContext = (reading = userContextReading) => drawUserContext(overlay, describeUserContext(reading, { tx: kit.tx }));
@@ -4939,6 +5121,47 @@ async function openSettingsModalNow(options = {}) {
4939
5121
  if (!link) return;
4940
5122
  try { applySettingsOpenOptions(overlay, JSON.parse(link.dataset.ucGoto)); } catch { /* a malformed target goes nowhere */ }
4941
5123
  });
5124
+ // Protect workers: the choice is saved at once and follows the server's answer; a save that failed puts the
5125
+ // dropdown back where it was, with the reason under it. Saves go one after the other, in the order of the picks.
5126
+ {
5127
+ const input = overlay.querySelector('#wg-mode');
5128
+ const dd = overlay.querySelector('.cc-dropdown[data-for="wg-mode"]');
5129
+ const status = overlay.querySelector('#wg-save-status');
5130
+ const detail = overlay.querySelector('#wg-mode-detail');
5131
+ const setStatus = (text) => { if (status) { status.textContent = text; status.hidden = !text; } };
5132
+ const showMode = (mode) => {
5133
+ if (input) input.value = mode;
5134
+ const view = describeWorkerGuard(workerGuardReading, { tx: kit.tx });
5135
+ const option = view.options.find(o => o.value === mode);
5136
+ dd?.querySelector('.cc-dropdown-value') && (dd.querySelector('.cc-dropdown-value').textContent = option?.label || mode);
5137
+ dd?.querySelectorAll('.cc-dropdown-item').forEach(item => item.classList.toggle('active', item.dataset.value === mode));
5138
+ if (detail) detail.textContent = option?.help || '';
5139
+ };
5140
+ let saves = Promise.resolve();
5141
+ input?.addEventListener('change', () => {
5142
+ if (workerGuardReading.state !== 'ready') return;
5143
+ const mode = String(input.value || '');
5144
+ if (!mode || mode === workerGuardReading.mode) { showMode(workerGuardReading.mode); return; }
5145
+ setStatus('');
5146
+ showMode(mode);
5147
+ if (dd) dd.style.opacity = '0.4';
5148
+ saves = saves.then(async () => {
5149
+ let failure = null;
5150
+ try {
5151
+ const res = await fetch('/api/assistant/worker-guard', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode }) });
5152
+ const body = await res.json().catch(() => null);
5153
+ const reading = readWorkerGuardResponse({ status: res.status, body });
5154
+ if (res.ok && reading.state === 'ready') workerGuardReading = reading;
5155
+ else failure = { status: res.status, body };
5156
+ } catch (err) {
5157
+ failure = { status: 0, error: err?.message };
5158
+ }
5159
+ if (dd) dd.style.opacity = '';
5160
+ showMode(workerGuardReading.mode);
5161
+ if (failure) setStatus(describeWorkerGuardSaveFailure({ ...failure, mode }, { tx: kit.tx }));
5162
+ });
5163
+ });
5164
+ }
4942
5165
  // The switch moves at once and follows the server's answer: the stored settings when the save went through,
4943
5166
  // back where it was (with the reason next to the switches) when it did not. Saves go one after the other, in
4944
5167
  // the order of the clicks, so a second click made before the first was answered is kept, not dropped.
@@ -5192,8 +5415,14 @@ async function openSettingsModalNow(options = {}) {
5192
5415
  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
5416
  });
5194
5417
 
5195
- // Skill target chips — one skill can be installed independently into Claude / Codex / OpenCode
5196
- const runtimeLabels = { claude: 'Claude Code', codex: 'Codex CLI', opencode: 'OpenCode CLI' };
5418
+ // Skills Studio, from Settings → Skills: Settings closes, the Studio opens on its Library.
5419
+ overlay.querySelector('[data-cc-open-studio]')?.addEventListener('click', () => {
5420
+ close();
5421
+ emit('skills:open', { place: 'library' });
5422
+ });
5423
+
5424
+ // Skill target chips — one skill can be installed independently into Claude / Codex / OpenCode / Antigravity
5425
+ const runtimeLabels = { claude: 'Claude Code', codex: 'Codex CLI', opencode: 'OpenCode CLI', antigravity: 'Antigravity CLI' };
5197
5426
  overlay.querySelectorAll('.cc-skill-target[data-cc-skill]').forEach(chip => {
5198
5427
  chip.addEventListener('click', async () => {
5199
5428
  const skillName = chip.dataset.ccSkill;
@@ -5201,28 +5430,48 @@ async function openSettingsModalNow(options = {}) {
5201
5430
  const isOn = chip.classList.contains('on');
5202
5431
  try {
5203
5432
  chip.style.opacity = '0.4'; chip.style.pointerEvents = 'none';
5204
- const res = await fetch('/api/skills/install', {
5433
+ const send = (extra = {}) => fetch('/api/skills/install', {
5205
5434
  method: isOn ? 'DELETE' : 'POST',
5206
5435
  headers: { 'Content-Type': 'application/json' },
5207
- body: JSON.stringify({ name: skillName, target }),
5208
- });
5209
- const data = await res.json();
5436
+ body: JSON.stringify({ name: skillName, target, ...extra }),
5437
+ }).then((res) => res.json());
5438
+ let data = await send();
5439
+ // A skill of that name that SynaBun did not install, or that was changed since, is neither replaced nor
5440
+ // removed unasked: the server says what it is, and only a yes sends its digest back (a backup is kept).
5441
+ if (!data.ok && data.currentDigest && (data.code === 'skill_unowned' || data.code === 'skill_modified')) {
5442
+ if (!confirm(data.question || data.error)) return;
5443
+ data = await send({ expectedDestinationDigest: data.currentDigest });
5444
+ }
5210
5445
  if (data.ok) {
5211
5446
  chip.classList.toggle('on');
5212
5447
  chip.dataset.state = isOn ? 'off' : 'on';
5448
+ // (a server from before the skill folders answers without `destination`: the chip then only says it is installed)
5449
+ const now = chipState(isOn ? null : (data.destination ? (data.shared ? { native: 'absent', shared: 'native' } : { native: 'native', bridge: !!data.bridge }) : undefined), !isOn);
5450
+ chip.dataset.installState = now.state;
5451
+ chip.setAttribute('aria-pressed', now.on ? 'true' : 'false');
5452
+ chip.title = kit.tx(now.key, { skill: skillName, assistant: chip.textContent.trim() });
5213
5453
  const row = chip.closest('.cc-skill-row');
5214
5454
  if (row) {
5215
5455
  const anyOn = !!row.querySelector('.cc-skill-target.on');
5216
5456
  row.classList.toggle('installed', anyOn);
5217
5457
  }
5218
5458
  const runtime = runtimeLabels[target] || target;
5219
- showCCToast(isOn ? kit.tx('settings.redesign.hooks.skillUninstalledRestartToApply', { runtime: runtime }) : kit.tx('settings.redesign.hooks.skillInstalledRestartToApply', { runtime: runtime }));
5459
+ // The shared ~/.agents/skills is read by Codex and OpenCode both: the other chips of the skill follow.
5460
+ if (data.shared) refreshSkillChips(overlay);
5461
+ // An install is a native skill folder now; the server says when an old bridge of it is still there.
5462
+ if (isOn) showCCToast(kit.tx('settings.redesign.hooks.skillUninstalledRestartToApply', { runtime: runtime }));
5463
+ else if (data.destination) showCCToast(kit.tx(installedToast(data), { skill: skillName, runtime: runtime }), 6000);
5464
+ else showCCToast(kit.tx('settings.redesign.hooks.skillInstalledRestartToApply', { runtime: runtime }));
5220
5465
  } else { alert(data.error || kit.tx('settings.redesign.hooks.failedToToggleSkill')); }
5221
5466
  } catch (err) { alert(kit.tx('settings.redesign.hooks.failed2', { message: err.message })); }
5222
5467
  finally { chip.style.opacity = ''; chip.style.pointerEvents = ''; }
5223
5468
  });
5224
5469
  });
5225
5470
 
5471
+ // Skill folders: the move of the old bridges to native skill folders, and the shared ~/.agents/skills switch.
5472
+ // After a move, an undo, or a change made anywhere else, the chips are read again in place.
5473
+ wireSkillFolders(overlay, { toast: (message, ms) => showCCToast(message, ms), onChanged: () => refreshSkillChips(overlay), signal: panelLife.signal });
5474
+
5226
5475
  // ── Setup tab: per-provider MCP toggles, CLI copy, config copy, SynaBun rules ──
5227
5476
  {
5228
5477
  const checkIcon = '<svg viewBox="0 0 24 24" style="width:12px;height:12px"><polyline points="20 6 9 17 4 12"/></svg>';
@@ -5378,7 +5627,7 @@ async function openSettingsModalNow(options = {}) {
5378
5627
  // An edited copy is removed only on an explicit yes (force). A file without SynaBun's markers is the
5379
5628
  // user's own and stays whatever is sent, so there is nothing to confirm for it.
5380
5629
  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
5630
+ // OpenCode: a line in opencode.json that SynaBun did not add keeps the file on a plain remove (the answer
5382
5631
  // says so, and the host is then no longer managed). Asked again, the confirm sends force, which takes that line too.
5383
5632
  const listed = info?.entry === 'user';
5384
5633
  const force = edited || (listed && info.managed === false);
@@ -5400,7 +5649,7 @@ async function openSettingsModalNow(options = {}) {
5400
5649
  const message = overlay.querySelector('#setup-rules-message');
5401
5650
  const legacyBox = overlay.querySelector('#setup-rules-legacy');
5402
5651
  const versionBadge = overlay.querySelector('#setup-rules-version');
5403
- const hostOf = { claude: 'Claude Code', codex: 'Codex', opencode: 'OpenCode', gemini: 'Gemini' };
5652
+ const hostOf = { claude: 'Claude Code', codex: 'Codex', opencode: 'OpenCode', antigravity: 'Antigravity' };
5404
5653
 
5405
5654
  if (autoToggle && installAllBtn && legacyBox) {
5406
5655
  rulesRenderers.push(() => {
@@ -5473,26 +5722,26 @@ async function openSettingsModalNow(options = {}) {
5473
5722
  : { command: 'node', args: [paths.mcpIndexPath || '<path-to>/mcp-server/run.mjs'] };
5474
5723
  };
5475
5724
 
5476
- // ── Gemini ──
5477
- wireSetupMcpToggle('gemini', '/api/setup/gemini/mcp', '~/.gemini/settings.json');
5478
- // Config preview
5725
+ // ── Antigravity ──
5726
+ wireSetupMcpToggle('antigravity', '/api/setup/antigravity/mcp', '~/.gemini/config/mcp_config.json');
5727
+ // Config preview (mcp_config.json: `disabled`, no `trust`)
5479
5728
  {
5480
- const preview = overlay.querySelector('#setup-gemini-config-preview');
5729
+ const preview = overlay.querySelector('#setup-antigravity-config-preview');
5481
5730
  let cachedConfig = '';
5482
5731
  if (preview) {
5483
- fetch('/api/setup/gemini/mcp').then(r => r.json()).then(data => {
5732
+ fetch('/api/setup/antigravity/mcp').then(r => r.json()).then(data => {
5484
5733
  if (data.ok && data.config) {
5485
5734
  cachedConfig = JSON.stringify(data.config, null, 2);
5486
5735
  preview.textContent = cachedConfig;
5487
5736
  } else if (data.ok) {
5488
- cachedConfig = JSON.stringify({ mcpServers: { SynaBun: { ...mcpLaunch(), env: { DOTENV_PATH: setupStatus.paths?.envPath || '<path-to>/synabun/.env' } } } }, null, 2);
5737
+ cachedConfig = JSON.stringify({ mcpServers: { SynaBun: { ...mcpLaunch(), env: { DOTENV_PATH: setupStatus.paths?.envPath || '<path-to>/synabun/.env' }, disabled: false } } }, null, 2);
5489
5738
  preview.textContent = cachedConfig;
5490
5739
  } else { preview.textContent = kit.tx('settings.redesign.setup.couldNotLoadConfig'); }
5491
5740
  }).catch(() => { preview.textContent = kit.tx('settings.redesign.setup.failedToLoad'); });
5492
5741
  }
5493
- wireCopyBtn('setup-gemini-config-copy', () => cachedConfig, kit.tx('settings.redesign.setup.copyJsonConfig'));
5742
+ wireCopyBtn('setup-antigravity-config-copy', () => cachedConfig, kit.tx('settings.redesign.setup.copyJsonConfig'));
5494
5743
  }
5495
- wireRulesControls('gemini', 'gemini');
5744
+ wireRulesControls('antigravity', 'antigravity');
5496
5745
 
5497
5746
  // ── Codex ──
5498
5747
  wireSetupMcpToggle('codex', '/api/setup/codex/mcp', '~/.codex/config.toml');
@@ -5521,8 +5770,8 @@ async function openSettingsModalNow(options = {}) {
5521
5770
  wireRulesControls('codex', 'codex');
5522
5771
 
5523
5772
  // ── 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
5773
+ wireSetupMcpToggle('opencode', '/api/opencode/mcp', '~/.config/opencode/opencode.json');
5774
+ // Config preview — OpenCode 2 keeps MCP servers under "mcp" → "servers" of opencode.json
5526
5775
  {
5527
5776
  const preview = overlay.querySelector('#setup-opencode-config-preview');
5528
5777
  let cachedConfig = '';
@@ -5530,15 +5779,18 @@ async function openSettingsModalNow(options = {}) {
5530
5779
  const ep = setupStatus.paths?.envPath || '<path-to>/synabun/.env';
5531
5780
  fetch('/api/opencode/mcp').then(r => r.json()).then(data => {
5532
5781
  if (data.ok && data.data && Object.keys(data.data).length) {
5533
- cachedConfig = JSON.stringify({ mcp: data.data }, null, 2);
5782
+ cachedConfig = JSON.stringify({ mcp: { servers: data.data } }, null, 2);
5534
5783
  preview.textContent = cachedConfig;
5535
5784
  } else if (data.ok) {
5785
+ const launch = mcpLaunch();
5536
5786
  cachedConfig = JSON.stringify({
5537
5787
  mcp: {
5538
- SynaBun: {
5539
- type: 'stdio',
5540
- ...mcpLaunch(),
5541
- env: { DOTENV_PATH: ep },
5788
+ servers: {
5789
+ SynaBun: {
5790
+ type: 'local',
5791
+ command: [launch.command, ...launch.args],
5792
+ environment: { DOTENV_PATH: ep },
5793
+ },
5542
5794
  },
5543
5795
  },
5544
5796
  }, null, 2);
@@ -5810,16 +6062,18 @@ async function openSettingsModalNow(options = {}) {
5810
6062
  let _moreloginDesiredEnvId = '';
5811
6063
  async function loadMoreLoginEnvs() {
5812
6064
  const sel = overlay.querySelector('#bc-morelogin-env-id');
6065
+ // A placeholder choice (loading / error / not running) keeps its reason visible in the closed dropdown.
6066
+ const only = (key) => setDropdownItems(sel, [{ value: '', label: kit.tx(key), selected: true }]);
5813
6067
  const statusEl = overlay.querySelector('#bc-morelogin-status-text');
5814
6068
  if (!sel) return;
5815
6069
  try {
5816
6070
  const r = await fetch('/api/browser/morelogin-envs');
5817
6071
  const d = await r.json();
5818
- if (!d || d.ok === false) { sel.innerHTML = '<option value="">' + kit.te('settings.redesign.browser.error') + '</option>'; if (statusEl) statusEl.textContent = (d && d.error) || kit.tx('settings.redesign.browser.failedToLoadMoreloginProfiles'); return; }
5819
- if (!d.installed) { sel.innerHTML = '<option value="">' + kit.te('settings.redesign.browser.moreloginNotInstalled') + '</option>'; if (statusEl) statusEl.textContent = kit.tx('settings.redesign.browser.moreloginDesktopAppIsNotInstalled'); return; }
5820
- if (!d.running) { sel.innerHTML = '<option value="">' + kit.te('settings.redesign.browser.startMorelogin') + '</option>'; if (statusEl) statusEl.textContent = kit.tx('settings.redesign.browser.moreloginNotRunningOpenTheApp', { port: d.port }); return; }
6072
+ if (!d || d.ok === false) { only('settings.redesign.browser.error'); if (statusEl) statusEl.textContent = (d && d.error) || kit.tx('settings.redesign.browser.failedToLoadMoreloginProfiles'); return; }
6073
+ if (!d.installed) { only('settings.redesign.browser.moreloginNotInstalled'); if (statusEl) statusEl.textContent = kit.tx('settings.redesign.browser.moreloginDesktopAppIsNotInstalled') + ' ' + kit.tx('settings.redesign.browser.moreloginCustomPathHint'); return; }
6074
+ if (!d.running) { only('settings.redesign.browser.startMorelogin'); if (statusEl) statusEl.textContent = kit.tx('settings.redesign.browser.moreloginNotRunningOpenTheApp', { port: d.port }) + (d.error ? ' ' + String(d.error) : ''); return; }
5821
6075
  const envs = d.envs || [];
5822
- if (!envs.length) { sel.innerHTML = '<option value="">' + kit.te('settings.redesign.browser.noProfiles') + '</option>'; if (statusEl) statusEl.textContent = kit.tx('settings.redesign.browser.noMoreloginProfilesYetOneIs'); return; }
6076
+ if (!envs.length) { only('settings.redesign.browser.noProfiles'); if (statusEl) statusEl.textContent = kit.tx('settings.redesign.browser.noMoreloginProfilesYetOneIs'); return; }
5823
6077
  const want = _moreloginDesiredEnvId || sel.value || '';
5824
6078
  // Show each env's proxy — without it there is no way to tell from SynaBun
5825
6079
  // whether a profile's traffic leaves the machine.
@@ -5828,8 +6082,9 @@ async function openSettingsModalNow(options = {}) {
5828
6082
  const where = [p.host, p.port].filter(Boolean).join(':');
5829
6083
  return ` · ${[p.type, where, p.country].filter(Boolean).join(' ')}`;
5830
6084
  };
5831
- sel.innerHTML = envs.map(e => `<option value="${escapeHtml(String(e.id))}">${escapeHtml(e.name)}${e.status ? ` (${escapeHtml(String(e.status))})` : ''}${escapeHtml(proxyLabel(e.proxy))}</option>`).join('');
5832
- if (want && envs.some(e => String(e.id) === String(want))) sel.value = String(want);
6085
+ // dropdownHTML escapes the labels itself.
6086
+ setDropdownItems(sel, envs.map(e => ({ value: String(e.id), label: `${e.name ?? ''}${e.status ? ` (${String(e.status)})` : ''}${proxyLabel(e.proxy)}` })));
6087
+ if (want && envs.some(e => String(e.id) === String(want))) setDropdownValue(sel, String(want));
5833
6088
  if (statusEl) {
5834
6089
  let msg = kit.tx('settings.redesign.browser.profileSApiPort', { count: envs.length, port: d.port });
5835
6090
  // MoreLogin silently redirects a navigation it refuses to its stop page: a
@@ -5844,7 +6099,7 @@ async function openSettingsModalNow(options = {}) {
5844
6099
  statusEl.textContent = msg;
5845
6100
  }
5846
6101
  } catch {
5847
- sel.innerHTML = '<option value="">' + kit.te('settings.redesign.browser.error') + '</option>';
6102
+ only('settings.redesign.browser.error');
5848
6103
  if (statusEl) statusEl.textContent = kit.tx('settings.redesign.browser.failedToReachMorelogin');
5849
6104
  }
5850
6105
  }
@@ -5892,6 +6147,7 @@ async function openSettingsModalNow(options = {}) {
5892
6147
  item.addEventListener('click', refreshAttachSeedRow);
5893
6148
  });
5894
6149
  }
6150
+ { const mlRow = overlay.querySelector('#bc-morelogin-row'); if (mlRow) wireDropdowns(mlRow, { signal: panelLife.signal }); }
5895
6151
  overlay.querySelector('#bc-morelogin-refresh')?.addEventListener('click', (e) => { e.preventDefault(); loadMoreLoginEnvs(); });
5896
6152
  const attachSeedDd = overlay.querySelector('.bc-dropdown[data-for="bc-attachSeed"]');
5897
6153
  if (attachSeedDd) {
@@ -7032,7 +7288,7 @@ async function openSettingsModalNow(options = {}) {
7032
7288
 
7033
7289
  // Explore project
7034
7290
  function openExploreModal(projectPath, projectLabel) {
7035
- const EXPLORE_CLI_IDS = ['claude-code', 'codex', 'gemini'];
7291
+ const EXPLORE_CLI_IDS = ['claude-code', 'codex', 'antigravity'];
7036
7292
  const CLI_OPTIONS = CLI_PROFILES
7037
7293
  .filter(p => EXPLORE_CLI_IDS.includes(p.id))
7038
7294
  .map(p => ({ id: p.id, label: p.label }));
@@ -7112,7 +7368,7 @@ async function openSettingsModalNow(options = {}) {
7112
7368
  selectedCli = cli;
7113
7369
  selectedModel = getExploreModels(cli)[0]?.id || '';
7114
7370
  renderModal();
7115
- if (cli === 'codex') {
7371
+ if (cli === 'codex' || cli === 'antigravity') {
7116
7372
  ensureModelsForProfile(cli).then(() => {
7117
7373
  if (exploreOverlay.isConnected && selectedCli === cli) renderModal();
7118
7374
  });
@@ -7315,6 +7571,9 @@ async function openSettingsModalNow(options = {}) {
7315
7571
  const ocpToolsEl = overlay.querySelector('#stg-ocp-tools');
7316
7572
  const ocpHistoryMetaEl = overlay.querySelector('#stg-ocp-history-meta');
7317
7573
  const ocpHistoryClearBtn = overlay.querySelector('#stg-ocp-clear-history');
7574
+ const ocpInstallEl = overlay.querySelector('#stg-ocp-install');
7575
+ const ocpConfigPathEl = overlay.querySelector('#stg-ocp-config-path');
7576
+ const ocpConfigJsoncEl = overlay.querySelector('#stg-ocp-config-jsonc');
7318
7577
  const esc = s => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
7319
7578
 
7320
7579
  // ── Helpers ──
@@ -7337,6 +7596,7 @@ async function openSettingsModalNow(options = {}) {
7337
7596
  let _ollamaConfiguring = false;
7338
7597
  let _ollamaPollTimer = null;
7339
7598
  let _ollamaLastModelNames = '';
7599
+ let _ollamaConfigureError = '';
7340
7600
  let _ocpHistoryStats = null;
7341
7601
 
7342
7602
  function formatBytes(bytes) {
@@ -7367,7 +7627,7 @@ async function openSettingsModalNow(options = {}) {
7367
7627
  ocpHistoryMetaEl.textContent = kit.tx('settings.redesign.opencode.historyWipeInProgress');
7368
7628
  return;
7369
7629
  }
7370
- const totalBytes = (Number(data.databaseBytes) || 0) + (Number(data.artifactBytes) || 0);
7630
+ const totalBytes = Number(data.databaseBytes) || 0;
7371
7631
  ocpHistoryMetaEl.textContent = kit.tx('settings.redesign.opencode.stored', { formatCount: formatCount(data.sessions, 'session'), formatBytes: formatBytes(totalBytes) });
7372
7632
  ocpHistoryClearBtn.disabled = false;
7373
7633
  } catch (error) {
@@ -7379,8 +7639,7 @@ async function openSettingsModalNow(options = {}) {
7379
7639
  if (document.querySelector('.ocp-history-clear-overlay')) return;
7380
7640
  const confirmation = _ocpHistoryStats?.confirmationPhrase || 'DELETE OPENCODE HISTORY';
7381
7641
  const sessionCount = Number(_ocpHistoryStats?.sessions) || 0;
7382
- const totalBytes = (Number(_ocpHistoryStats?.databaseBytes) || 0)
7383
- + (Number(_ocpHistoryStats?.artifactBytes) || 0);
7642
+ const totalBytes = Number(_ocpHistoryStats?.databaseBytes) || 0;
7384
7643
  const modal = document.createElement('div');
7385
7644
  modal.className = 'tag-delete-overlay ocp-history-clear-overlay';
7386
7645
  modal.innerHTML = `
@@ -7394,9 +7653,9 @@ async function openSettingsModalNow(options = {}) {
7394
7653
  <p class="ocp-history-modal-subtitle">${formatCount(sessionCount, 'session')} ${kit.th('settings.redesign.opencode.currentlyStored', { formatBytes: formatBytes(totalBytes) })}</p>
7395
7654
  </div>
7396
7655
  </div>
7397
- <div class="ocp-history-warning"><strong>${kit.te('settings.redesign.opencode.thisCannotBeUndone')}</strong> ${kit.te('settings.redesign.opencode.activeConversationsOrAutomationsMustBe')}</div>
7656
+ <div class="ocp-history-warning"><strong>${kit.te('settings.redesign.opencode.thisCannotBeUndone')}</strong> ${kit.te('settings.redesign.opencode.historyClearSteps')}</div>
7398
7657
  <ul class="ocp-history-scope">
7399
- <li>${kit.te('settings.redesign.opencode.deletesConversationsMessagesToolOutputTo')}</li>
7658
+ <li>${kit.te('settings.redesign.opencode.historyClearDeletes')}</li>
7400
7659
  <li>${kit.te('settings.redesign.opencode.deletesSynabunSDerivedOpencodeResume')}</li>
7401
7660
  <li>${kit.te('settings.redesign.opencode.keepsProviderCredentialsAccountConfigSet')}</li>
7402
7661
  </ul>
@@ -7444,7 +7703,7 @@ async function openSettingsModalNow(options = {}) {
7444
7703
  confirmButton.disabled = true;
7445
7704
  confirmButton.textContent = kit.tx('settings.redesign.opencode.clearing');
7446
7705
  feedback.dataset.state = 'working';
7447
- feedback.textContent = kit.tx('settings.redesign.opencode.stoppingOpencodeAndClearingItsHistory');
7706
+ feedback.textContent = kit.tx('settings.redesign.opencode.historyClearWorking');
7448
7707
  const requestId = crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`;
7449
7708
  sessionStorage.setItem('synabun-opencode-history-clear-request', requestId);
7450
7709
  try {
@@ -7457,9 +7716,7 @@ async function openSettingsModalNow(options = {}) {
7457
7716
  if (!response.ok || !data.ok) throw new Error(data.error || kit.tx('settings.redesign.opencode.historyWipeFailedHttp', { status: response.status }));
7458
7717
 
7459
7718
  const result = data.result || {};
7460
- const reclaimed = Math.max(0,
7461
- (Number(result.databaseBytesBefore) || 0) - (Number(result.databaseBytesAfter) || 0)
7462
- + (Number(result.artifactBytesDeleted) || 0));
7719
+ const reclaimed = Math.max(0, (Number(result.databaseBytesBefore) || 0) - (Number(result.databaseBytesAfter) || 0));
7463
7720
  const warnings = Array.isArray(result.warnings) ? result.warnings.filter(Boolean) : [];
7464
7721
  feedback.dataset.state = warnings.length ? 'warning' : 'working';
7465
7722
  feedback.textContent = warnings.length
@@ -7500,7 +7757,7 @@ async function openSettingsModalNow(options = {}) {
7500
7757
  dotColor = '#facc15';
7501
7758
  statusText = kit.tx('settings.redesign.opencode.configuring');
7502
7759
  statusCls = 'stg-provider-status-key';
7503
- bodyHtml = '<div class="stg-provider-note">' + kit.te('settings.redesign.opencode.writingProviderConfigAndRestartingOpenco') + '</div>';
7760
+ bodyHtml = '<div class="stg-provider-note">' + kit.te('settings.redesign.opencode.writingProviderConfig') + '</div>';
7504
7761
  } else if (!_ollamaRunning) {
7505
7762
  dotColor = 'rgba(248,113,113,0.7)';
7506
7763
  statusText = kit.tx('settings.redesign.opencode.notDetected');
@@ -7511,6 +7768,11 @@ async function openSettingsModalNow(options = {}) {
7511
7768
  statusText = kit.tx('settings.redesign.opencode.runningVersion', { version: _ollamaVersion ? ' v' + _ollamaVersion : '' });
7512
7769
  statusCls = 'stg-provider-status-key';
7513
7770
  bodyHtml = '<div class="stg-provider-note">' + kit.te('settings.redesign.opencode.noModelsRun') + ' <code class="stg-code">ollama pull &lt;model&gt;</code></div>';
7771
+ } else if (_ollamaConfigureError) {
7772
+ dotColor = 'rgba(248,113,113,0.7)';
7773
+ statusText = kit.tx('settings.redesign.opencode.runningVersion', { version: _ollamaVersion ? ' v' + _ollamaVersion : '' });
7774
+ statusCls = 'stg-provider-status-disconnected';
7775
+ bodyHtml = '<div class="stg-provider-note">' + esc(kit.tx('settings.redesign.opencode.error2', { message: _ollamaConfigureError })) + '</div>';
7514
7776
  } else if (_ollamaConfigured) {
7515
7777
  dotColor = '#4ade80';
7516
7778
  statusText = kit.tx('settings.redesign.opencode.runningVersion', { version: _ollamaVersion ? ' v' + _ollamaVersion : '' });
@@ -7546,27 +7808,16 @@ async function openSettingsModalNow(options = {}) {
7546
7808
  const btn = existing.querySelector('.stg-ollama-remove');
7547
7809
  if (btn) { btn.disabled = true; btn.textContent = kit.tx('settings.redesign.opencode.removing'); }
7548
7810
  try {
7549
- await fetch('/api/ollama/configure', { method: 'DELETE' });
7811
+ const resp = await fetch('/api/ollama/configure', { method: 'DELETE' });
7812
+ const data = await resp.json().catch(() => ({}));
7813
+ if (!resp.ok || data.ok === false) throw new Error(data.error || `HTTP ${resp.status}`);
7550
7814
  _ollamaConfigured = false;
7551
7815
  _ollamaConfiguredCount = 0;
7552
7816
  _ollamaUserRemoved = true;
7553
7817
  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);
7818
+ showCCToast(kit.tx('settings.redesign.opencode.ollamaProviderRemoved'));
7819
+ await refreshOcpProviders();
7820
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
7570
7821
  } catch (e) {
7571
7822
  showCCToast(kit.tx('settings.redesign.opencode.error2', { message: e.message }));
7572
7823
  }
@@ -7600,7 +7851,9 @@ async function openSettingsModalNow(options = {}) {
7600
7851
 
7601
7852
  // Auto-configure if: running + has models + not configured + not user-removed
7602
7853
  // Also re-configure if models changed while already configured
7603
- const shouldConfigure = _ollamaRunning && _ollamaModels.length > 0 && !_ollamaUserRemoved
7854
+ // A refusal (an opencode.json SynaBun cannot change now) is asked again only when the models change.
7855
+ if (modelsChanged) _ollamaConfigureError = '';
7856
+ const shouldConfigure = _ollamaRunning && _ollamaModels.length > 0 && !_ollamaUserRemoved && !_ollamaConfigureError
7604
7857
  && (!_ollamaConfigured || modelsChanged);
7605
7858
 
7606
7859
  if (shouldConfigure && !_ollamaConfiguring) {
@@ -7608,31 +7861,16 @@ async function openSettingsModalNow(options = {}) {
7608
7861
  renderOllamaCard();
7609
7862
  try {
7610
7863
  const cfgResp = await fetch('/api/ollama/configure', { method: 'POST' });
7611
- const cfgData = await cfgResp.json();
7612
- if (cfgData.ok) {
7864
+ const cfgData = await cfgResp.json().catch(() => ({}));
7865
+ _ollamaConfiguring = false;
7866
+ if (cfgResp.ok && cfgData.ok) {
7867
+ // OpenCode reads the provider at once: its models are listed now.
7613
7868
  _ollamaConfigured = true;
7614
7869
  _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);
7870
+ await refreshOcpProviders();
7871
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
7634
7872
  } else {
7635
- _ollamaConfiguring = false;
7873
+ _ollamaConfigureError = cfgData.error || `HTTP ${cfgResp.status}`;
7636
7874
  }
7637
7875
  } catch {
7638
7876
  _ollamaConfiguring = false;
@@ -7884,7 +8122,7 @@ async function openSettingsModalNow(options = {}) {
7884
8122
  }
7885
8123
  for (const om of oauthMethods) {
7886
8124
  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>`);
8125
+ 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
8126
  }
7889
8127
  authHtml = btns.join('');
7890
8128
  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 +8136,7 @@ async function openSettingsModalNow(options = {}) {
7898
8136
  }
7899
8137
  for (const om of oauthMethods) {
7900
8138
  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>`);
8139
+ 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
8140
  }
7903
8141
  authHtml = btns.join('');
7904
8142
  }
@@ -7914,9 +8152,16 @@ async function openSettingsModalNow(options = {}) {
7914
8152
  </div>`;
7915
8153
 
7916
8154
  // OAuth wait indicator (hidden via inline style — JS removes it to show)
8155
+ // OAuth wait (hidden via inline style — JS removes it to show): OpenCode's instructions (a device code),
8156
+ // and, for a sign-in that ends with a code the provider shows, the field it is pasted in.
7917
8157
  const oauthWaitHtml = `<div class="stg-ocp-oauth-wait" data-provider="${esc(id)}" style="display:none">
7918
- ${kit.te('settings.redesign.opencode.waitingForAuthorization')}
8158
+ <span class="stg-ocp-oauth-status">${kit.te('settings.redesign.opencode.waitingForAuthorization')}</span>
7919
8159
  <span class="stg-ocp-oauth-code" style="display:none"></span>
8160
+ <div class="stg-ocp-key-row stg-ocp-oauth-code-row" style="display:none">
8161
+ <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">
8162
+ <button type="button" class="stg-action-btn compact stg-ocp-oauth-code-submit">${kit.te('settings.redesign.opencode.oauthConnect')}</button>
8163
+ </div>
8164
+ <div class="stg-ocp-key-error"></div>
7920
8165
  </div>`;
7921
8166
 
7922
8167
  const hiddenSet = getHiddenModels();
@@ -8112,28 +8357,15 @@ async function openSettingsModalNow(options = {}) {
8112
8357
  try {
8113
8358
  const resp = await fetch(`/api/opencode/auth/${encodeURIComponent(provId)}`, {
8114
8359
  method: 'PUT', headers: { 'Content-Type': 'application/json' },
8115
- body: JSON.stringify({ type: 'api', key }),
8360
+ body: JSON.stringify({ key }),
8116
8361
  });
8117
- const data = await resp.json();
8362
+ const data = await resp.json().catch(() => ({}));
8118
8363
  if (resp.ok && data.ok !== false) {
8119
8364
  form.style.display = 'none';
8120
8365
  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);
8366
+ showCCToast(kit.tx('settings.redesign.opencode.apiKeySaved', { provId: provId }));
8367
+ await refreshOcpProviders();
8368
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8137
8369
  } else {
8138
8370
  const msg = data.error || kit.tx('settings.redesign.opencode.failedToSaveKey');
8139
8371
  if (errEl) { errEl.textContent = msg; errEl.style.display = ''; }
@@ -8166,24 +8398,11 @@ async function openSettingsModalNow(options = {}) {
8166
8398
  method: 'DELETE',
8167
8399
  headers: { 'Content-Type': 'application/json' },
8168
8400
  });
8169
- const data = await resp.json();
8401
+ const data = await resp.json().catch(() => ({}));
8170
8402
  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);
8403
+ showCCToast(kit.tx('settings.redesign.opencode.apiKeyRemoved', { provId: provId }));
8404
+ await refreshOcpProviders();
8405
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8187
8406
  } else {
8188
8407
  showCCToast(data.error || kit.tx('settings.redesign.opencode.failedToRemoveKey'));
8189
8408
  }
@@ -8200,43 +8419,22 @@ async function openSettingsModalNow(options = {}) {
8200
8419
  btn.addEventListener('click', async () => {
8201
8420
  const provId = btn.dataset.provider;
8202
8421
  const methodIdx = parseInt(btn.dataset.method, 10);
8422
+ const card = btn.closest('.stg-provider-card');
8423
+ const providerName = card?.querySelector('.stg-provider-name')?.textContent?.trim() || provId;
8203
8424
  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
8425
  try {
8207
8426
  const resp = await fetch(`/api/opencode/oauth/${encodeURIComponent(provId)}/authorize`, {
8208
8427
  method: 'POST',
8209
8428
  headers: { 'Content-Type': 'application/json' },
8210
- body: JSON.stringify({ method: isNaN(methodIdx) ? 0 : methodIdx }),
8429
+ body: JSON.stringify({ methodID: btn.dataset.methodId || undefined, method: isNaN(methodIdx) ? 0 : methodIdx }),
8211
8430
  });
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);
8431
+ const data = await resp.json().catch(() => ({}));
8432
+ const auth = data.data || {};
8433
+ if (resp.ok && data.ok !== false && auth.url) {
8434
+ window.open(auth.url, '_blank', 'noopener');
8435
+ showOcpOAuthWait(provId, providerName, auth);
8238
8436
  } else {
8239
- showCCToast(authData.error || data.error || kit.tx('settings.redesign.opencode.oauthFailedNoAuthorizationUrlReturned'));
8437
+ showCCToast(data.error || kit.tx('settings.redesign.opencode.oauthFailedNoAuthorizationUrlReturned'));
8240
8438
  }
8241
8439
  } catch (e) {
8242
8440
  showCCToast(kit.tx('settings.redesign.opencode.oauthError', { message: e.message }));
@@ -8246,6 +8444,81 @@ async function openSettingsModalNow(options = {}) {
8246
8444
  });
8247
8445
  }
8248
8446
 
8447
+ // ── A sign-in under way (OpenCode 2's attempt: `auto` finishes by itself, `code` with the code pasted) ──
8448
+ const _ocpOAuthPolls = new Map(); // provider id → timer
8449
+ function endOcpOAuth(provId) {
8450
+ clearInterval(_ocpOAuthPolls.get(provId));
8451
+ _ocpOAuthPolls.delete(provId);
8452
+ const waitEl = ocpProvidersEl?.querySelector(`.stg-ocp-oauth-wait[data-provider="${CSS.escape(provId)}"]`);
8453
+ if (waitEl) waitEl.style.display = 'none';
8454
+ }
8455
+ async function finishOcpOAuth(provId) {
8456
+ endOcpOAuth(provId);
8457
+ showCCToast(kit.tx('settings.redesign.opencode.connected', { provId: provId }));
8458
+ await refreshOcpProviders();
8459
+ document.dispatchEvent(new CustomEvent('ocp-providers-changed'));
8460
+ }
8461
+ function showOcpOAuthWait(provId, providerName, auth) {
8462
+ endOcpOAuth(provId);
8463
+ const waitEl = ocpProvidersEl?.querySelector(`.stg-ocp-oauth-wait[data-provider="${CSS.escape(provId)}"]`);
8464
+ const codeEl = waitEl?.querySelector('.stg-ocp-oauth-code');
8465
+ const codeRow = waitEl?.querySelector('.stg-ocp-oauth-code-row');
8466
+ const errEl = waitEl?.querySelector('.stg-ocp-key-error');
8467
+ if (waitEl) waitEl.style.display = '';
8468
+ if (codeEl) { codeEl.textContent = auth.instructions || ''; codeEl.style.display = auth.instructions ? '' : 'none'; }
8469
+ if (errEl) errEl.textContent = '';
8470
+ if (auth.method === 'code') {
8471
+ if (!codeRow) return;
8472
+ codeRow.style.display = '';
8473
+ const input = codeRow.querySelector('.stg-ocp-oauth-code-input');
8474
+ const submit = codeRow.querySelector('.stg-ocp-oauth-code-submit');
8475
+ input?.focus();
8476
+ const send = async () => {
8477
+ const code = input?.value?.trim();
8478
+ if (!code) return;
8479
+ submit.disabled = true;
8480
+ try {
8481
+ const resp = await fetch(`/api/opencode/oauth/${encodeURIComponent(provId)}/callback`, {
8482
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
8483
+ body: JSON.stringify({ attemptID: auth.attemptID, code }),
8484
+ });
8485
+ const data = await resp.json().catch(() => ({}));
8486
+ if (!resp.ok || data.ok === false) throw new Error(data.error || `HTTP ${resp.status}`);
8487
+ input.value = '';
8488
+ await finishOcpOAuth(provId);
8489
+ } catch (e) {
8490
+ if (errEl) errEl.textContent = kit.tx('settings.redesign.opencode.oauthFailed', { message: e.message });
8491
+ } finally {
8492
+ submit.disabled = false;
8493
+ }
8494
+ };
8495
+ if (submit) submit.onclick = send;
8496
+ if (input) input.onkeydown = (event) => { if (event.key === 'Enter') { event.preventDefault(); send(); } };
8497
+ return;
8498
+ }
8499
+ if (codeRow) codeRow.style.display = 'none';
8500
+ if (!auth.attemptID) return;
8501
+ // Every 2 s for five minutes: then the attempt is let go (OpenCode's own attempt ends by itself).
8502
+ let attempts = 0;
8503
+ _ocpOAuthPolls.set(provId, setInterval(async () => {
8504
+ attempts++;
8505
+ if (attempts > 150) { endOcpOAuth(provId); showCCToast(kit.tx('settings.redesign.opencode.oauthGaveUp')); return; }
8506
+ try {
8507
+ const resp = await fetch(`/api/opencode/oauth/${encodeURIComponent(provId)}/status?attemptID=${encodeURIComponent(auth.attemptID)}`);
8508
+ const data = await resp.json().catch(() => ({}));
8509
+ if (!resp.ok || data.ok === false) throw new Error(data.error || `HTTP ${resp.status}`);
8510
+ const status = data.data?.status;
8511
+ if (status === 'complete') await finishOcpOAuth(provId);
8512
+ else if (status === 'expired') { endOcpOAuth(provId); showCCToast(kit.tx('settings.redesign.opencode.oauthExpired')); }
8513
+ else if (status === 'failed') { endOcpOAuth(provId); showCCToast(kit.tx('settings.redesign.opencode.oauthFailed', { message: data.data?.message || status })); }
8514
+ } catch (e) {
8515
+ endOcpOAuth(provId);
8516
+ showCCToast(kit.tx('settings.redesign.opencode.oauthFailed', { message: e.message }));
8517
+ }
8518
+ }, 2000));
8519
+ }
8520
+ panelLife.signal.addEventListener('abort', () => { for (const provId of [..._ocpOAuthPolls.keys()]) endOcpOAuth(provId); });
8521
+
8249
8522
  // ── Delegated card click-to-expand (survives re-renders) ──
8250
8523
  if (ocpProvidersEl) {
8251
8524
  ocpProvidersEl.addEventListener('click', (e) => {
@@ -8263,64 +8536,60 @@ async function openSettingsModalNow(options = {}) {
8263
8536
  }
8264
8537
 
8265
8538
  // ── 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;
8539
+ // What each of OpenCode's own tools may do: the rules of OpenCode's global opencode.json
8540
+ // (GET /api/opencode/config: `actions`, `tools`), read and written there whether or not OpenCode runs.
8541
+ const OCP_EFFECTS = ['allow', 'ask', 'deny'];
8542
+ if (ocpToolsEl) {
8543
+ wireDropdowns(ocpToolsEl, { signal: panelLife.signal });
8544
+ ocpToolsEl.addEventListener('change', async (event) => {
8545
+ const input = event.target;
8546
+ const action = input?.dataset?.tool;
8547
+ if (!action || !OCP_EFFECTS.includes(input.value)) return;
8548
+ try {
8549
+ const resp = await fetch('/api/opencode/config', {
8550
+ method: 'PATCH', headers: { 'Content-Type': 'application/json' },
8551
+ body: JSON.stringify({ tools: { [action]: input.value } }),
8552
+ });
8553
+ const data = await resp.json().catch(() => ({}));
8554
+ if (!resp.ok || data.ok === false) throw new Error(data.error || `HTTP ${resp.status}`);
8555
+ applyOcpConfig(data.data);
8556
+ } catch (err) {
8557
+ showCCToast(kit.tx('settings.redesign.opencode.saveFailed', { message: err.message }));
8558
+ renderToolPermissions();
8283
8559
  }
8560
+ }, { signal: panelLife.signal });
8561
+ }
8284
8562
 
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>
8563
+ function renderToolPermissionsFrom(view) {
8564
+ if (!ocpToolsEl) return;
8565
+ const actions = Array.isArray(view?.actions) ? view.actions : [];
8566
+ if (!actions.length) {
8567
+ ocpToolsEl.innerHTML = '<div class="settings-hint" style="opacity:0.4">' + kit.te('settings.redesign.opencode.noToolsAvailable') + '</div>';
8568
+ return;
8569
+ }
8570
+ const tools = view.tools || {};
8571
+ const labels = { allow: kit.tx('settings.redesign.opencode.allow'), ask: kit.tx('settings.redesign.opencode.ask'), deny: kit.tx('settings.redesign.opencode.deny') };
8572
+ ocpToolsEl.innerHTML = `<div class="stg-tool-grid">${actions.map((action) => {
8573
+ const current = OCP_EFFECTS.includes(tools[action]) ? tools[action] : 'allow';
8574
+ const id = `stg-ocp-tool-${String(action).replace(/[^a-z0-9_-]/gi, '-')}`;
8575
+ return `<div class="stg-tool-row">
8576
+ <span class="stg-tool-name">${esc(action)}</span>
8577
+ ${dropdownHTML(id, OCP_EFFECTS.map((effect) => ({ value: effect, label: labels[effect], selected: effect === current })), {
8578
+ classes: 'stg-dropdown stg-dropdown-inline stg-ocp-tool-perm',
8579
+ label: kit.tx('settings.redesign.opencode.permissionFor', { tool: action }),
8580
+ inputAttrs: `data-tool="${esc(action)}"`,
8581
+ })}
8298
8582
  </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
- });
8583
+ }).join('')}</div>`;
8584
+ }
8585
+
8586
+ async function renderToolPermissions() {
8587
+ if (!ocpToolsEl) return;
8588
+ try {
8589
+ const resp = await fetch('/api/opencode/config');
8590
+ const data = await resp.json();
8591
+ if (!resp.ok || data.ok === false) throw new Error(data.error || `HTTP ${resp.status}`);
8592
+ applyOcpConfig(data.data);
8324
8593
  } catch {
8325
8594
  ocpToolsEl.innerHTML = '<div class="settings-hint" style="opacity:0.4">' + kit.te('settings.redesign.opencode.failedToLoadTools') + '</div>';
8326
8595
  }
@@ -8338,155 +8607,37 @@ async function openSettingsModalNow(options = {}) {
8338
8607
  return null;
8339
8608
  }
8340
8609
 
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
- });
8610
+ let _settingsToolCatalog = null;
8611
+ const defaultToolsWrap = overlay.querySelector('#stg-mcp-default-tools');
8612
+ if (defaultToolsWrap) wireDropdowns(defaultToolsWrap, { signal: panelLife.signal });
8613
+ function renderDefaultTools() {
8614
+ const wrap = overlay.querySelector('#stg-mcp-default-tools');
8615
+ if (!wrap) return;
8616
+ wrap.innerHTML = _settingsToolCatalog ? toolPickerHTML('stg-default-tools', _settingsToolCatalog.defaultTools, { catalog: _settingsToolCatalog, provider: 'all', label: toolWord('defaultTools') }) : '';
8617
+ wrap.querySelector('#stg-default-tools')?.addEventListener('change', async (event) => {
8618
+ const tools = readToolPicker(event.target, { inherit: false });
8619
+ try {
8620
+ const response = await fetch('/api/mcp/tools/default', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tools }) });
8621
+ const result = await response.json();
8622
+ if (!response.ok || !result.ok) throw new Error(result.error || toolWord('saveFailed'));
8623
+ _settingsToolCatalog.defaultTools = result.tools;
8624
+ showCCToast(toolWord('saved'));
8625
+ } catch (error) { showCCToast(error.message || toolWord('saveFailed')); }
8626
+ renderDefaultTools();
8442
8627
  });
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
8628
  }
8469
8629
 
8470
8630
  function renderAlwaysOn() {
8471
8631
  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>`;
8632
+ if (!el) return;
8633
+ 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
8634
  }
8484
8635
 
8485
8636
  function renderExtServerList() {
8486
8637
  const el = overlay.querySelector('#stg-mcp-ext-servers');
8487
8638
  if (!el || !_mcpRegistry) return;
8488
- const servers = _mcpRegistry.servers || {};
8489
- const profiles = Object.entries(_mcpRegistry.profiles || {});
8639
+ const servers = { ...(_mcpRegistry.servers || {}) };
8640
+ for (const row of _settingsToolCatalog?.servers || []) { const name = row.ref.slice(4); if (!servers[name]) servers[name] = { type: toolWord('configuredExternally'), discovered: true }; }
8490
8641
  const entries = Object.entries(servers).filter(([, info]) => !info.builtin);
8491
8642
  if (!entries.length) {
8492
8643
  el.innerHTML = '<div class="stg-mcp-empty-note">' + kit.te('settings.redesign.mcp.noExternalServersConfigured') + '</div>';
@@ -8501,38 +8652,31 @@ async function openSettingsModalNow(options = {}) {
8501
8652
  <span class="stg-mx-ext-name">${esc(name)}</span>
8502
8653
  <span class="stg-mx-ext-type">${esc(info.type || '?')}</span>
8503
8654
  </div>
8504
- <div class="stg-mx-ext-sub">${kit.te('settings.redesign.mcp.availableToAnyProfileYouEnable')}</div>
8655
+ <div class="stg-mx-ext-sub">${esc(toolWord('agentAccessHelp'))}</div>
8505
8656
  </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>
8657
+ ${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
8658
  </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('')}
8659
+ <div class="stg-field"><span>${esc(toolWord('agentAccess'))}</span>
8660
+ <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
8661
  </div>
8516
8662
  </div>`
8517
8663
  ).join('');
8518
8664
 
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();
8665
+ el.querySelectorAll('[data-agent-server]').forEach(input => {
8666
+ input.addEventListener('click', async () => {
8667
+ const enabled = input.getAttribute('aria-checked') !== 'true';
8668
+ input.disabled = true;
8669
+ try {
8670
+ const response = await fetch(`/api/mcp/registry/agent-access/${encodeURIComponent(input.dataset.agentServer)}`, {
8671
+ method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled }),
8672
+ });
8673
+ const result = await response.json();
8674
+ if (!response.ok || !result.ok) throw new Error(result.error || toolWord('saveFailed'));
8675
+ _mcpRegistry.agentAccess = result.agentAccess;
8676
+ input.classList.toggle('on', enabled);
8677
+ input.setAttribute('aria-checked', String(enabled));
8678
+ } catch (error) { showCCToast(error.message || toolWord('saveFailed')); }
8679
+ finally { input.disabled = false; }
8536
8680
  });
8537
8681
  });
8538
8682
 
@@ -8575,7 +8719,7 @@ async function openSettingsModalNow(options = {}) {
8575
8719
  claudeCode: { label: 'CC', title: 'Claude Code (~/.claude.json)' },
8576
8720
  opencode: { label: 'OC', title: 'OpenCode' },
8577
8721
  codex: { label: 'CX', title: 'Codex (~/.codex/config.toml)' },
8578
- gemini: { label: 'GE', title: 'Gemini (~/.gemini/settings.json)' },
8722
+ antigravity: { label: 'AG', title: 'Antigravity (~/.gemini/config/mcp_config.json)' },
8579
8723
  };
8580
8724
 
8581
8725
  async function renderInstalledAll() {
@@ -8687,7 +8831,8 @@ async function openSettingsModalNow(options = {}) {
8687
8831
 
8688
8832
  async function refreshMcpTab() {
8689
8833
  await fetchMcpRegistry();
8690
- renderMcpMatrix();
8834
+ _settingsToolCatalog = await fetchToolCatalog(true);
8835
+ renderDefaultTools();
8691
8836
  renderAlwaysOn();
8692
8837
  renderExtServerList();
8693
8838
  renderPluginsList();
@@ -8845,13 +8990,18 @@ async function openSettingsModalNow(options = {}) {
8845
8990
  const platformsEl = overlay.querySelector('#stg-mcp-platforms');
8846
8991
  const feedbackEl = overlay.querySelector('#stg-mcp-sync-feedback');
8847
8992
 
8993
+ // The type is a styled dropdown: filled from code it fires no change, so the form's transport follows here too.
8994
+ function setServerType(type) {
8995
+ if (typeEl) setDropdownValue(typeEl, type);
8996
+ if (addForm) addForm.dataset.transport = typeEl?.value || type;
8997
+ }
8998
+
8848
8999
  function resetAddForm() {
8849
9000
  if (pasteZone) pasteZone.value = '';
8850
9001
  if (nameEl) nameEl.value = '';
8851
9002
  if (cmdEl) cmdEl.value = '';
8852
9003
  if (urlEl) urlEl.value = '';
8853
- if (typeEl) typeEl.value = 'stdio';
8854
- if (addForm) addForm.dataset.transport = 'stdio';
9004
+ setServerType('stdio');
8855
9005
  // Clear arg chips
8856
9006
  if (argChips) argChips.querySelectorAll('.stg-mcp-arg-chip').forEach(c => c.remove());
8857
9007
  // Clear env + header rows
@@ -8935,10 +9085,7 @@ async function openSettingsModalNow(options = {}) {
8935
9085
  function populateAddForm(parsed) {
8936
9086
  if (!parsed) return;
8937
9087
  if (parsed.name && nameEl) nameEl.value = parsed.name;
8938
- if (parsed.type && typeEl) {
8939
- typeEl.value = parsed.type;
8940
- if (addForm) addForm.dataset.transport = parsed.type;
8941
- }
9088
+ if (parsed.type) setServerType(parsed.type);
8942
9089
  if (parsed.command && cmdEl) cmdEl.value = parsed.command;
8943
9090
  if (parsed.url && urlEl) urlEl.value = parsed.url;
8944
9091
  if (parsed.args && parsed.args.length) {
@@ -9045,7 +9192,7 @@ async function openSettingsModalNow(options = {}) {
9045
9192
  function applyDetected(name, detected) {
9046
9193
  if (!detected) return;
9047
9194
  if (nameEl && name) nameEl.value = name;
9048
- if (typeEl) typeEl.value = detected.type || 'stdio';
9195
+ setServerType(detected.type || 'stdio');
9049
9196
  if (cmdEl) cmdEl.value = detected.command || '';
9050
9197
  // Rebuild arg chips
9051
9198
  if (argChips && argInput) {
@@ -9199,55 +9346,62 @@ async function openSettingsModalNow(options = {}) {
9199
9346
  });
9200
9347
 
9201
9348
  // ── Config save helpers ──
9349
+ // PATCH /api/opencode/config (OpenCode 2's global opencode.json): the answer is the configuration as it is now.
9202
9350
  async function saveOcpConfig(patch) {
9203
9351
  try {
9204
- await fetch('/api/opencode/config', {
9352
+ const resp = await fetch('/api/opencode/config', {
9205
9353
  method: 'PATCH', headers: { 'Content-Type': 'application/json' },
9206
9354
  body: JSON.stringify(patch),
9207
9355
  });
9208
- } catch {}
9356
+ const data = await resp.json().catch(() => ({}));
9357
+ if (!resp.ok || data.ok === false) throw new Error(data.error || `HTTP ${resp.status}`);
9358
+ applyOcpConfig(data.data);
9359
+ return true;
9360
+ } catch (err) {
9361
+ showCCToast(kit.tx('settings.redesign.opencode.saveFailed', { message: err.message }));
9362
+ loadOcpConfig();
9363
+ return false;
9364
+ }
9209
9365
  }
9210
9366
 
9211
9367
  // ── Compaction handlers ──
9212
9368
  const compactAutoEl = overlay.querySelector('#stg-ocp-compact-auto');
9213
- const compactPruneEl = overlay.querySelector('#stg-ocp-compact-prune');
9214
9369
  const compactReservedEl = overlay.querySelector('#stg-ocp-compact-reserved');
9215
9370
 
9371
+ // An emptied field is OpenCode's own default again (buffer: null).
9216
9372
  function saveCompaction() {
9217
- saveOcpConfig({
9218
- compaction: {
9219
- auto: compactAutoEl?.checked ?? true,
9220
- prune: compactPruneEl?.checked ?? true,
9221
- reserved: Number(compactReservedEl?.value) || 10000,
9222
- },
9223
- });
9373
+ const raw = String(compactReservedEl?.value ?? '').trim();
9374
+ const buffer = raw === '' ? null : Math.round(Number(raw));
9375
+ if (buffer !== null && !(Number.isFinite(buffer) && buffer >= 0 && buffer <= 1_000_000)) { loadOcpConfig(); return; }
9376
+ saveOcpConfig({ compaction: { auto: compactAutoEl?.checked ?? true, buffer } });
9224
9377
  }
9225
9378
  compactAutoEl?.addEventListener('change', saveCompaction);
9226
- compactPruneEl?.addEventListener('change', saveCompaction);
9227
9379
  compactReservedEl?.addEventListener('change', saveCompaction);
9228
9380
 
9229
- // ── Snapshot toggle (persists to config.json; read by OpenCode at startup) ──
9381
+ // ── Snapshot toggle (OpenCode's `snapshots`, read at once) ──
9230
9382
  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
- });
9383
+ snapshotToggle?.addEventListener('click', () => {
9384
+ const next = !snapshotToggle.classList.contains('on');
9385
+ snapshotToggle.classList.toggle('on', next); // optimistic; the answer (or a failed save's reload) sets it
9386
+ saveOcpConfig({ snapshots: next });
9387
+ });
9388
+
9389
+ // The configuration as GET / PATCH /api/opencode/config give it: the file SynaBun edits, compaction,
9390
+ // snapshots and the tools' permissions.
9391
+ function applyOcpConfig(view) {
9392
+ if (!view || typeof view !== 'object') return;
9393
+ _ocpConfig = view;
9394
+ const comp = view.compaction || {};
9395
+ if (compactAutoEl) compactAutoEl.checked = comp.auto !== false;
9396
+ if (compactReservedEl && document.activeElement !== compactReservedEl) compactReservedEl.value = Number.isFinite(comp.buffer) ? String(comp.buffer) : '';
9397
+ snapshotToggle?.classList.toggle('on', view.snapshots !== false);
9398
+ if (ocpConfigPathEl && view.file) ocpConfigPathEl.textContent = view.file;
9399
+ const jsonc = (Array.isArray(view.files) ? view.files : []).find((file) => file?.exists && /\.jsonc$/i.test(String(file.path || '')));
9400
+ if (ocpConfigJsoncEl) {
9401
+ ocpConfigJsoncEl.hidden = !jsonc;
9402
+ ocpConfigJsoncEl.innerHTML = jsonc ? kit.th('settings.redesign.opencode.jsoncWins', { path: `<code class="stg-code">${esc(jsonc.path)}</code>` }) : '';
9403
+ }
9404
+ renderToolPermissionsFrom(view);
9251
9405
  }
9252
9406
 
9253
9407
  // Capability filter pills — use CSS vars, not inline color
@@ -9285,28 +9439,16 @@ async function openSettingsModalNow(options = {}) {
9285
9439
  });
9286
9440
 
9287
9441
  // ── Open config file in Synabun's editor ──
9442
+ // OpenCode 2's global opencode.json (made with its $schema when there is none yet).
9288
9443
  overlay.querySelector('#stg-ocp-open-config')?.addEventListener('click', async () => {
9289
9444
  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;
9445
+ const fileResp = await fetch('/api/opencode/config/file');
9446
+ const fileData = await fileResp.json().catch(() => ({}));
9447
+ if (!fileResp.ok || !fileData?.path) {
9448
+ showCCToast(fileData?.error || kit.tx('settings.redesign.opencode.failedToOpenOpencodeConfig'));
9449
+ return;
9307
9450
  }
9308
-
9309
- emit('open-file-editor', { filePath });
9451
+ emit('open-file-editor', { filePath: fileData.path });
9310
9452
  } catch (err) {
9311
9453
  console.error('[settings] Failed to open OpenCode config:', err);
9312
9454
  showCCToast(kit.tx('settings.redesign.opencode.failedToOpenOpencodeConfig'));
@@ -9334,38 +9476,46 @@ async function openSettingsModalNow(options = {}) {
9334
9476
  } catch {}
9335
9477
  }
9336
9478
 
9337
- // ── Load config values into UI ──
9479
+ // ── Load config values into UI (read from the files: OpenCode need not run) ──
9338
9480
  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 {}
9481
+ await renderToolPermissions();
9482
+ }
9483
+
9484
+ // The OpenCode SynaBun found: its version, or why SynaBun does not drive it and how to get OpenCode 2.
9485
+ function renderOcpInstall(installed) {
9486
+ const note = describeOpencodeInstall(installed, { th: kit.th, tx: kit.tx });
9487
+ if (ocpInstallEl) {
9488
+ ocpInstallEl.hidden = !note.html;
9489
+ ocpInstallEl.dataset.tone = note.tone;
9490
+ ocpInstallEl.innerHTML = note.html;
9491
+ }
9492
+ return note;
9348
9493
  }
9349
9494
 
9350
9495
  // ── Master refresh ──
9351
9496
  async function refreshOcpStatus() {
9352
9497
  setOcpStatus(kit.tx('settings.redesign.opencode.checking'), 'dim');
9498
+ loadOcpConfig();
9353
9499
  try {
9354
9500
  const resp = await fetch('/api/opencode/status');
9355
9501
  const data = await resp.json();
9502
+ const note = renderOcpInstall(data.installed);
9356
9503
  if (data.clearingHistory) {
9357
9504
  setOcpStatus(kit.tx('settings.redesign.opencode.clearingHistory'), 'warn');
9358
9505
  } else if (data.running) {
9359
9506
  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
- ]);
9507
+ await refreshOcpProviders();
9365
9508
  } else {
9366
- setOcpStatus(kit.tx('settings.redesign.opencode.offline'), 'dim');
9509
+ if (note.status === 'notSupported') setOcpStatus(kit.tx('settings.redesign.opencode.notSupportedStatus'), 'err');
9510
+ else if (note.status === 'notInstalled') setOcpStatus(kit.tx('settings.redesign.opencode.notInstalledStatus'), 'dim');
9511
+ else setOcpStatus(kit.tx('settings.redesign.opencode.offline'), 'dim');
9367
9512
  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>';
9513
+ // Something else stands in the way (another serve on the port, a serve that did not answer): the server's sentence.
9514
+ if (!note.html && data.problem && ocpInstallEl) {
9515
+ ocpInstallEl.hidden = false;
9516
+ ocpInstallEl.dataset.tone = 'warn';
9517
+ ocpInstallEl.textContent = data.problem;
9518
+ }
9369
9519
  }
9370
9520
  } catch (err) {
9371
9521
  setOcpStatus(kit.tx('settings.redesign.opencode.error'), 'err');
@@ -9420,6 +9570,11 @@ async function openSettingsModalNow(options = {}) {
9420
9570
  }
9421
9571
 
9422
9572
  // ── Shell behaviours: jump lists, keyboard disclosure, page picker ──
9573
+ // The three styled dropdowns of the shell and the panes (page picker, MCP server type, YouTube upload transport).
9574
+ for (const id of ['stg-page-select', 'stg-mcp-srv-type', 'yt-upload-transport']) {
9575
+ const box = overlay.querySelector(`.cc-dropdown[data-for="${id}"]`)?.parentElement;
9576
+ if (box) wireDropdowns(box, { signal: panelLife.signal });
9577
+ }
9423
9578
  wireSettingsShell(overlay, { activate: (pageId) => activateSettingsTab(overlay, pageId) });
9424
9579
  // ── Search: the index is built from the strings of the active locale; what the panes load is read at query time ──
9425
9580
  // In another locale the English names are searched too (someone who learned the settings in English still finds
@@ -9635,3 +9790,5 @@ export function initSettings() {
9635
9790
  window.checkSyncStatus = checkSyncStatus;
9636
9791
  window.copySyncPrompt = copySyncPrompt;
9637
9792
  }
9793
+
9794
+ configureToolPickerTranslation((key, params) => kit.tx(key, params));