synabun 2.0.10 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (517) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +25 -2
  3. package/THIRD-PARTY-LICENSES.md +22 -0
  4. package/bin/synabun.js +71 -24
  5. package/docs/whatsapp.md +2 -2
  6. package/hooks/claude-code/pre-websearch.mjs +12 -1
  7. package/hooks/claude-code/shared.mjs +5 -0
  8. package/hooks/claude-code/stop.mjs +1 -1
  9. package/lib/claude-hooks.js +172 -18
  10. package/lib/client-config-repair.js +17 -8
  11. package/lib/desktop-app.js +7 -1
  12. package/lib/external-tools.js +1 -1
  13. package/lib/first-launch-protection.js +57 -12
  14. package/lib/mcp-dist.js +176 -0
  15. package/lib/release-npm-ci.mjs +138 -0
  16. package/lib/release-publish.mjs +91 -0
  17. package/lib/release-scan.mjs +50 -0
  18. package/lib/server-crash-record.js +123 -0
  19. package/lib/server-exit.js +318 -0
  20. package/lib/server-supervisor.js +508 -0
  21. package/lib/start-launcher.js +37 -4
  22. package/lib/tools-cli.js +45 -0
  23. package/lib/update-snapshot.js +228 -0
  24. package/lib/virustotal.mjs +192 -0
  25. package/lib/windows-crash-event.js +269 -0
  26. package/mcp-server/dist/http.d.ts +20 -1
  27. package/mcp-server/dist/http.d.ts.map +1 -1
  28. package/mcp-server/dist/http.js +59 -12
  29. package/mcp-server/dist/http.js.map +1 -1
  30. package/mcp-server/dist/index.d.ts +33 -27
  31. package/mcp-server/dist/index.d.ts.map +1 -1
  32. package/mcp-server/dist/index.js +128 -143
  33. package/mcp-server/dist/index.js.map +1 -1
  34. package/mcp-server/dist/preload.js +1 -1
  35. package/mcp-server/dist/preload.js.map +1 -1
  36. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  37. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  38. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  39. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  40. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  41. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  42. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  43. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  44. package/mcp-server/dist/services/neural-interface.d.ts +16 -10
  45. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  46. package/mcp-server/dist/services/neural-interface.js +67 -11
  47. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  48. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  49. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  50. package/mcp-server/dist/services/server-instructions.js +20 -62
  51. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  52. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  53. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  54. package/mcp-server/dist/services/tool-catalog.js +339 -0
  55. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  56. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  57. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  58. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  59. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  60. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  61. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  62. package/mcp-server/dist/services/tool-list.js +334 -0
  63. package/mcp-server/dist/services/tool-list.js.map +1 -0
  64. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  65. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  66. package/mcp-server/dist/services/tool-runtime.js +400 -0
  67. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  68. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  69. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  70. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  71. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  72. package/mcp-server/dist/tools/agents.d.ts +10 -10
  73. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  74. package/mcp-server/dist/tools/agents.js +22 -13
  75. package/mcp-server/dist/tools/agents.js.map +1 -1
  76. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  77. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  78. package/mcp-server/dist/tools/image-tools.js +1 -1
  79. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  80. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  81. package/mcp-server/dist/tools/skill-tools.js +639 -0
  82. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  83. package/mcp-server/dist/tools/skill.d.ts +7 -0
  84. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  85. package/mcp-server/dist/tools/skill.js +11 -0
  86. package/mcp-server/dist/tools/skill.js.map +1 -0
  87. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  88. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  89. package/mcp-server/dist/tools/tool-groups.js +147 -0
  90. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  91. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  92. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  93. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  94. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  95. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  96. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  97. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  98. package/mcp-server/package-lock.json +2 -2
  99. package/mcp-server/package.json +1 -1
  100. package/mcp-server/src/http.ts +75 -12
  101. package/mcp-server/src/index.ts +147 -154
  102. package/mcp-server/src/preload.ts +12 -12
  103. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  104. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  105. package/mcp-server/src/services/neural-interface.ts +79 -20
  106. package/mcp-server/src/services/server-instructions.ts +27 -63
  107. package/mcp-server/src/services/tool-catalog.ts +403 -0
  108. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  109. package/mcp-server/src/services/tool-list.ts +410 -0
  110. package/mcp-server/src/services/tool-runtime.ts +457 -0
  111. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  112. package/mcp-server/src/tools/agents.ts +22 -14
  113. package/mcp-server/src/tools/image-tools.ts +1 -1
  114. package/mcp-server/src/tools/skill-tools.ts +589 -0
  115. package/mcp-server/src/tools/skill.ts +13 -0
  116. package/mcp-server/src/tools/tool-groups.ts +164 -0
  117. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  118. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  119. package/neural-interface/i18n/en.json +3463 -200
  120. package/neural-interface/i18n/pt-BR.json +3411 -151
  121. package/neural-interface/lib/assistant-api.js +127 -28
  122. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  123. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  124. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  125. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  126. package/neural-interface/lib/assistant-brains/opencode.js +120 -54
  127. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
  128. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
  129. package/neural-interface/lib/assistant-catalog.js +6 -7
  130. package/neural-interface/lib/assistant-dispatch.js +871 -156
  131. package/neural-interface/lib/assistant-envelope.js +3 -1
  132. package/neural-interface/lib/assistant-persona.js +80 -11
  133. package/neural-interface/lib/assistant-plan-permissions.js +10 -9
  134. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  135. package/neural-interface/lib/assistant-route-gate.js +6 -4
  136. package/neural-interface/lib/assistant-router.js +13 -8
  137. package/neural-interface/lib/assistant-runtime.js +56 -18
  138. package/neural-interface/lib/assistant-task-prompt.js +78 -14
  139. package/neural-interface/lib/assistant-tools.js +51 -0
  140. package/neural-interface/lib/backup-service.js +620 -3
  141. package/neural-interface/lib/backup-zip-reader.js +10 -5
  142. package/neural-interface/lib/backup-zip-writer.js +257 -0
  143. package/neural-interface/lib/browser-execution.js +10 -2
  144. package/neural-interface/lib/browser-guard-cdp.js +83 -0
  145. package/neural-interface/lib/browser-network-guard.js +284 -0
  146. package/neural-interface/lib/browser-policy-roots.js +64 -0
  147. package/neural-interface/lib/browser-tool-policy.js +186 -31
  148. package/neural-interface/lib/claude-agent-bridge.js +4 -2
  149. package/neural-interface/lib/claude-hooks-status.js +103 -0
  150. package/neural-interface/lib/claude-temporary.js +5 -11
  151. package/neural-interface/lib/cli-install.js +18 -138
  152. package/neural-interface/lib/cli-resolver.js +4 -1
  153. package/neural-interface/lib/cli-update-planner.js +32 -31
  154. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  155. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  156. package/neural-interface/lib/codex-worker-app-server.js +172 -0
  157. package/neural-interface/lib/confined-fs.js +30 -3
  158. package/neural-interface/lib/crash-notice-api.js +98 -0
  159. package/neural-interface/lib/editor-completion-opencode.js +68 -68
  160. package/neural-interface/lib/exec-loop-command.js +4 -8
  161. package/neural-interface/lib/http-guards.js +8 -2
  162. package/neural-interface/lib/install-details.js +4 -1
  163. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  164. package/neural-interface/lib/mcp-client-config.js +0 -29
  165. package/neural-interface/lib/mcp-external-servers.js +323 -0
  166. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  167. package/neural-interface/lib/morelogin.js +30 -0
  168. package/neural-interface/lib/native-loop-providers.js +547 -47
  169. package/neural-interface/lib/native-loop-runtime.js +268 -41
  170. package/neural-interface/lib/opencode-config.js +557 -0
  171. package/neural-interface/lib/opencode-db-schema.js +17 -34
  172. package/neural-interface/lib/opencode-history.js +48 -218
  173. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  174. package/neural-interface/lib/opencode-native-boundary.js +44 -0
  175. package/neural-interface/lib/opencode-protocol.js +249 -112
  176. package/neural-interface/lib/opencode-runtime-config.js +32 -22
  177. package/neural-interface/lib/opencode-serve-records.js +39 -21
  178. package/neural-interface/lib/opencode-v2-client.js +48 -153
  179. package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
  180. package/neural-interface/lib/opencode-worker-data.js +27 -0
  181. package/neural-interface/lib/opencode2-client.js +73 -37
  182. package/neural-interface/lib/opencode2-proxy.js +23 -63
  183. package/neural-interface/lib/opencode2-translate.js +51 -2
  184. package/neural-interface/lib/owner-authority.js +249 -0
  185. package/neural-interface/lib/page-session.js +275 -0
  186. package/neural-interface/lib/peer-identity.js +763 -0
  187. package/neural-interface/lib/request-gate.js +17 -2
  188. package/neural-interface/lib/rulesets/api.js +1 -1
  189. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  190. package/neural-interface/lib/rulesets/installer.js +92 -27
  191. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  192. package/neural-interface/lib/session-cache-builder.js +17 -30
  193. package/neural-interface/lib/session-title-generator.js +58 -36
  194. package/neural-interface/lib/skills/api-v2.js +495 -0
  195. package/neural-interface/lib/skills/bundle.js +418 -0
  196. package/neural-interface/lib/skills/catalog.js +294 -0
  197. package/neural-interface/lib/skills/creation-api.js +191 -0
  198. package/neural-interface/lib/skills/creation-brief.js +247 -0
  199. package/neural-interface/lib/skills/creation-sources.js +267 -0
  200. package/neural-interface/lib/skills/creation.js +843 -0
  201. package/neural-interface/lib/skills/deploy.js +564 -0
  202. package/neural-interface/lib/skills/digest.js +119 -0
  203. package/neural-interface/lib/skills/discover.js +111 -0
  204. package/neural-interface/lib/skills/draft-confinement.js +214 -0
  205. package/neural-interface/lib/skills/eval-confinement.js +397 -0
  206. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  207. package/neural-interface/lib/skills/eval-trace.js +119 -0
  208. package/neural-interface/lib/skills/evals-api.js +212 -0
  209. package/neural-interface/lib/skills/evals.js +1573 -0
  210. package/neural-interface/lib/skills/export.js +200 -0
  211. package/neural-interface/lib/skills/frontmatter.js +64 -0
  212. package/neural-interface/lib/skills/health.js +75 -0
  213. package/neural-interface/lib/skills/json-edit.js +165 -0
  214. package/neural-interface/lib/skills/known-bridge-digests.json +387 -0
  215. package/neural-interface/lib/skills/known-bridges.js +96 -0
  216. package/neural-interface/lib/skills/library.js +823 -0
  217. package/neural-interface/lib/skills/local-store.js +21 -0
  218. package/neural-interface/lib/skills/migration.js +433 -0
  219. package/neural-interface/lib/skills/operations.js +45 -0
  220. package/neural-interface/lib/skills/ownership.js +427 -0
  221. package/neural-interface/lib/skills/packages.js +88 -0
  222. package/neural-interface/lib/skills/reviews.js +212 -0
  223. package/neural-interface/lib/skills/revision.js +80 -0
  224. package/neural-interface/lib/skills/roots.js +262 -0
  225. package/neural-interface/lib/skills/runtime-checks.js +355 -0
  226. package/neural-interface/lib/skills/scan-assets.js +252 -0
  227. package/neural-interface/lib/skills/scan-context.js +26 -0
  228. package/neural-interface/lib/skills/scan-shell.js +324 -0
  229. package/neural-interface/lib/skills/scan.js +364 -0
  230. package/neural-interface/lib/skills/scanners.js +54 -0
  231. package/neural-interface/lib/skills/service.js +522 -0
  232. package/neural-interface/lib/skills/telemetry.js +113 -0
  233. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  234. package/neural-interface/lib/tool-lists.js +124 -0
  235. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  236. package/neural-interface/lib/worker-confinement.js +1192 -0
  237. package/neural-interface/lib/worker-guard-setting.js +56 -0
  238. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  239. package/neural-interface/lib/worker-relay-ports.js +227 -0
  240. package/neural-interface/lib/worker-resume.js +229 -0
  241. package/neural-interface/lib/worker-sandbox.js +299 -0
  242. package/neural-interface/package-lock.json +18 -12
  243. package/neural-interface/package.json +2 -2
  244. package/neural-interface/public/index.html +3 -1
  245. package/neural-interface/public/index2d.html +19 -11
  246. package/neural-interface/public/onboarding.html +2 -2
  247. package/neural-interface/public/shared/account-menu.js +8 -5
  248. package/neural-interface/public/shared/agent-runtime-options.js +35 -19
  249. package/neural-interface/public/shared/api.js +1527 -1336
  250. package/neural-interface/public/shared/app-frame.js +192 -0
  251. package/neural-interface/public/shared/assistant/asst-agents-dock.js +33 -2
  252. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  253. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  254. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  255. package/neural-interface/public/shared/assistant/asst-brain-picker.js +49 -35
  256. package/neural-interface/public/shared/assistant/asst-control.js +157 -26
  257. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  258. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  259. package/neural-interface/public/shared/assistant/asst-limit-dock.js +40 -0
  260. package/neural-interface/public/shared/assistant/asst-limit-wait.js +294 -0
  261. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  262. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  263. package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
  264. package/neural-interface/public/shared/assistant/asst-panel.js +106 -11
  265. package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
  266. package/neural-interface/public/shared/assistant/asst-render.js +401 -77
  267. package/neural-interface/public/shared/assistant/asst-route.js +83 -12
  268. package/neural-interface/public/shared/assistant/asst-scenes.js +2 -0
  269. package/neural-interface/public/shared/assistant/asst-sidepanel.js +67 -4
  270. package/neural-interface/public/shared/assistant/asst-slash.js +9 -7
  271. package/neural-interface/public/shared/assistant/asst-state.js +11 -15
  272. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  273. package/neural-interface/public/shared/assistant/asst-styles.js +59 -21
  274. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +156 -16
  275. package/neural-interface/public/shared/automation-context.js +5 -2
  276. package/neural-interface/public/shared/cc-dropdown.js +89 -19
  277. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  278. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  279. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  280. package/neural-interface/public/shared/cdx/cdx-output.js +8 -7
  281. package/neural-interface/public/shared/cdx/cdx-panel.js +45 -149
  282. package/neural-interface/public/shared/cdx/cdx-protocol.js +28 -1
  283. package/neural-interface/public/shared/cdx/cdx-render.js +47 -29
  284. package/neural-interface/public/shared/cdx/cdx-requests.js +3 -3
  285. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  286. package/neural-interface/public/shared/cdx/cdx-styles.js +11 -3
  287. package/neural-interface/public/shared/cdx/cdx-tabs.js +52 -39
  288. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  289. package/neural-interface/public/shared/cli-install-view.js +4 -8
  290. package/neural-interface/public/shared/cli-status.js +2 -2
  291. package/neural-interface/public/shared/constants.js +2 -2
  292. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  293. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  294. package/neural-interface/public/shared/context-menu-model.js +366 -0
  295. package/neural-interface/public/shared/context-menu.js +372 -0
  296. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  297. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  298. package/neural-interface/public/shared/desktop-update-view.js +20 -0
  299. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  300. package/neural-interface/public/shared/html-shell.js +9 -11
  301. package/neural-interface/public/shared/i18n.js +2 -0
  302. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  303. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  304. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  305. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
  306. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  307. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  308. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  309. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  310. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  311. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  312. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  313. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +47 -252
  314. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  315. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
  316. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +6 -21
  317. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
  318. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  319. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
  320. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  321. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  322. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  323. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  324. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +0 -21
  325. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  326. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +14 -47
  327. package/neural-interface/public/shared/page-session.js +96 -0
  328. package/neural-interface/public/shared/registry.js +5 -3
  329. package/neural-interface/public/shared/retired-2d.js +3 -0
  330. package/neural-interface/public/shared/schedules-view.js +195 -61
  331. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  332. package/neural-interface/public/shared/settings/settings-data.js +3 -1
  333. package/neural-interface/public/shared/settings/settings-ia.js +6 -6
  334. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  335. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  336. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  337. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  338. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  339. package/neural-interface/public/shared/skills/ss-creation.js +219 -0
  340. package/neural-interface/public/shared/skills/ss-dialog.js +130 -0
  341. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  342. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  343. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  344. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  345. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  346. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  347. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  348. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  349. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  350. package/neural-interface/public/shared/skills/ss-lint.js +141 -0
  351. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  352. package/neural-interface/public/shared/skills/ss-profiles.js +475 -0
  353. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  354. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  355. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  356. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  357. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  358. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  359. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  360. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +236 -0
  361. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  362. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  363. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  364. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  365. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  366. package/neural-interface/public/shared/skills/studio/sx-dom.js +229 -0
  367. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  368. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  369. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  370. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  371. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  372. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  373. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +279 -0
  374. package/neural-interface/public/shared/skills/studio/sx-install.js +296 -0
  375. package/neural-interface/public/shared/skills/studio/sx-library-view.js +639 -0
  376. package/neural-interface/public/shared/skills/studio/sx-library.js +475 -0
  377. package/neural-interface/public/shared/skills/studio/sx-main.js +681 -0
  378. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  379. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  380. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  381. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  382. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  383. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  384. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  385. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  386. package/neural-interface/public/shared/skills/studio/sx-subject.js +248 -0
  387. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  388. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  389. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  390. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +425 -0
  391. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  392. package/neural-interface/public/shared/styles.css +1525 -1779
  393. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  394. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  395. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  396. package/neural-interface/public/shared/tool-list-view.js +112 -0
  397. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  398. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  399. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  400. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  401. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  402. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  403. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  404. package/neural-interface/public/shared/tour/tour.css +144 -0
  405. package/neural-interface/public/shared/ui-automation-studio.js +91 -128
  406. package/neural-interface/public/shared/ui-claude-panel.js +77 -5
  407. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  408. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  409. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  410. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  411. package/neural-interface/public/shared/ui-detail.js +15 -3
  412. package/neural-interface/public/shared/ui-image-gallery.js +50 -47
  413. package/neural-interface/public/shared/ui-keybinds.js +26 -1
  414. package/neural-interface/public/shared/ui-menubar.js +8 -18
  415. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  416. package/neural-interface/public/shared/ui-native-loop-router.js +7 -1
  417. package/neural-interface/public/shared/ui-navbar.js +4 -24
  418. package/neural-interface/public/shared/ui-panels.js +96 -65
  419. package/neural-interface/public/shared/ui-schedules-studio.js +507 -423
  420. package/neural-interface/public/shared/ui-sessions.js +5 -3
  421. package/neural-interface/public/shared/ui-settings.js +594 -462
  422. package/neural-interface/public/shared/ui-sidepanel-sessions.js +1 -1
  423. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  424. package/neural-interface/public/shared/ui-sidepanel-windows.js +16 -11
  425. package/neural-interface/public/shared/ui-skills.js +33 -2830
  426. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  427. package/neural-interface/public/shared/ui-sync.js +15 -5
  428. package/neural-interface/public/shared/ui-terminal.js +124 -180
  429. package/neural-interface/public/shared/ui-tool-updates.js +3 -1
  430. package/neural-interface/public/shared/ui-tooltip.js +10 -5
  431. package/neural-interface/public/shared/ui-trash.js +6 -4
  432. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  433. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  434. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
  435. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  436. package/neural-interface/public/shared/ui-tutorial.js +285 -509
  437. package/neural-interface/public/shared/ui-update.js +38 -2
  438. package/neural-interface/public/shared/ui-whiteboard.js +311 -58
  439. package/neural-interface/public/shared/ui-worker-end.js +4 -0
  440. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  441. package/neural-interface/public/shared/view-variant.js +66 -0
  442. package/neural-interface/public/shared/whatsapp/wa-view.js +1 -1
  443. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  444. package/neural-interface/public/shared/worker-end.js +10 -3
  445. package/neural-interface/public/shared/worker-restart.js +12 -0
  446. package/neural-interface/public/sidepanel-session.html +1 -1
  447. package/neural-interface/public/variant/2d/background.js +2 -1
  448. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  449. package/neural-interface/public/variant/2d/graph.js +42 -20
  450. package/neural-interface/public/variant/2d/hulls.js +14 -9
  451. package/neural-interface/public/variant/2d/lasso.js +11 -7
  452. package/neural-interface/public/variant/2d/main.js +32 -1
  453. package/neural-interface/public/variant/2d/minimap.js +11 -13
  454. package/neural-interface/public/variant/3d/main.js +69 -6
  455. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  456. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  457. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  458. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  459. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  460. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  461. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  462. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  463. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  464. package/neural-interface/scripts/build-vendor.mjs +32 -0
  465. package/neural-interface/server.js +3934 -2454
  466. package/neural-interface/templates/rulesets/core.md +1 -1
  467. package/neural-interface/templates/rulesets/legacy-hashes.json +18 -0
  468. package/neural-interface/templates/rulesets/manifest.json +8 -8
  469. package/npm-shrinkwrap.json +18 -12
  470. package/package.json +16 -3
  471. package/postinstall.js +9 -4
  472. package/preuninstall.js +1 -1
  473. package/setup.js +97 -322
  474. package/skills/skill-forge/SKILL.md +177 -0
  475. package/skills/skill-forge/evals/evals.json +54 -0
  476. package/skills/skill-forge/references/package.md +105 -0
  477. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  478. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  479. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  480. package/skills/synabun/SKILL.md +10 -7
  481. package/skills/synabun/modules/audit.md +1 -5
  482. package/skills/synabun/modules/changelog.md +1 -5
  483. package/skills/synabun/modules/idea.md +1 -1
  484. package/skills/synabun/modules/memorize.md +6 -9
  485. package/skills/synabun/modules/schedule.md +4 -4
  486. package/mcp-server/dist/services/profiles.d.ts +0 -120
  487. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  488. package/mcp-server/dist/services/profiles.js +0 -387
  489. package/mcp-server/dist/services/profiles.js.map +0 -1
  490. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  491. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  492. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  493. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  494. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  495. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  496. package/mcp-server/dist/tools/leonardo.js +0 -21
  497. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  498. package/mcp-server/dist/tools/profile.d.ts +0 -17
  499. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  500. package/mcp-server/dist/tools/profile.js +0 -114
  501. package/mcp-server/dist/tools/profile.js.map +0 -1
  502. package/mcp-server/src/services/profiles.ts +0 -428
  503. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  504. package/mcp-server/src/tools/leonardo.ts +0 -30
  505. package/mcp-server/src/tools/profile.ts +0 -122
  506. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  507. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  508. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  509. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  510. package/skills/leonardo/SKILL.md +0 -168
  511. package/skills/leonardo/icon.svg +0 -1
  512. package/skills/leonardo/modules/browser-guide.md +0 -378
  513. package/skills/leonardo/modules/image-prompter.md +0 -283
  514. package/skills/leonardo/modules/model-advisor.md +0 -100
  515. package/skills/leonardo/modules/prompt-library.md +0 -110
  516. package/skills/leonardo/modules/style-guide.md +0 -186
  517. package/skills/leonardo/modules/video-prompter.md +0 -317
