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
@@ -0,0 +1,243 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Context menu: what decides
3
+ // No DOM and no imports: the component (context-menu.js) and the app's
4
+ // global registration (ui-context-menu.js) draw what these functions say,
5
+ // and node --test checks them (tests/context-menu-logic.test.mjs).
6
+ // ═══════════════════════════════════════════
7
+
8
+ /**
9
+ * @typedef {object} MenuItem
10
+ * @property {string} [id]
11
+ * @property {string} [label]
12
+ * @property {string} [icon] trusted SVG markup (the app's own icons, never page text)
13
+ * @property {string} [shortcut] the key as menus show it ('' when the action has none)
14
+ * @property {boolean} [disabled]
15
+ * @property {boolean} [checked] a toggle: drawn with a check, role menuitemcheckbox
16
+ * @property {boolean} [hidden] left out of the menu
17
+ * @property {MenuItem[]} [submenu]
18
+ * @property {() => unknown} [run]
19
+ * @property {boolean} [separator]
20
+ */
21
+
22
+ export const VIEWPORT_MARGIN = 8;
23
+ export const SUBMENU_GAP = 2;
24
+ // What a submenu's first row sits under: the menu's padding plus the row's margin.
25
+ export const SUBMENU_LEAD = 6;
26
+
27
+ /**
28
+ * Items as the menu draws them: hidden ones and ones without a label left out, a submenu with nothing in it left
29
+ * out, no separator at either end or next to another.
30
+ * @param {(MenuItem | null | undefined)[]} items
31
+ * @returns {MenuItem[]}
32
+ */
33
+ export function normalizeItems(items) {
34
+ const out = [];
35
+ for (const item of items || []) {
36
+ if (!item || item.hidden) continue;
37
+ if (item.separator) {
38
+ if (out.length && !out[out.length - 1].separator) out.push({ separator: true });
39
+ continue;
40
+ }
41
+ if (!item.label) continue;
42
+ if (item.submenu) {
43
+ const submenu = normalizeItems(item.submenu);
44
+ if (!submenu.length) continue;
45
+ out.push({ ...item, submenu });
46
+ continue;
47
+ }
48
+ out.push(item);
49
+ }
50
+ while (out.length && out[out.length - 1].separator) out.pop();
51
+ return out;
52
+ }
53
+
54
+ /**
55
+ * Sections in order, one separator between two that have something in them.
56
+ * @param {(MenuItem[] | null | undefined)[]} sections
57
+ * @returns {MenuItem[]}
58
+ */
59
+ export function joinSections(sections) {
60
+ const out = [];
61
+ for (const section of sections || []) {
62
+ const items = normalizeItems(section);
63
+ if (!items.length) continue;
64
+ if (out.length) out.push({ separator: true });
65
+ out.push(...items);
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /** How many rows a menu has at its top level (separators do not count). */
71
+ export function countRows(items) {
72
+ return (items || []).filter((item) => item && !item.separator).length;
73
+ }
74
+
75
+ /**
76
+ * Where a menu of `width` × `height` goes for a press at (x, y): down and to the right of it, flipped up or left
77
+ * when that side has no room, and kept inside the window either way. `bounds` ({ left, top, right, bottom }) is the
78
+ * area it is kept in instead of the window: the app frame (app-frame.js) for a press inside it.
79
+ * @returns {{ left: number, top: number }}
80
+ */
81
+ export function placeMenu({ x, y, width, height, viewportWidth, viewportHeight, margin = VIEWPORT_MARGIN, bounds = null }) {
82
+ const b = bounds || { left: 0, top: 0, right: viewportWidth, bottom: viewportHeight };
83
+ const fit = (at, size, lo, hi) => {
84
+ const first = lo + margin; // the first place it may start
85
+ const furthest = hi - margin - size; // the last place it may start and still end inside the margin
86
+ if (furthest < first) return first; // larger than the area: from the margin (it scrolls)
87
+ if (at + size <= hi - margin) return Math.max(first, at);
88
+ if (at - size >= first) return Math.min(at - size, furthest);
89
+ return furthest;
90
+ };
91
+ return {
92
+ left: Math.round(fit(x, width, b.left, b.right)),
93
+ top: Math.round(fit(y, height, b.top, b.bottom)),
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Where a submenu goes: beside the menu it opens from, its first row level with the row that opened it; on the left
99
+ * when the window (or `bounds`, as for placeMenu) has no room on the right, and kept inside it.
100
+ * @param {{ left: number, right: number }} o.menuRect the menu it opens from
101
+ * @param {{ top: number }} o.rowRect the row that opens it
102
+ * @returns {{ left: number, top: number, side: 'right' | 'left' }}
103
+ */
104
+ export function placeSubmenu({ menuRect, rowRect, width, height, viewportWidth, viewportHeight,
105
+ margin = VIEWPORT_MARGIN, gap = SUBMENU_GAP, lead = SUBMENU_LEAD, bounds = null }) {
106
+ const b = bounds || { left: 0, top: 0, right: viewportWidth, bottom: viewportHeight };
107
+ let side = 'right';
108
+ let left = menuRect.right + gap;
109
+ if (left + width > b.right - margin) {
110
+ const leftSide = menuRect.left - gap - width;
111
+ if (leftSide >= b.left + margin) { left = leftSide; side = 'left'; }
112
+ else left = Math.max(b.left + margin, b.right - margin - width);
113
+ }
114
+ const top = Math.max(b.top + margin, Math.min(rowRect.top - lead, b.bottom - margin - height));
115
+ return { left: Math.round(left), top: Math.round(top), side };
116
+ }
117
+
118
+ /**
119
+ * Where a menu opened from the keyboard points: inside the focused element near its top-left corner, or at its
120
+ * middle when it fills most of the window (the memory map).
121
+ */
122
+ export function anchorPoint(rect, viewportWidth, viewportHeight) {
123
+ if (!rect) return { x: Math.round(viewportWidth / 2), y: Math.round(viewportHeight / 3) };
124
+ if (rect.width > viewportWidth * 0.6 && rect.height > viewportHeight * 0.6) {
125
+ return { x: Math.round(rect.left + rect.width / 2), y: Math.round(rect.top + rect.height / 2) };
126
+ }
127
+ return { x: Math.round(rect.left + Math.min(rect.width / 2, 16)), y: Math.round(rect.top + Math.min(rect.height / 2, 16)) };
128
+ }
129
+
130
+ const selectable = (item) => !!item && !item.separator && !item.disabled;
131
+
132
+ /**
133
+ * The next row the keyboard can land on from `from` in direction `dir` (1 down, -1 up), wrapping; -1 when none can.
134
+ * From -1, down is the first row and up the last.
135
+ */
136
+ export function stepIndex(items, from, dir) {
137
+ const n = items?.length || 0;
138
+ if (!n) return -1;
139
+ let i = from;
140
+ for (let k = 0; k < n; k++) {
141
+ i = i < 0 ? (dir > 0 ? 0 : n - 1) : (i + dir + n) % n;
142
+ if (selectable(items[i])) return i;
143
+ }
144
+ return -1;
145
+ }
146
+
147
+ /** The first and the last row the keyboard can land on. */
148
+ export const firstIndex = (items) => stepIndex(items, -1, 1);
149
+ export const lastIndex = (items) => stepIndex(items, -1, -1);
150
+
151
+ /**
152
+ * The row typed letters go to: the next one whose label starts with them, from the row after `from` (a first
153
+ * letter) or from `from` itself (more letters of the same word). -1 when none does.
154
+ */
155
+ export function typeaheadIndex(items, from, typed) {
156
+ const n = items?.length || 0;
157
+ const want = String(typed || '').toLowerCase();
158
+ if (!n || !want) return -1;
159
+ const start = want.length > 1 ? Math.max(0, from) : from + 1;
160
+ for (let k = 0; k < n; k++) {
161
+ const i = (((start + k) % n) + n) % n;
162
+ if (selectable(items[i]) && String(items[i].label || '').toLowerCase().startsWith(want)) return i;
163
+ }
164
+ return -1;
165
+ }
166
+
167
+ const TEXT_INPUT_TYPES = new Set(['', 'text', 'search', 'url', 'tel', 'email', 'password', 'number',
168
+ 'date', 'datetime-local', 'month', 'time', 'week']);
169
+
170
+ /**
171
+ * Whether an element is a field the browser's own menu belongs to (spelling, paste): a text input, a textarea, a
172
+ * native select or anything editable. Checkboxes, buttons and the like are not.
173
+ * @param {{ tagName?: string, type?: string, isContentEditable?: boolean, closest?: Function } | null} el
174
+ */
175
+ export function isEditableTarget(el) {
176
+ if (!el) return false;
177
+ if (el.isContentEditable) return true;
178
+ const tag = String(el.tagName || '').toUpperCase();
179
+ if (tag === 'TEXTAREA' || tag === 'SELECT') return true;
180
+ if (tag === 'INPUT') return TEXT_INPUT_TYPES.has(String(el.type || '').toLowerCase());
181
+ return !!el.closest?.('[contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"]');
182
+ }
183
+
184
+ /**
185
+ * Why the app's menu stays out of a right-click, or null when it opens. In order: another handler took the event
186
+ * (it called preventDefault: an area with a menu of its own), the press is on the app's menu itself, Shift is held
187
+ * (the escape hatch to the browser's menu), the target is a field, or it is in an area that opted out.
188
+ * @param {{ defaultPrevented?: boolean, shiftKey?: boolean }} event
189
+ * @param {{ inOwnMenu?: boolean, editable?: boolean, optedOut?: boolean }} target
190
+ * @returns {null | 'handled' | 'own-menu' | 'shift' | 'editable' | 'opt-out'}
191
+ */
192
+ export function nativeMenuReason(event, target = {}) {
193
+ if (event?.defaultPrevented) return 'handled';
194
+ if (target.inOwnMenu) return 'own-menu';
195
+ if (event?.shiftKey) return 'shift';
196
+ if (target.editable) return 'editable';
197
+ if (target.optedOut) return 'opt-out';
198
+ return null;
199
+ }
200
+
201
+ /** Whether a key press asks for a context menu: the ContextMenu key, or Shift+F10, with no other modifier. */
202
+ export function isMenuKey(event) {
203
+ if (!event || event.ctrlKey || event.metaKey || event.altKey) return false;
204
+ return event.key === 'ContextMenu' || (event.key === 'F10' && !!event.shiftKey);
205
+ }
206
+
207
+ /**
208
+ * A link the menu can offer: http(s) only, and not one that only points into the page (`#`, `#…`) or runs script.
209
+ * @param {string | null} raw the href as written
210
+ * @param {string | null} resolved the href the browser resolved
211
+ * @returns {string | null} the address to open or copy
212
+ */
213
+ export function usableLink(raw, resolved) {
214
+ const written = String(raw ?? '').trim();
215
+ if (!written || written.startsWith('#')) return null;
216
+ if (/^\s*javascript:/i.test(written)) return null;
217
+ return /^https?:\/\//i.test(String(resolved || '')) ? String(resolved) : null;
218
+ }
219
+
220
+ /**
221
+ * An image the menu can offer. A data: or blob: picture can be copied by address; only an http(s) one opens in a
222
+ * new tab (browsers refuse to open a data: address there).
223
+ * @returns {{ src: string, canOpen: boolean } | null}
224
+ */
225
+ export function usableImage(src) {
226
+ const value = String(src || '');
227
+ if (/^https?:\/\//i.test(value)) return { src: value, canOpen: true };
228
+ if (/^(data|blob):/i.test(value)) return { src: value, canOpen: false };
229
+ return null;
230
+ }
231
+
232
+ /** A selection as a menu label shows it: one line, at most `max` characters, an ellipsis when cut. */
233
+ export function quoteSelection(text, max = 24) {
234
+ const flat = String(text || '').replace(/\s+/g, ' ').trim();
235
+ return flat.length > max ? `${flat.slice(0, max - 1).trimEnd()}…` : flat;
236
+ }
237
+
238
+ /** The selected text an action takes: trimmed, and capped so a whole page selected is not sent anywhere at once. */
239
+ export const SELECTION_MAX = 4000;
240
+ export function selectionPayload(text, max = SELECTION_MAX) {
241
+ const value = String(text || '').trim();
242
+ return value.length > max ? value.slice(0, max) : value;
243
+ }
@@ -0,0 +1,366 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — The app's context menu: what is in it
3
+ // DOM-free. buildAppMenu(ctx, env) turns what was right-clicked into menu
4
+ // items. ui-context-menu.js supplies `env`: the strings (t), the keys as the
5
+ // central keybind registry has them now, what the page can do at this moment
6
+ // and the actions, which are the app's own functions (registered keybind
7
+ // actions, the event bus, the panels' buttons, the whiteboard's toolbar).
8
+ // Nothing here runs an action itself, so tests/context-menu-model.test.mjs
9
+ // can check every context.
10
+ // ═══════════════════════════════════════════
11
+
12
+ import { joinSections, quoteSelection } from './context-menu-logic.js';
13
+ import { resolveViewVariant } from './view-variant.js';
14
+
15
+ const svg = (body) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
16
+
17
+ // The stroke icons of the menubar's dropdowns (same viewBox, stroke and caps).
18
+ export const ICONS = {
19
+ back: svg('<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>'),
20
+ forward: svg('<line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/>'),
21
+ reload: svg('<path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>'),
22
+ copy: svg('<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"/>'),
23
+ link: svg('<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>'),
24
+ external: svg('<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>'),
25
+ image: svg('<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>'),
26
+ search: svg('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>'),
27
+ fullscreen: svg('<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>'),
28
+ exitFullscreen: svg('<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>'),
29
+ send: svg('<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>'),
30
+ memory: svg('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="13" y2="17"/>'),
31
+ // The memory card's Linked (focus) button
32
+ linked: svg('<circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4m-7.07-14.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83"/>'),
33
+ hash: svg('<line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/>'),
34
+ select: svg('<polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>'),
35
+ // Apps › Map
36
+ map: svg('<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/>'),
37
+ target: svg('<circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/>'),
38
+ frame: svg('<path d="M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="3"/>'),
39
+ sliders: svg('<line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/>'),
40
+ layers: svg('<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/>'),
41
+ clear: svg('<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>'),
42
+ // Apps › Browser apps
43
+ grid: svg('<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>'),
44
+ keyboard: svg('<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M6 12h.01M10 12h.01M14 12h.01M18 12h.01M7 16h10"/>'),
45
+ settings: svg('<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>'),
46
+ page: svg('<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/>'),
47
+ panel: svg('<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="15" y1="3" x2="15" y2="21"/>'),
48
+ terminal: svg('<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>'),
49
+ folder: svg('<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>'),
50
+ list: svg('<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>'),
51
+ tag: svg('<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/>'),
52
+ pen: svg('<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>'),
53
+ star: svg('<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>'),
54
+ zap: svg('<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>'),
55
+ clock: svg('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>'),
56
+ play: svg('<polygon points="6 3 20 12 6 21 6 3"/>'),
57
+ bell: svg('<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>'),
58
+ help: svg('<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/>'),
59
+ // The focus-mode whiteboard's toolbar (html-shell.js #wb-toolbar)
60
+ undo: svg('<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>'),
61
+ redo: svg('<polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>'),
62
+ pointer: svg('<path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/>'),
63
+ // The navbar's focus-mode button (#titlebar-viz-toggle)
64
+ exitFocus: svg('<circle cx="12" cy="12" r="4"/><circle cx="12" cy="12" r="1" fill="currentColor" stroke="none"/><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/>'),
65
+ };
66
+
67
+ /**
68
+ * The whiteboard's tools, in the order of its toolbar (`data-tool` of #wb-toolbar). The whiteboard has no rebindable
69
+ * keys for them; its own fixed keys (ui-whiteboard.js onKeyDown) are WHITEBOARD_KEYS.
70
+ */
71
+ export const WHITEBOARD_TOOLS = ['select', 'text', 'list', 'arrow', 'shape', 'pen', 'section'];
72
+
73
+ /** The whiteboard's fixed keys, written as the keybind registry writes a key (Ctrl stands for Cmd on a Mac too). */
74
+ export const WHITEBOARD_KEYS = { multi: 'M', undo: 'Ctrl + Z', redo: 'Ctrl + Shift + Z', selectAll: 'Ctrl + A' };
75
+
76
+ /** Map options. The deprecated 2D definitions remain for the archived source; current contexts resolve to 3D. */
77
+ export const MAP_OPTIONS = { '3d': ['neighbors', 'recency', 'grid'], '2d': ['minimap', 'regionGlow'] };
78
+ // A switch with a registered key shows it.
79
+ const MAP_OPTION_ACTIONS = { minimap: 'toggle-minimap' };
80
+
81
+ /**
82
+ * The Open › submenu: the app's panels, windows and tools, each through the path its own button or key uses.
83
+ * `action`: a registered keybind action (its key is shown); `press`: the button that opens it; `event`: the bus
84
+ * event the menubar emits. `perm`: what a guest needs (the menubar's MENU_PERM_MAP). `active`: the element whose
85
+ * `.active` says it is open now (a `!` in front: open while that element is NOT active). `appIcon`: the Apps
86
+ * menu row whose icon it shows. `null` is a separator.
87
+ */
88
+ export const OPEN_ENTRIES = [
89
+ { id: 'open-assistant', key: 'assistant', action: 'launch-assistant', appIcon: 'menu-terminal-assistant', icon: 'panel' },
90
+ { id: 'open-claude-panel', key: 'claudePanel', action: 'toggle-claude-panel', active: 'topright-claude-panel-btn', appIcon: 'menu-terminal-claude', icon: 'panel' },
91
+ { id: 'open-codex-panel', key: 'codexPanel', press: 'topright-codex-panel-btn', active: 'topright-codex-panel-btn', appIcon: 'menu-terminal-codex', icon: 'panel' },
92
+ { id: 'open-opencode-panel', key: 'opencodePanel', press: 'topright-opencode-panel-btn', active: 'topright-opencode-panel-btn', appIcon: 'menu-terminal-opencode', icon: 'panel' },
93
+ null,
94
+ { id: 'open-terminal', key: 'terminal', event: ['terminal:open', { profile: 'shell' }], perm: 'terminal', appIcon: 'menu-terminal-shell', icon: 'terminal' },
95
+ { id: 'open-claude-terminal', key: 'claudeTerminal', action: 'launch-claude', perm: 'terminal', icon: 'terminal' },
96
+ { id: 'open-browser', key: 'browser', action: 'launch-browser', perm: 'browser', appIcon: 'menu-terminal-browser', icon: 'page' },
97
+ null,
98
+ { id: 'open-file-explorer', key: 'fileExplorer', action: 'toggle-file-explorer', active: 'menu-toggle-file-explorer', icon: 'folder' },
99
+ { id: 'open-memory-explorer', key: 'memoryExplorer', action: 'toggle-explorer', active: 'menu-toggle-explorer', icon: 'list' },
100
+ { id: 'open-categories', key: 'categories', action: 'toggle-categories', active: 'menu-toggle-categories', icon: 'tag' },
101
+ { id: 'open-focus-mode', key: 'focusMode', action: 'toggle-focus-mode', active: '!titlebar-viz-toggle', perm: 'whiteboard', icon: 'pen' },
102
+ null,
103
+ { id: 'open-skills', key: 'skills', action: 'open-skills', perm: 'skills', icon: 'star' },
104
+ { id: 'open-automations', key: 'automations', action: 'open-automations', perm: 'automations', icon: 'zap' },
105
+ { id: 'open-schedules', key: 'schedules', event: ['schedules:toggle'], active: 'menubar-schedules-btn', perm: 'automations', icon: 'clock' },
106
+ { id: 'open-style-guide', key: 'styleGuide', event: ['styleguide:open'], perm: 'styleGuide', appIcon: 'menu-open-styleguide', icon: 'grid' },
107
+ { id: 'open-command-runner', key: 'commandRunner', event: ['command-runner:open'], appIcon: 'menu-command-runner', icon: 'play' },
108
+ { id: 'open-image-gallery', key: 'imageGallery', action: 'toggle-image-gallery', active: 'titlebar-gallery-btn', icon: 'image' },
109
+ { id: 'open-notifications', key: 'notifications', action: 'toggle-session-monitor', active: 'titlebar-sessions-btn', icon: 'bell' },
110
+ { id: 'open-help', key: 'help', action: 'toggle-help', icon: 'help' },
111
+ ];
112
+
113
+ /**
114
+ * @typedef {object} MenuEnv
115
+ * @property {(key: string, params?: object) => string} t
116
+ * @property {(actionId: string) => string} shortcut the key as menus show it, '' while unbound
117
+ * @property {(actionId: string) => boolean} hasAction a registered keybind action
118
+ * @property {(id: string) => boolean} hasElement a button on this page
119
+ * @property {(id: string) => boolean} isActive that element has `.active`
120
+ * @property {(perm: string) => boolean} allowed false for a guest without that permission
121
+ * @property {(id: string) => string} [appIcon] the icon markup an Apps menu row shows, '' when none
122
+ * @property {{ url: string, canGoBack: boolean, canGoForward: boolean, fullscreen: boolean, fullscreenEnabled: boolean }} page
123
+ * @property {Record<string, Function>} act the actions (see ui-context-menu.js)
124
+ */
125
+
126
+ /**
127
+ * What a right-click is on.
128
+ * @typedef {object} MenuContext
129
+ * @property {string} [selection] the selected text it is over ('' or missing: none)
130
+ * @property {string | null} [link] an http(s) link under it
131
+ * @property {{ src: string, canOpen: boolean } | null} [image]
132
+ * @property {{ kind: 'memory', memory: MemoryTarget, act?: object } | { kind: 'map', map: MapTarget, act?: object }
133
+ * | { kind: 'focus', focus: FocusTarget, act?: object } | null} [area]
134
+ */
135
+
136
+ /**
137
+ * @typedef {object} MemoryTarget
138
+ * @property {string} id
139
+ * @property {boolean} [card] a memory card: it is open already, so Close card instead of Open
140
+ * @property {boolean} [onMap] a point on the memory map: it can join the multi-selection and fly to its island
141
+ * @property {boolean} [selected] in the multi-selection now
142
+ * @property {string} [island] its category (the island it is on)
143
+ */
144
+
145
+ /**
146
+ * @typedef {object} MapTarget
147
+ * @property {Record<string, boolean>} [options] the switches of MAP_OPTIONS[variant], as they are now
148
+ * @property {boolean} [searching] a search is dimming the map
149
+ * @property {boolean} [selected] a memory is selected on it (3D) or memories are in the multi-selection (2D)
150
+ * @property {{ name: string, type: string } | null} [label] an island or continent name under the pointer
151
+ * @property {'2d' | '3d'} [variant]
152
+ */
153
+
154
+ /**
155
+ * The focus-mode screen (the whiteboard), as it is now.
156
+ * @typedef {object} FocusTarget
157
+ * @property {string} [tool] the whiteboard's active tool (WHITEBOARD_TOOLS)
158
+ * @property {boolean} [multi] multi-select mode is on
159
+ * @property {boolean} [canUndo]
160
+ * @property {boolean} [canRedo]
161
+ * @property {boolean} [hasElements] something is on the board (Select all)
162
+ * @property {boolean} [canEdit] false for a guest without the whiteboard permission: only Select is left
163
+ */
164
+
165
+ /** The top level stays short: about 12 rows at most (tests check every context against it). */
166
+ export const MAX_TOP_LEVEL = 12;
167
+
168
+ /**
169
+ * The menu for a right-click.
170
+ * @param {MenuContext} ctx
171
+ * @param {MenuEnv} env
172
+ */
173
+ export function buildAppMenu(ctx = {}, env) {
174
+ const act = { ...env.act, ...(ctx.area?.act || {}) };
175
+ const selection = ctx.selection || '';
176
+ const targeted = !!(selection || ctx.link || ctx.image || ctx.area);
177
+ const sections = [];
178
+ if (!targeted) sections.push(pageNavigation(env, act), pageTools(env, act, { withCopy: true }));
179
+ if (selection) sections.push(selectionItems(selection, env, act));
180
+ if (ctx.link) sections.push(linkItems(ctx.link, env, act));
181
+ if (ctx.image) sections.push(imageItems(ctx.image, env, act));
182
+ if (ctx.area?.kind === 'memory') sections.push(memoryItems(ctx.area.memory, env, act));
183
+ if (ctx.area?.kind === 'map') sections.push(mapItems(ctx.area.map, env, act));
184
+ if (ctx.area?.kind === 'focus') sections.push(focusItems(ctx.area.focus, env, act));
185
+ sections.push(appItems(env, act, { search: !selection }));
186
+ if (targeted) sections.push([pageSubmenu(env, act)]);
187
+ return joinSections(sections);
188
+ }
189
+
190
+ // ── Page ──
191
+
192
+ function pageNavigation({ t, page }, act) {
193
+ return [
194
+ { id: 'back', label: t('contextMenu.page.back'), icon: ICONS.back, disabled: !page.canGoBack, run: () => act.back() },
195
+ { id: 'forward', label: t('contextMenu.page.forward'), icon: ICONS.forward, disabled: !page.canGoForward, run: () => act.forward() },
196
+ { id: 'reload', label: t('contextMenu.page.reload'), icon: ICONS.reload, run: () => act.reload() },
197
+ ];
198
+ }
199
+
200
+ function pageTools({ t, page }, act, { withCopy = false } = {}) {
201
+ return [
202
+ // Nothing is selected where the menu opened: Copy is there, and off.
203
+ withCopy && { id: 'copy', label: t('contextMenu.selection.copy'), icon: ICONS.copy, disabled: true },
204
+ { id: 'copy-url', label: t('contextMenu.page.copyUrl'), icon: ICONS.link, run: () => act.copyText(page.url) },
205
+ {
206
+ id: 'fullscreen',
207
+ label: t(page.fullscreen ? 'contextMenu.page.exitFullscreen' : 'contextMenu.page.fullscreen'),
208
+ icon: page.fullscreen ? ICONS.exitFullscreen : ICONS.fullscreen,
209
+ disabled: !page.fullscreenEnabled,
210
+ run: () => act.toggleFullscreen(),
211
+ },
212
+ ];
213
+ }
214
+
215
+ function pageSubmenu(env, act) {
216
+ return {
217
+ id: 'page',
218
+ label: env.t('contextMenu.page.submenu'),
219
+ icon: ICONS.page,
220
+ submenu: [...pageNavigation(env, act), { separator: true }, ...pageTools(env, act)],
221
+ };
222
+ }
223
+
224
+ // ── Selection, link, image ──
225
+
226
+ function selectionItems(text, { t }, act) {
227
+ return [
228
+ { id: 'copy', label: t('contextMenu.selection.copy'), icon: ICONS.copy, run: () => act.copyText(text) },
229
+ { id: 'search-selection', label: t('contextMenu.selection.search', { text: quoteSelection(text) }), icon: ICONS.search, run: () => act.searchMemories(text) },
230
+ { id: 'send-selection', label: t('contextMenu.selection.sendToAi'), icon: ICONS.send, run: () => act.sendText(text) },
231
+ ];
232
+ }
233
+
234
+ function linkItems(href, { t }, act) {
235
+ return [
236
+ { id: 'open-link', label: t('contextMenu.link.open'), icon: ICONS.external, run: () => act.openInNewTab(href) },
237
+ { id: 'copy-link', label: t('contextMenu.link.copy'), icon: ICONS.link, run: () => act.copyText(href) },
238
+ ];
239
+ }
240
+
241
+ function imageItems(image, { t }, act) {
242
+ return [
243
+ { id: 'open-image', label: t('contextMenu.image.open'), icon: ICONS.image, disabled: !image.canOpen, run: () => act.openInNewTab(image.src) },
244
+ { id: 'copy-image', label: t('contextMenu.image.copy'), icon: ICONS.copy, run: () => act.copyText(image.src) },
245
+ ];
246
+ }
247
+
248
+ // ── A memory (a point on the map, an Explorer row, a memory card) ──
249
+
250
+ function memoryItems(memory, { t }, act) {
251
+ const { id } = memory;
252
+ return [
253
+ !memory.card && { id: 'memory-open', label: t('contextMenu.memory.open'), icon: ICONS.memory, run: () => act.openMemory(id) },
254
+ { id: 'memory-linked', label: t('contextMenu.memory.linked'), icon: ICONS.linked, run: () => act.showLinked(id) },
255
+ { id: 'memory-copy-id', label: t('contextMenu.memory.copyId'), icon: ICONS.hash, run: () => act.copyText(id) },
256
+ { id: 'memory-send', label: t('contextMenu.memory.sendToAi'), icon: ICONS.send, run: () => act.sendMemory(id) },
257
+ memory.onMap && {
258
+ id: 'memory-select',
259
+ label: t(memory.selected ? 'contextMenu.memory.deselect' : 'contextMenu.memory.select'),
260
+ icon: ICONS.select,
261
+ run: () => act.toggleSelect(id),
262
+ },
263
+ memory.onMap && memory.island && {
264
+ id: 'memory-island',
265
+ label: t('contextMenu.memory.goToIsland', { name: memory.island }),
266
+ icon: ICONS.target,
267
+ run: () => act.flyToIsland(memory.island),
268
+ },
269
+ memory.card && { id: 'memory-close-card', label: t('contextMenu.memory.closeCard'), icon: ICONS.clear, run: () => act.closeCard(id) },
270
+ ];
271
+ }
272
+
273
+ // ── The memory map's empty space or a name on it ──
274
+
275
+ function mapItems(map, { t, shortcut }, act) {
276
+ const options = map.options || {};
277
+ const variant = resolveViewVariant(map.variant);
278
+ const toggle = (key) => ({
279
+ id: `map-${key}`,
280
+ label: t(`contextMenu.map.${key}`),
281
+ checked: !!options[key],
282
+ shortcut: MAP_OPTION_ACTIONS[key] ? shortcut(MAP_OPTION_ACTIONS[key]) : '',
283
+ run: () => act.toggleOption(key),
284
+ });
285
+ return [
286
+ map.label && { id: 'map-go-to', label: t('contextMenu.map.goTo', { name: map.label.name }), icon: ICONS.target, run: () => act.flyToLabel() },
287
+ { id: 'map-frame-all', label: t('contextMenu.map.frameAll'), icon: ICONS.frame, shortcut: shortcut('map-frame-all'), run: () => act.frameAll() },
288
+ { id: 'map-options', label: t('contextMenu.map.options'), icon: ICONS.sliders, submenu: MAP_OPTIONS[variant].map(toggle) },
289
+ map.searching && { id: 'map-clear-search', label: t('contextMenu.map.clearSearch'), icon: ICONS.clear, run: () => act.clearSearch() },
290
+ map.selected && { id: 'map-clear-selection', label: t('contextMenu.map.clearSelection'), icon: ICONS.clear, run: () => act.clearSelection() },
291
+ ];
292
+ }
293
+
294
+ // ── The focus-mode screen: the whiteboard's own tools, and the way out ──
295
+
296
+ function focusItems(focus, { t, shortcut }, act) {
297
+ const tool = focus.tool || 'select';
298
+ const canEdit = focus.canEdit !== false;
299
+ const tools = WHITEBOARD_TOOLS.map((key) => ({
300
+ id: `focus-tool-${key}`,
301
+ label: t(`contextMenu.focus.tools.${key}`),
302
+ checked: !focus.multi && tool === key,
303
+ disabled: !canEdit && key !== 'select',
304
+ run: () => act.setTool(key),
305
+ }));
306
+ return [
307
+ {
308
+ id: 'focus-tool',
309
+ label: t('contextMenu.focus.tool'),
310
+ icon: ICONS.pointer,
311
+ submenu: [
312
+ ...tools,
313
+ { separator: true },
314
+ { id: 'focus-multi', label: t('contextMenu.focus.multiSelect'), checked: !!focus.multi, disabled: !canEdit, shortcut: WHITEBOARD_KEYS.multi, run: () => act.toggleMulti() },
315
+ ],
316
+ },
317
+ { id: 'focus-undo', label: t('contextMenu.focus.undo'), icon: ICONS.undo, shortcut: WHITEBOARD_KEYS.undo, disabled: !canEdit || !focus.canUndo, run: () => act.undo() },
318
+ { id: 'focus-redo', label: t('contextMenu.focus.redo'), icon: ICONS.redo, shortcut: WHITEBOARD_KEYS.redo, disabled: !canEdit || !focus.canRedo, run: () => act.redo() },
319
+ { id: 'focus-select-all', label: t('contextMenu.focus.selectAll'), icon: ICONS.select, shortcut: WHITEBOARD_KEYS.selectAll, disabled: !focus.hasElements, run: () => act.selectAll() },
320
+ { id: 'focus-exit', label: t('contextMenu.focus.exit'), icon: ICONS.exitFocus, shortcut: shortcut('toggle-focus-mode'), run: () => act.exitFocus() },
321
+ ];
322
+ }
323
+
324
+ // ── The app ──
325
+
326
+ function openItems(env, act) {
327
+ const { t, shortcut, hasAction, hasElement, isActive, allowed } = env;
328
+ return OPEN_ENTRIES.map((entry) => {
329
+ if (!entry) return { separator: true };
330
+ const item = {
331
+ id: entry.id,
332
+ label: t(`contextMenu.open.${entry.key}`),
333
+ icon: env.appIcon?.(entry.appIcon) || ICONS[entry.icon] || '',
334
+ disabled: entry.perm ? !allowed(entry.perm) : false,
335
+ };
336
+ if (entry.action) {
337
+ item.hidden = !hasAction(entry.action);
338
+ item.shortcut = shortcut(entry.action);
339
+ item.run = () => act.runAction(entry.action);
340
+ } else if (entry.press) {
341
+ item.hidden = !hasElement(entry.press);
342
+ item.run = () => act.press(entry.press);
343
+ } else {
344
+ item.run = () => act.emit(...entry.event);
345
+ }
346
+ if (entry.active) {
347
+ const negate = entry.active.startsWith('!');
348
+ const id = negate ? entry.active.slice(1) : entry.active;
349
+ if (hasElement(id)) item.checked = negate ? !isActive(id) : isActive(id);
350
+ }
351
+ return item;
352
+ });
353
+ }
354
+
355
+ function appItems(env, act, { search = true } = {}) {
356
+ const { t, shortcut, hasAction } = env;
357
+ const registered = (id, actionId, key, icon) => ({
358
+ id, label: t(key), icon, shortcut: shortcut(actionId), hidden: !hasAction(actionId), run: () => act.runAction(actionId),
359
+ });
360
+ return [
361
+ { id: 'open', label: t('contextMenu.app.open'), icon: ICONS.grid, submenu: openItems(env, act) },
362
+ search && registered('search', 'focus-search', 'contextMenu.app.search', ICONS.search),
363
+ registered('shortcuts', 'open-keybinds', 'contextMenu.app.shortcuts', ICONS.keyboard),
364
+ registered('settings', 'open-settings', 'contextMenu.app.settings', ICONS.settings),
365
+ ];
366
+ }