@@ -16,6 +16,7 @@
16
16
  // open. `synabun.route` events (phase card|auto|decided|pending|expired|
17
17
  // declined|cancelled) update the line in place by routeId.
18
18
 
19
+ import { fetchClaudeAccounts, fetchCodexAccounts, getEffortLevelsForModel } from '../agent-runtime-options.js';
19
20
  import { getProviderMeta } from '../provider-icons.js';
20
21
  import { fetchManageCatalog, patchHiddenModels } from './asst-api.js';
21
22
  import { catalogModelRow, fmtModelPrice, hiddenModelFor, ICON_CARET, invalidateModelCatalog, openModelMenu } from './asst-brain-picker.js';
@@ -107,7 +108,7 @@ function normalizeRouteOption(o) {
107
108
  provider: normalizeProvider(src.provider, str(src.provider) || null),
108
109
  model: str(src.model),
109
110
  effort: str(src.effort) || null,
110
- ...(Object.hasOwn(src, 'accountId') ? { accountId: src.accountId, account: obj(src.account), accountError: obj(src.accountError) } : {}),
111
+ ...(Object.hasOwn(src, 'accountId') ? { accountId: src.accountId, accountChoice: src.accountChoice || null, account: obj(src.account), accountError: obj(src.accountError) } : {}),
111
112
  label: str(src.label),
112
113
  badge: str(src.badge) || null,
113
114
  tier: str(src.tier) || null,
@@ -303,6 +304,7 @@ export function buildRouteResponse(n, choice = {}) {
303
304
  provider: normalizeProvider(target.provider, str(target.provider) || null),
304
305
  model: str(target.model),
305
306
  effort: str(target.effort) || null,
307
+ accountId: str(target.accountId) || null,
306
308
  };
307
309
  }
308
310
  return out;
@@ -804,11 +806,12 @@ export function renderRouteCard(container, input, hooks = {}) {
804
806
  row.classList.toggle('selected', on);
805
807
  row.tabIndex = on ? 0 : -1;
806
808
  }
809
+ syncFields();
807
810
  runBtn.disabled = state.pending || state.resolved || state.expired || !state.selectedId;
808
811
  }
809
812
 
810
813
  function select(id, { focus = false } = {}) {
811
- if (state.pending || state.resolved) return;
814
+ if (state.pending || state.resolved || state.expired) return;
812
815
  const o = allOptions().find(x => x.id === id);
813
816
  if (!o || o.disabled) return;
814
817
  state.selectedId = id;
@@ -816,8 +819,62 @@ export function renderRouteCard(container, input, hooks = {}) {
816
819
  if (focus) list.querySelector(`.asst-route-option[data-option-id="${CSS.escape(id)}"]`)?.focus();
817
820
  }
818
821
 
819
- // Other model…
820
- const otherBtn = el('button', 'asst-link asst-route-other', tr('assistant.routes.other', 'Other model…'));
822
+ // Task-local controls; these never edit the routing preference table.
823
+ const fields = el('div', 'asst-route-fields');
824
+ function taskField(className) {
825
+ const button = el('button', `asst-dd ${className}`);
826
+ button.type = 'button';
827
+ button.setAttribute('aria-haspopup', 'menu');
828
+ button.innerHTML = `<span class="asst-dd-label"></span><span class="asst-dd-caret" aria-hidden="true">${ICON_CARET}</span>`;
829
+ return button;
830
+ }
831
+ const effortBtn = taskField('asst-route-effort');
832
+ const accountBtn = taskField('asst-route-account');
833
+ const setFieldLabel = (button, text) => { button.querySelector('.asst-dd-label').textContent = text; button.title = text; };
834
+ const selected = () => allOptions().find(o => o.id === state.selectedId);
835
+ function editTarget(patch) {
836
+ if (state.pending || state.resolved || state.expired) return;
837
+ state.other = { ...selected(), accountId: selected()?.accountChoice || selected()?.accountId || null, accountChoice: Object.hasOwn(patch, 'accountId') ? patch.accountId : selected()?.accountChoice || null, disabled: false, disabledReason: null, accountError: null, ...patch, id: 'other', number: options.length + 1, badge: null, badgeText: tr('assistant.routes.badge.other', 'Your choice') };
838
+ state.selectedId = 'other';
839
+ renderOptions();
840
+ }
841
+ function syncFields() {
842
+ const target = selected();
843
+ setFieldLabel(otherBtn, target ? routeTargetLabel(target) : tr('assistant.routes.other', 'Other model…'));
844
+ otherBtn.setAttribute('aria-label', tr('assistant.brain.model', 'Model'));
845
+ otherBtn.disabled = state.pending || state.resolved || state.expired;
846
+ setFieldLabel(effortBtn, target?.effort || tr('assistant.routes.editorEffortAuto', 'Auto'));
847
+ effortBtn.setAttribute('aria-label', tr('assistant.brain.effort', 'Thinking effort'));
848
+ setFieldLabel(accountBtn, routeDisplayAccount(target, hooks.t) || tr('assistant.routes.account.workersDefault', 'Workers default'));
849
+ accountBtn.setAttribute('aria-label', tr('assistant.brain.account', 'Account profile'));
850
+ const levels = target ? getEffortLevelsForModel(target.provider, catalogModelRow(target.provider, target.model)) : [];
851
+ effortBtn.disabled = state.pending || state.resolved || state.expired || !levels.some(e => e.id !== 'off');
852
+ accountBtn.hidden = !['claude-code', 'codex'].includes(target?.provider);
853
+ accountBtn.disabled = state.pending || state.resolved || state.expired;
854
+ }
855
+ effortBtn.addEventListener('click', () => {
856
+ const target = selected();
857
+ const levels = getEffortLevelsForModel(target.provider, catalogModelRow(target.provider, target.model));
858
+ openMenu(effortBtn, { title: tr('assistant.brain.effort', 'Thinking effort'), placement: 'auto', items: [
859
+ { kind: 'radio', label: tr('assistant.routes.editorEffortAuto', 'Auto'), selected: !target.effort, onSelect: () => editTarget({ effort: null }) },
860
+ ...levels.filter(e => e.id !== 'off').map(e => ({ kind: 'radio', label: e.label || e.id, selected: target.effort === e.id, onSelect: () => editTarget({ effort: e.id }) })),
861
+ ] });
862
+ });
863
+ accountBtn.addEventListener('click', async () => {
864
+ const target = selected();
865
+ try {
866
+ const rows = await (target.provider === 'codex' ? fetchCodexAccounts() : fetchClaudeAccounts());
867
+ if (!card.isConnected || state.pending || state.resolved || state.expired || selected() !== target) return;
868
+ openMenu(accountBtn, { title: tr('assistant.brain.account', 'Account profile'), placement: 'auto', items: [
869
+ { kind: 'radio', label: tr('assistant.routes.account.workersDefault', 'Workers default'), selected: !(target.accountChoice || target.accountId), onSelect: () => editTarget({ accountId: null, account: null }) },
870
+ { kind: 'radio', label: autoAccountLabel(hooks.t), selected: (target.accountChoice || target.accountId) === 'auto', onSelect: () => editTarget({ accountId: 'auto', account: null }) },
871
+ ...(Array.isArray(rows) ? rows : []).map(a => ({ kind: 'radio', label: accountSummary(a, target.provider === 'codex' ? 'codex' : 'claude'), disabled: a.signedIn === false, selected: (target.accountChoice || target.accountId) === a.id, onSelect: () => editTarget({ accountId: a.id, account: a }) })),
872
+ ] });
873
+ } catch (error) { hooks.onToast?.(error.message); }
874
+ });
875
+
876
+ // Model menu, scoped to this task.
877
+ const otherBtn = taskField('asst-route-other');
821
878
  otherBtn.type = 'button';
822
879
  otherBtn.setAttribute('aria-haspopup', 'menu');
823
880
  otherBtn.hidden = !n.other.allowed;
@@ -827,21 +884,24 @@ export function renderRouteCard(container, input, hooks = {}) {
827
884
  const brain = normalizeBrain(hooks.getBrain?.() || { provider: n.brain.provider || 'claude-code', model: n.brain.model });
828
885
  openModelMenu(otherBtn, {
829
886
  brain,
887
+ shouldOpen: () => card.isConnected && !state.pending && !state.resolved && !state.expired,
830
888
  t: hooks.t,
831
889
  title: tr('assistant.routes.otherTitle', 'Run on another model'),
832
890
  placement: 'auto',
833
891
  providers: n.other.providers.length ? n.other.providers.map(p => normalizeProvider(p, p)) : null,
834
- current: state.other ? { provider: state.other.provider, model: state.other.model } : null,
892
+ current: selected() ? { provider: selected().provider, model: selected().model } : null,
835
893
  onToast: hooks.onToast,
836
894
  // Image / video creation: only models that generate the medium (no custom id: its output is unknown).
837
895
  ...(n.requires ? { accept: (_p, m) => m?.outputs?.[n.requires] === true, allowCustom: false, emptyLabel: tr('assistant.routes.noCapableModel', 'No model in your catalog can make {medium} yet.', { medium: mediumWords(n.requires, tr) }) } : {}),
838
896
  // Design: only models that can see.
839
897
  ...(!n.requires && n.visionOnly ? { accept: (_p, m) => m?.vision === true && m?.status !== 'unavailable', allowCustom: false, emptyLabel: tr('assistant.routes.noSeeingModel', 'No model in your catalog can see images yet.') } : {}),
840
898
  onSelect: (target) => {
841
- if (!target?.model) return;
842
- const kind = n.dispatchOnly || (target.provider && target.provider !== brain.provider) ? 'dispatch' : 'direct';
899
+ if (!target?.model || !card.isConnected) return;
900
+ if (state.pending || state.resolved || state.expired) return;
901
+ const previous = selected();
902
+ const kind = n.dispatchOnly || previous?.kind === 'dispatch' || (target.provider && target.provider !== brain.provider) ? 'dispatch' : 'direct';
843
903
  state.other = {
844
- id: 'other', kind, provider: target.provider, model: target.model, effort: null,
904
+ id: 'other', kind, provider: target.provider, model: target.model, effort: null, accountId: previous?.provider === target.provider ? previous.accountChoice || previous.accountId || null : null, account: previous?.provider === target.provider ? previous.account || null : null,
845
905
  label: target.label || '', badge: null, number: options.length + 1, here: false,
846
906
  title: target.label || target.model, badgeText: tr('assistant.routes.badge.other', 'Your choice'),
847
907
  vision: target.vision === true, price: target.price || null, reason: '', warning: null, disabled: false,
@@ -852,7 +912,8 @@ export function renderRouteCard(container, input, hooks = {}) {
852
912
  },
853
913
  });
854
914
  });
855
- card.appendChild(otherBtn);
915
+ fields.append(otherBtn, effortBtn, accountBtn);
916
+ card.appendChild(fields);
856
917
 
857
918
  // Remember for ‹class› tasks
858
919
  let rememberInput = null;
@@ -906,7 +967,7 @@ export function renderRouteCard(container, input, hooks = {}) {
906
967
  decidedBy: 'user',
907
968
  optionId: state.selectedId,
908
969
  remembered: state.remember,
909
- target: o ? { kind: o.kind || 'direct', provider: o.provider, model: o.model, effort: o.effort, label: o.label || (o.here ? routeTargetLabel(o) : o.title) } : null,
970
+ target: o ? { kind: o.kind || 'direct', provider: o.provider, model: o.model, effort: o.effort, accountId: o.accountId || null, label: o.label || (o.here ? routeTargetLabel(o) : o.title) } : null,
910
971
  alternatives: allOptions().filter(x => x.id !== state.selectedId).map(x => ({ provider: x.provider, model: x.model, effort: x.effort, label: x.label })),
911
972
  };
912
973
  }
@@ -926,6 +987,9 @@ export function renderRouteCard(container, input, hooks = {}) {
926
987
  state.line = line;
927
988
  state.collapsed = true;
928
989
  state.resolved = true;
990
+ // No longer waits: the renderer puts it back from the open card's slot at the transcript's bottom
991
+ // (asst-ask-tail.js) before it becomes the route line in its own place.
992
+ try { card._ask?.onChange?.(); } catch { /* the renderer is gone */ }
929
993
  if (card.isConnected) card.replaceWith(line.el);
930
994
  try { hooks.onCollapse?.(n.routeId, line); } catch { /* ignore */ }
931
995
  if (hadFocus) { try { hooks.onDone?.(); } catch { /* ignore */ } }
@@ -937,6 +1001,8 @@ export function renderRouteCard(container, input, hooks = {}) {
937
1001
  if (state.line?.el?.isConnected) state.line.el.replaceWith(card);
938
1002
  state.collapsed = false;
939
1003
  state.resolved = false;
1004
+ // Open again (ROUTE_INVALID): it waits on the user, so it goes back to the bottom slot.
1005
+ try { card._ask?.onChange?.(); } catch { /* the renderer is gone */ }
940
1006
  }
941
1007
 
942
1008
  function clearTimers() {
@@ -955,7 +1021,7 @@ export function renderRouteCard(container, input, hooks = {}) {
955
1021
  }
956
1022
 
957
1023
  function cancel() {
958
- if (state.pending || state.resolved) return;
1024
+ if (state.pending || state.resolved || state.expired) return;
959
1025
  if (!n.decline.allowed) {
960
1026
  // Nothing to cancel on the server: leave the card and hand focus back.
961
1027
  try { hooks.onCancel?.(buildRouteCancel(n, 'user'), n); } catch { /* ignore */ }
@@ -1021,11 +1087,16 @@ export function renderRouteCard(container, input, hooks = {}) {
1021
1087
  state.timer = null;
1022
1088
  countdown.textContent = tr('assistant.routes.state.expired', 'route expired');
1023
1089
  syncSelection();
1090
+ // No answer is coming from the user any more: it goes back to its own place (asst-ask-tail.js).
1091
+ try { card._ask?.onChange?.(); } catch { /* the renderer is gone */ }
1024
1092
  }
1025
1093
  }
1026
- if (expires && Number.isFinite(expires)) { tick(); state.timer = setInterval(tick, 1000); }
1094
+ if (expires && Number.isFinite(expires)) { tick(); if (!state.expired) state.timer = setInterval(tick, 1000); }
1027
1095
 
1028
1096
  renderOptions();
1097
+ // The renderer lifts a card that waits on the user to the transcript's bottom while it is open
1098
+ // (asst-ask-tail.js): a route card waits like a follow-ups set or a question does.
1099
+ card._ask = { el: card, presenter: null, isOpen: () => !state.collapsed && !state.resolved && !state.expired, onChange: null };
1029
1100
  if (container) container.appendChild(card);
1030
1101
  try { hooks.scrollEnd?.(); } catch { /* ignore */ }
1031
1102
 
@@ -118,6 +118,8 @@ export function runChip(run, t = tFallback) {
118
118
  const tone = runTone(run);
119
119
  if (run?.turnState === 'awaiting_permission') return { tone, label: t('assistant.run.needsApproval', 'needs approval') };
120
120
  if (run?.turnState === 'needs_input') return { tone, label: t('assistant.run.needsInput', 'needs input') };
121
+ // A Claude Code worker a server restart cut off, until the next start has resumed it (lib/assistant-dispatch.js).
122
+ if (run?.state === 'suspended' || run?.state === 'resuming') return { tone, label: t('assistant.run.resuming', 'resuming after a server restart') };
121
123
  if (run?.queued && run.status !== 'running') return { tone, label: t('assistant.run.queued', 'queued') };
122
124
  if (isRunIdle(run)) {
123
125
  const outcome = run.outcome || run.lastResult?.status;
@@ -30,6 +30,7 @@ import { brainLabel, readStoredBrain, restoredSessionName } from './asst-state.j
30
30
  import { closeMenu, isMenuOpen, openMenu } from './asst-menu.js';
31
31
  import { injectAssistantSidepanelStyles } from './asst-sidepanel-styles.js';
32
32
  import { createMascot, mascotCameoSvg } from '../synabun-mascot.js';
33
+ import { createTutorialPreview } from '../ui-tutorial-preview.js';
33
34
 
34
35
  const PANEL_OWNER = 'assistant-sidepanel';
35
36
  const HOST_ID = 'sidepanel';
@@ -55,6 +56,7 @@ let _toastTimer = null;
55
56
  let _renaming = false;
56
57
  const _tabs = []; // { sessionId, viewport, toolbar, ctl, pill, label, userRenamed, status, brain }
57
58
  let _activeIdx = -1;
59
+ let _tourPresenting = false;
58
60
 
59
61
  // ── Helpers ──────────────────────────────────────────────────────────────────
60
62
 
@@ -408,13 +410,13 @@ function focusSession(sessionId, { prompt = '' } = {}) {
408
410
  if (prompt) _tabs[idx].ctl?.send(prompt);
409
411
  }
410
412
 
411
- async function startSession() {
413
+ async function startSession(prompt = '') {
412
414
  if (_starting || _tabs.length) return;
413
415
  _starting = true;
414
416
  _startError = '';
415
417
  renderPlaceholder('starting');
416
418
  try {
417
- const res = await openAssistant({ host: HOST_ID });
419
+ const res = await openAssistant(prompt ? { host: HOST_ID, prompt } : { host: HOST_ID });
418
420
  if (!_tabs.length) renderPlaceholder(res ? null : 'failed');
419
421
  } finally {
420
422
  _starting = false;
@@ -563,8 +565,8 @@ function applyVisibility(nextVisible) {
563
565
  const tab = activeTab();
564
566
  if (_visible) {
565
567
  state.lastActivePanel = 'assistant';
566
- if (!_tabs.length) startSession();
567
- else if (tab) {
568
+ if (!_tabs.length && !_tourPresenting) startSession();
569
+ else if (tab && !_tourPresenting) {
568
570
  if (tab.status === 'done') tab.status = 'idle';
569
571
  try { tab.ctl?.onShown(); } catch { /* ignore */ }
570
572
  noteAssistantFocus(tab.sessionId, HOST_ID);
@@ -617,6 +619,67 @@ export function assistantPanelPeekable() {
617
619
  return !assistantBlocked() && _tabs.length > 0;
618
620
  }
619
621
 
622
+ /**
623
+ * Present the panel for the Workspace tour (ui-tutorial.js), reversibly: it never
624
+ * creates a session. The tour plays on the active tab when its empty state shows
625
+ * its character (the tab's own controls, composer and director: tourHost());
626
+ * otherwise (no tab, or a conversation on screen) on a session-free preview laid
627
+ * over the tab. Returns { host, live, restore({ reason }) } or null when the
628
+ * Assistant is blocked. restore() puts back the panel's visibility, the docked
629
+ * peers and the last-active panels, unless the tour ended on its hand-off to a
630
+ * live tab ('done'): then the panel stays and the tab's composer takes the focus.
631
+ */
632
+ export function beginAssistantTour({ onPreviewSend = () => {} } = {}) {
633
+ if (assistantBlocked()) return null;
634
+ ensurePanel();
635
+ const wasVisible = _visible;
636
+ const previousPanel = state.lastActivePanel;
637
+ const previousSidepanel = state.lastActiveSidepanel;
638
+ const peers = ['#topright-claude-panel-btn', '#topright-codex-panel-btn', '#topright-opencode-panel-btn']
639
+ .map(s => document.querySelector(s)).filter(b => b?.classList.contains('active'));
640
+ _tourPresenting = true;
641
+ const tab = activeTab();
642
+ let liveHost = null;
643
+ try { liveHost = tab?.ctl?.tourHost?.() || null; } catch { liveHost = null; }
644
+ const preview = liveHost ? null : createTutorialPreview(_panel, { onSend: (text) => onPreviewSend(text) });
645
+ if (preview) _panel.classList.add('tour-covering');
646
+ setVisible(true);
647
+ let restored = false;
648
+ const host = liveHost || preview.host;
649
+ return {
650
+ host,
651
+ live: !!liveHost,
652
+ /**
653
+ * The user sent `text` from the preview's composer (after restore({ reason: 'sent' })):
654
+ * with no session, it starts one with that message (the user's own send); with a
655
+ * conversation on screen, it becomes that conversation's draft, never sent for them.
656
+ */
657
+ deliver(text) {
658
+ const message = String(text || '').trim();
659
+ if (!message) return;
660
+ const current = activeTab();
661
+ if (current?.ctl) { focusSession(current.sessionId); current.ctl.insertText?.(message); current.ctl.focus?.(); return; }
662
+ startSession(message);
663
+ },
664
+ restore({ reason = 'skip' } = {}) {
665
+ if (restored) return;
666
+ restored = true;
667
+ closeMenu();
668
+ preview?.destroy();
669
+ _panel.classList.remove('tour-covering');
670
+ _tourPresenting = false;
671
+ const keep = (reason === 'done' && !!liveHost) || reason === 'sent';
672
+ if (keep) {
673
+ if (tab && findTab(tab.sessionId)) { try { tab.ctl?.onShown(); } catch { /* ignore */ } }
674
+ return;
675
+ }
676
+ if (!wasVisible) setVisible(false);
677
+ for (const button of peers) if (!button.classList.contains('active')) button.click();
678
+ state.lastActivePanel = previousPanel; state.lastActiveSidepanel = previousSidepanel;
679
+ },
680
+ };
681
+ }
682
+
620
683
  /** The top-right SynaBun button. */
621
684
  export async function toggleAssistantPanel() {
622
685
  if (assistantBlocked()) {
@@ -7,7 +7,7 @@
7
7
  import { normalizeComputerMode, normalizeProvider, normalizeRouteMode } from './asst-state.js';
8
8
 
9
9
  export const SLASH_COMMANDS = [
10
- { name: 'dispatch', usage: '/dispatch <provider> [--model m] [--effort e] [--agent a] [--project p] [--mcp profile] [--account id] [--ask] [--title "t"] <task>', descKey: 'assistant.slash.dispatch', desc: 'Dispatch a task to a sidepanel agent' },
10
+ { name: 'dispatch', usage: '/dispatch <provider> [--model m] [--effort e] [--agent a] [--project p] [--tools a,b] [--account id] [--ask] [--title "t"] <task>', descKey: 'assistant.slash.dispatch', desc: 'Dispatch a task to a sidepanel agent' },
11
11
  { name: 'agents', usage: '/agents [all]', descKey: 'assistant.slash.agents', desc: 'List dispatched runs' },
12
12
  { name: 'recall', usage: '/recall <query>', descKey: 'assistant.slash.recall', desc: 'Search memories' },
13
13
  { name: 'remember', usage: '/remember <text>', descKey: 'assistant.slash.remember', desc: 'Store a memory' },
@@ -17,7 +17,7 @@ export const SLASH_COMMANDS = [
17
17
  { name: 'models', usage: '/models [list | archived | hide <id…> | show <id…>]', descKey: 'assistant.slash.models', desc: 'Switch models off or on for the Assistant (or open the Models list)' },
18
18
  { name: 'budget', usage: '/budget', descKey: 'assistant.slash.budget', desc: 'Set the session budget and view provider shares' },
19
19
  { name: 'computer', usage: '/computer [on|off]', descKey: 'assistant.slash.computer', desc: 'Let the assistant use your Mac' },
20
- { name: 'stop', usage: '/stop [runId|all]', descKey: 'assistant.slash.stop', desc: 'Stop a run, everything, or the current turn' },
20
+ { name: 'stop', usage: '/stop [runId|title|all]', descKey: 'assistant.slash.stop', desc: 'Stop a run, everything, or the current turn' },
21
21
  { name: 'new', usage: '/new [provider]', descKey: 'assistant.slash.new', desc: 'Start a fresh session' },
22
22
  { name: 'resume', usage: '/resume [sessionId]', descKey: 'assistant.slash.resume', desc: 'Resume a previous session' },
23
23
  { name: 'compact', usage: '/compact', descKey: 'assistant.slash.compact', desc: 'Compact the context' },
@@ -33,7 +33,7 @@ export const DISPATCH_FLAGS = {
33
33
  effort: 'effort', e: 'effort',
34
34
  agent: 'agent',
35
35
  project: 'project', p: 'project', cwd: 'project',
36
- mcp: 'mcp', profile: 'mcp',
36
+ tools: 'tools',
37
37
  account: 'account', a: 'account',
38
38
  title: 'title', t: 'title',
39
39
  budget: 'budget',
@@ -219,10 +219,10 @@ export function matchSlashCommands(query) {
219
219
 
220
220
  /**
221
221
  * Build the POST /api/assistant/dispatch body from a parsed /dispatch command.
222
- * Only explicit flags set model/effort — the brain's project, MCP profile and
222
+ * Only explicit flags set model/effort — the brain's project and
223
223
  * (same-provider) account are inherited, models are left to the server default.
224
224
  */
225
- export function buildDispatchSpec(parsed, brain = {}, { assistantSessionId = null } = {}) {
225
+ export function buildDispatchSpec(parsed, brain = {}, { assistantSessionId = null, toolCatalog = null } = {}) {
226
226
  if (!parsed || parsed.name !== 'dispatch' || parsed.error) return null;
227
227
  const o = parsed.options || {};
228
228
  const spec = {
@@ -236,8 +236,7 @@ export function buildDispatchSpec(parsed, brain = {}, { assistantSessionId = nul
236
236
  if (o.model) spec.model = o.model;
237
237
  if (o.effort) spec.effort = o.effort;
238
238
  if (o.agent) spec.agent = o.agent;
239
- const mcp = o.mcp || brain.mcpProfile;
240
- if (mcp) spec.mcpProfile = mcp;
239
+ if (o.tools !== undefined) spec.tools = String(o.tools).split(',').map(id => id.trim()).filter(Boolean);
241
240
  const account = o.account || (brain.provider === parsed.provider ? brain.accountId : null);
242
241
  if (account) {
243
242
  if (parsed.provider === 'codex') spec.codexAccountId = account;
@@ -247,6 +246,9 @@ export function buildDispatchSpec(parsed, brain = {}, { assistantSessionId = nul
247
246
  if (o.budget !== undefined && Number.isFinite(Number(o.budget))) spec.budgetUsd = Number(o.budget);
248
247
  if (o.minutes !== undefined && Number.isFinite(Number(o.minutes))) spec.maxMinutes = Number(o.minutes);
249
248
  if (o.browser) spec.usesBrowser = true;
249
+ // A browser-reading slash launch uses the workspace guard, just like a routed browser task.
250
+ // Group metadata comes from the catalog, never from a second list of browser ids.
251
+ if (o.browser || (spec.tools || []).some(id => id === 'all' || toolCatalog?.groups?.some(group => group.id === id && group.browser))) spec.capability = 'workspace';
250
252
  if (assistantSessionId) spec.assistantSessionId = assistantSessionId;
251
253
  return spec;
252
254
  }
@@ -64,7 +64,7 @@ export const DEFAULT_BRAIN = Object.freeze({
64
64
  effort: null,
65
65
  agent: null,
66
66
  project: null,
67
- mcpProfile: '',
67
+ tools: Object.freeze(['all']),
68
68
  accountId: null,
69
69
  permissionMode: 'default',
70
70
  planMode: false,
@@ -105,7 +105,7 @@ export function normalizeBrain(raw, fallback = DEFAULT_BRAIN) {
105
105
  effort: str(src.effort) || null,
106
106
  agent: provider === 'opencode' ? (str(src.agent) || null) : null,
107
107
  project: str(src.project ?? src.cwd) || null,
108
- mcpProfile: str(src.mcpProfile),
108
+ tools: provider === 'opencode' ? (Array.isArray(src.tools) ? [...src.tools] : null) : ['all'],
109
109
  accountId: ACCOUNT_PROVIDERS.has(provider) ? (str(src.accountId) || null) : null,
110
110
  permissionMode: modes.some(m => m.id === mode) ? mode : 'default',
111
111
  planMode: PLAN_MODE_PROVIDERS.has(provider) && (src.planMode === true || legacyPlan),
@@ -145,7 +145,7 @@ function pickDefault(list) {
145
145
  * efforts: { [provider]: [{ id, tier? }] },
146
146
  * accounts: { [provider]: [{ id, label }] },
147
147
  * agents: [{ name, mode? }], // OpenCode
148
- * mcpProfiles: { [id]: { label } }, defaultMcpProfile,
148
+ * toolCatalog: { default, groups },
149
149
  * projects: [{ path, label }],
150
150
  * }
151
151
  */
@@ -188,14 +188,8 @@ export function resolveBrainDefaults(brain, catalog = {}) {
188
188
  b.accountId = null;
189
189
  }
190
190
 
191
- const profiles = catalog.mcpProfiles && typeof catalog.mcpProfiles === 'object' ? catalog.mcpProfiles : null;
192
- if (profiles) {
193
- const ids = Object.keys(profiles);
194
- if (ids.length && !ids.includes(b.mcpProfile)) {
195
- b.mcpProfile = ids.includes(catalog.defaultMcpProfile) ? catalog.defaultMcpProfile : (ids.includes('full') ? 'full' : ids[0]);
196
- }
197
- } else if (!b.mcpProfile && catalog.defaultMcpProfile) {
198
- b.mcpProfile = catalog.defaultMcpProfile;
191
+ if (b.provider === 'opencode' && b.tools == null && catalog.toolCatalog) {
192
+ b.tools = [...new Set([...(catalog.toolCatalog.default || []).filter(id => id !== 'git'), 'browser'])];
199
193
  }
200
194
 
201
195
  if (!b.project && Array.isArray(catalog.projects) && catalog.projects.length === 1) {
@@ -285,7 +279,7 @@ export function emptyProjectView(project, projects = [], tr = (_key, fallback, p
285
279
  export function brainRequiresSwitch(a, b) {
286
280
  const x = normalizeBrain(a);
287
281
  const y = normalizeBrain(b);
288
- return x.provider !== y.provider || x.accountId !== y.accountId || x.mcpProfile !== y.mcpProfile;
282
+ return x.provider !== y.provider || x.accountId !== y.accountId || JSON.stringify(x.tools) !== JSON.stringify(y.tools);
289
283
  }
290
284
 
291
285
  // ── Runs ────────────────────────────────────────────────────────────────────
@@ -295,9 +289,11 @@ export function isTerminalRunStatus(status) {
295
289
  }
296
290
 
297
291
  // Dispatcher views carry `state` (queued, awaiting_route, starting, running,
298
- // idle, awaiting_permission, completed, failed, stopped, interrupted); runtime
299
- // descriptors (sidepanel:run-*) carry `status`. The tray reads `status`.
300
- const STATE_STATUS = { queued: 'queued', awaiting_route: 'queued', starting: 'starting', running: 'running', idle: 'running', awaiting_permission: 'running' };
292
+ // idle, awaiting_permission, suspended, resuming, completed, failed, stopped,
293
+ // interrupted); runtime descriptors (sidepanel:run-*) carry `status`. The tray
294
+ // reads `status`. A run a server restart suspended is still running; one being
295
+ // resumed is starting.
296
+ const STATE_STATUS = { queued: 'queued', awaiting_route: 'queued', starting: 'starting', running: 'running', idle: 'running', awaiting_permission: 'running', suspended: 'running', resuming: 'starting' };
301
297
 
302
298
  export function runStatusFromState(state) {
303
299
  const value = str(state);
@@ -0,0 +1,127 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Assistant — what a stop names
3
+ // ═══════════════════════════════════════════
4
+ // Pure module (no DOM, no i18n import) so node:test can exercise it.
5
+ //
6
+ // A stop ends the work the user named and nothing else:
7
+ // resolveStopTarget `/stop <target>`: the one run of this session the target names (its id, the
8
+ // start of its id, its title or the words of its title), or why it names none.
9
+ // Two candidates stop nothing: the user picks one.
10
+ // createConfirmGate the agents tray's "Stop all": the first press arms it, a second press inside
11
+ // the window stops every run of the session at once. A single stray click, a
12
+ // double click or a held key never does (2026-10-24: one "Stop all" press
13
+ // ended both runs of a session while the user meant one of them).
14
+ // Words the user types to stop everything ("stop", "cancel") go to the brain, which stops runs
15
+ // with agent_stop (assistant-persona.js): nothing here reads a message.
16
+
17
+ import { isTerminalRunStatus } from './asst-state.js';
18
+
19
+ /** How long an armed "Stop all" waits for its second press, and the least time between the two. */
20
+ export const STOP_ALL_CONFIRM_MS = 4000;
21
+ export const STOP_ALL_MIN_GAP_MS = 300;
22
+
23
+ // Words that say "a run" without saying which one: "/stop the OpenCode settings run" names it by the rest.
24
+ const FILLER = new Set(['the', 'a', 'an', 'that', 'this', 'my', 'our', 'one', 'run', 'runs', 'agent', 'agents', 'worker', 'workers', 'task', 'job', 'please', 'o', 'os', 'as', 'do', 'da', 'de']);
25
+ const FULL_RUN_ID = /^[0-9a-f]{32}$/i;
26
+ const ID_PREFIX = /^[0-9a-f][0-9a-f-]{3,}$/i;
27
+
28
+ function words(text) {
29
+ return String(text ?? '').toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
30
+ }
31
+
32
+ function unquote(text) {
33
+ const s = String(text ?? '').trim();
34
+ const m = s.match(/^(["'“‘])([\s\S]*)(["'”’])$/);
35
+ return m ? m[2].trim() : s;
36
+ }
37
+
38
+ /**
39
+ * The run `/stop <target>` names among `runs` (the tray's runs of this session).
40
+ * → { run, by: 'id' | 'prefix' | 'title' | 'words' }
41
+ * | { error: 'empty' | 'none' | 'finished' | 'ambiguous', matches?: run[], run? }
42
+ * An exact id names that run whatever its state (the server says when it already ended). Every
43
+ * other way names a run that still works; one that ended is `finished`. A full id the tray does not
44
+ * know is handed on as `{ error: 'none', passthrough: id }`: the server knows more runs than a tray.
45
+ */
46
+ export function resolveStopTarget(target, runs = []) {
47
+ const raw = unquote(target);
48
+ if (!raw) return { error: 'empty' };
49
+ const list = (Array.isArray(runs) ? runs : []).filter((run) => run && run.runId && run.removed !== true);
50
+ const live = list.filter((run) => !isTerminalRunStatus(run.status));
51
+ const one = (matches, by) => {
52
+ if (matches.length === 1) return isTerminalRunStatus(matches[0].status) ? { error: 'finished', run: matches[0] } : { run: matches[0], by };
53
+ if (matches.length > 1) return { error: 'ambiguous', matches };
54
+ return null;
55
+ };
56
+ const lower = raw.toLowerCase();
57
+
58
+ const exact = list.find((run) => String(run.runId).toLowerCase() === lower);
59
+ if (exact) return { run: exact, by: 'id' };
60
+ if (ID_PREFIX.test(raw)) {
61
+ const byPrefix = one(list.filter((run) => String(run.runId).toLowerCase().startsWith(lower)), 'prefix');
62
+ if (byPrefix) return byPrefix;
63
+ }
64
+
65
+ const title = (run) => words(run.title || run.task || '').join(' ');
66
+ const wanted = words(raw).join(' ');
67
+ if (wanted) {
68
+ const byTitle = one(live.filter((run) => title(run) === wanted), 'title');
69
+ if (byTitle) return byTitle;
70
+ }
71
+ // The words that say which run: each one is a word of its title, or the start of one.
72
+ const keys = words(raw).filter((word) => !FILLER.has(word));
73
+ if (keys.length) {
74
+ const fits = (run) => { const have = words(run.title || run.task || ''); return keys.every((key) => have.some((word) => word === key || (key.length >= 3 && word.startsWith(key)))); };
75
+ const byWords = one(live.filter(fits), 'words');
76
+ if (byWords) return byWords;
77
+ const ended = list.filter((run) => isTerminalRunStatus(run.status) && (title(run) === wanted || fits(run)));
78
+ if (ended.length === 1) return { error: 'finished', run: ended[0] };
79
+ }
80
+ return FULL_RUN_ID.test(raw) ? { error: 'none', passthrough: raw } : { error: 'none' };
81
+ }
82
+
83
+ /**
84
+ * A press that only counts as the second of two. `press()` → 'armed' (the first: `onChange(true)`),
85
+ * 'confirmed' (the second, inside `windowMs` and at least `minGapMs` after the first:
86
+ * `onChange(false)`, the caller stops everything) or 'ignored' (too soon after the first: a double
87
+ * click, a held key). The arm lapses after `windowMs` by itself (`onChange(false)`).
88
+ */
89
+ export function createConfirmGate({
90
+ windowMs = STOP_ALL_CONFIRM_MS,
91
+ minGapMs = STOP_ALL_MIN_GAP_MS,
92
+ now = Date.now,
93
+ setTimer = (fn, ms) => setTimeout(fn, ms),
94
+ clearTimer = (id) => clearTimeout(id),
95
+ onChange = () => {},
96
+ } = {}) {
97
+ let armedAt = null;
98
+ let timer = null;
99
+ const notify = (armed) => { try { onChange(armed); } catch { /* the button is gone */ } };
100
+ function reset() {
101
+ if (timer !== null) { clearTimer(timer); timer = null; }
102
+ if (armedAt === null) return false;
103
+ armedAt = null;
104
+ notify(false);
105
+ return true;
106
+ }
107
+ function press() {
108
+ const at = now();
109
+ // An arm whose timer never ran (a throttled background tab) has lapsed all the same.
110
+ if (armedAt !== null && at - armedAt > windowMs) reset();
111
+ if (armedAt === null) {
112
+ armedAt = at;
113
+ timer = setTimer(() => { timer = null; reset(); }, windowMs);
114
+ notify(true);
115
+ return 'armed';
116
+ }
117
+ if (at - armedAt < minGapMs) return 'ignored';
118
+ reset();
119
+ return 'confirmed';
120
+ }
121
+ return {
122
+ press,
123
+ reset,
124
+ isArmed: () => armedAt !== null,
125
+ destroy() { if (timer !== null) { clearTimer(timer); timer = null; } armedAt = null; },
126
+ };
127
+ }